Files
siftlode/frontend/src/lib/claimedStores.dom.test.ts
T
peter cb8b99e0e7 fix(notifications): keep the history under a key of its own, so an older build never reads its new shape
A build from before the change renders history rows as text; an object there crashed its
Notifications page after a rollback. The old key is read once while the new one does not exist,
and never written.
2026-10-29 23:54:48 +01:00

89 lines
3.5 KiB
TypeScript

// @vitest-environment jsdom
//
// The notification and hints stores read the account's localStorage when they are imported, which is
// before BootGate knows the account, so they read as nobody's; they must read again on the claim. The
// notification store used to keep its empty, nobody's history, and the tab's first notice replaced the
// account's stored history with it.
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { raw, sayNotif, type NotifText } from "./notifications";
const entry = (id: number, message: string) => ({
id,
level: "info",
message: raw(message),
ts: 1,
read: true,
dismissed: true,
transient: false,
repeat: 1,
requiresInteraction: false,
});
const storedMessages = () =>
(
JSON.parse(localStorage.getItem("siftlode.notifHistory") ?? "[]") as {
message: NotifText;
}[]
).map((n) => sayNotif(n.message));
// Warm the transform cache once (see verifyEntry.dom.test.tsx): the first cold import costs ~0.8 s
// alone and outran the 5 s case timeout under a full coverage run. Transformed code outlives
// vi.resetModules, so each case still gets fresh module instances, only faster.
beforeAll(async () => {
await import("./notifications");
await import("./hints");
await import("./storage");
}, 60_000);
// Warm or not, each case still loads and evaluates the modules it imports afresh after its module
// reset: tens of ms idle, but the same reload put sibling suites past 5 s when the full suite's
// workers share a busy machine.
const CASE_BUDGET = 30_000;
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
vi.resetModules(); // both stores load at import: each test needs a fresh one
});
describe("stores read at import, before the account is known", { timeout: CASE_BUDGET }, () => {
it("keep the account's notification history and settings once it is claimed", async () => {
localStorage.setItem("siftlode.owner", "5");
localStorage.setItem("siftlode.notifHistory", JSON.stringify([entry(7, "older")]));
localStorage.setItem("siftlode.notifSettings", JSON.stringify({ durationMs: 0 }));
const notifications = await import("./notifications");
const { claimLocalData } = await import("./storage");
notifications.notify({ message: raw("before the claim") }); // e.g. a failed boot probe
claimLocalData(5);
notifications.notify({ message: raw("after the claim") });
expect(storedMessages()).toEqual(["older", "before the claim", "after the claim"]);
expect(notifications.getNotifSettings().durationMs).toBe(0);
});
it("read the account's hints choice once it is claimed", async () => {
localStorage.setItem("siftlode.owner", "5");
localStorage.setItem("siftlode.hints", "0");
const hints = await import("./hints");
const { claimLocalData } = await import("./storage");
expect(hints.hintsEnabled()).toBe(true); // nobody's: the default
claimLocalData(5);
expect(hints.hintsEnabled()).toBe(false);
});
});
describe("a browser that held another account's state", { timeout: CASE_BUDGET }, () => {
it("starts the new account's notices from an empty history", async () => {
localStorage.setItem("siftlode.owner", "4");
localStorage.setItem("siftlode.notifHistory", JSON.stringify([entry(7, "four's")]));
const notifications = await import("./notifications");
const { claimLocalData } = await import("./storage");
claimLocalData(5);
notifications.notify({ message: raw("as five") });
expect(storedMessages()).toEqual(["as five"]);
});
});