Skip to content

Commit

Permalink
Added build workflow and unit tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Dec 8, 2023
1 parent bc38e16 commit 3da17a3
Show file tree
Hide file tree
Showing 23 changed files with 597 additions and 21 deletions.
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

0 comments on commit 3da17a3

Please sign in to comment.