Prechádzať zdrojové kódy

chore: bumped coverage

Sv443 3 týždňov pred
rodič
commit
a603ed2ad5

+ 94 - 2
lib/DataStore.spec.ts

@@ -13,17 +13,23 @@ class TestDataStore<TData extends object = object> extends DataStore<TData> {
 }
 
 describe("DataStore", () => {
+  //#region base
   it("Basic usage", async () => {
     const store = new DataStore({
       id: "test-1",
       defaultData: { a: 1, b: 2 },
       formatVersion: 1,
-      storageMethod: "sessionStorage",
+      storageMethod: "localStorage",
+      encodeData: (d) => d,
+      decodeData: (d) => d,
     });
 
     // should equal defaultData:
     expect(store.getData().a).toBe(1);
 
+    // deepCopy should return a new object:
+    expect(store.getData(true) === store.getData(true)).toBe(false);
+
     await store.loadData();
 
     // synchronous in-memory change:
@@ -41,12 +47,13 @@ describe("DataStore", () => {
     await store.loadData();
     expect(store.getData().a).toBe(1);
 
-    expect(store.encodingEnabled()).toBe(false);
+    expect(store.encodingEnabled()).toBe(true);
 
     // restore initial state:
     await store.deleteData();
   });
 
+  //#region encoding
   it("Works with encoding", async () => {
     const store = new DataStore({
       id: "test-2",
@@ -66,8 +73,12 @@ describe("DataStore", () => {
     expect(store.getData()).toEqual({ a: 2, b: 2 });
 
     expect(store.encodingEnabled()).toBe(true);
+
+    // restore initial state:
+    await store.deleteData();
   });
 
+  //#region data & ID migrations
   it("Data and ID migrations work", async () => {
     const firstStore = new DataStore({
       id: "test-3",
@@ -126,5 +137,86 @@ describe("DataStore", () => {
 
     expect(await thirdStore.test_getValue("_uucfgver-test-3", "")).toBe("");
     expect(await thirdStore.test_getValue("_uucfgver-test-4", "")).toBe("");
+
+    // restore initial state:
+    await firstStore.deleteData();
+    await secondStore.deleteData();
+    await thirdStore.deleteData();
+  });
+
+  //#region migration error
+  it("Migration error", async () => {
+    const store1 = new DataStore({
+      id: "test-migration-error",
+      defaultData: { a: 1, b: 2 },
+      formatVersion: 1,
+      storageMethod: "localStorage",
+    });
+
+    await store1.loadData();
+
+    const store2 = new DataStore({
+      id: "test-migration-error",
+      defaultData: { a: 5, b: 5, c: 5 },
+      formatVersion: 2,
+      storageMethod: "localStorage",
+      migrations: {
+        2: (_oldData: typeof store1["defaultData"]) => {
+          throw new Error("Some error in the migration function");
+        },
+      },
+    });
+
+    // should reset to defaultData, because of the migration error:
+    await store2.loadData();
+
+    expect(store2.getData().a).toBe(5);
+    expect(store2.getData().b).toBe(5);
+    expect(store2.getData().c).toBe(5);
+  });
+
+  //#region invalid persistent data
+  it("Invalid persistent data", async () => {
+    const store1 = new TestDataStore({
+      id: "test-6",
+      defaultData: { a: 1, b: 2 },
+      formatVersion: 1,
+      storageMethod: "sessionStorage",
+    });
+
+    await store1.loadData();
+    await store1.setData({ ...store1.getData(), a: 2 });
+
+    await store1.test_setValue(`_uucfg-${store1.id}`, "invalid");
+
+    // should reset to defaultData:
+    await store1.loadData();
+
+    expect(store1.getData().a).toBe(1);
+    expect(store1.getData().b).toBe(2);
+
+    // @ts-expect-error
+    window.GM = {
+      getValue: async () => 1337,
+      setValue: async () => undefined,
+    }
+
+    const store2 = new TestDataStore({
+      id: "test-7",
+      defaultData: { a: 1, b: 2 },
+      formatVersion: 1,
+      storageMethod: "GM",
+    });
+
+    await store1.setData({ ...store1.getData(), a: 2 });
+
+    // invalid type number should reset to defaultData:
+    await store2.loadData();
+
+    expect(store2.getData().a).toBe(1);
+    expect(store2.getData().b).toBe(2);
+
+    // @ts-expect-error
+    delete window.GM;
   });
 });

+ 3 - 2
lib/DataStoreSerializer.spec.ts

@@ -39,6 +39,7 @@ describe("DataStoreSerializer", () => {
 
   it("Serialization", async () => {
     const ser = new DataStoreSerializer(getStores());
+    await ser.loadStoresData();
 
     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"}]`);
@@ -75,12 +76,12 @@ describe("DataStoreSerializer", () => {
     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}]`);
+    await ser.deserializePartial(["dss-test-1"], `[{"id":"dss-test-1","data":"{\\"a\\":421,\\"b\\":421}","checksum":"ad33b8f6a1d18c781a80390496b1b7dfaf56d73cf25a9497cb156ba83214357d","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}]`);
+    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}","checksum":"ab1d18cf13554369cea6bb517a9034e3d6548f19a40d176b16ac95c8e02d65bb","formatVersion":1,"encoded":false}]`);
     expect(store1.getData().a).toBe(1);
     expect(store2.getData().c).toBe(422);
 

+ 51 - 7
lib/Debouncer.spec.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from "vitest";
-import { Debouncer } from "./Debouncer.js";
+import { debounce, Debouncer } from "./Debouncer.js";
 import { pauseFor } from "./misc.js";
 
 describe("Debouncer", () => {
@@ -21,6 +21,7 @@ describe("Debouncer", () => {
     for(let i = 0; i < 2; i++) {
       for(let j = 0; j < 6; j++) {
         deb.call(i, j);
+        expect(deb.isTimeoutActive()).toBe(true);
         await pauseFor(50);
       }
       await pauseFor(300);
@@ -70,6 +71,30 @@ describe("Debouncer", () => {
     expect(avg + 10).toBeGreaterThanOrEqual(minDeltaT);
   });
 
+  //#region modify props & listeners
+  it("Modify props and listeners", async () => {
+    const deb = new Debouncer(200);
+
+    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");
+
+    const l = () => {};
+    deb.addListener(l);
+    deb.addListener(() => {});
+    expect(deb.getListeners()).toHaveLength(2);
+
+    deb.removeListener(l);
+    expect(deb.getListeners()).toHaveLength(1);
+
+    deb.removeAllListeners();
+    expect(deb.getListeners()).toHaveLength(0);
+  });
+
   //#region all methods
   // TODO:FIXME:
   it.skip("All methods", async () => {
@@ -89,15 +114,9 @@ describe("Debouncer", () => {
     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);
@@ -124,5 +143,30 @@ describe("Debouncer", () => {
     await callPaused();
     expect(callAmt).toEqual(evtCallAmt + 1); // evtCallAmt is always behind by 1
   });
+
+  //#region errors
+  it("Errors", () => {
+    try {
+      // @ts-expect-error
+      const deb = new Debouncer(200, "invalid");
+      deb.call();
+    }
+    catch(e) {
+      expect(e).toBeInstanceOf(TypeError);
+    }
+  });
+
+  //#region debounce function
+  it("Debounce function", async () => {
+    let callAmt = 0;
+    const callFn = debounce(() => ++callAmt, 200);
+
+    for(let i = 0; i < 4; i++) {
+      callFn();
+      await pauseFor(25);
+    }
+
+    expect(callAmt).toBe(1);
+  });
 });
   

+ 8 - 0
lib/Mixins.spec.ts

@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
 import { Mixins } from "./Mixins.js";
 
 describe("Mixins", () => {
+  //#region base
   it("Base resolution", () => {
     const mixins = new Mixins<{
       foo: (v: number, ctx: { a: number }) => number;
@@ -18,8 +19,12 @@ describe("Mixins", () => {
     // result: 0b0001 = 1
 
     expect(mixins.resolve("foo", 0b1100, { a: 0b0100 })).toBe(1);
+
+    expect(mixins.list()).toHaveLength(3);
+    expect(mixins.list().every(m => m.key === "foo")).toBe(true);
   });
 
+  //#region priority
   it("Priority resolution", () => {
     const mixins = new Mixins<{
       foo: (v: number) => number;
@@ -40,6 +45,7 @@ describe("Mixins", () => {
     expect(mixins.resolve("foo", 100)).toBe(32);
   });
 
+  //#region sync/async & cleanup
   it("Sync/async resolution & cleanup", async () => {
     const acAll = new AbortController();
 
@@ -57,12 +63,14 @@ describe("Mixins", () => {
       await new Promise((r) => setTimeout(r, 50));
       return v + 2;
     });
+    const rem4 = mixins.add("foo", async (v) => v); // 4 (prio 0, index 3)
 
     const res1 = mixins.resolve("foo", 100);
     expect(res1).toBeInstanceOf(Promise);
     expect(await res1).toBe(10002);
 
     rem3();
+    rem4();
 
     const res2 = mixins.resolve("foo", 100);
     expect(res2).not.toBeInstanceOf(Promise);

+ 24 - 0
lib/NanoEmitter.spec.ts

@@ -12,6 +12,28 @@ describe("NanoEmitter", () => {
     setTimeout(() => evts.emit("val", 5, 5), 1);
     const [v1, v2] = await evts.once("val");
     expect(v1 + v2).toBe(10);
+
+    let v3 = 0, v4 = 0;
+    const unsub = evts.on("val", (v1, v2) => {
+      v3 = v1;
+      v4 = v2;
+    });
+    evts.emit("val", 10, 10);
+    expect(v3 + v4).toBe(20);
+
+    unsub();
+    evts.emit("val", 20, 20);
+    expect(v3 + v4).toBe(20);
+
+    evts.on("val", (v1, v2) => {
+      v3 = v1;
+      v4 = v2;
+    });
+    evts.emit("val", 30, 30);
+    expect(v3 + v4).toBe(60);
+    evts.unsubscribeAll();
+    evts.emit("val", 40, 40);
+    expect(v3 + v4).toBe(60);
   });
 
   it("Object oriented", async () => {
@@ -32,5 +54,7 @@ describe("NanoEmitter", () => {
     setTimeout(() => evts.run(), 1);
     const [v1, v2] = await evts.once("val");
     expect(v1 + v2).toBe(10);
+
+    expect(evts.emit("val", 0, 0)).toBe(false);
   });
 });

+ 21 - 1
lib/array.spec.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from "vitest";
-import { randomItem, randomItemIndex, randomizeArray } from "./array.js";
+import { randomItem, randomItemIndex, randomizeArray, takeRandomItem } from "./array.js";
 
 //#region randomItem
 describe("array/randomItem", () => {
@@ -37,6 +37,26 @@ describe("array/randomItemIndex", () => {
   });
 });
 
+//#region takeRandomItem
+describe("array/takeRandomItem", () => {
+  it("Returns a random item and removes it from the array", () => {
+    const arr = [1, 2];
+
+    const itm = takeRandomItem(arr);
+    expect(arr).not.toContain(itm);
+
+    takeRandomItem(arr);
+
+    const itm2 = takeRandomItem(arr);
+    expect(itm2).toBeUndefined();
+    expect(arr).toHaveLength(0);
+  });
+
+  it("Returns undefined for an empty array", () => {
+    expect(takeRandomItem([])).toBeUndefined();
+  });
+});
+
 //#region randomizeArray
 describe("array/randomizeArray", () => {
   it("Returns a copy of the array with a random item order", () => {

+ 20 - 3
lib/dom.spec.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from "vitest";
-import { addGlobalStyle, addParent, getSiblingsFrame, getUnsafeWindow, interceptEvent, isDomLoaded, observeElementProp, preloadImages, probeElementStyle, setInnerHtmlUnsafe } from "./dom.js";
+import { addGlobalStyle, addParent, getSiblingsFrame, getUnsafeWindow, interceptEvent, isDomLoaded, isScrollable, observeElementProp, openInNewTab, preloadImages, probeElementStyle, setInnerHtmlUnsafe } from "./dom.js";
 
 //#region getUnsafeWindow
 describe("dom/getUnsafeWindow", () => {
@@ -51,8 +51,25 @@ describe.skip("dom/preloadImages", () => {
 });
 
 //#region openInNewTab
-describe.skip("dom/openInNewTab", () => {
-  // obviously cant test this
+describe("dom/openInNewTab", () => {
+  it("Via GM.openInTab", () => {
+    let link = "", bg;
+    // @ts-expect-error
+    window.GM = {
+      openInTab(href: string, background?: boolean) {
+        link = href;
+        bg = background;
+      }
+    };
+
+    openInNewTab("https://example.org", true);
+
+    expect(link).toBe("https://example.org");
+    expect(bg).toBe(true);
+
+    // @ts-expect-error
+    delete window.GM;
+  });
 });
 
 //#region interceptEvent

+ 13 - 4
lib/translation.spec.ts

@@ -1,13 +1,19 @@
 import { describe, expect, it } from "vitest";
-import { tr } from "./translation.js";
+import { tr, TrKeys } from "./translation.js";
 
 describe("Translation", () => {
   //#region base
   it("Base translation", () => {
-    tr.addTranslations("en", {
+    const trEn = {
       hello: "Hello",
       goodbye: "Goodbye",
-    });
+      nested: {
+        foo: {
+          bar: "Baz",
+        },
+      },
+    } as const;
+    tr.addTranslations("en", trEn);
     tr.addTranslations("de", {
       hello: "Hallo",
     });
@@ -28,8 +34,11 @@ describe("Translation", () => {
     expect(tr.getTranslations("en")?.hello).toBe("Hello");
     expect(tr.getTranslations("de")?.hello).toBeUndefined();
 
-    const t = tr.use("en");
+    const t = tr.use<TrKeys<typeof trEn>>("en");
     expect(t("hello")).toBe("Hello");
+
+    expect(t("nested.foo.bar")).toBe("Baz");
+    expect(tr.hasKey("en", "nested.foo.bar")).toBe(true);
   });
 
   //#region transforms