diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/apq.test.ts | 70 | ||||
| -rw-r--r-- | tests/format.test.ts | 3 | ||||
| -rw-r--r-- | tests/history.test.ts | 18 | ||||
| -rw-r--r-- | tests/keyboard.test.ts | 24 | ||||
| -rw-r--r-- | tests/operations.test.ts | 8 | ||||
| -rw-r--r-- | tests/session-io.test.ts | 51 | ||||
| -rw-r--r-- | tests/storage.test.ts | 80 | ||||
| -rw-r--r-- | tests/timing.test.ts | 21 |
8 files changed, 154 insertions, 121 deletions
diff --git a/tests/apq.test.ts b/tests/apq.test.ts index 66607cd..2097377 100644 --- a/tests/apq.test.ts +++ b/tests/apq.test.ts @@ -10,43 +10,9 @@ import { expect, test } from "vitest"; import { createApqFetcher } from "../source/library/fetcher/apq.ts"; -/*** HELPERS ------------------------------------------ ***/ - type Call = { body: Record<string, unknown>; url: string }; -function createStub(queue: Array<Record<string, unknown>>): { - calls: Call[]; - stub: typeof fetch; -} { - const calls: Call[] = []; - - const stub: typeof fetch = (input, init) => { - const body = JSON.parse((init?.body as string) ?? "null") as Record<string, unknown>; - calls.push({ body, url: String(input) }); - - const next = queue.shift(); - - if (next === undefined) - throw new Error("stub fetch exhausted"); - - return Promise.resolve(new Response(JSON.stringify(next), { status: 200 })); - }; - - return { calls, stub }; -} - -async function expectedHash(query: string): Promise<string> { - const buf = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(query)); - return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join(""); -} - -function readPersistedHash(body: Record<string, unknown>): string { - const extensions = body.extensions as Record<string, unknown>; - const persistedQuery = extensions.persistedQuery as Record<string, unknown>; - return persistedQuery.sha256Hash as string; -} - -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("apq cache miss retries with the full query", async () => { const { calls, stub } = createStub([ @@ -149,3 +115,37 @@ test("apq accepts PERSISTED_QUERY_NOT_FOUND extension code", async () => { expect(calls.length).toEqual(2); expect(calls[1].body.query).toEqual("{ hello }"); }); + +/*** HELPER ------------------------------------------- ***/ + +function createStub(queue: Array<Record<string, unknown>>): { + calls: Call[]; + stub: typeof fetch; +} { + const calls: Call[] = []; + + const stub: typeof fetch = (input, init) => { + const body = JSON.parse((init?.body as string) ?? "null") as Record<string, unknown>; + calls.push({ body, url: String(input) }); + + const next = queue.shift(); + + if (next === undefined) + throw new Error("stub fetch exhausted"); + + return Promise.resolve(new Response(JSON.stringify(next), { status: 200 })); + }; + + return { calls, stub }; +} + +async function expectedHash(query: string): Promise<string> { + const buf = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(query)); + return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join(""); +} + +function readPersistedHash(body: Record<string, unknown>): string { + const extensions = body.extensions as Record<string, unknown>; + const persistedQuery = extensions.persistedQuery as Record<string, unknown>; + return persistedQuery.sha256Hash as string; +} diff --git a/tests/format.test.ts b/tests/format.test.ts index ae5d40c..e0b9f1b 100644 --- a/tests/format.test.ts +++ b/tests/format.test.ts @@ -10,11 +10,12 @@ import { expect, test } from "vitest"; import { format } from "../source/library/graphql/format.ts"; -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("format pretty-prints a valid query", () => { const input = "query Foo{viewer{id name}}"; const expected = "query Foo {\n viewer {\n id\n name\n }\n}"; + expect(format(input)).toEqual(expected); }); diff --git a/tests/history.test.ts b/tests/history.test.ts index a08c014..581c0ef 100644 --- a/tests/history.test.ts +++ b/tests/history.test.ts @@ -16,11 +16,7 @@ type Entry = { timestamp: number; }; -function entry(id: string, timestamp: number, favorite = false): Entry { - return { favorite, id, timestamp }; -} - -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("evict keeps everything when under cap", () => { const entries = [entry("a", 3), entry("b", 2), entry("c", 1)]; @@ -35,7 +31,9 @@ test("evict drops the oldest non-favorites above cap", () => { entry("d", 2), entry("e", 1) ]; + const kept = evict(entries, 3); + expect(kept.map((e) => e.id)).toEqual(["a", "b", "c"]); }); @@ -47,6 +45,7 @@ test("evict never drops favorites", () => { entry("c", 8), entry("d", 7) ]; + const kept = evict(entries, 3); expect(kept.some((e) => e.id === "fav-old")).toEqual(true); @@ -60,6 +59,7 @@ test("evict can exceed cap when favorites alone do so", () => { entry("fav-3", 3, true), entry("regular", 2) ]; + const kept = evict(entries, 2); expect(kept.length).toEqual(3); @@ -73,6 +73,14 @@ test("evict sorts by timestamp descending", () => { entry("b", 2), entry("d", 0) ]; + const kept = evict(entries, 3); + expect(kept.map((e) => e.id)).toEqual(["a", "b", "c"]); }); + +/*** HELPER ------------------------------------------- ***/ + +function entry(id: string, timestamp: number, favorite = false): Entry { + return { favorite, id, timestamp }; +} diff --git a/tests/keyboard.test.ts b/tests/keyboard.test.ts index 3a7f3cc..6550e98 100644 --- a/tests/keyboard.test.ts +++ b/tests/keyboard.test.ts @@ -18,17 +18,7 @@ type EventInit = { shiftKey?: boolean; }; -function makeEvent(init: EventInit): KeyboardEvent { - return { - altKey: init.altKey ?? false, - ctrlKey: init.ctrlKey ?? false, - key: init.key, - metaKey: init.metaKey ?? false, - shiftKey: init.shiftKey ?? false - } as KeyboardEvent; -} - -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("matchShortcut returns null for plain Enter", () => { expect(matchShortcut(makeEvent({ key: "Enter" }))).toEqual(null); @@ -109,3 +99,15 @@ test("matchShortcut ignores Cmd+Alt+Enter", () => { matchShortcut(makeEvent({ altKey: true, key: "Enter", metaKey: true })) ).toEqual(null); }); + +/*** HELPER ------------------------------------------- ***/ + +function makeEvent(init: EventInit): KeyboardEvent { + return { + altKey: init.altKey ?? false, + ctrlKey: init.ctrlKey ?? false, + key: init.key, + metaKey: init.metaKey ?? false, + shiftKey: init.shiftKey ?? false + } as KeyboardEvent; +} diff --git a/tests/operations.test.ts b/tests/operations.test.ts index 14fe768..6acca31 100644 --- a/tests/operations.test.ts +++ b/tests/operations.test.ts @@ -8,9 +8,12 @@ import { expect, test } from "vitest"; /*** UTILITY ------------------------------------------ ***/ -import { deriveTitle, parseOperations } from "../source/library/graphql/operations.ts"; +import { + deriveTitle, + parseOperations +} from "../source/library/graphql/operations.ts"; -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("parseOperations returns empty for blank query", () => { expect(parseOperations("")).toEqual([]); @@ -37,6 +40,7 @@ test("parseOperations captures multiple operations", () => { mutation Bar { b } subscription Baz { c } `); + expect(ops).toEqual([ { name: "Foo", type: "query" }, { name: "Bar", type: "mutation" }, diff --git a/tests/session-io.test.ts b/tests/session-io.test.ts index 6de919b..88c1ee6 100644 --- a/tests/session-io.test.ts +++ b/tests/session-io.test.ts @@ -16,29 +16,7 @@ import { type TabInput = Parameters<typeof tabToExport>[0]; -function validExport(): SessionExport { - return { - exportedAt: "2026-04-24T00:00:00.000Z", - tabs: [ - { - headers: "{}", - operationName: "MyOp", - query: "query MyOp { hello }", - title: "MyOp", - variables: "{}" - } - ], - version: 1 - }; -} - -function isError(result: unknown): result is { error: string } { - return typeof result === "object" && - result !== null && - "error" in result; -} - -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("validateSessionExport round-trips a valid payload unchanged", () => { const data = validExport(); @@ -58,6 +36,7 @@ test("validateSessionExport rejects non-object input", () => { test("validateSessionExport rejects wrong version", () => { const zero = validateSessionExport({ ...validExport(), version: 0 }); const two = validateSessionExport({ ...validExport(), version: 2 }); + const missing = validateSessionExport({ exportedAt: "2026-04-24T00:00:00.000Z", tabs: [] @@ -142,6 +121,7 @@ test("validateSessionExport accepts null operationName", () => { test("validateSessionExport rejects string field > 1 MB", () => { const big = "x".repeat(1024 * 1024 + 1); + const result = validateSessionExport({ exportedAt: "2026-04-24T00:00:00.000Z", tabs: [ @@ -167,6 +147,7 @@ test("validateSessionExport rejects > 50 tabs", () => { title: "t", variables: "{}" })); + const result = validateSessionExport({ exportedAt: "2026-04-24T00:00:00.000Z", tabs, @@ -199,3 +180,27 @@ test("tabToExport strips id, result, operations, titleDirty", () => { variables: "{}" }); }); + +/*** HELPER ------------------------------------------- ***/ + +function isError(result: unknown): result is { error: string } { + return typeof result === "object" && + result !== null && + "error" in result; +} + +function validExport(): SessionExport { + return { + exportedAt: "2026-04-24T00:00:00.000Z", + tabs: [ + { + headers: "{}", + operationName: "MyOp", + query: "query MyOp { hello }", + title: "MyOp", + variables: "{}" + } + ], + version: 1 + }; +} diff --git a/tests/storage.test.ts b/tests/storage.test.ts index 434a67d..3fbbdd9 100644 --- a/tests/storage.test.ts +++ b/tests/storage.test.ts @@ -13,47 +13,11 @@ import { createMemoryStorage } from "../source/library/state/storage.ts"; -/*** HELPERS ------------------------------------------ ***/ - -function installLocalStorage(): void { - if (typeof globalThis.localStorage !== "undefined") - return; - - const store = new Map<string, string>(); - - const shim: Storage = { - clear(): void { - store.clear(); - }, - getItem(key: string): string | null { - return store.has(key) ? store.get(key) ?? null : null; - }, - key(index: number): string | null { - return Array.from(store.keys())[index] ?? null; - }, - get length(): number { - return store.size; - }, - removeItem(key: string): void { - store.delete(key); - }, - setItem(key: string, value: string): void { - store.set(key, String(value)); - } - }; - - Object.defineProperty(globalThis, "localStorage", { - configurable: true, - value: shim, - writable: true - }); -} - beforeAll(() => { installLocalStorage(); }); -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("memory storage round-trips objects", () => { const storage = createMemoryStorage(); @@ -68,15 +32,19 @@ test("memory storage returns null for missing keys", () => { test("memory storage remove clears a key", () => { const storage = createMemoryStorage(); + storage.set("k", 42); storage.remove("k"); + expect(storage.get("k")).toEqual(null); }); test("memory storage instances are isolated", () => { const a = createMemoryStorage(); const b = createMemoryStorage(); + a.set("shared", 1); + expect(b.get("shared")).toEqual(null); }); @@ -105,8 +73,8 @@ test("local storage remove respects the namespace", () => { alpha.set("k", 1); beta.set("k", 2); - alpha.remove("k"); + expect(alpha.get("k")).toEqual(null); expect(beta.get<number>("k")).toEqual(2); @@ -122,3 +90,39 @@ test("local storage returns null on malformed JSON", () => { globalThis.localStorage.clear(); }); + +/*** HELPER ------------------------------------------- ***/ + +function installLocalStorage(): void { + if (typeof globalThis.localStorage !== "undefined") + return; + + const store = new Map<string, string>(); + + const shim: Storage = { + clear(): void { + store.clear(); + }, + getItem(key: string): string | null { + return store.has(key) ? store.get(key) ?? null : null; + }, + key(index: number): string | null { + return Array.from(store.keys())[index] ?? null; + }, + get length(): number { + return store.size; + }, + removeItem(key: string): void { + store.delete(key); + }, + setItem(key: string, value: string): void { + store.set(key, String(value)); + } + }; + + Object.defineProperty(globalThis, "localStorage", { + configurable: true, + value: shim, + writable: true + }); +} diff --git a/tests/timing.test.ts b/tests/timing.test.ts index e2a0a06..589443d 100644 --- a/tests/timing.test.ts +++ b/tests/timing.test.ts @@ -8,15 +8,15 @@ import { expect, test } from "vitest"; /*** UTILITY ------------------------------------------ ***/ -import type { Fetcher, FetcherResult } from "../source/library/fetcher/types.ts"; -import { SessionStore } from "../source/library/state/session.svelte.ts"; import { createMemoryStorage } from "../source/library/state/storage.ts"; +import { SessionStore } from "../source/library/state/session.svelte.ts"; -function delay(ms: number): Promise<void> { - return new Promise((resolve) => setTimeout(resolve, ms)); -} +import { + type Fetcher, + type FetcherResult +} from "../source/library/fetcher/types.ts"; -/*** TESTS -------------------------------------------- ***/ +/*** PROGRAM ------------------------------------------ ***/ test("run() populates timing for a one-shot fetcher", async () => { const store = new SessionStore(createMemoryStorage()); @@ -54,8 +54,10 @@ test("run() records intervals between async iterable payloads", async () => { async function* stream(): AsyncGenerator<FetcherResult> { yield { data: { n: 1 } }; await delay(5); + yield { data: { n: 2 } }; await delay(5); + yield { data: { n: 3 } }; } @@ -88,6 +90,7 @@ test("run() resets timing and streamIntervals on each invocation", async () => { async function* stream(): AsyncGenerator<FetcherResult> { yield { data: { n: 1 } }; await delay(5); + yield { data: { n: 2 } }; } @@ -98,3 +101,9 @@ test("run() resets timing and streamIntervals on each invocation", async () => { expect(tab.streamIntervals.length).toEqual(0); expect(tab.timing).not.toBeNull(); }); + +/*** HELPER ------------------------------------------- ***/ + +function delay(ms: number): Promise<void> { + return new Promise((resolve) => setTimeout(resolve, ms)); +} |