Skip to content
Open
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
1,224 changes: 1,131 additions & 93 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"format": "biome format --write",
"check": "biome check --write",
"typecheck": "tsc --noEmit",
"updateAce": "node ./utils/updateAce.js"
"updateAce": "node ./utils/updateAce.js",
"test": "vitest run",
"test:watch": "vitest"
Comment on lines +20 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unit tests are not automated

The new Vitest suite is exposed only through local npm scripts; the existing CI workflows never invoke npm test, so regressions covered by these tests can merge while all required checks pass.

},
"keywords": [
"ecosystem:cordova"
Expand Down Expand Up @@ -112,6 +114,7 @@
"terminal": "^0.1.4",
"ts-loader": "^9.5.7",
"typescript": "^5.9.3",
"vitest": "^4.1.10",
"vscode-languageserver-types": "^3.17.5",
"ws": "^8.21.0"
},
Expand Down
81 changes: 81 additions & 0 deletions tests/unit/binaryExtensions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import {
isBinaryFile,
isBinaryMime,
isBinaryPath,
isTextPath,
} from "utils/binaryExtensions";

describe("isBinaryPath", () => {
it("detects binary extensions", () => {
expect(isBinaryPath("photo.jpg")).toBe(true);
expect(isBinaryPath("downloads/app.apk")).toBe(true);
});

it("is case-insensitive", () => {
expect(isBinaryPath("IMAGE.PNG")).toBe(true);
});

it("detects compound extensions", () => {
expect(isBinaryPath("archive.tar.gz")).toBe(true);
});

it("ignores query strings and hashes", () => {
expect(isBinaryPath("https://x.com/a.zip?dl=1")).toBe(true);
});

it("returns false for text or extensionless paths", () => {
expect(isBinaryPath("script.js")).toBe(false);
expect(isBinaryPath("README")).toBe(false);
});
});

describe("isTextPath", () => {
it("detects text extensions", () => {
expect(isTextPath("notes.txt")).toBe(true);
expect(isTextPath("src/main.js")).toBe(true);
});

it("returns false for binary or extensionless paths", () => {
expect(isTextPath("app.apk")).toBe(false);
expect(isTextPath("Makefile")).toBe(false);
});
});

describe("isBinaryMime", () => {
it("detects binary mime types", () => {
expect(isBinaryMime("application/zip")).toBe(true);
expect(isBinaryMime("application/octet-stream")).toBe(true);
});

it("returns false for text mime types", () => {
expect(isBinaryMime("text/plain")).toBe(false);
expect(isBinaryMime("text/html; charset=utf-8")).toBe(false);
});

it("returns false for empty input", () => {
expect(isBinaryMime("")).toBe(false);
expect(isBinaryMime(null)).toBe(false);
});
});

describe("isBinaryFile", () => {
it("accepts a plain path string", () => {
expect(isBinaryFile("x.png")).toBe(true);
expect(isBinaryFile("x.txt")).toBe(false);
});

it("returns false for falsy input", () => {
expect(isBinaryFile(null)).toBe(false);
expect(isBinaryFile(undefined)).toBe(false);
});

it("detects binary files by extension", () => {
expect(isBinaryFile({ name: "photo.jpg" })).toBe(true);
});

it("gives text mime types precedence over the extension", () => {
expect(isBinaryFile({ mime: "text/csv", url: "file.apk" })).toBe(false);
expect(isBinaryFile({ type: "text/plain", name: "x.png" })).toBe(false);
});
});
103 changes: 103 additions & 0 deletions tests/unit/color.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, expect, it } from "vitest";
import Hex from "utils/color/hex";
import Hsl from "utils/color/hsl";
import { isValidColor } from "utils/color/regex";
import Rgb from "utils/color/rgb";

