Skip to content
File

Blob: tests/client/lib/doc-cache-registry.dom.test.ts

typescript139 lines
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2 
3let docCache: typeof import("@/client/lib/doc-cache-registry").docCache;
4 
5beforeEach(async () => {
6 localStorage.clear();
7 vi.resetModules();
8 const mod = await import("@/client/lib/doc-cache-registry");
9 docCache = mod.docCache;
10});
11 
12afterEach(() => {
13 localStorage.clear();
14 vi.restoreAllMocks();
15});
16 
17describe("docCache.mark / docCache.has", () => {
18 it("marks a doc as cached and reads it back", () => {
19 expect(docCache.has("p1")).toBe(false);
20 docCache.mark("p1");
21 expect(docCache.has("p1")).toBe(true);
22 });
23 
24 it("is idempotent — marking the same doc twice does not duplicate", () => {
25 docCache.mark("p1");
26 docCache.mark("p1");
27 const raw = localStorage.getItem("bland:cached-docs");
28 expect(JSON.parse(raw!)).toEqual({ version: 1, value: ["p1"] });
29 });
30 
31 it("tracks multiple docs independently", () => {
32 docCache.mark("p1");
33 docCache.mark("p2");
34 expect(docCache.has("p1")).toBe(true);
35 expect(docCache.has("p2")).toBe(true);
36 expect(docCache.has("p3")).toBe(false);
37 });
38});
39 
40describe("docCache.remove", () => {
41 it("removes the hint and dispatches an IndexedDB clear for both doc and canvas namespaces", async () => {
42 const clearDocument = vi.fn().mockResolvedValue(undefined);
43 vi.doMock("y-indexeddb", () => ({ clearDocument }));
44 
45 docCache.mark("p1");
46 docCache.mark("p2");
47 docCache.remove("p1");
48 
49 expect(docCache.has("p1")).toBe(false);
50 expect(docCache.has("p2")).toBe(true);
51 await vi.dynamicImportSettled();
52 expect(clearDocument).toHaveBeenCalledWith("bland:doc:p1");
53 expect(clearDocument).toHaveBeenCalledWith("bland:canvas:p1");
54 });
55 
56 it("no-ops cleanly when the page was never cached but still clears both IDB namespaces best-effort", async () => {
57 const clearDocument = vi.fn().mockResolvedValue(undefined);
58 vi.doMock("y-indexeddb", () => ({ clearDocument }));
59 
60 expect(() => docCache.remove("p1")).not.toThrow();
61 expect(docCache.has("p1")).toBe(false);
62 // Fires IDB clear best-effort even if the hint set didn't track the id —
63 // handles cases where the hint was already pruned but the IDB entries remain.
64 await vi.dynamicImportSettled();
65 expect(clearDocument).toHaveBeenCalledWith("bland:doc:p1");
66 expect(clearDocument).toHaveBeenCalledWith("bland:canvas:p1");
67 });
68 
69 it("does not throw when y-indexeddb import fails", () => {
70 vi.doMock("y-indexeddb", () => {
71 throw new Error("module not found");
72 });
73 
74 docCache.mark("p1");
75 expect(() => docCache.remove("p1")).not.toThrow();
76 });
77});
78 
79describe("docCache.clearAll", () => {
80 it("clears hints and calls clearDocument for both doc and canvas namespaces of each cached page", async () => {
81 const clearDocument = vi.fn().mockResolvedValue(undefined);
82 vi.doMock("y-indexeddb", () => ({ clearDocument }));
83 
84 docCache.mark("p1");
85 docCache.mark("p2");
86 docCache.clearAll();
87 
88 await vi.dynamicImportSettled();
89 expect(clearDocument).toHaveBeenCalledWith("bland:doc:p1");
90 expect(clearDocument).toHaveBeenCalledWith("bland:canvas:p1");
91 expect(clearDocument).toHaveBeenCalledWith("bland:doc:p2");
92 expect(clearDocument).toHaveBeenCalledWith("bland:canvas:p2");
93 expect(localStorage.getItem("bland:cached-docs")).toBeNull();
94 });
95 
96 it("does not throw when y-indexeddb import fails", () => {
97 vi.doMock("y-indexeddb", () => {
98 throw new Error("module not found");
99 });
100 
101 docCache.mark("p1");
102 expect(() => docCache.clearAll()).not.toThrow();
103 expect(localStorage.getItem("bland:cached-docs")).toBeNull();
104 });
105 
106 it("does not throw when clearDocument rejects", async () => {
107 const clearDocument = vi.fn().mockRejectedValue(new Error("IDB error"));
108 vi.doMock("y-indexeddb", () => ({ clearDocument }));
109 
110 docCache.mark("p1");
111 expect(() => docCache.clearAll()).not.toThrow();
112 await vi.dynamicImportSettled();
113 expect(clearDocument).toHaveBeenCalledWith("bland:doc:p1");
114 expect(clearDocument).toHaveBeenCalledWith("bland:canvas:p1");
115 });
116 
117 it("skips clearDocument when no docs are cached", async () => {
118 const clearDocument = vi.fn();
119 vi.doMock("y-indexeddb", () => ({ clearDocument }));
120 
121 docCache.clearAll();
122 await vi.dynamicImportSettled();
123 expect(clearDocument).not.toHaveBeenCalled();
124 });
125});
126 
127describe("malformed localStorage", () => {
128 it("returns empty set when JSON is malformed", () => {
129 localStorage.setItem("bland:cached-docs", "{not-valid-json");
130 expect(docCache.has("p1")).toBe(false);
131 });
132 
133 it("allows marking after recovering from malformed data", () => {
134 localStorage.setItem("bland:cached-docs", "null");
135 docCache.mark("p1");
136 expect(docCache.has("p1")).toBe(true);
137 });
138});