瀏覽代碼

chore: more unit tests (DataStoreSerializer, Debouncer, math, misc)

Sv443 3 周之前
父節點
當前提交
886c75bb2c
共有 5 個文件被更改,包括 472 次插入0 次删除
  1. 91 0
      lib/DataStoreSerializer.spec.ts
  2. 130 0
      lib/Debouncer.spec.ts
  3. 117 0
      lib/math.spec.ts
  4. 133 0
      lib/misc.spec.ts
  5. 1 0
      vitest.config.ts

+ 91 - 0
lib/DataStoreSerializer.spec.ts

@@ -0,0 +1,91 @@
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+import { DataStoreSerializer } from "./DataStoreSerializer.js";
+import { DataStore } from "./DataStore.js";
+import { beforeEach } from "node:test";
+import { compress, decompress } from "./crypto.js";
+
+const store1 = new DataStore({
+  id: "dss-test-1",
+  defaultData: { a: 1, b: 2 },
+  formatVersion: 1,
+  storageMethod: "sessionStorage",
+});
+
+const store2 = new DataStore({
+  id: "dss-test-2",
+  defaultData: { c: 1, d: 2 },
+  formatVersion: 1,
+  storageMethod: "sessionStorage",
+  encodeData: async (data) => await compress(data, "deflate-raw", "string"),
+  decodeData: async (data) => await decompress(data, "deflate-raw", "string"),
+});
+
+const getStores = () => [
+  store1,
+  store2,
+];
+
+describe("DataStoreSerializer", () => {
+  beforeEach(async () => {
+    const ser = new DataStoreSerializer(getStores());
+    await ser.deleteStoresData();
+    await ser.resetStoresData();
+    await ser.loadStoresData();
+  });
+
+  afterAll(async () => {
+    await new DataStoreSerializer(getStores()).deleteStoresData();
+  });
+
+  it("Serialization", async () => {
+    const ser = new DataStoreSerializer(getStores());
+
+    const full = await ser.serialize();
+    expect(full).toEqual(`[{"id":"dss-test-1","data":"{\\"a\\":1,\\"b\\":2}","formatVersion":1,"encoded":false,"checksum":"43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777"},{"id":"dss-test-2","data":"q1ZKVrIy1FFKUbIyqgUA","formatVersion":1,"encoded":true,"checksum":"b1020c3faac493009494fa622f701b831657c11ea53f8c8236f0689089c7e2d3"}]`);
+
+    const partial = await ser.serializePartial(["dss-test-1"]);
+    expect(partial).toEqual(`[{"id":"dss-test-1","data":"{\\"a\\":1,\\"b\\":2}","formatVersion":1,"encoded":false,"checksum":"43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777"}]`);
+
+    const unencoded = await ser.serializePartial(["dss-test-2"], false);
+    expect(unencoded).toEqual(`[{"id":"dss-test-2","data":"{\\"c\\":1,\\"d\\":2}","formatVersion":1,"encoded":false,"checksum":"86cada6157f4b726bf413e0371a2f461a82d2809e6eb3c095ec796fcfd8d72ee"}]`);
+
+    const notStringified = await ser.serializePartial(["dss-test-2"], false, false);
+    expect(DataStoreSerializer.isSerializedDataStoreObjArray(notStringified)).toBe(true);
+    expect(DataStoreSerializer.isSerializedDataStoreObj(notStringified?.[0])).toBe(true);
+    expect(notStringified).toEqual([
+      {
+        id: "dss-test-2",
+        data: "{\"c\":1,\"d\":2}",
+        encoded: false,
+        formatVersion: 1,
+        checksum: "86cada6157f4b726bf413e0371a2f461a82d2809e6eb3c095ec796fcfd8d72ee",
+      },
+    ]);
+  });
+
+  it("Deserialization", async () => {
+    const stores = getStores();
+    const ser = new DataStoreSerializer(stores);
+
+    await ser.deserialize(`[{"id":"dss-test-2","data":"{\\"c\\":420,\\"d\\":420}","formatVersion":1,"encoded":false}]`);
+    expect(store2.getData().c).toBe(420);
+
+    await ser.resetStoresData();
+    expect(store1.getData().a).toBe(1);
+    expect(store2.getData().c).toBe(1);
+
+    await ser.resetStoresData();
+    await ser.deserializePartial(["dss-test-1"], `[{"id":"dss-test-1","data":"{\\"a\\":421,\\"b\\":421}","formatVersion":1,"encoded":false}, {"id":"dss-test-2","data":"{\\"c\\":421,\\"d\\":421}","formatVersion":1,"encoded":false}]`);
+    expect(store1.getData().a).toBe(421);
+    expect(store2.getData().c).toBe(1);
+
+    await ser.resetStoresData();
+    await ser.deserializePartial(["dss-test-2"], `[{"id":"dss-test-1","data":"{\\"a\\":422,\\"b\\":422}","formatVersion":1,"encoded":false}, {"id":"dss-test-2","data":"{\\"c\\":422,\\"d\\":422}","formatVersion":1,"encoded":false}]`);
+    expect(store1.getData().a).toBe(1);
+    expect(store2.getData().c).toBe(422);
+
+    await ser.resetStoresData(() => false);
+    expect(store1.getData().a).toBe(1);
+    expect(store2.getData().c).toBe(422);
+  });
+});

