Files
loto/web/src/lib/master-store.test.js
T

139 lines
4.4 KiB
JavaScript

// @vitest-environment happy-dom
import { beforeEach, describe, expect, it } from "vitest";
import {
drawNext,
loadMaster,
masterState,
saveMaster,
startNewGame,
} from "./master-store.svelte.js";
beforeEach(() => {
localStorage.clear();
masterState.called = [];
masterState.remaining = [];
masterState.hydrated = false;
});
describe("master-store", () => {
it("starts empty", () => {
expect(masterState.called).toEqual([]);
expect(masterState.remaining).toEqual([]);
});
it("startNewGame fills remaining with 90 unique 1..90", () => {
startNewGame();
expect(masterState.called).toEqual([]);
expect(masterState.remaining).toHaveLength(90);
const set = new Set(masterState.remaining);
expect(set.size).toBe(90);
for (let n = 1; n <= 90; n++) expect(set.has(n)).toBe(true);
});
it("drawNext appends called and shifts remaining; returns drawn", () => {
startNewGame();
const first = masterState.remaining[0];
const drawn = drawNext();
expect(drawn).toBe(first);
expect(masterState.called).toEqual([first]);
expect(masterState.remaining).toHaveLength(89);
expect(masterState.remaining.includes(first)).toBe(false);
});
it("drawNext returns null when exhausted", () => {
startNewGame();
for (let i = 0; i < 90; i++) drawNext();
expect(drawNext()).toBeNull();
expect(masterState.called).toHaveLength(90);
expect(masterState.remaining).toEqual([]);
});
it("save + load round-trip preserves state", () => {
startNewGame();
drawNext();
drawNext();
const calledBefore = [...masterState.called];
const remainingBefore = [...masterState.remaining];
saveMaster();
masterState.called = [];
masterState.remaining = [];
expect(masterState.called).toEqual([]);
loadMaster();
expect(masterState.called).toEqual(calledBefore);
expect(masterState.remaining).toEqual(remainingBefore);
});
it("loadMaster ignores corrupt JSON", () => {
localStorage.setItem("loto_master", "{not valid");
loadMaster();
expect(masterState.called).toEqual([]);
expect(masterState.remaining).toEqual([]);
});
it("loadMaster ignores non-int range entries", () => {
localStorage.setItem(
"loto_master",
JSON.stringify({ called: [1, 91], remaining: [] }),
);
loadMaster();
expect(masterState.called).toEqual([]);
});
it("loadMaster rejects payloads exceeding the size cap", () => {
const big = "x".repeat(20_000);
localStorage.setItem("loto_master", big);
loadMaster();
expect(masterState.called).toEqual([]);
});
describe("hydrated flag (reclaim-a-frozen-tab fix, H2)", () => {
it("starts false and flips true after the first loadMaster()", () => {
expect(masterState.hydrated).toBe(false);
loadMaster();
expect(masterState.hydrated).toBe(true);
});
it("flips true even when there is nothing to load", () => {
// No localStorage entry at all — still counts as "a load happened",
// so a consumer's save-effect gate (MasterPanel) is safe to write.
loadMaster();
expect(masterState.hydrated).toBe(true);
});
it("flips true even when the stored payload is corrupt", () => {
localStorage.setItem("loto_master", "{not valid");
loadMaster();
expect(masterState.hydrated).toBe(true);
});
it("re-loading picks up a peer tab's newer writes instead of this tab's stale copy", () => {
// Simulates the reclaim sequence: this tab drew a couple of numbers,
// froze (its in-memory masterState is untouched but stale), a peer
// tab drew more and persisted them, then this tab reclaims and must
// re-read localStorage before anything re-saves.
startNewGame();
drawNext();
drawNext();
saveMaster();
const staleCalled = [...masterState.called];
const thirdDraw = masterState.remaining[0];
// Peer tab: independently draws one more and persists.
const peerCalled = [...staleCalled, thirdDraw];
localStorage.setItem(
"loto_master",
JSON.stringify({
called: peerCalled,
remaining: masterState.remaining.slice(1),
}),
);
// Reclaim: claimActiveTab()'s fix re-hydrates before re-enabling.
loadMaster();
expect(masterState.called).toEqual(peerCalled);
expect(masterState.called).not.toEqual(staleCalled);
});
});
});