From 3da17a35f00fe03d555c58e3cb2243a6bb16953a Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Fri, 8 Dec 2023 17:30:34 -0500 Subject: [PATCH 1/2] Added build workflow and unit tests. --- .github/workflows/build.yml | 18 +++ package.json | 3 +- src/components/__tests__/HelloWorld.spec.ts | 11 -- src/helpers/__tests__/arrayUtils.spec.ts | 71 +++++++++ src/helpers/__tests__/objectUtils.spec.ts | 45 ++++++ src/helpers/__tests__/stringUtils.spec.ts | 138 ++++++++++++++++++ src/helpers/arrayUtils.ts | 23 ++- src/helpers/deviceUtils.ts | 16 ++ src/helpers/stringUtils.ts | 20 ++- src/stores/__tests__/account.spec.ts | 44 ++++++ src/stores/__tests__/counter.spec.ts | 21 +++ src/stores/__tests__/i18n.spec.ts | 39 +++++ .../rules/__tests__/confirmed.spec.ts | 13 ++ .../rules/__tests__/identifier.spec.ts | 18 +++ .../rules/__tests__/requireDigit.spec.ts | 17 +++ .../rules/__tests__/requireLowercase.spec.ts | 17 +++ .../__tests__/requireNonAlphanumeric.spec.ts | 16 ++ .../rules/__tests__/requireUppercase.spec.ts | 17 +++ src/validation/rules/__tests__/slug.spec.ts | 16 ++ .../rules/__tests__/uniqueChars.spec.ts | 14 ++ src/validation/rules/__tests__/url.spec.ts | 18 +++ .../rules/__tests__/username.spec.ts | 22 +++ src/validation/rules/url.ts | 1 + 23 files changed, 597 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 src/components/__tests__/HelloWorld.spec.ts create mode 100644 src/helpers/__tests__/arrayUtils.spec.ts create mode 100644 src/helpers/__tests__/objectUtils.spec.ts create mode 100644 src/helpers/__tests__/stringUtils.spec.ts create mode 100644 src/helpers/deviceUtils.ts create mode 100644 src/stores/__tests__/account.spec.ts create mode 100644 src/stores/__tests__/counter.spec.ts create mode 100644 src/stores/__tests__/i18n.spec.ts create mode 100644 src/validation/rules/__tests__/confirmed.spec.ts create mode 100644 src/validation/rules/__tests__/identifier.spec.ts create mode 100644 src/validation/rules/__tests__/requireDigit.spec.ts create mode 100644 src/validation/rules/__tests__/requireLowercase.spec.ts create mode 100644 src/validation/rules/__tests__/requireNonAlphanumeric.spec.ts create mode 100644 src/validation/rules/__tests__/requireUppercase.spec.ts create mode 100644 src/validation/rules/__tests__/slug.spec.ts create mode 100644 src/validation/rules/__tests__/uniqueChars.spec.ts create mode 100644 src/validation/rules/__tests__/url.spec.ts create mode 100644 src/validation/rules/__tests__/username.spec.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7fa686f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,18 @@ +name: Build Vue3 Template + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - run: | + npm ci + npm run build + npm run test diff --git a/package.json b/package.json index 6d856ab..31c2fbb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "vite", "build": "run-p type-check build-only", "preview": "vite preview", - "test:unit": "vitest", + "test": "vitest run --coverage", + "test:dev": "vitest", "build-only": "vite build", "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", diff --git a/src/components/__tests__/HelloWorld.spec.ts b/src/components/__tests__/HelloWorld.spec.ts deleted file mode 100644 index 5c51c9e..0000000 --- a/src/components/__tests__/HelloWorld.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { describe, it, expect } from "vitest"; - -// import { mount } from "@vue/test-utils"; -// import HelloWorld from "../HelloWorld.vue"; - -// describe("HelloWorld", () => { -// it("renders properly", () => { -// const wrapper = mount(HelloWorld, { props: { msg: "Hello Vitest" } }); -// expect(wrapper.text()).toContain("Hello Vitest"); -// }); -// }); diff --git a/src/helpers/__tests__/arrayUtils.spec.ts b/src/helpers/__tests__/arrayUtils.spec.ts new file mode 100644 index 0000000..67eef98 --- /dev/null +++ b/src/helpers/__tests__/arrayUtils.spec.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; + +import { orderBy, orderByDescending } from "../arrayUtils"; + +type User = { + name: string; + email?: string; +}; + +describe("arrayUtils.orderBy", () => { + it.concurrent("should sort an empty array", () => { + expect(orderBy([]).join(";")).toBe(""); + expect(orderBy([], "name").join(";")).toBe(""); + expect(orderBy([], undefined, true).join(";")).toBe(""); + expect(orderBy([], "name", true).join(";")).toBe(""); + }); + + it.concurrent("should sort ascendingly a primitive array", () => { + expect(orderBy([3, undefined, 1, 2]).join(",")).toBe("1,2,3,"); + expect(orderBy(["orange", "mango", "apple"], undefined, false).join("|")).toBe("apple|mango|orange"); + }); + + it.concurrent("should sort ascendingly an object array by key", () => { + const users: User[] = [{ name: "Charles" }, { name: "Christopher" }, { name: "Abby" }]; + expect( + orderBy(users, "name") + .map(({ name }) => name) + .join("|") + ).toBe("Abby|Charles|Christopher"); + expect( + orderBy(users, "name", false) + .map(({ name }) => name) + .join("|") + ).toBe("Abby|Charles|Christopher"); + }); + + it.concurrent("should sort descendingly a primitive array", () => { + expect(orderBy([3, 1, undefined, 2], undefined, true).join(",")).toBe("3,2,1,"); + expect(orderBy(["orange", "apple", "mango"], undefined, true).join("|")).toBe("orange|mango|apple"); + }); + + it.concurrent("should sort descendingly an object array by key", () => { + const users: User[] = [{ name: "Charles", email: "carlosluyz@gmail.com" }, { name: "Christopher" }, { name: "Abby", email: "xxxabbyxxx@hotmail.com" }]; + expect( + orderBy(users, "email", true) + .map(({ name }) => name) + .join("|") + ).toBe("Abby|Charles|Christopher"); + }); +}); + +describe("arrayUtils.orderByDescending", () => { + it.concurrent("should sort descendingly a primitive array", () => { + expect(orderByDescending(["Acura", "Nissan", "Chevrolet"]).join(", ")).toBe("Nissan, Chevrolet, Acura"); + expect(orderByDescending([undefined, "Acura", "Nissan", "Chevrolet"]).join(", ")).toBe("Nissan, Chevrolet, Acura, "); + }); + + it.concurrent("should sort descendingly an empty array", () => { + expect(orderByDescending([]).join("|")).toBe(""); + expect(orderByDescending([], "name").join("|")).toBe(""); + }); + + it.concurrent("should sort descendingly an object array", () => { + const users: User[] = [{ name: "Charles", email: "carlosluyz@gmail.com" }, { name: "Christopher" }, { name: "Abby", email: "xxxabbyxxx@hotmail.com" }]; + expect( + orderByDescending(users, "email") + .map(({ name }) => name) + .join("|") + ).toBe("Abby|Charles|Christopher"); + }); +}); diff --git a/src/helpers/__tests__/objectUtils.spec.ts b/src/helpers/__tests__/objectUtils.spec.ts new file mode 100644 index 0000000..ddc36ba --- /dev/null +++ b/src/helpers/__tests__/objectUtils.spec.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; + +import { assign, isEmpty } from "../objectUtils"; + +type User = { + name: string; + email?: string; +}; + +describe("objectUtils.assign", () => { + it.concurrent("should assign a value to a key", () => { + const user: User = { name: "Charles Luiz" }; + assign(user, "name", "Carlos Luyz"); + expect(user.name).toBe("Carlos Luyz"); + }); + + it.concurrent("should assign a value to an optional key", () => { + const user: User = { name: "Charles Luiz" }; + assign(user, "email", "carlosluyz@gmail.com"); + expect(user.email).toBe("carlosluyz@gmail.com"); + }); + + it.concurrent("should assign undefined to an optional key", () => { + const user: User = { name: "Charles Luis", email: "carlosluyz@gmail.com" }; + assign(user, "email", undefined); + expect(user.email).toBeUndefined(); + }); +}); + +describe("objectUtils.isEmpty", () => { + it.concurrent("should return false if the object has at least one key", () => { + const user: User = { name: "Charles Luiz" }; + expect(isEmpty(user)).toBe(false); + user.email = "carlosluyz@gmail.com"; + expect(isEmpty(user)).toBe(false); + }); + + it.concurrent("should return true if the object has no key", () => { + const obj: any = {}; + expect(isEmpty(obj)).toBe(true); + obj.key = true; + delete obj.key; + expect(isEmpty(obj)).toBe(true); + }); +}); diff --git a/src/helpers/__tests__/stringUtils.spec.ts b/src/helpers/__tests__/stringUtils.spec.ts new file mode 100644 index 0000000..838daac --- /dev/null +++ b/src/helpers/__tests__/stringUtils.spec.ts @@ -0,0 +1,138 @@ +import { describe, it, expect } from "vitest"; + +import { combineURL, isAbsoluteURL, isDigit, isLetter, isLetterOrDigit, shortify, slugify, unaccent } from "../stringUtils"; + +describe("stringUtils.combineURL", () => { + it.concurrent("should combine the segments with a slash (/)", () => { + expect(combineURL()).toBe("/"); + expect(combineURL("Hello World!")).toBe("/Hello World!"); + expect(combineURL("hello", "world")).toBe("/hello/world"); + expect(combineURL("http://api.google.com", "users/123")).toBe("http://api.google.com/users/123"); + expect(combineURL("https://api.google.com", "users", "123")).toBe("https://api.google.com/users/123"); + }); + + it.concurrent("should ignore empty values", () => { + expect(combineURL("")).toBe("/"); + expect(combineURL(" ")).toBe("/"); + expect(combineURL("", "api", "user")).toBe("/api/user"); + expect(combineURL(" ", "api", "user")).toBe("/api/user"); + expect(combineURL("api", "", "user")).toBe("/api/user"); + expect(combineURL("api", " ", "user")).toBe("/api/user"); + expect(combineURL("api", "user", "")).toBe("/api/user"); + expect(combineURL("api", "user", " ")).toBe("/api/user"); + }); + + it.concurrent("should ignore undefined values", () => { + const u: any = undefined; + expect(combineURL(u)).toBe("/"); + expect(combineURL(u, "api", "user")).toBe("/api/user"); + expect(combineURL("api", u, "user")).toBe("/api/user"); + expect(combineURL("api", "user", u)).toBe("/api/user"); + }); + + it.concurrent("should remove starting & ending slashes (/) from segments", () => { + expect(combineURL("/api", "users/123")).toBe("/api/users/123"); + expect(combineURL("api", "users/123/")).toBe("/api/users/123"); + expect(combineURL("/api", "users/123/")).toBe("/api/users/123"); + }); +}); + +describe("stringUtils.isAbsoluteURL", () => { + it.concurrent("should return false when input string is not an absolute URL", () => { + expect(isAbsoluteURL("")).toBe(false); + expect(isAbsoluteURL(" ")).toBe(false); + expect(isAbsoluteURL("/api/users/123")).toBe(false); + }); + + it.concurrent("should return true when input string is an absolute URL", () => { + expect(isAbsoluteURL("http://api.test.com/users/123")).toBe(true); + expect(isAbsoluteURL("https://api.test.com/users/123")).toBe(true); + expect(isAbsoluteURL("ftp://api.test.com/users/123")).toBe(true); + }); +}); + +describe("stringUtils.isDigit", () => { + it.concurrent("should return false when input character is not a digit", () => { + expect(isDigit("")).toBe(false); + expect(isDigit(" ")).toBe(false); + expect(isDigit("A")).toBe(false); + }); + + it.concurrent("should return true when input character is a digit", () => { + expect(isDigit("0")).toBe(true); + expect(isDigit("4")).toBe(true); + }); +}); + +describe("stringUtils.isLetter", () => { + it.concurrent("should return false when input character is not a letter", () => { + expect(isLetter("")).toBe(false); + expect(isLetter(" ")).toBe(false); + expect(isLetter("0")).toBe(false); + expect(isLetter("|")).toBe(false); + }); + + it.concurrent("should return true when input character is a letter", () => { + expect(isLetter("A")).toBe(true); + expect(isLetter("Z")).toBe(true); + }); +}); + +describe("stringUtils.isLetterOrDigit", () => { + it.concurrent("should return false when input character is not a letter, nor a digit", () => { + expect(isLetterOrDigit("")).toBe(false); + expect(isLetterOrDigit(" ")).toBe(false); + expect(isLetterOrDigit("|")).toBe(false); + }); + + it.concurrent("should return true when input character is a letter or a digit", () => { + expect(isLetterOrDigit("B")).toBe(true); + expect(isLetterOrDigit("D")).toBe(true); + expect(isLetterOrDigit("1")).toBe(true); + expect(isLetterOrDigit("3")).toBe(true); + }); +}); + +describe("stringUtils.shortify", () => { + it.concurrent("should return the same string when it is not too long", () => { + expect(shortify("", 20)).toBe(""); + expect(shortify(" ", 20)).toBe(" "); + expect(shortify("Hello World!", 20)).toBe("Hello World!"); + }); + + it.concurrent("should return the shortified string when it is too long", () => { + expect(shortify("", -1)).toBe("…"); + expect(shortify(" ", 2)).toBe(" …"); + expect(shortify("Hello World!", 10)).toBe("Hello Wor…"); + }); +}); + +describe("stringUtils.slugify", () => { + it.concurrent("should return an empty string when it is empty or undefined", () => { + expect(slugify(undefined)).toBe(""); + expect(slugify("")).toBe(""); + expect(slugify(" ")).toBe(""); + }); + + it.concurrent( + "should separate the input string by non-alphanumeric characters, join them using hyphens (-), remove accents and return a lowercased slug", + () => { + expect(slugify("arc-en-ciel")).toBe("arc-en-ciel"); + expect(slugify("Héllo Wôrld!")).toBe("hello-world"); + } + ); +}); + +describe("stringUtils.unaccent", () => { + it.concurrent("should return the same string when it contains no supported accents", () => { + expect(unaccent("")).toBe(""); + expect(unaccent(" ")).toBe(" "); + expect(unaccent("Hello World!")).toBe("Hello World!"); + }); + + it.concurrent("should return the string without accents when it contains supported accents", () => { + expect(unaccent("français")).toBe("francais"); + expect(unaccent(" Noël ")).toBe(" Noel "); + expect(unaccent("Héllo Wôrld!")).toBe("Hello World!"); + }); +}); diff --git a/src/helpers/arrayUtils.ts b/src/helpers/arrayUtils.ts index 963972d..2dacb77 100644 --- a/src/helpers/arrayUtils.ts +++ b/src/helpers/arrayUtils.ts @@ -1,3 +1,22 @@ -export function orderBy(items: T[], key?: keyof T): T[] { - return key ? [...items].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)) : [...items].sort(); +function compare(a: T, b: T, weight: number): number { + if (typeof a === "undefined" || a === null || typeof b === "undefined" || b === null) { + if ((a ?? null) === null && (b ?? null) !== null) { + return -weight; + } else if ((a ?? null) !== null && (b ?? null) === null) { + return weight; + } + return 0; + } + return a < b ? -weight : a > b ? weight : 0; +} +export function orderBy(items: T[], key?: keyof T, isDescending?: boolean): T[] { + const weight = isDescending ? -1 : 1; + if (key) { + return [...items].sort((a, b) => compare(a[key], b[key], weight)); + } + return [...items].sort((a, b) => compare(a, b, weight)); +} + +export function orderByDescending(items: T[], key?: keyof T) { + return orderBy(items, key, true); } diff --git a/src/helpers/deviceUtils.ts b/src/helpers/deviceUtils.ts new file mode 100644 index 0000000..9327e0c --- /dev/null +++ b/src/helpers/deviceUtils.ts @@ -0,0 +1,16 @@ +export function isMobile(): boolean { + let check = false; + (function (a) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + a + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4) + ) + ) + check = true; + // @ts-ignore + })(navigator.userAgent || navigator.vendor || window.opera); + return check; +} diff --git a/src/helpers/stringUtils.ts b/src/helpers/stringUtils.ts index d51f9fc..26f4e3b 100644 --- a/src/helpers/stringUtils.ts +++ b/src/helpers/stringUtils.ts @@ -1,3 +1,16 @@ +export function combineURL(...segments: string[]): string { + const url = segments + .map((v) => v?.trim().replace(/^\/+|\/+$/g, "") ?? "") + .filter((v) => v.length) + .join("/"); + return isAbsoluteURL(url) ? url : `/${url}`; +} + +const absoluteUrlRegex = new RegExp("^(?:[a-z+]+:)?//", "i"); +export function isAbsoluteURL(url: string): boolean { + return absoluteUrlRegex.test(url); +} + export function isDigit(c: string): boolean { return c.trim() !== "" && !isNaN(Number(c)); } @@ -35,13 +48,6 @@ export function slugify(s?: string): string { return unaccent(words.join("-").toLowerCase()); } -export function combineURL(...segments: string[]): string { - return segments - .map((v) => v?.replace(/^\/+|\/+$/g, "") ?? "") - .filter((v) => v.length) - .join("/"); -} - const accents = new Map([ ["à", "a"], ["â", "a"], diff --git a/src/stores/__tests__/account.spec.ts b/src/stores/__tests__/account.spec.ts new file mode 100644 index 0000000..6374909 --- /dev/null +++ b/src/stores/__tests__/account.spec.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, it, expect } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; + +import type { UserProfile } from "../../types/users"; +import { useAccountStore } from "../account"; + +const createdOn: string = new Date().toISOString(); +const user: UserProfile = { + username: "carluis", + email: { + address: "carlosluis@gmail.com", + isVerified: true, + }, + firstName: "Carlos", + lastName: "Luis", + fullName: "Carlos Luis", + locale: "es-MX", + createdOn, + updatedOn: createdOn, +}; + +describe("accountStore", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it.concurrent("should be initially empty", () => { + const account = useAccountStore(); + expect(account.authenticated).toBeUndefined(); + }); + + it.concurrent("should sign-in an authenticated user", () => { + const account = useAccountStore(); + account.signIn(user); + expect(account.authenticated.username).toBe(user.username); + }); + + it.concurrent("should sign-out the authenticated user", () => { + const account = useAccountStore(); + account.signIn(user); + account.signOut(); + expect(account.authenticated).toBeUndefined(); + }); +}); diff --git a/src/stores/__tests__/counter.spec.ts b/src/stores/__tests__/counter.spec.ts new file mode 100644 index 0000000..d43abd9 --- /dev/null +++ b/src/stores/__tests__/counter.spec.ts @@ -0,0 +1,21 @@ +import { beforeEach, describe, it, expect } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; + +import { useCounterStore } from "../counter"; + +describe("counterStore", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it.concurrent("should increment the counter", () => { + const counter = useCounterStore(); + expect(counter.count).toBe(0); + counter.increment(); + expect(counter.count).toBe(1); + expect(counter.doubleCount).toBe(2); + counter.increment(); + expect(counter.count).toBe(2); + expect(counter.doubleCount).toBe(4); + }); +}); diff --git a/src/stores/__tests__/i18n.spec.ts b/src/stores/__tests__/i18n.spec.ts new file mode 100644 index 0000000..3dc285f --- /dev/null +++ b/src/stores/__tests__/i18n.spec.ts @@ -0,0 +1,39 @@ +import { beforeEach, describe, it, expect } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; + +import type { Locale } from "../../types/i18n"; +import { useI18nStore } from "../i18n"; + +const enUS: Locale = { + id: 1033, + code: "en-US", + displayName: "English (United States)", + englishName: "English (United States)", + nativeName: "English (United States)", +}; +const frCA: Locale = { + id: 3084, + code: "fr-CA", + displayName: "French (Canada)", + englishName: "French (Canada)", + nativeName: "français (Canada)", +}; + +describe("accountStore", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it.concurrent("should be initially empty", () => { + const i18n = useI18nStore(); + expect(i18n.locale).toBeUndefined(); + }); + + it.concurrent("should set the current locale", () => { + const i18n = useI18nStore(); + i18n.setLocale(enUS); + expect(i18n.locale.id).toBe(enUS.id); + i18n.setLocale(frCA); + expect(i18n.locale.id).toBe(frCA.id); + }); +}); diff --git a/src/validation/rules/__tests__/confirmed.spec.ts b/src/validation/rules/__tests__/confirmed.spec.ts new file mode 100644 index 0000000..2434b44 --- /dev/null +++ b/src/validation/rules/__tests__/confirmed.spec.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from "vitest"; + +import confirmed from "../confirmed"; + +describe("confirmed", () => { + it.concurrent("should return false when the values are different", () => { + expect(confirmed("password", ["P@s$W0rD"])).toBe(false); + }); + + it.concurrent("should return true when the values are the same", () => { + expect(confirmed("Test123!", ["Test123!"])).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/identifier.spec.ts b/src/validation/rules/__tests__/identifier.spec.ts new file mode 100644 index 0000000..34e625c --- /dev/null +++ b/src/validation/rules/__tests__/identifier.spec.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest"; + +import identifier from "../identifier"; + +describe("identifier", () => { + it.concurrent("should return false when the value is not a valid identifier", () => { + expect(identifier()).toBe(false); + expect(identifier("")).toBe(false); + expect(identifier(" ")).toBe(false); + expect(identifier("0name")).toBe(false); + expect(identifier("person-name")).toBe(false); + }); + + it.concurrent("should return true when the value is a valid identifier", () => { + expect(identifier("_name")).toBe(true); + expect(identifier("PersonName")).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/requireDigit.spec.ts b/src/validation/rules/__tests__/requireDigit.spec.ts new file mode 100644 index 0000000..261545b --- /dev/null +++ b/src/validation/rules/__tests__/requireDigit.spec.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest"; + +import requireDigit from "../requireDigit"; + +describe("requireDigit", () => { + it.concurrent("should return false when the value does not contain a digit", () => { + expect(requireDigit()).toBe(false); + expect(requireDigit("")).toBe(false); + expect(requireDigit(" ")).toBe(false); + expect(requireDigit("AAaa!!!!")).toBe(false); + }); + + it.concurrent("should return true when the value contains a digit", () => { + expect(requireDigit("AAaa!!11")).toBe(true); + expect(requireDigit("Test123!")).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/requireLowercase.spec.ts b/src/validation/rules/__tests__/requireLowercase.spec.ts new file mode 100644 index 0000000..1f09626 --- /dev/null +++ b/src/validation/rules/__tests__/requireLowercase.spec.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest"; + +import requireLowercase from "../requireLowercase"; + +describe("requireLowercase", () => { + it.concurrent("should return false when the value does not contain a lowercase letter", () => { + expect(requireLowercase()).toBe(false); + expect(requireLowercase("")).toBe(false); + expect(requireLowercase(" ")).toBe(false); + expect(requireLowercase("AAAA!!11")).toBe(false); + }); + + it.concurrent("should return true when the value contains a lowercase letter", () => { + expect(requireLowercase("AAaa!!11")).toBe(true); + expect(requireLowercase("Test123!")).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/requireNonAlphanumeric.spec.ts b/src/validation/rules/__tests__/requireNonAlphanumeric.spec.ts new file mode 100644 index 0000000..0b082cb --- /dev/null +++ b/src/validation/rules/__tests__/requireNonAlphanumeric.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest"; + +import requireNonAlphanumeric from "../requireNonAlphanumeric"; + +describe("requireNonAlphanumeric", () => { + it.concurrent("should return false when the value does not contain a non-alphanumeric character", () => { + expect(requireNonAlphanumeric()).toBe(false); + expect(requireNonAlphanumeric("")).toBe(false); + expect(requireNonAlphanumeric("AAaa1111")).toBe(false); + }); + + it.concurrent("should return true when the value contains a non-alphanumeric character", () => { + expect(requireNonAlphanumeric("AAaa!!11")).toBe(true); + expect(requireNonAlphanumeric("Test123!")).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/requireUppercase.spec.ts b/src/validation/rules/__tests__/requireUppercase.spec.ts new file mode 100644 index 0000000..dc550e0 --- /dev/null +++ b/src/validation/rules/__tests__/requireUppercase.spec.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest"; + +import requireUppercase from "../requireUppercase"; + +describe("requireUppercase", () => { + it.concurrent("should return false when the value does not contain an uppercase letter", () => { + expect(requireUppercase()).toBe(false); + expect(requireUppercase("")).toBe(false); + expect(requireUppercase(" ")).toBe(false); + expect(requireUppercase("aaaa!!11")).toBe(false); + }); + + it.concurrent("should return true when the value contains an uppercase letter", () => { + expect(requireUppercase("AAaa!!11")).toBe(true); + expect(requireUppercase("Test123!")).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/slug.spec.ts b/src/validation/rules/__tests__/slug.spec.ts new file mode 100644 index 0000000..0ba5a77 --- /dev/null +++ b/src/validation/rules/__tests__/slug.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest"; + +import slug from "../slug"; + +describe("slug", () => { + it.concurrent("should return false when the value is not a valid slug", () => { + expect(slug()).toBe(false); + expect(slug("-")).toBe(false); + expect(slug("hello-world-!")).toBe(false); + }); + + it.concurrent("should return true when the value is a valid slug", () => { + expect(slug("helloworld123")).toBe(true); + expect(slug("hello-world-123")).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/uniqueChars.spec.ts b/src/validation/rules/__tests__/uniqueChars.spec.ts new file mode 100644 index 0000000..8a0a409 --- /dev/null +++ b/src/validation/rules/__tests__/uniqueChars.spec.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from "vitest"; + +import uniqueChars from "../uniqueChars"; + +describe("uniqueChars", () => { + it.concurrent("should return false when the value does not have enough different characters", () => { + expect(uniqueChars(undefined, [0])).toBe(false); + expect(uniqueChars("AAaa!!11", [8])).toBe(false); + }); + + it.concurrent("should return true when the value has enough different characters", () => { + expect(uniqueChars("Test123!", [8])).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/url.spec.ts b/src/validation/rules/__tests__/url.spec.ts new file mode 100644 index 0000000..53aa504 --- /dev/null +++ b/src/validation/rules/__tests__/url.spec.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest"; + +import url from "../url"; + +describe("url", () => { + it.concurrent("should return false when the value is not a valid URL", () => { + expect(url("/api/maps")).toBe(false); + expect(url("ftp://maps.google.com")).toBe(false); + }); + + it.concurrent("should return true when the value is a valid URL", () => { + expect(url()).toBe(true); + expect(url("")).toBe(true); + expect(url(" ")).toBe(true); + expect(url("http://www.google.com")).toBe(true); + expect(url("https://api.google.com/maps")).toBe(true); + }); +}); diff --git a/src/validation/rules/__tests__/username.spec.ts b/src/validation/rules/__tests__/username.spec.ts new file mode 100644 index 0000000..6165dd3 --- /dev/null +++ b/src/validation/rules/__tests__/username.spec.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest"; + +import username from "../username"; + +const allowedCharacters: string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + +describe("username", () => { + it.concurrent("should return false when the value contains characters that are not allowed", () => { + expect(username(undefined, [undefined])).toBe(false); + expect(username(undefined, [""])).toBe(false); + expect(username(undefined, [allowedCharacters])).toBe(false); + expect(username("test!", [allowedCharacters])).toBe(false); + expect(username(" test", [allowedCharacters])).toBe(false); + }); + + it.concurrent("should return true when the value only contains allowed characters", () => { + expect(username("carluiz", [undefined])).toBe(true); + expect(username("carluiz", [""])).toBe(true); + expect(username("carluiz", [allowedCharacters])).toBe(true); + expect(username("carlos.luiz@test.com", [allowedCharacters])).toBe(true); + }); +}); diff --git a/src/validation/rules/url.ts b/src/validation/rules/url.ts index 418e7d7..c954407 100644 --- a/src/validation/rules/url.ts +++ b/src/validation/rules/url.ts @@ -1,4 +1,5 @@ export default function (s?: string): boolean { + s = s?.trim(); if (!s) { return true; } From 8fcd712a964e811a995cc9e3ec81f1c6a55ed716 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Fri, 8 Dec 2023 17:34:48 -0500 Subject: [PATCH 2/2] Code Review. --- src/validation/rules/__tests__/url.spec.ts | 2 +- src/validation/rules/url.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/validation/rules/__tests__/url.spec.ts b/src/validation/rules/__tests__/url.spec.ts index 53aa504..c78ea1e 100644 --- a/src/validation/rules/__tests__/url.spec.ts +++ b/src/validation/rules/__tests__/url.spec.ts @@ -4,12 +4,12 @@ import url from "../url"; describe("url", () => { it.concurrent("should return false when the value is not a valid URL", () => { + expect(url()).toBe(false); expect(url("/api/maps")).toBe(false); expect(url("ftp://maps.google.com")).toBe(false); }); it.concurrent("should return true when the value is a valid URL", () => { - expect(url()).toBe(true); expect(url("")).toBe(true); expect(url(" ")).toBe(true); expect(url("http://www.google.com")).toBe(true); diff --git a/src/validation/rules/url.ts b/src/validation/rules/url.ts index c954407..069e00e 100644 --- a/src/validation/rules/url.ts +++ b/src/validation/rules/url.ts @@ -1,5 +1,8 @@ export default function (s?: string): boolean { - s = s?.trim(); + if (typeof s !== "string") { + return false; + } + s = s.trim(); if (!s) { return true; }