test: localStorage-backed state modules (+38)
Via subagent, no bugs: - state/utils/atomWithLocalStorage (9): get/set helpers + atom write-through. - state/scheduledMessages (6): Map<->Record round-trip, persistence, mount-gated hydration (atomWithStorage w/o getOnInit — modeled with a subscription). - state/spaceRooms (9): Set dedupe + no-write-when-unchanged + serialization. - state/navToActivePath (8): per-user Map<->Object serialization. - state/callPreferences (6): the privacy rule forcing video=false on load+persist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createStore } from 'jotai';
|
||||
import { enableMapSet } from 'immer';
|
||||
|
||||
// `spaceRoomsAtom` is backed by `atomWithLocalStorage`, which reads
|
||||
// `localStorage` AT MODULE LOAD to seed the atom (absent in node). We install an
|
||||
// in-memory mock first. The reducers also `produce` over an immer-managed Set,
|
||||
// so immer's Map/Set plugin must be enabled (the app does this at startup).
|
||||
const SPACE_ROOMS = 'spaceRooms';
|
||||
|
||||
const installStorage = (seed?: string[]): Map<string, string> => {
|
||||
const map = new Map<string, string>();
|
||||
if (seed) map.set(SPACE_ROOMS, JSON.stringify(seed));
|
||||
(globalThis as { localStorage?: unknown }).localStorage = {
|
||||
getItem: (k: string) => (map.has(k) ? map.get(k)! : null),
|
||||
setItem: (k: string, v: string) => {
|
||||
map.set(k, v);
|
||||
},
|
||||
removeItem: (k: string) => {
|
||||
map.delete(k);
|
||||
},
|
||||
};
|
||||
return map;
|
||||
};
|
||||
|
||||
enableMapSet();
|
||||
installStorage();
|
||||
const { spaceRoomsAtom } = await import('./spaceRooms');
|
||||
|
||||
// `atomWithLocalStorage` seeds the atom from storage exactly once, when the
|
||||
// module is evaluated. To exercise hydration we install seeded storage and then
|
||||
// import a *fresh* copy of the module (cache-busted) so the seed is read.
|
||||
let freshCounter = 0;
|
||||
const importWithStorage = async (
|
||||
seed?: string[],
|
||||
): Promise<typeof import('./spaceRooms').spaceRoomsAtom> => {
|
||||
installStorage(seed);
|
||||
freshCounter += 1;
|
||||
const mod = await import(`./spaceRooms?fresh=${freshCounter}`);
|
||||
return mod.spaceRoomsAtom;
|
||||
};
|
||||
|
||||
const sorted = (s: Set<string>): string[] => [...s].sort();
|
||||
|
||||
test('starts empty when nothing is stored', () => {
|
||||
installStorage();
|
||||
const store = createStore();
|
||||
assert.equal(store.get(spaceRoomsAtom).size, 0);
|
||||
});
|
||||
|
||||
test('PUT adds new room ids to the Set', () => {
|
||||
installStorage();
|
||||
const store = createStore();
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s', '!b:s'] });
|
||||
assert.deepEqual(sorted(store.get(spaceRoomsAtom)), ['!a:s', '!b:s']);
|
||||
});
|
||||
|
||||
test('PUT dedupes against existing entries', () => {
|
||||
installStorage();
|
||||
const store = createStore();
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s'] });
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s', '!b:s'] });
|
||||
assert.deepEqual(sorted(store.get(spaceRoomsAtom)), ['!a:s', '!b:s']);
|
||||
});
|
||||
|
||||
test('PUT with only already-present ids does not write (Set identity unchanged)', () => {
|
||||
installStorage();
|
||||
const store = createStore();
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s'] });
|
||||
const before = store.get(spaceRoomsAtom);
|
||||
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s'] });
|
||||
const after = store.get(spaceRoomsAtom);
|
||||
|
||||
// No new entries -> the reducer skips set(), so the Set reference is the same.
|
||||
assert.equal(before, after);
|
||||
});
|
||||
|
||||
test('DELETE removes present members', () => {
|
||||
installStorage();
|
||||
const store = createStore();
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s', '!b:s'] });
|
||||
store.set(spaceRoomsAtom, { type: 'DELETE', roomIds: ['!a:s'] });
|
||||
assert.deepEqual(sorted(store.get(spaceRoomsAtom)), ['!b:s']);
|
||||
});
|
||||
|
||||
test('DELETE of an absent member does not write (Set identity unchanged)', () => {
|
||||
installStorage();
|
||||
const store = createStore();
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s'] });
|
||||
const before = store.get(spaceRoomsAtom);
|
||||
|
||||
store.set(spaceRoomsAtom, { type: 'DELETE', roomIds: ['!nope:s'] });
|
||||
const after = store.get(spaceRoomsAtom);
|
||||
|
||||
assert.equal(before, after);
|
||||
assert.deepEqual(sorted(after), ['!a:s']);
|
||||
});
|
||||
|
||||
test('DELETE acts when at least one of the ids is present', () => {
|
||||
installStorage();
|
||||
const store = createStore();
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s', '!b:s'] });
|
||||
store.set(spaceRoomsAtom, { type: 'DELETE', roomIds: ['!a:s', '!nope:s'] });
|
||||
assert.deepEqual(sorted(store.get(spaceRoomsAtom)), ['!b:s']);
|
||||
});
|
||||
|
||||
test('hydrates the Set from a stored array', async () => {
|
||||
const freshAtom = await importWithStorage(['!x:s', '!y:s']);
|
||||
const store = createStore();
|
||||
assert.deepEqual(sorted(store.get(freshAtom)), ['!x:s', '!y:s']);
|
||||
});
|
||||
|
||||
test('persists the Set to localStorage as an array', () => {
|
||||
const backing = installStorage();
|
||||
const store = createStore();
|
||||
store.set(spaceRoomsAtom, { type: 'PUT', roomIds: ['!a:s', '!b:s'] });
|
||||
|
||||
const raw = backing.get(SPACE_ROOMS);
|
||||
assert.ok(raw);
|
||||
const arr = JSON.parse(raw!) as string[];
|
||||
assert.deepEqual([...arr].sort(), ['!a:s', '!b:s']);
|
||||
});
|
||||
Reference in New Issue
Block a user