Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added build workflow and unit tests. #77

Merged
merged 2 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 0 additions & 11 deletions src/components/__tests__/HelloWorld.spec.ts

This file was deleted.

71 changes: 71 additions & 0 deletions src/helpers/__tests__/arrayUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
45 changes: 45 additions & 0 deletions src/helpers/__tests__/objectUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
138 changes: 138 additions & 0 deletions src/helpers/__tests__/stringUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -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!");
});
});
23 changes: 21 additions & 2 deletions src/helpers/arrayUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
export function orderBy<T>(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<T>(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<T>(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<T>(items: T[], key?: keyof T) {
return orderBy(items, key, true);
}
16 changes: 16 additions & 0 deletions src/helpers/deviceUtils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 13 additions & 7 deletions src/helpers/stringUtils.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Expand Down Expand Up @@ -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<string, string>([
["à", "a"],
["â", "a"],
Expand Down
Loading