// The toast that outlived the thing it was talking about. // // Toasts are news about the last action, and they carry an Undo. Take another action // and both of those go wrong at once: the sentence describes a step that is now two // steps back, and the Undo behind it restores a snapshot from before everything since. // "Next hand — button on seat 2. [Undo]" left up while a hand is dealt and three // players call is an offer to move the button, wired to a button that deletes the hand. // // It is also simply in the way. The toast is a fixed strip near the bottom of the // screen; on a 360x640 phone there is nowhere for it to sit that is clear of both the // felt and the action bar, so a stale one covers seats and controls for eight seconds. // // So the bar drops it as it takes the next action. These tests hold that funnel shut: // every action a dealer can take from the bar goes through `commit`, and `commit` // clears the toast before it does anything else. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const fs = require("fs"); const path = require("path"); const { ActionBar, Betting, makeTable } = require("./load"); const SRC = path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem"); const barSource = fs.readFileSync(path.join(SRC, "actionBar.js"), "utf8"); // A bar sat at a real table, with the toast replaced by something that keeps a log // and the re-render stubbed out — none of these rules need the DOM back. function onTable() { const state = makeTable({ seatCount: 6, stack: 200, smallBlind: 1, bigBlind: 2, dealerSeatIndex: 0 }); const log = []; globalThis.Toast = { onScreen: null, show(message) { this.onScreen = message; log.push(`show ${message}`); }, dismiss() { this.onScreen = null; log.push("dismiss"); }, }; ActionBar.state = state; ActionBar.render = () => {}; ActionBar.hooks = { beginAction: label => log.push(`begin ${label}`), save: () => log.push("save"), rerender: () => log.push("rerender"), }; return { bar: ActionBar, state, log }; } test.afterEach(() => { delete globalThis.Toast; }); describe("a toast does not outlive its own news", () => { test("the next action from the bar clears it", () => { const { bar, state, log } = onTable(); Toast.show("Next hand — button on seat 2."); bar.commit("dealing the hand", () => Betting.beginHand(state)); assert.equal(Toast.onScreen, null, "the hand is dealt; the sentence about seating the table is not news any more"); assert.ok(log.indexOf("dismiss") < log.indexOf("begin dealing the hand"), `the toast goes before the action is recorded, so nothing shown during the action is swept up with it: ${log.join(", ")}`); }); test("a toast raised by the action itself survives", () => { const { bar, log } = onTable(); bar.commit("something that talks back", () => Toast.show("Bob is all in.")); assert.equal(Toast.onScreen, "Bob is all in.", "commit clears what was there when it started, not what its own work puts up"); assert.deepEqual(log, ["dismiss", "begin something that talks back", "show Bob is all in.", "save", "rerender"]); }); test("with no toast on screen it is still just one dismiss", () => { const { bar, log } = onTable(); bar.commit("a fold", () => {}); assert.equal(log.filter(l => l === "dismiss").length, 1); }); test("a page with no Toast at all does not throw", () => { const { bar, state } = onTable(); delete globalThis.Toast; assert.doesNotThrow(() => bar.commit("dealing the hand", () => Betting.beginHand(state))); }); }); describe("every bar action really does go through that funnel", () => { // The dismiss is worth nothing if some button reaches past `commit` to mutate the // table itself. Actions are written as `this.commit(...)`; a `Betting.` call made // straight from a click handler would be one that skips the clean-up — and the // undo history with it, which is the older reason this funnel exists. test("no click handler calls the betting engine directly", () => { const strays = barSource .split("\n") .map((line, i) => [i + 1, line]) .filter(([, line]) => /=>\s*Betting\.(fold|check|call|allIn|raiseTo|beginHand|closeStreet)\(/.test(line)) .filter(([, line]) => !/this\.commit\(/.test(line)) .map(([n, line]) => `actionBar.js:${n}: ${line.trim()}`); assert.deepEqual(strays, [], "these change the table without going through commit"); }); test("commit is what clears the toast, not each button", () => { const dismissals = barSource.split("\n").filter(l => /Toast\.dismiss\(\)/.test(l)).length; // Two: this one, and the raise pad — which opens without committing anything, // and needs the strip clear because the pad makes the bar tall enough to push // a toast up over the seats. assert.equal(dismissals, 2, "a third dismissal means a button is doing by hand what commit already does"); }); });