describe("Rgb", () => {
it("stringifies opaque colors as rgb()", () => {
expect(new Rgb(255, 0, 0).toString()).toBe("rgb(255, 0, 0)");
});

it("stringifies translucent colors as rgba()", () => {
expect(new Rgb(255, 0, 0, 0.5).toString()).toBe("rgba(255, 0, 0, 0.5)");
});

it("can force the alpha channel on or off", () => {
expect(new Rgb(255, 0, 0).toString(true)).toBe("rgba(255, 0, 0, 1)");
expect(new Rgb(255, 0, 0, 0.5).toString(false)).toBe("rgb(255, 0, 0)");
});
});

describe("Hex", () => {
it("stringifies colors as uppercase hex", () => {
expect(new Hex(255, 0, 0, 255).toString()).toBe("#FF0000");
expect(new Hex(0, 128, 255, 255).toString()).toBe("#0080FF");
});

it("includes the alpha channel when it is not opaque", () => {
expect(new Hex(255, 0, 0, 128).toString()).toBe("#FF000080");
});

it("can force the alpha channel on or off", () => {
expect(new Hex(255, 0, 0, 128).toString(false)).toBe("#FF0000");
expect(new Hex(255, 0, 0, 255).toString(true)).toBe("#FF0000FF");
});

it("pads single-digit channels", () => {
expect(new Hex(1, 2, 3, 255).toString()).toBe("#010203");
});

it("converts to an Rgb instance", () => {
const rgb = new Hex(10, 20, 30, 40).rgb;
expect(rgb).toBeInstanceOf(Rgb);
expect([rgb.r, rgb.g, rgb.b, rgb.a]).toEqual([10, 20, 30, 40]);
});
});

describe("Hsl", () => {
it("exposes degree/percentage getters", () => {
const hsl = new Hsl(0.5, 0.25, 1, 0.5);
expect(hsl.hue).toBe(180);
expect(hsl.saturation).toBe(25);
expect(hsl.lightness).toBe(100);
expect(hsl.toString()).toBe("hsla(180, 25%, 100%, 0.5)");
});

it("converts pure red from rgb", () => {
const hsl = Hsl.fromRgb(new Rgb(255, 0, 0));
expect(hsl.h).toBe(0);
expect(hsl.s).toBe(1);
expect(hsl.l).toBe(0.5);
expect(hsl.toString()).toBe("hsl(0, 100%, 50%)");
});

it("round-trips primary colors through rgb", () => {
for (const [r, g, b] of [
[255, 0, 0],
[0, 255, 0],
[0, 0, 255],
[255, 255, 255],
[0, 0, 0],
]) {
const rgb = Hsl.fromRgb(new Rgb(r, g, b)).rgb;
expect([rgb.r, rgb.g, rgb.b]).toEqual([r, g, b]);
}
});
});

describe("isValidColor", () => {
it("accepts hex colors", () => {
expect(isValidColor("#ff0000")).toBe(true);
expect(isValidColor("#FFF")).toBe(true);
expect(isValidColor("#FF000080")).toBe(true);
});

it("accepts functional notations", () => {
expect(isValidColor("rgb(255, 0, 0)")).toBe(true);
expect(isValidColor("rgba(255, 0, 0, 0.5)")).toBe(true);
expect(isValidColor("hsl(120, 50%, 50%)")).toBe(true);
});

it("accepts named colors", () => {
expect(isValidColor("red")).toBe(true);
expect(isValidColor("rebeccapurple")).toBe(true);
});

it("rejects invalid values", () => {
expect(isValidColor("not-a-color")).toBe(false);
expect(isValidColor("")).toBe(false);
expect(isValidColor("rgb(300, 0, 0)")).toBe(false);
expect(isValidColor("#gg0000")).toBe(false);
});
});
23 changes: 23 additions & 0 deletions tests/unit/installSource.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { INSTALL_SOURCE_PLAY, isPlayStoreInstall } from "utils/installSource";

afterEach(() => {
vi.unstubAllGlobals();
});

