Files
twitch-panels/tests/unit/states/persisted.svelte.test.ts
T

216 lines
5.5 KiB
TypeScript

import { withPersistence } from "$states/persisted.svelte";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
function createMock() {
let store: Record<string, string> = {};
return {
getItem: vi.fn((key: string) => store[key] || null),
setItem: vi.fn((key: string, value: string) => {
store[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
};
}
describe("persisted.svelte", () => {
let localStorageMock: ReturnType<typeof createMock>;
beforeAll(() => {
localStorageMock = createMock();
vi.stubGlobal("localStorage", localStorageMock);
});
afterAll(() => {
vi.unstubAllGlobals();
});
beforeEach(() => {
localStorageMock.clear();
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("withPersistence", () => {
it("should return state unchanged when not in browser", () => {
const data = $state({ test: "value" });
const mockState = {
toSnapshot: () => data,
fromSnapshot: () => {},
};
const result = withPersistence("test-key", mockState);
expect(result).toBe(mockState);
});
it("should restore state from localStorage when valid JSON exists", () => {
interface State {
value: number;
name: string;
}
const savedData: State = { value: 42, name: "test" };
localStorageMock.getItem.mockReturnValue(JSON.stringify(savedData));
const data: State = $state({ value: 0, name: "" });
const state = {
toSnapshot: () => data,
fromSnapshot: (newData: State) => {
if (newData.value !== undefined) data.value = newData.value;
if (newData.name !== undefined) data.name = newData.name;
},
};
withPersistence("test-key", state);
expect(state.toSnapshot()).toEqual(savedData);
expect(localStorageMock.getItem).toHaveBeenCalledWith("test-key");
});
it("should handle corrupted JSON in localStorage gracefully", () => {
localStorageMock.getItem.mockReturnValue("{ invalid json");
const data = $state({ value: 0 });
const state = {
toSnapshot: () => data,
fromSnapshot: () => {},
};
expect(() => withPersistence("test-key", state)).not.toThrow();
expect(state.toSnapshot()).toEqual({ value: 0 });
});
it("should handle empty localStorage (no saved data)", () => {
localStorageMock.getItem.mockReturnValue(null);
const data = $state({ value: 0 });
const state = {
toSnapshot: () => data,
fromSnapshot: () => {},
};
withPersistence("test-key", state);
expect(state.toSnapshot()).toEqual({ value: 0 });
});
it("should save state to localStorage with debounce", async () => {
vi.useFakeTimers();
interface State {
count: number;
}
const data: State = $state({ count: 1 });
const state = {
toSnapshot: () => data,
fromSnapshot: (newData: State) => {
if (newData.count !== undefined) data.count = newData.count;
},
};
withPersistence("test-key", state, 500);
state.fromSnapshot({ count: 2 });
await Promise.resolve();
vi.advanceTimersByTime(500);
await Promise.resolve();
expect(localStorageMock.setItem).toHaveBeenCalledWith(
"test-key",
JSON.stringify({ count: 2 }),
);
vi.useRealTimers();
});
it("should save state immediately when debounce is 0", async () => {
interface State {
count: number;
}
const data: State = $state({ count: 1 });
const state = {
toSnapshot: () => data,
fromSnapshot: (newData: State) => {
if (newData.count !== undefined) data.count = newData.count;
},
};
withPersistence("test-key", state, 0);
state.fromSnapshot({ count: 2 });
await Promise.resolve();
expect(localStorageMock.setItem).toHaveBeenCalledWith(
"test-key",
JSON.stringify({ count: 2 }),
);
});
it("should cleanup timeout on state change during debounce", async () => {
vi.useFakeTimers();
interface State {
count: number;
}
const data: State = $state({ count: 1 });
const state = {
toSnapshot: () => data,
fromSnapshot: (newData: State) => {
if (newData.count !== undefined) data.count = newData.count;
},
};
withPersistence("test-key", state, 500);
state.fromSnapshot({ count: 2 });
await Promise.resolve();
state.fromSnapshot({ count: 3 });
await Promise.resolve();
vi.runOnlyPendingTimers();
await Promise.resolve();
expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
expect(localStorageMock.setItem).toHaveBeenCalledWith(
"test-key",
JSON.stringify({ count: 3 }),
);
vi.useRealTimers();
});
it("should use DEBOUNCE_DURATION constant as default", async () => {
interface State {
test: boolean;
}
const data: State = $state({ test: true });
const state = {
toSnapshot: () => data,
fromSnapshot: (newData: State) => {
if (newData.test !== undefined) data.test = newData.test;
},
};
withPersistence("test-key", state);
state.fromSnapshot({ test: false });
await Promise.resolve();
expect(localStorageMock.setItem).toHaveBeenCalled();
});
});
});