+ 130 - 0
lib/Debouncer.spec.ts

@@ -0,0 +1,130 @@
+import { describe, expect, it } from "vitest";
+import { Debouncer } from "./Debouncer.js";
+import { pauseFor } from "./misc.js";
+
+describe("Debouncer", () => {
+  //#region deltaT
+  it("deltaT test with type \"immediate\"", async () => {
+    const deb = new Debouncer(200, "immediate");
+
+    deb.addListener(debCalled);
+
+    const deltaTs: number[] = [];
+    let lastCall: number | undefined;
+    function debCalled() {
+      const n = Date.now(),
+        deltaT = lastCall ? n - lastCall : undefined;
+      typeof deltaT === "number" && deltaT > 0 && deltaTs.push(deltaT);
+      lastCall = n;
+    }
+
+    for(let i = 0; i < 2; i++) {
+      for(let j = 0; j < 6; j++) {
+        deb.call(i, j);
+        await pauseFor(50);
+      }
+      await pauseFor(300);
+    }
+
+    const avg = deltaTs
+      .reduce((a, b) => a + b, 0) / deltaTs.length;
+
+    expect(deltaTs.every(t => t >= deb.getTimeout())).toBe(true);
+    expect(avg).toBeLessThanOrEqual(deb.getTimeout() + 50);
+  });
+
+  //#region idle
+  it("deltaT test with type \"idle\"", async () => {
+    const deb = new Debouncer(200, "idle");
+
+    deb.addListener(debCalled);
+
+    const deltaTs: number[] = [];
+    let callCount = 0;
+    let lastCall: number | undefined;
+    function debCalled() {
+      callCount++;
+      const n = Date.now(),
+        deltaT = lastCall ? n - lastCall : undefined;
+      typeof deltaT === "number" && deltaT > 0 && deltaTs.push(deltaT);
+      lastCall = n;
+    }
+
+    const jAmt = 6,
+      iTime = 400,
+      jTime = 30;
+    for(let i = 0; i < 2; i++) {
+      for(let j = 0; j < jAmt; j++) {
+        deb.call(i, j);
+        await pauseFor(jTime);
+      }
+      await pauseFor(iTime);
+    }
+
+    expect(callCount).toBeLessThanOrEqual(5); // expected 2~3 calls
+
+    /** Minimum possible deltaT between calls */
+    const minDeltaT = jAmt * jTime + iTime;
+    const avg = deltaTs
+      .reduce((a, b) => a + b, 0) / deltaTs.length;
+
+    expect(deltaTs.every(t => t >= deb.getTimeout())).toBe(true);
+    expect(avg).toBeGreaterThanOrEqual(minDeltaT);
+  });
+
+  //#region all methods
+  // TODO:FIXME:
+  it.skip("All methods", async () => {
+    const deb = new Debouncer<(v?: number) => void>(200);
+
+    let callAmt = 0, evtCallAmt = 0;
+    const debCalled = (): number => ++callAmt;
+    const debCalledEvt = (): number => ++evtCallAmt;
+
+    // hook debCalled first, then call, then hook debCalledEvt:
+    deb.addListener(debCalled);
+
+    deb.call();
+
+    deb.on("call", debCalledEvt);
+
+    expect(callAmt).toBe(1);
+    expect(evtCallAmt).toBe(0);
+
+    expect(deb.getTimeout()).toBe(200);
+    deb.setTimeout(10);
+    expect(deb.getTimeout()).toBe(10);
+
+    expect(deb.getType()).toBe("immediate");
+    deb.setType("idle");
+    expect(deb.getType()).toBe("idle");
+    deb.setType("immediate");
+
+    const callPaused = (v?: number): Promise<void> => {
+      deb.call(v);
+      return pauseFor(50);
+    };
+
+    let onceAmt = 0;
+    deb.once("call", () => ++onceAmt);
+    await callPaused();
+    await callPaused();
+    await callPaused();
+    expect(onceAmt).toBe(1);
+
+    let args = 0;
+    const setArgs = (v?: number) => args = v ?? args;
+    deb.addListener(setArgs);
+    await callPaused(1);
+    expect(args).toBe(1);
+
+    deb.removeListener(setArgs);
+    await callPaused(2);
+    expect(args).toBe(1);
+
+    deb.removeAllListeners();
+    await callPaused();
+    expect(callAmt).toEqual(evtCallAmt + 1); // evtCallAmt is always behind by 1
+  });
+});
+  

