// @vitest-environment jsdom // // A browser holds ONE account's local state: the keys carry no account id, and the browser records // whose they are. These pin the boot-time claim — the move of the old per-account keys onto the plain // ones, the wipe when another account starts here — and the guard that keeps a tab from reading or // writing them before its account is known, or after another tab has claimed them for someone else. import { beforeEach, describe, expect, it, vi } from "vitest"; beforeEach(() => { localStorage.clear(); sessionStorage.clear(); vi.resetModules(); // the claim lives in module state: each case starts unclaimed }); const storage = () => import("./storage"); const keys = (s: Storage) => Object.keys(s).sort(); describe("claiming the browser's local data", () => { it("moves the account's old per-account copies onto the plain keys and drops other accounts' copies", async () => { localStorage.setItem("siftlode.filters.1", '{"q":"mine"}'); localStorage.setItem("siftlode.filters.7", '{"q":"theirs"}'); localStorage.setItem("siftlode.onboarding.dismissed.1", "1"); localStorage.setItem("siftlode.notifications.7", "[]"); sessionStorage.setItem("siftlode.settingsTab.1", "account"); sessionStorage.setItem("siftlode.settingsTab.7", "plex"); const { claimLocalData, LS } = await storage(); claimLocalData(1); expect(localStorage.getItem(LS.filters)).toBe('{"q":"mine"}'); expect(localStorage.getItem(LS.onboardingDismissed)).toBe("1"); expect(sessionStorage.getItem(LS.settingsTab)).toBe("account"); expect(keys(localStorage)).toEqual( ["siftlode.filters", "siftlode.onboarding.dismissed", "siftlode.owner"].sort(), ); expect(keys(sessionStorage)).toEqual(["siftlode.owner", "siftlode.settingsTab"]); }); it("an old per-account copy wins over a plain key already there", async () => { localStorage.setItem("siftlode.hints", "1"); localStorage.setItem("siftlode.hints.1", "0"); const { claimLocalData } = await storage(); claimLocalData(1); expect(localStorage.getItem("siftlode.hints")).toBe("0"); }); it("leaves a key that only looks like an account copy alone", async () => { localStorage.setItem("siftlode.somethingElse.3", "x"); const { claimLocalData } = await storage(); claimLocalData(1); expect(localStorage.getItem("siftlode.somethingElse.3")).toBe("x"); }); it("drops the per-tab account pin the multi-account build kept", async () => { sessionStorage.setItem("siftlode.activeAccount", "1"); sessionStorage.setItem("siftlode.nextAccount", "7"); const { claimLocalData } = await storage(); claimLocalData(1); expect(sessionStorage.getItem("siftlode.activeAccount")).toBeNull(); expect(sessionStorage.getItem("siftlode.nextAccount")).toBeNull(); }); it("another account starting here wipes the previous one's state, but not the browser's own", async () => { localStorage.setItem("siftlode.owner", "7"); localStorage.setItem("siftlode.filters", '{"q":"theirs"}'); localStorage.setItem("siftlode.theme", "dark"); localStorage.setItem("siftlode.watchResume", '{"tok":{"at":5,"t":1}}'); localStorage.setItem("siftlode.watchRepeat", "false"); localStorage.setItem("unrelated.key", "kept"); sessionStorage.setItem("siftlode.settingsTab", "plex"); const { claimLocalData } = await storage(); claimLocalData(1); expect(keys(localStorage)).toEqual( ["siftlode.owner", "siftlode.watchRepeat", "siftlode.watchResume", "unrelated.key"].sort(), ); expect(localStorage.getItem("siftlode.owner")).toBe("1"); expect(keys(sessionStorage)).toEqual(["siftlode.owner"]); }); it("a tab reloading for the account another tab claimed the browser for drops its own view state", async () => { // Tab 2 signed in as 7 and claimed the browser; this tab still holds 1's sub-tab and the // onboarding flow 1 was in. Its sessionStorage is its own, so the browser's owner says nothing // about it. localStorage.setItem("siftlode.owner", "7"); sessionStorage.setItem("siftlode.owner", "1"); sessionStorage.setItem("siftlode.settingsTab", "plex"); sessionStorage.setItem("siftlode.onboarding.active", "1"); const { claimLocalData } = await storage(); claimLocalData(7); expect(keys(sessionStorage)).toEqual(["siftlode.owner"]); expect(sessionStorage.getItem("siftlode.owner")).toBe("7"); }); it("the same account starting again keeps everything", async () => { localStorage.setItem("siftlode.owner", "1"); localStorage.setItem("siftlode.filters", '{"q":"mine"}'); sessionStorage.setItem("siftlode.settingsTab", "account"); const { claimLocalData } = await storage(); claimLocalData(1); expect(localStorage.getItem("siftlode.filters")).toBe('{"q":"mine"}'); expect(sessionStorage.getItem("siftlode.settingsTab")).toBe("account"); }); it("tells the stores read at import to read again", async () => { const { claimLocalData, onLocalDataClaimed } = await storage(); const reread = vi.fn(); onLocalDataClaimed(reread); claimLocalData(1); expect(reread).toHaveBeenCalledOnce(); }); }); describe("the account's local state", () => { it("is neither read nor written before the account is known", async () => { localStorage.setItem("siftlode.hints", "0"); const { getAccountRaw, setAccountRaw, readAccount, writeAccount, ownKey } = await storage(); expect(getAccountRaw("siftlode.hints")).toBeNull(); expect(readAccount("siftlode.filters", "fallback")).toBe("fallback"); expect(ownKey("siftlode.filters")).toBeNull(); setAccountRaw("siftlode.page", "plex"); writeAccount("siftlode.filters", { q: "x" }); expect(localStorage.getItem("siftlode.page")).toBeNull(); expect(localStorage.getItem("siftlode.filters")).toBeNull(); }); it("is read and written under the plain key once claimed", async () => { const { claimLocalData, getAccountRaw, setAccountRaw, ownKey } = await storage(); claimLocalData(1); setAccountRaw("siftlode.page", "plex"); expect(localStorage.getItem("siftlode.page")).toBe("plex"); expect(getAccountRaw("siftlode.page")).toBe("plex"); expect(ownKey("siftlode.filters")).toBe("siftlode.filters"); }); it("stops being touched once another tab has claimed the browser for someone else", async () => { const { claimLocalData, getAccountRaw, setAccountRaw } = await storage(); claimLocalData(1); setAccountRaw("siftlode.page", "plex"); // Another tab signed in as account 6: its boot wiped the state and recorded the new owner. localStorage.clear(); localStorage.setItem("siftlode.owner", "6"); localStorage.setItem("siftlode.hints", "0"); // account 6's setAccountRaw("siftlode.page", "feed"); expect(localStorage.getItem("siftlode.page")).toBeNull(); expect(getAccountRaw("siftlode.hints")).toBeNull(); }); });