// What happens when the saved table can't be read. // // An unreadable save can only become an empty table. What it must not do is become // one silently and then get written over by the first edit. const test = require("node:test"); const assert = require("node:assert"); const { HoldemState } = require("./load"); function withStored(value, { readOnly = false } = {}) { const store = new Map(); if (value !== undefined) store.set(HoldemState.STORAGE_KEY, value); globalThis.localStorage = { getItem: key => (store.has(key) ? store.get(key) : null), setItem(key, val) { if (readOnly) throw new Error("QuotaExceededError"); store.set(key, val); }, removeItem: key => store.delete(key), }; return store; } test.beforeEach(() => { HoldemState.loadFailed = false; HoldemState.onLoadFailed = null; }); test.after(() => { delete globalThis.localStorage; HoldemState.loadFailed = false; HoldemState.onLoadFailed = null; }); test("nothing stored is a new table, not a failure", () => { withStored(undefined); const state = HoldemState.load(); assert.equal(HoldemState.loadFailed, false); assert.ok(Array.isArray(state.seats)); }); test("a good save loads and reports nothing", () => { const store = withStored(undefined); const original = HoldemState.createDefault(); original.handNumber = 34; HoldemState.save(original); const state = HoldemState.load(); assert.equal(HoldemState.loadFailed, false); assert.equal(state.handNumber, 34); assert.equal(store.has(HoldemState.RECOVERY_KEY), false); }); test("a truncated save is reported rather than silently becoming an empty table", () => { const good = JSON.stringify(HoldemState.createDefault()); withStored(good.slice(0, Math.floor(good.length * 0.7))); const state = HoldemState.load(); assert.equal(HoldemState.loadFailed, true); assert.ok(Array.isArray(state.seats), "it still hands back a usable table"); }); test("valid JSON of the wrong shape counts as unreadable too", () => { withStored(JSON.stringify({ hello: "world" })); HoldemState.load(); assert.equal(HoldemState.loadFailed, true); }); test("the unreadable copy is kept instead of being written over", () => { // This is the half that matters: whatever a person could have picked out of the // raw JSON used to be gone the moment the dealer touched a seat. const broken = '{"seats":[{"name":"Ann","stack":250},{"name":"Ben"'; const store = withStored(broken); HoldemState.load(); assert.equal(store.get(HoldemState.RECOVERY_KEY), broken); }); test("a later save doesn't touch the copy that was set aside", () => { const broken = '{"seats":[{"name":"Ann"'; const store = withStored(broken); const state = HoldemState.load(); state.handNumber = 2; HoldemState.save(state); assert.equal(store.get(HoldemState.RECOVERY_KEY), broken); assert.notEqual(store.get(HoldemState.STORAGE_KEY), broken); }); test("the listener fires once, not on every read", () => { withStored("{not json"); let told = 0; HoldemState.onLoadFailed = () => { told++; }; HoldemState.load(); HoldemState.load(); HoldemState.load(); assert.equal(told, 1); }); test("no room to keep the copy is not a crash", () => { // Storage that refuses writes is exactly the storage most likely to hand back a // half-written value in the first place. withStored("{not json", { readOnly: true }); let state; assert.doesNotThrow(() => { state = HoldemState.load(); }); assert.ok(Array.isArray(state.seats)); assert.equal(HoldemState.loadFailed, true); });