+ 117 - 0
lib/math.spec.ts

@@ -0,0 +1,117 @@
+import { describe, expect, it } from "vitest";
+import { bitSetHas, clamp, digitCount, mapRange, randRange, roundFixed } from "./math.js";
+
+//#region clamp
+describe("math/clamp", () => {
+  it("Clamps a value between min and max", () => {
+    expect(clamp(5, 0, 10)).toBe(5);
+    expect(clamp(-5, 0, 10)).toBe(0);
+    expect(clamp(15, 0, 10)).toBe(10);
+    expect(clamp(Number.MAX_SAFE_INTEGER, 0, Infinity)).toBe(Number.MAX_SAFE_INTEGER);
+  });
+
+  it("Handles edge cases", () => {
+    expect(clamp(0, 1, 0)).toThrow(TypeError);
+    // @ts-expect-error
+    expect(clamp("1", 0, 1)).toThrow(TypeError);
+  });
+});
+
+//#region mapRange
+describe("math/mapRange", () => {
+  it("Maps a value from one range to another", () => {
+    expect(mapRange(2, 0, 5, 0, 10)).toBe(4);
+    expect(mapRange(2, 5, 10, 0, 5)).toBe(-3);
+    expect(mapRange(2, 0, 5, 0, 10)).toBe(4);
+    expect(mapRange(3, 15, 100)).toBe(20);
+  });
+
+  it("Handles edge cases", () => {
+    expect(mapRange(0, 0, NaN, 0, 0)).toBe(NaN);
+    expect(mapRange(NaN, 0, 0)).toBe(NaN);
+    expect(mapRange(0, 0, 0)).toBe(NaN);
+    expect(mapRange(Infinity, 10, 1000)).toBe(Infinity);
+    expect(mapRange(-Infinity, -Infinity, Infinity)).toBe(NaN);
+  });
+});
+
+//#region randRange
+describe("math/randRange", () => {
+  it("Returns a random number between min and max", () => {
+    const nums: number[] = [];
+
+    const startTsA = Date.now();
+    for(let i = 0; i < 100_000; i++)
+      nums.push(randRange(0, 10));
+    const timeA = Date.now() - startTsA;
+
+    const startTsB = Date.now();
+    for(let i = 0; i < 100_000; i++)
+      nums.push(randRange(10, true));
+    const timeB = Date.now() - startTsB;
+
+    expect(nums.every(n => n >= 0 && n <= 10)).toBe(true);
+
+    // about a 5x speed difference
+    expect(timeA).toBeLessThanOrEqual(25);
+    expect(timeB).toBeGreaterThanOrEqual(100);
+
+    expect(randRange(0, 0)).toBe(0);
+    expect(randRange(0)).toBe(0);
+  });
+});
+
+//#region digitCount
+describe("math/digitCount", () => {
+  it("Counts the number of digits in a number", () => {
+    expect(digitCount(0)).toBe(1);
+    expect(digitCount(1)).toBe(1);
+    expect(digitCount(10)).toBe(2);
+    expect(digitCount(100_000_000.000_001)).toBe(15);
+    expect(digitCount(100_000_000.000_001, false)).toBe(9);
+  });
+
+  it("Handles edge cases", () => {
+    expect(digitCount(NaN)).toBe(NaN);
+    expect(digitCount(Infinity)).toBe(Infinity);
+  });
+});
+
+//#region roundFixed
+describe("math/roundFixed", () => {
+  it("Rounds a number to a fixed amount of decimal places", () => {
+    expect(roundFixed(1234.5678, -1)).toBe(1230);
+    expect(roundFixed(1234.5678, 0)).toBe(1235);
+    expect(roundFixed(1234.5678, 1)).toBe(1234.6);
+    expect(roundFixed(1234.5678, 3)).toBe(1234.568);
+    expect(roundFixed(1234.5678, 5)).toBe(1234.5678);
+  });
+
+  it("Handles edge cases", () => {
+    expect(roundFixed(NaN, 0)).toBe(NaN);
+    expect(roundFixed(1234.5678, NaN)).toBe(NaN);
+    expect(roundFixed(1234.5678, Infinity)).toBe(NaN);
+    expect(roundFixed(Infinity, 0)).toBe(Infinity);
+  });
+});
+
+//#region bitSetHas
+describe("math/bitSetHas", () => {
+  it("Checks if a bit is set in a number", () => {
+    expect(bitSetHas(0b1010, 0b1000)).toBe(true);
+    expect(bitSetHas(0b1010, 0b0100)).toBe(false);
+    expect(bitSetHas(0b1010, 0b0010)).toBe(true);
+    expect(bitSetHas(0b1010, 0b0001)).toBe(false);
+
+    expect(bitSetHas(BigInt(0b10), BigInt(0b10))).toBe(true);
+    expect(bitSetHas(BigInt(0b10), BigInt(0b01))).toBe(false);
+  });
+
+  it("Handles edge cases", () => {
+    expect(bitSetHas(0, 0)).toBe(true);
+    expect(bitSetHas(1, 0)).toBe(true);
+    expect(bitSetHas(0, 1)).toBe(false);
+    expect(bitSetHas(1, 1)).toBe(true);
+    expect(bitSetHas(NaN, NaN)).toBe(false);
+  });
+});

