Przeglądaj źródła

chore: unit tests for array, colors, crypto, DataStore

Sv443 3 tygodni temu
rodzic
commit
9792e4abca
8 zmienionych plików z 350 dodań i 4 usunięć
  1. 2 0
      docs.md
  2. 130 0
      lib/DataStore.spec.ts
  3. 60 0
      lib/array.spec.ts
  4. 69 0
      lib/colors.spec.ts
  5. 65 0
      lib/crypto.spec.ts
  6. 3 0
      lib/crypto.ts
  7. 1 2
      package.json
  8. 20 2
      pnpm-lock.yaml

+ 2 - 0
docs.md

@@ -2775,6 +2775,8 @@ Note that this makes the function call take longer, but the generated IDs will h
 If `randomCase` is set to true (which it is by default), the generated ID will contain both upper and lower case letters.  
 This randomization is also affected by the `enhancedEntropy` setting, unless there are no alphabetic characters in the output in which case it will be skipped.  
   
+Throws a RangeError if the length is less than 1 or the radix is less than 2 or greater than 36.  
+  
 ⚠️ This is not suitable for generating anything related to cryptography! Use [SubtleCrypto's `generateKey()`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey) for that instead.  
   
 <details><summary><b>Example - click to view</b></summary>

+ 130 - 0
lib/DataStore.spec.ts

@@ -0,0 +1,130 @@
+import { describe, expect, it } from "vitest";
+import { DataStore } from "./DataStore.js";
+import { compress, decompress } from "./crypto.js";
+
+class TestDataStore<TData extends object = object> extends DataStore<TData> {
+  public async test_getValue<TValue extends GM.Value = string>(name: string, defaultValue: TValue): Promise<string | TValue> {
+    return await this.getValue(name, defaultValue);
+  }
+
+  public async test_setValue(name: string, value: GM.Value): Promise<void> {
+    return await this.setValue(name, value);
+  }
+}
+
+describe("DataStore", () => {
+  it("Basic usage", async () => {
+    const store = new DataStore({
+      id: "test-1",
+      defaultData: { a: 1, b: 2 },
+      formatVersion: 1,
+      storageMethod: "sessionStorage",
+    });
+
+    // should equal defaultData:
+    expect(store.getData().a).toBe(1);
+
+    await store.loadData();
+
+    // synchronous in-memory change:
+    const prom = store.setData({ ...store.getData(), a: 2 });
+
+    expect(store.getData().a).toBe(2);
+
+    await prom;
+
+    // only clears persistent data, not the stuff in memory:
+    await store.deleteData();
+    expect(store.getData().a).toBe(2);
+
+    // refreshes memory data:
+    await store.loadData();
+    expect(store.getData().a).toBe(1);
+
+    expect(store.encodingEnabled()).toBe(false);
+
+    // restore initial state:
+    await store.deleteData();
+  });
+
+  it("Works with encoding", async () => {
+    const store = new DataStore({
+      id: "test-2",
+      defaultData: { a: 1, b: 2 },
+      formatVersion: 1,
+      storageMethod: "sessionStorage",
+      encodeData: async (data) => await compress(data, "deflate-raw", "string"),
+      decodeData: async (data) => await decompress(data, "deflate-raw", "string"),
+    });
+
+    await store.loadData();
+
+    await store.setData({ ...store.getData(), a: 2 });
+
+    await store.loadData();
+
+    expect(store.getData()).toEqual({ a: 2, b: 2 });
+
+    expect(store.encodingEnabled()).toBe(true);
+  });
+
+  it("Data and ID migrations work", async () => {
+    const firstStore = new DataStore({
+      id: "test-3",
+      defaultData: { a: 1, b: 2 },
+      formatVersion: 1,
+      storageMethod: "sessionStorage",
+    });
+
+    await firstStore.loadData();
+
+    await firstStore.setData({ ...firstStore.getData(), a: 2 });
+
+    // new store with increased format version & new ID:
+    const secondStore = new DataStore({
+      id: "test-4",
+      migrateIds: [firstStore.id],
+      defaultData: { a: -1337, b: -1337, c: 69 },
+      formatVersion: 2,
+      storageMethod: "sessionStorage",
+      migrations: {
+        2: (oldData: Record<string, unknown>) => ({ ...oldData, c: 1 }),
+      },
+    });
+
+    const data1 = await secondStore.loadData();
+
+    expect(data1.a).toBe(2);
+    expect(data1.b).toBe(2);
+    expect(data1.c).toBe(1);
+
+    await secondStore.saveDefaultData();
+    const data2 = secondStore.getData();
+
+    expect(data2.a).toBe(-1337);
+    expect(data2.b).toBe(-1337);
+    expect(data2.c).toBe(69);
+
+    // migrate with migrateId method:
+    const thirdStore = new TestDataStore({
+      id: "test-5",
+      defaultData: secondStore.defaultData,
+      formatVersion: 3,
+      storageMethod: "sessionStorage",
+    });
+
+    await thirdStore.migrateId(secondStore.id);
+    const thirdData = await thirdStore.loadData();
+
+    expect(thirdData.a).toBe(-1337);
+    expect(thirdData.b).toBe(-1337);
+    expect(thirdData.c).toBe(69);
+
+    expect(await thirdStore.test_getValue("_uucfgver-test-5", "")).toBe("2");
+    await thirdStore.setData(thirdStore.getData());
+    expect(await thirdStore.test_getValue("_uucfgver-test-5", "")).toBe("3");
+
+    expect(await thirdStore.test_getValue("_uucfgver-test-3", "")).toBe("");
+    expect(await thirdStore.test_getValue("_uucfgver-test-4", "")).toBe("");
+  });
+});

+ 60 - 0
lib/array.spec.ts

@@ -0,0 +1,60 @@
+import { describe, expect, it } from "vitest";
+import { randomItem, randomItemIndex, randomizeArray } from "./array.js";
+
+//#region randomItem
+describe("array/randomItem", () => {
+  it("Returns a random item", () => {
+    const arr = [1, 2, 3, 4];
+    const items = [] as number[];
+
+    for(let i = 0; i < 500; i++)
+      items.push(randomItem(arr)!);
+
+    const missing = arr.filter(item => !items.some(i => i === item));
+    expect(missing).toHaveLength(0);
+  });
+
+  it("Returns undefined for an empty array", () => {
+    expect(randomItem([])).toBeUndefined();
+  });
+});
+
+//#region randomItemIndex
+describe("array/randomItemIndex", () => {
+  it("Returns a random item with the correct index", () => {
+    const arr = [1, 2, 3, 4];
+    const items = [] as [number, number][];
+
+    for(let i = 0; i < 500; i++)
+      items.push(randomItemIndex(arr) as [number, number]);
+
+    const missing = arr.filter((item, index) => !items.some(([it, idx]) => it === item && idx === index));
+    expect(missing).toHaveLength(0);
+  });
+
+  it("Returns undefined for an empty array", () => {
+    expect(randomItemIndex([])).toEqual([undefined, undefined]);
+  });
+});
+
+//#region randomizeArray
+describe("array/randomizeArray", () => {
+  it("Returns a copy of the array with a random item order", () => {
+    const arr = Array.from({ length: 100 }, (_, i) => i);
+    const randomized = randomizeArray(arr);
+
+    expect(randomized === arr).toBe(false);
+    expect(randomized).toHaveLength(arr.length);
+
+    const sameItems = arr.filter((item, i) => randomized[i] === item);
+    expect(sameItems.length).toBeLessThan(arr.length);
+  });
+
+  it("Returns an empty array as-is", () => {
+    const arr = [] as number[];
+    const randomized = randomizeArray(arr);
+
+    expect(randomized === arr).toBe(false);
+    expect(randomized).toHaveLength(0);
+  });
+});

+ 69 - 0
lib/colors.spec.ts

@@ -0,0 +1,69 @@
+import { describe, expect, it } from "vitest";
+import { darkenColor, hexToRgb, lightenColor, rgbToHex } from "./colors.js";
+
+//#region hexToRgb
+describe("colors/hexToRgb", () => {
+  it("Converts a hex color string to an RGB tuple", () => {
+    const hex = "#FF0000";
+    const [r, g, b, a] = hexToRgb(hex);
+
+    expect(r).toBe(255);
+    expect(g).toBe(0);
+    expect(b).toBe(0);
+    expect(a).toBeUndefined();
+  });
+
+  it("Converts a hex color string with an alpha channel to an RGBA tuple", () => {
+    const hex = "#FF0000FF";
+    const [r, g, b, a] = hexToRgb(hex);
+
+    expect(r).toBe(255);
+    expect(g).toBe(0);
+    expect(b).toBe(0);
+    expect(a).toBe(1);
+  });
+
+  it("Works as expected with invalid input", () => {
+    expect(hexToRgb("")).toEqual([0, 0, 0, undefined]);
+  });
+});
+
+//#region rgbToHex
+describe("colors/rgbToHex", () => {
+  it("Converts an RGB tuple to a hex color string", () => {
+    expect(rgbToHex(255, 0, 0, undefined, true, true)).toBe("#FF0000");
+    expect(rgbToHex(255, 0, 0, undefined, true, false)).toBe("#ff0000");
+    expect(rgbToHex(255, 0, 0, undefined, false, false)).toBe("ff0000");
+    expect(rgbToHex(255, 0, 127, 0.5, false, false)).toBe("ff007f80");
+    expect(rgbToHex(0, 0, 0, 1)).toBe("#000000ff");
+  });
+
+  it("Handles special values as expected", () => {
+    expect(rgbToHex(NaN, Infinity, -1, 255)).toBe("#nanff00ff");
+    expect(rgbToHex(256, -1, 256, -1, false, true)).toBe("FF00FF00");
+  });
+
+  it("Works as expected with invalid input", () => {
+    expect(rgbToHex(0, 0, 0, 0)).toBe("#000000");
+    //@ts-ignore
+    expect(rgbToHex(NaN, "ello", 0, -1)).toBe("#nannan0000");
+  });
+});
+
+//#region lightenColor
+describe("colors/lightenColor", () => {
+  it("Lightens a color by a given percentage", () => {
+    expect(lightenColor("#ab35de", 50)).toBe("#ff50ff");
+    expect(lightenColor("ab35de", Infinity, true)).toBe("FFFFFF");
+    expect(lightenColor("rgba(255, 50, 127, 0.5)", 50)).toBe("rgba(255, 75, 190.5, 0.5)");
+    expect(lightenColor("rgb(255, 50, 127)", 50)).toBe("rgb(255, 75, 190.5)");
+  });
+});
+
+//#region darkenColor
+describe("colors/darkenColor", () => {
+  it("Darkens a color by a given percentage", () => {
+    // since both functions are the exact same but with a different sign, only one test is needed:
+    expect(darkenColor("#1affe3", 50)).toBe(lightenColor("#1affe3", -50));
+  });
+});

+ 65 - 0
lib/crypto.spec.ts

@@ -0,0 +1,65 @@
+import { describe, expect, it } from "vitest";
+import { compress, computeHash, decompress, randomId } from "./crypto.js";
+
+//#region compress
+describe("crypto/compress", () => {
+  it("Compresses strings and buffers as expected", async () => {
+    const input = "Hello, world!".repeat(100);
+
+    expect(await compress(input, "gzip", "string")).toBe("H4sIAAAAAAAACvNIzcnJ11Eozy/KSVH0GOWMckY5o5yRzQEAatVNcBQFAAA=");
+    expect(await compress(input, "deflate", "string")).toBe("eJzzSM3JyddRKM8vyklR9BjljHJGOaOckc0BAOWGxZQ=");
+    expect(await compress(input, "deflate-raw", "string")).toBe("80jNycnXUSjPL8pJUfQY5YxyRjmjnJHNAQA=");
+    expect(await compress(input, "gzip", "arrayBuffer")).toBeInstanceOf(ArrayBuffer);
+  });
+});
+
+//#region decompress
+describe("crypto/decompress", () => {
+  it("Decompresses strings and buffers as expected", async () => {
+    const inputGz = "H4sIAAAAAAAACvNIzcnJ11Eozy/KSVH0GOWMckY5o5yRzQEAatVNcBQFAAA=";
+    const inputDf = "eJzzSM3JyddRKM8vyklR9BjljHJGOaOckc0BAOWGxZQ=";
+    const inputDfRaw = "80jNycnXUSjPL8pJUfQY5YxyRjmjnJHNAQA=";
+
+    const expectedDecomp = "Hello, world!".repeat(100);
+
+    expect(await decompress(inputGz, "gzip", "string")).toBe(expectedDecomp);
+    expect(await decompress(inputDf, "deflate", "string")).toBe(expectedDecomp);
+    expect(await decompress(inputDfRaw, "deflate-raw", "string")).toBe(expectedDecomp);
+  });
+});
+
+//#region computeHash
+describe("crypto/computeHash", () => {
+  it("Computes hashes as expected", async () => {
+    const input1 = "Hello, world!";
+    const input2 = input1.repeat(10);
+
+    expect(await computeHash(input1, "SHA-1")).toBe("943a702d06f34599aee1f8da8ef9f7296031d699");
+    expect(await computeHash(input1, "SHA-256")).toBe("315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3");
+    expect(await computeHash(input1, "SHA-512")).toBe("c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421");
+    expect(await computeHash(input2, "SHA-256")).toBe(await computeHash(input2, "SHA-256"));
+  });
+});
+
+//#region randomId
+describe("crypto/randomId", () => {
+  it("Generates random IDs as expected", () => {
+    const id1 = randomId(32, 36, false, true);
+
+    expect(id1).toHaveLength(32);
+    expect(id1).toMatch(/^[0-9a-zA-Z]+$/);
+
+    const id2 = randomId(32, 36, true, true);
+
+    expect(id2).toHaveLength(32);
+    expect(id2).toMatch(/^[0-9a-zA-Z]+$/);
+
+    expect(randomId(32, 2, false, false)).toMatch(/^[01]+$/);
+  });
+
+  it("Handles all edge cases", () => {
+    expect(() => randomId(16, 1)).toThrow(RangeError);
+    expect(() => randomId(16, 37)).toThrow(RangeError);
+    expect(() => randomId(-1)).toThrow(RangeError);
+  });
+});

+ 3 - 0
lib/crypto.ts

@@ -81,6 +81,9 @@ export async function computeHash(input: string | ArrayBuffer, algorithm = "SHA-
  * @param randomCase If set to false, the generated ID will be lowercase only - also makes use of the `enhancedEntropy` parameter unless the output doesn't contain letters
  */
 export function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true): string {
+  if(length < 1)
+    throw new RangeError("The length argument must be at least 1");
+
   if(radix < 2 || radix > 36)
     throw new RangeError("The radix argument must be between 2 and 36");
 

+ 1 - 2
package.json

@@ -37,8 +37,7 @@
     "test-gm-serve": "node --import tsx ./test/TestPage/server.mts",
     "test-gm-dev": "cd test/TestScript && pnpm dev",
     "test-gm": "concurrently \"pnpm test-gm-serve\" \"pnpm test-gm-dev\"",
-    "test": "vitest",
-    "test-dev": "vitest --watch"
+    "test": "vitest --passWithNoTests"
   },
   "repository": {
     "type": "git",

+ 20 - 2
pnpm-lock.yaml

@@ -74,7 +74,7 @@ importers:
         version: 5.7.3
       vitest:
         specifier: ^3.0.9
-        version: 3.0.9(@types/[email protected])([email protected])([email protected])([email protected])
+        version: 3.0.9(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
 
 packages:
 
@@ -1482,6 +1482,10 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
 
+  [email protected]:
+    resolution: {integrity: sha512-/Pb0ctk3HTZ5xEL3BZ0hK1AqDSAUuRQitOmROPHhfUYEWpmTImwfD8vFDGADmMAX0JYgbcgxWoLFKtsWhcpuVA==}
+    engines: {node: '>=18.0.0'}
+
   [email protected]:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
@@ -2326,6 +2330,10 @@ packages:
     resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
     engines: {node: '>=18'}
 
+  [email protected]:
+    resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+    engines: {node: '>=12'}
+
   [email protected]:
     resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
     engines: {node: '>=18'}
@@ -3797,6 +3805,12 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      webidl-conversions: 7.0.0
+      whatwg-mimetype: 3.0.0
+    optional: true
+
   [email protected]: {}
 
   [email protected]: {}
@@ -4525,7 +4539,7 @@ snapshots:
       tsx: 4.19.2
       yaml: 2.6.1
 
-  [email protected](@types/[email protected])([email protected])([email protected])([email protected]):
+  [email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]):
     dependencies:
       '@vitest/expect': 3.0.9
       '@vitest/mocker': 3.0.9([email protected](@types/[email protected])([email protected])([email protected]))
@@ -4549,6 +4563,7 @@ snapshots:
       why-is-node-running: 2.3.0
     optionalDependencies:
       '@types/node': 22.10.5
+      happy-dom: 17.4.4
       jsdom: 26.0.0
     transitivePeerDependencies:
       - jiti
@@ -4576,6 +4591,9 @@ snapshots:
     dependencies:
       iconv-lite: 0.6.3
 
+  [email protected]:
+    optional: true
+
   [email protected]: {}
 
   [email protected]: