1import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts";
2import { afterEach, describe, expect, it, vi } from "vite-plus/test";
3
4function createLocalStorageStub(): Storage {
5 const store = new Map<string, string>();
6 return {
7 getItem: (key) => store.get(key) ?? null,
8 setItem: (key, value) => {
9 store.set(key, value);
10 },
11 removeItem: (key) => {
12 store.delete(key);
13 },
14 clear: () => {
15 store.clear();
16 },
17 key: (index) => [...store.keys()][index] ?? null,
18 get length() {
19 return store.size;
20 },
21 };
22}
23
24function getTestWindow(): Window & typeof globalThis {
25 const localStorage = createLocalStorageStub();
26 const testWindow = {
27 localStorage,
28 } as Window & typeof globalThis;
29 vi.stubGlobal("window", testWindow);
30 vi.stubGlobal("localStorage", localStorage);
31 return testWindow;
32}
33
34afterEach(() => {
35 vi.resetModules();
36 vi.unstubAllGlobals();
37 vi.restoreAllMocks();
38});
39
40describe("clientPersistenceStorage", () => {
41 it("persists client settings in browser storage", async () => {
42 getTestWindow();
43 const { readBrowserClientSettings, writeBrowserClientSettings } =
44 await import("./clientPersistenceStorage");
45 const settings = {
46 ...DEFAULT_CLIENT_SETTINGS,
47 timestampFormat: "24-hour" as const,
48 };
49
50 writeBrowserClientSettings(settings);
51
52 expect(readBrowserClientSettings()).toEqual(settings);
53 });
54
55 it.each(["not-json", '{"wordWrap":"invalid"}'])(
56 "does not treat invalid saved settings as absent: %s",
57 async (value) => {
58 const testWindow = getTestWindow();
59 testWindow.localStorage.setItem("t3code:client-settings:v1", value);
60 const { readBrowserClientSettings } = await import("./clientPersistenceStorage");
61
62 expect(() => readBrowserClientSettings()).toThrow(
63 expect.objectContaining({
64 _tag: "LocalStorageOperationError",
65 operation: "decode",
66 storageKey: "t3code:client-settings:v1",
67 }),
68 );
69 expect(testWindow.localStorage.getItem("t3code:client-settings:v1")).toBe(value);
70 },
71 );
72
73 it("preserves saved settings across a transient read failure", async () => {
74 const testWindow = getTestWindow();
75 const settings = { ...DEFAULT_CLIENT_SETTINGS, timestampFormat: "12-hour" as const };
76 testWindow.localStorage.setItem("t3code:client-settings:v1", JSON.stringify(settings));
77 const write = vi.spyOn(testWindow.localStorage, "setItem");
78 const failure = new Error("storage unavailable");
79 vi.spyOn(testWindow.localStorage, "getItem").mockImplementationOnce(() => {
80 throw failure;
81 });
82 const { readBrowserClientSettings } = await import("./clientPersistenceStorage");
83
84 expect(() => readBrowserClientSettings()).toThrow(
85 expect.objectContaining({
86 _tag: "LocalStorageOperationError",
87 operation: "read",
88 storageKey: "t3code:client-settings:v1",
89 cause: failure,
90 }),
91 );
92 expect(readBrowserClientSettings()).toEqual(settings);
93 expect(write).not.toHaveBeenCalled();
94 });
95
96 it("defaults word wrap on and discards obsolete wrapping preferences", async () => {
97 const testWindow = getTestWindow();
98 testWindow.localStorage.setItem(
99 "t3code:client-settings:v1",
100 JSON.stringify({
101 chatWordWrap: false,
102 diffWordWrap: false,
103 }),
104 );
105 const { readBrowserClientSettings } = await import("./clientPersistenceStorage");
106 const settings = readBrowserClientSettings();
107
108 expect(settings).toEqual(
109 expect.objectContaining({
110 wordWrap: true,
111 }),
112 );
113 expect(settings).not.toHaveProperty("chatWordWrap");
114 expect(settings).not.toHaveProperty("diffWordWrap");
115 });
116
117 it("keeps the default diff file state across reloads and defaults it to collapsed", async () => {
118 const testWindow = getTestWindow();
119 const { readBrowserClientSettings, writeBrowserClientSettings } =
120 await import("./clientPersistenceStorage");
121
122 testWindow.localStorage.setItem("t3code:client-settings:v1", JSON.stringify({}));
123 expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(true);
124
125 writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, diffFilesCollapsed: true });
126 expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(true);
127
128 writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, diffFilesCollapsed: false });
129 expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(false);
130 });
131
132 it("keeps the diff layout across reloads and defaults it to stacked", async () => {
133 const testWindow = getTestWindow();
134 const { readBrowserClientSettings, writeBrowserClientSettings } =
135 await import("./clientPersistenceStorage");
136
137 expect(readBrowserClientSettings()).toBeNull();
138 testWindow.localStorage.setItem("t3code:client-settings:v1", JSON.stringify({}));
139 expect(readBrowserClientSettings()?.diffLayout).toBe("stacked");
140
141 writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, diffLayout: "split" });
142 expect(readBrowserClientSettings()?.diffLayout).toBe("split");
143 });
144});