From 8a59f92d031963e23ecc84b75feecf43eb4dd146 Mon Sep 17 00:00:00 2001 From: "netop://ウィビ" Date: Fri, 24 Apr 2026 11:33:25 -0700 Subject: Initial commit: @eol/graphiql v0.3 Svelte 5 GraphiQL alternative for JSR. Covers: - HTTP fetcher with injectable fetch; SSE/WS stubs - Session store with tabs, auto-titling, persistence, rename - Operation detection via graphql parse(); Toolbar picker - CodeMirror 6 editor via cm6-graphql with theme prop - Light theme preset (hand-rolled EditorView.theme) - Doc explorer with breadcrumb nav and type guards - History panel with 100-entry cap, favorite pinning - Deno tests for operations, storage, and history eviction --- tests/history.test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++ tests/operations.test.ts | 64 +++++++++++++++++++++++++++++++++++++ tests/storage.test.ts | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 tests/history.test.ts create mode 100644 tests/operations.test.ts create mode 100644 tests/storage.test.ts (limited to 'tests') diff --git a/tests/history.test.ts b/tests/history.test.ts new file mode 100644 index 0000000..ecd7785 --- /dev/null +++ b/tests/history.test.ts @@ -0,0 +1,77 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { assertEquals } from "jsr:@std/assert@^1.0.0"; + +/*** UTILITY ------------------------------------------ ***/ + +import { evict } from "../source/library/state/history-logic.ts"; + +type Entry = { + favorite: boolean; + id: string; + timestamp: number; +}; + +function entry(id: string, timestamp: number, favorite = false): Entry { + return { favorite, id, timestamp }; +} + +/*** TESTS -------------------------------------------- ***/ + +Deno.test("evict keeps everything when under cap", () => { + const entries = [entry("a", 3), entry("b", 2), entry("c", 1)]; + assertEquals(evict(entries, 5), entries); +}); + +Deno.test("evict drops the oldest non-favorites above cap", () => { + const entries = [ + entry("a", 5), + entry("b", 4), + entry("c", 3), + entry("d", 2), + entry("e", 1) + ]; + const kept = evict(entries, 3); + assertEquals(kept.map((e) => e.id), ["a", "b", "c"]); +}); + +Deno.test("evict never drops favorites", () => { + const entries = [ + entry("a", 10), + entry("b", 9), + entry("fav-old", 1, true), + entry("c", 8), + entry("d", 7) + ]; + const kept = evict(entries, 3); + + assertEquals(kept.some((e) => e.id === "fav-old"), true); + assertEquals(kept.length, 3); +}); + +Deno.test("evict can exceed cap when favorites alone do so", () => { + const entries = [ + entry("fav-1", 5, true), + entry("fav-2", 4, true), + entry("fav-3", 3, true), + entry("regular", 2) + ]; + const kept = evict(entries, 2); + + assertEquals(kept.length, 3); + assertEquals(kept.every((e) => e.favorite), true); +}); + +Deno.test("evict sorts by timestamp descending", () => { + const entries = [ + entry("c", 1), + entry("a", 3), + entry("b", 2), + entry("d", 0) + ]; + const kept = evict(entries, 3); + assertEquals(kept.map((e) => e.id), ["a", "b", "c"]); +}); diff --git a/tests/operations.test.ts b/tests/operations.test.ts new file mode 100644 index 0000000..99357ea --- /dev/null +++ b/tests/operations.test.ts @@ -0,0 +1,64 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { assertEquals } from "jsr:@std/assert@^1.0.0"; + +/*** UTILITY ------------------------------------------ ***/ + +import { deriveTitle, parseOperations } from "../source/library/graphql/operations.ts"; + +/*** TESTS -------------------------------------------- ***/ + +Deno.test("parseOperations returns empty for blank query", () => { + assertEquals(parseOperations(""), []); + assertEquals(parseOperations(" "), []); +}); + +Deno.test("parseOperations returns empty on syntax error", () => { + assertEquals(parseOperations("query { ..."), []); +}); + +Deno.test("parseOperations captures a single named query", () => { + const ops = parseOperations("query Foo { viewer { id } }"); + assertEquals(ops, [{ name: "Foo", type: "query" }]); +}); + +Deno.test("parseOperations returns null name for anonymous ops", () => { + const ops = parseOperations("{ viewer { id } }"); + assertEquals(ops, [{ name: null, type: "query" }]); +}); + +Deno.test("parseOperations captures multiple operations", () => { + const ops = parseOperations(` + query Foo { a } + mutation Bar { b } + subscription Baz { c } + `); + assertEquals(ops, [ + { name: "Foo", type: "query" }, + { name: "Bar", type: "mutation" }, + { name: "Baz", type: "subscription" } + ]); +}); + +Deno.test("deriveTitle prefers the first operation name", () => { + const ops = parseOperations("query Foo { a }"); + assertEquals(deriveTitle("query Foo { a }", ops), "Foo"); +}); + +Deno.test("deriveTitle falls back to operation type", () => { + const ops = parseOperations("mutation { a }"); + assertEquals(deriveTitle("mutation { a }", ops), "mutation"); +}); + +Deno.test("deriveTitle falls back to the first 20 chars when unparsable", () => { + const query = "this is not valid graphql at all"; + assertEquals(deriveTitle(query, parseOperations(query)), "this is not valid gr"); +}); + +Deno.test("deriveTitle returns 'untitled' for empty input", () => { + assertEquals(deriveTitle("", []), "untitled"); + assertEquals(deriveTitle(" ", []), "untitled"); +}); diff --git a/tests/storage.test.ts b/tests/storage.test.ts new file mode 100644 index 0000000..7d6ba73 --- /dev/null +++ b/tests/storage.test.ts @@ -0,0 +1,83 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { assertEquals } from "jsr:@std/assert@^1.0.0"; + +/*** UTILITY ------------------------------------------ ***/ + +import { + createLocalStorage, + createMemoryStorage +} from "../source/library/state/storage.ts"; + +/*** TESTS -------------------------------------------- ***/ + +Deno.test("memory storage round-trips objects", () => { + const storage = createMemoryStorage(); + storage.set("k", { hello: "world" }); + assertEquals(storage.get<{ hello: string }>("k"), { hello: "world" }); +}); + +Deno.test("memory storage returns null for missing keys", () => { + const storage = createMemoryStorage(); + assertEquals(storage.get("missing"), null); +}); + +Deno.test("memory storage remove clears a key", () => { + const storage = createMemoryStorage(); + storage.set("k", 42); + storage.remove("k"); + assertEquals(storage.get("k"), null); +}); + +Deno.test("memory storage instances are isolated", () => { + const a = createMemoryStorage(); + const b = createMemoryStorage(); + a.set("shared", 1); + assertEquals(b.get("shared"), null); +}); + +Deno.test("local storage namespaces keys", () => { + globalThis.localStorage.clear(); + + const alpha = createLocalStorage("alpha"); + const beta = createLocalStorage("beta"); + + alpha.set("shared", { tag: "a" }); + beta.set("shared", { tag: "b" }); + + assertEquals(alpha.get<{ tag: string }>("shared"), { tag: "a" }); + assertEquals(beta.get<{ tag: string }>("shared"), { tag: "b" }); + assertEquals(globalThis.localStorage.getItem("alpha:shared"), JSON.stringify({ tag: "a" })); + assertEquals(globalThis.localStorage.getItem("beta:shared"), JSON.stringify({ tag: "b" })); + + globalThis.localStorage.clear(); +}); + +Deno.test("local storage remove respects the namespace", () => { + globalThis.localStorage.clear(); + + const alpha = createLocalStorage("alpha"); + const beta = createLocalStorage("beta"); + + alpha.set("k", 1); + beta.set("k", 2); + + alpha.remove("k"); + assertEquals(alpha.get("k"), null); + assertEquals(beta.get("k"), 2); + + globalThis.localStorage.clear(); +}); + +Deno.test("local storage returns null on malformed JSON", () => { + globalThis.localStorage.clear(); + globalThis.localStorage.setItem("alpha:bad", "not-json"); + + const alpha = createLocalStorage("alpha"); + assertEquals(alpha.get("bad"), null); + + globalThis.localStorage.clear(); +}); -- cgit v1.2.3