describe("isPlayStoreInstall", () => {
it("returns true when the app was installed from the Play Store", () => {
vi.stubGlobal("window", { appInstallSource: INSTALL_SOURCE_PLAY });
expect(isPlayStoreInstall()).toBe(true);
});

it("returns false for any other install source", () => {
vi.stubGlobal("window", { appInstallSource: "com.android.packageinstaller" });
expect(isPlayStoreInstall()).toBe(false);
});

it("returns false when the install source is unknown", () => {
vi.stubGlobal("window", {});
expect(isPlayStoreInstall()).toBe(false);
});
});
122 changes: 122 additions & 0 deletions tests/unit/path.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it } from "vitest";
import Path from "utils/Path";

describe("Path.dirname", () => {
it("returns the parent directory", () => {
expect(Path.dirname("/foo/bar/baz.txt")).toBe("/foo/bar");
expect(Path.dirname("/foo/bar")).toBe("/foo");
});

it("ignores trailing separators", () => {
expect(Path.dirname("foo/bar/")).toBe("./foo");
});

it("returns root for top-level absolute paths", () => {
expect(Path.dirname("/foo")).toBe("/");
});

it("returns '.' for bare filenames", () => {
expect(Path.dirname("file.txt")).toBe(".");
});
});

describe("Path.basename", () => {
it("returns the last portion of a path", () => {
expect(Path.basename("/foo/bar.txt")).toBe("bar.txt");
expect(Path.basename("foo/bar/baz")).toBe("baz");
});

it("strips the given extension", () => {
expect(Path.basename("/foo/bar.txt", ".txt")).toBe("bar");
});

it("ignores trailing separators", () => {
expect(Path.basename("/foo/bar/")).toBe("bar");
});

it("handles edge cases", () => {
expect(Path.basename("/")).toBe("/");
expect(Path.basename("")).toBe("");
});
});

describe("Path.extname", () => {
it("returns the extension including the dot", () => {
expect(Path.extname("file.txt")).toBe(".txt");
expect(Path.extname("archive.tar.gz")).toBe(".gz");
});

it("returns empty string when there is no extension", () => {
expect(Path.extname("foo/bar")).toBe("");
});

it("returns empty string for dotfiles", () => {
expect(Path.extname(".gitignore")).toBe("");
});
});

describe("Path.join / normalize", () => {
it("joins segments and normalizes", () => {
expect(Path.join("foo", "bar", "baz")).toBe("foo/bar/baz");
expect(Path.join("/foo/", "/bar/")).toBe("/foo/bar/");
});

it("resolves '..' segments", () => {
expect(Path.join("foo", "bar", "../baz")).toBe("foo/baz");
});

it("collapses duplicate separators and '.' segments", () => {
expect(Path.normalize("/foo//bar/./baz/../qux")).toBe("/foo/bar/qux");
});
});

describe("Path.resolve", () => {
it("resolves relative segments against a base", () => {
expect(Path.resolve("path/to/some/dir/", "../../dir")).toBe(
"/path/to/dir",
);
});

it("resets on absolute segments", () => {
expect(Path.resolve("/foo", "/bar")).toBe("/bar");
});

it("throws without arguments", () => {
expect(() => Path.resolve()).toThrow();
});
});

describe("Path.parse / format", () => {
it("parses a path into its parts", () => {
expect(Path.parse("/foo/bar.txt")).toEqual({
root: "/",
dir: "/foo",
base: "bar.txt",
ext: ".txt",
name: "bar",
});
});

it("formats a path object back to a string", () => {
expect(Path.format({ dir: "/foo", name: "bar", ext: ".txt" })).toBe(
"/foo/bar.txt",
);
});

it("ignores a malformed extension when formatting", () => {
expect(Path.format({ root: "/", name: "bar", ext: "txt" })).toBe("/bar");
});
});

describe("Path.isAbsolute / convertToRelative", () => {
it("detects absolute paths", () => {
expect(Path.isAbsolute("/foo")).toBe(true);
expect(Path.isAbsolute("foo/bar")).toBe(false);
});

it("computes a path relative to a base", () => {
expect(Path.convertToRelative("/foo/bar", "/foo/baz/qux")).toBe(
"baz/qux",
);
});
});
Loading