1import * as Schema from "effect/Schema";
2import { afterEach, describe, expect, it, vi } from "vite-plus/test";
3
4function createStorage(overrides: Partial<Storage> = {}): Storage {
5 const store = new Map<string, string>();
6 return {
7 clear: () => store.clear(),
8 getItem: (key) => store.get(key) ?? null,
9 key: (index) => [...store.keys()][index] ?? null,
10 get length() {
11 return store.size;
12 },
13 removeItem: (key) => {
14 store.delete(key);
15 },
16 setItem: (key, value) => {
17 store.set(key, value);
18 },
19 ...overrides,
20 };
21}
22
23async function loadWithStorage(storage: Storage) {
24 vi.stubGlobal("window", { localStorage: storage });
25 vi.stubGlobal("localStorage", storage);
26 return import("./useLocalStorage");
27}
28
29afterEach(() => {
30 vi.resetModules();
31 vi.unstubAllGlobals();
32});
33
34describe("local storage errors", () => {
35 it("preserves read failure context", async () => {
36 const cause = new Error("storage unavailable");
37 const { getLocalStorageItem, LocalStorageOperationError } = await loadWithStorage(
38 createStorage({
39 getItem: () => {
40 throw cause;
41 },
42 }),
43 );
44
45 try {
46 getLocalStorageItem("read-key", Schema.String);
47 expect.unreachable("expected the read to fail");
48 } catch (error) {
49 expect(error).toBeInstanceOf(LocalStorageOperationError);
50 expect(error).toMatchObject({
51 operation: "read",
52 storageKey: "read-key",
53 cause,
54 });
55 }
56 });
57
58 it("retries when access to browser storage becomes available", async () => {
59 const storage = createStorage();
60 storage.setItem("read-key", JSON.stringify("saved value"));
61 let blocked = true;
62 vi.stubGlobal("window", {
63 get localStorage() {
64 if (blocked) throw new Error("storage unavailable");
65 return storage;
66 },
67 });
68 const { getLocalStorageItem, LocalStorageOperationError } = await import("./useLocalStorage");
69
70 expect(() => getLocalStorageItem("read-key", Schema.String)).toThrow(
71 LocalStorageOperationError,
72 );
73 blocked = false;
74 expect(getLocalStorageItem("read-key", Schema.String)).toBe("saved value");
75 });
76
77 it.each(["", "not-json"])("preserves decode failure context for %j", async (value) => {
78 const { getLocalStorageItem, LocalStorageOperationError } = await loadWithStorage(
79 createStorage({ getItem: () => value }),
80 );
81
82 try {
83 getLocalStorageItem("decode-key", Schema.String);
84 expect.unreachable("expected decoding to fail");
85 } catch (error) {
86 expect(error).toBeInstanceOf(LocalStorageOperationError);
87 expect(error).toMatchObject({
88 operation: "decode",
89 storageKey: "decode-key",
90 cause: expect.anything(),
91 });
92 }
93 });
94
95 it("preserves write failure context", async () => {
96 const cause = new Error("storage quota exceeded");
97 const { LocalStorageOperationError, setLocalStorageItem } = await loadWithStorage(
98 createStorage({
99 setItem: () => {
100 throw cause;
101 },
102 }),
103 );
104
105 try {
106 setLocalStorageItem("write-key", "value", Schema.String);
107 expect.unreachable("expected the write to fail");
108 } catch (error) {
109 expect(error).toBeInstanceOf(LocalStorageOperationError);
110 expect(error).toMatchObject({
111 operation: "write",
112 storageKey: "write-key",
113 cause,
114 });
115 }
116 });
117
118 it("preserves removal failure context", async () => {
119 const cause = new Error("storage unavailable");
120 const { LocalStorageOperationError, removeLocalStorageItem } = await loadWithStorage(
121 createStorage({
122 removeItem: () => {
123 throw cause;
124 },
125 }),
126 );
127
128 try {
129 removeLocalStorageItem("remove-key");
130 expect.unreachable("expected the removal to fail");
131 } catch (error) {
132 expect(error).toBeInstanceOf(LocalStorageOperationError);
133 expect(error).toMatchObject({
134 operation: "remove",
135 storageKey: "remove-key",
136 cause,
137 });
138 }
139 });
140});