+ 133 - 0
lib/misc.spec.ts

@@ -0,0 +1,133 @@
+import { describe, expect, it } from "vitest";
+import { autoPlural, consumeGen, consumeStringGen, fetchAdvanced, getListLength, insertValues, pauseFor, purifyObj } from "./misc.js";
+
+//#region autoPlural
+describe("misc/autoPlural", () => {
+  it("Tests if autoPlural uses the correct forms", () => {
+    expect(autoPlural("apple", -1)).toBe("apples");
+    expect(autoPlural("apple", 0)).toBe("apples");
+    expect(autoPlural("apple", 1)).toBe("apple");
+    expect(autoPlural("apple", 2)).toBe("apples");
+
+    expect(autoPlural("cherry", -1)).toBe("cherries");
+    expect(autoPlural("cherry", 0)).toBe("cherries");
+    expect(autoPlural("cherry", 1)).toBe("cherry");
+    expect(autoPlural("cherry", 2)).toBe("cherries");
+
+    const cont = document.createElement("div");
+    for(let i = 0; i < 3; i++) {
+      const span = document.createElement("span");
+      cont.append(span);
+    }
+
+    expect(autoPlural("cherry", [1])).toBe("cherry");
+    expect(autoPlural("cherry", cont.querySelectorAll("span"))).toBe("cherries");
+    expect(autoPlural("cherry", { count: 3 })).toBe("cherries");
+  });
+
+  it("Handles edge cases", () => {
+    expect(autoPlural("apple", 2, "-ies")).toBe("applies");
+    expect(autoPlural("cherry", 2, "-s")).toBe("cherrys");
+  });
+});
+
+//#region insertValues
+describe("misc/insertValues", () => {
+  it("Stringifies and inserts values correctly", () => {
+    expect(insertValues("a:%1,b:%2,c:%3", "A", "B", "C")).toBe("a:A,b:B,c:C");
+    expect(insertValues("a:%1,b:%2,c:%3", "A", 2, true)).toBe("a:A,b:2,c:true");
+    expect(insertValues("a:%1,b:%2,c:%3", { toString: () => "[A]" }, {})).toBe("a:[A],b:[object Object],c:%3");
+  });
+});
+
+//#region pauseFor
+describe("misc/pauseFor", () => {
+  it("Pauses for the correct time and can be aborted", async () => {
+    const startTs = Date.now();
+    await pauseFor(100);
+
+    expect(Date.now() - startTs).toBeGreaterThanOrEqual(100);
+
+    const ac = new AbortController();
+    const startTs2 = Date.now();
+
+    setTimeout(() => ac.abort(), 20);
+    await pauseFor(100, ac.signal);
+
+    expect(Date.now() - startTs2).toBeLessThan(100);
+  });
+});
+
+//#region fetchAdvanced
+describe("misc/fetchAdvanced", () => {
+  it("Fetches a resource correctly", async () => {
+    try {
+      const res = await fetchAdvanced("https://jsonplaceholder.typicode.com/todos/1");
+      const json = await res.json();
+
+      expect(json?.id).toBe(1);
+    }
+    catch(e) {
+      expect(e).toBeUndefined();
+    }
+  });
+});
+
+//#region consumeGen
+describe("misc/consumeGen", () => {
+  it("Consumes a ValueGen properly", async () => {
+    expect(await consumeGen(() => 1)).toBe(1);
+    expect(await consumeGen(() => Promise.resolve(1))).toBe(1);
+    expect(await consumeGen(1)).toBe(1);
+
+    expect(await consumeGen(() => true)).toBe(true);
+    expect(await consumeGen(async () => false)).toBe(false);
+
+    // @ts-expect-error
+    expect(await consumeGen()).toThrow(TypeError);
+  });
+});
+
+//#region consumeStringGen
+describe("misc/consumeStringGen", () => {
+  it("Consumes a StringGen properly", async () => {
+    expect(await consumeStringGen("a")).toBe("a");
+    expect(await consumeStringGen(() => "b")).toBe("b");
+    expect(await consumeStringGen(() => Promise.resolve("c"))).toBe("c");
+  });
+});
+
+//#region getListLength
+describe("misc/getListLength", () => {
+  it("Resolves all types of ListWithLength", () => {
+    const cont = document.createElement("div");
+    for(let i = 0; i < 3; i++) {
+      const span = document.createElement("span");
+      cont.append(span);
+    }
+    expect(getListLength(cont.querySelectorAll("span"))).toBe(3);
+    expect(getListLength([1, 2, 3])).toBe(3);
+    expect(getListLength({ length: 3 })).toBe(3);
+    expect(getListLength({ size: 3 })).toBe(3);
+    expect(getListLength({ count: 3 })).toBe(3);
+
+    // @ts-expect-error
+    expect(getListLength({})).toThrow(TypeError);
+  });
+});
+
+//#region purifyObj
+describe("misc/purifyObj", () => {
+  it("Removes the prototype chain of a passed object", () => {
+    const obj = { a: 1, b: 2 };
+    const pure = purifyObj(obj);
+
+    // @ts-expect-error
+    expect(obj.__proto__).toBeDefined();
+    // @ts-expect-error
+    expect(pure.__proto__).toBeUndefined();
+
+    expect(pure.a).toBe(1);
+    expect(pure.b).toBe(2);
+  });
+});

+ 1 - 0
vitest.config.ts

@@ -4,5 +4,6 @@ export default defineConfig({
   test: {
     include: ["lib/**/*.spec.ts"],
     environment: "jsdom",
+    testTimeout: 10_000,
   },
 });