// @vitest-environment jsdom // // Covers the Part-1 additions to `@djangocfg/ui-core`'s `useLocalStorage`: // partial-merge `patch`, no-op skip, write coalescing, and same-tab // cross-instance sync. ui-core ships no test runner, so the test lives // here (ui-tools has vitest + jsdom and resolves ui-core via workspace). import { act, createElement } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { useLocalStorage } from '@djangocfg/ui-core/hooks'; (globalThis as Record).IS_REACT_ACT_ENVIRONMENT = true; interface Prefs { a: number; b: number; c: string; } let container: HTMLDivElement; let root: Root; beforeEach(() => { window.localStorage.clear(); container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); }); afterEach(() => { act(() => root.unmount()); container.remove(); window.localStorage.clear(); }); /** Flush pending microtask-coalesced writes. */ async function flushMicrotasks() { await act(async () => { await Promise.resolve(); }); } describe('useLocalStorage — patch / coalescing / sync', () => { it('patch shallow-merges a subset of keys', async () => { let api: ReturnType> | null = null; function Probe() { api = useLocalStorage('t.patch', { a: 1, b: 2, c: 'x' }); return null; } await act(async () => root.render(createElement(Probe))); await act(async () => { api![3]({ b: 99 }); }); expect(api![0]).toEqual({ a: 1, b: 99, c: 'x' }); await flushMicrotasks(); expect(JSON.parse(window.localStorage.getItem('t.patch')!)).toEqual({ a: 1, b: 99, c: 'x', }); }); it('patch is a no-op when nothing changes', async () => { let renders = 0; let api: ReturnType> | null = null; function Probe() { renders += 1; api = useLocalStorage('t.noop', { a: 1, b: 2, c: 'x' }); return null; } await act(async () => root.render(createElement(Probe))); await flushMicrotasks(); const rendersAfterMount = renders; // Patching with the same values must not trigger a write/re-render. await act(async () => { api![3]({ a: 1, b: 2 }); }); await flushMicrotasks(); expect(renders).toBe(rendersAfterMount); // No write happened — key stays absent. expect(window.localStorage.getItem('t.noop')).toBeNull(); }); it('coalesces several patches in one tick into a single write', async () => { let api: ReturnType> | null = null; function Probe() { api = useLocalStorage('t.coalesce', { a: 0, b: 0, c: '' }); return null; } await act(async () => root.render(createElement(Probe))); // Spy on the prototype — jsdom's per-instance `setItem` is read-only. let writes = 0; const realSetItem = Storage.prototype.setItem; Storage.prototype.setItem = function patched(this: Storage, k, v) { if (k === 't.coalesce') writes += 1; return realSetItem.call(this, k, v); }; await act(async () => { api![3]({ a: 1 }); api![3]({ b: 2 }); api![3]({ c: 'z' }); }); await flushMicrotasks(); Storage.prototype.setItem = realSetItem; // State reflects all three patches... expect(api![0]).toEqual({ a: 1, b: 2, c: 'z' }); // ...but they collapsed into exactly one localStorage write. expect(writes).toBe(1); }); it('two hook instances on the same key stay in sync (same tab)', async () => { let a: ReturnType> | null = null; let b: ReturnType> | null = null; function Probe() { a = useLocalStorage('t.sync', { a: 1, b: 1, c: 'x' }); b = useLocalStorage('t.sync', { a: 1, b: 1, c: 'x' }); return null; } await act(async () => root.render(createElement(Probe))); await act(async () => { a![3]({ b: 42 }); }); await flushMicrotasks(); // The sibling instance saw the write without its own setter being called. expect(a![0]).toEqual({ a: 1, b: 42, c: 'x' }); expect(b![0]).toEqual({ a: 1, b: 42, c: 'x' }); }); });