mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-11 03:13:40 +00:00
139 lines
4.4 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|