// What a shut sidebar panel says about itself. // // The sidebar is eight
, every one exactly 60px tall with nothing in it but // its own name — 480px of a phone's screen and 480px of a 660px desktop column, saying // "Standings", "Hands", "Waiting list" and nothing else. Whether the box balances, // whether anyone is waiting for a seat, how many hands have been played: the page had // all three worked out and charged a tap for each of them, on rows two thirds empty. // // So each one now carries the fact you would have opened it for. Two rules hold this // together, and both are checked here. // // A glance appears only when it has something a dealer would act on. Eight rows that // always say something are eight rows that say nothing again, and a row reading // "Nobody waiting" all night is the clutter this went in to remove. // // And a glance never says something the panel under it would contradict. It is the // panel's own sentence with the explanation taken off, so it has to be read off the // same state — never off the panel's DOM, which for a shut
isn't rendered // at all. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { Render, HoldemState, makeTable } = require("./load"); const glances = state => Render.panelGlances(state); const textOf = (state, id) => (glances(state)[id] || {}).text; // A table part-way through a night: money on it, so the bank check has an opinion. const night = () => { const state = makeTable({ seatCount: 9, seated: [0, 1, 2, 3, 4], stack: 200, smallBlind: 5, bigBlind: 10 }); state.seats.slice(0, 5).forEach(p => { p.buyIns = 200; }); return state; }; describe("the rows that always have something to say", () => { test("the table's own shape and stakes", () => { assert.equal(textOf(night(), "glanceTableSetup"), "9 seats · $5/$10"); }); test("and the stakes drop out rather than reading as free", () => { // A table set up with no blinds yet is a real state — it is what the panel // opens on — and "9 seats · $0/$0" would be a claim about the game rather // than an admission that nobody has said yet. const state = night(); state.smallBlind = 0; state.bigBlind = 0; assert.equal(textOf(state, "glanceTableSetup"), "9 seats"); }); test("the chip values, as a range rather than a count of rows", () => { // "Are the values right for tonight's set" is the question this answers. // Five colours is the answer to a different one. assert.equal(textOf(night(), "glanceChips"), "5 colours · $1–$500"); }); test("with a chip nobody has priced yet left out of the range", () => { // A blank row in the legend is a chip the dealer hasn't got to, not a // zero-dollar chip — and letting it in would read the range as starting at $0. const state = night(); state.chipLegend.push({ id: "pink", color: "#ff88aa", value: 0 }); assert.equal(textOf(state, "glanceChips"), "5 colours · $1–$500"); }); test("and one chip is a value, not a range of one", () => { const state = night(); state.chipLegend = [{ id: "red", color: "#c0392b", value: 5 }]; assert.equal(textOf(state, "glanceChips"), "1 colour · $5"); }); }); describe("the bank check, which is the one worth a colour", () => { test("says so when the cash covers the chips", () => { const state = night(); const { text, tone } = glances(state).glanceStandings; assert.equal(text, "Balanced"); assert.equal(tone, "ok"); }); test("and names the gap when it doesn't", () => { // Chips on the table that nobody paid for. The glance is the alarm and the // amount; which seat is holding them is a paragraph, and that is the half // that stays behind the tap. const state = night(); state.seats[0].buyIns = 100; const { text, tone } = glances(state).glanceStandings; assert.equal(text, "Off by $100"); assert.equal(tone, "warn"); }); test("the same way round when the cash is the side that's over", () => { // One word covers both signs on purpose: the number and the alarm are what a // dealer acts on, and "short" against "over" is a direction they would have to // hold in their head to read. The panel says which way in a sentence. const state = night(); state.seats[0].buyIns = 400; const { text, tone } = glances(state).glanceStandings; assert.equal(text, "Off by $200"); assert.equal(tone, "warn"); }); test("and stays quiet over a table nobody has bought into", () => { // The panel hides its own check here for the same reason. A glance reading // "Balanced" over an empty table is answering for a count never taken. const state = makeTable({ seatCount: 9, seated: [], smallBlind: 5, bigBlind: 10 }); state.seats.forEach(p => { p.stack = null; p.buyIns = 0; }); assert.equal(textOf(state, "glanceStandings"), ""); assert.equal(glances(state).glanceStandings.tone, ""); }); test("and agrees with the sentence the panel would print", () => { // The two are read off one call apiece and must never disagree — a glance // saying "Balanced" over a panel saying "$40 more in chips than cash taken" // is worse than no glance at all. [0, 100, 400].forEach(buyIn => { const state = night(); state.seats[0].buyIns = buyIn; const bank = HoldemState.bankReconciliation(state); const { text, tone } = glances(state).glanceStandings; assert.equal(tone === "ok", bank.balanced, `buy-in ${buyIn}`); if (!bank.balanced) { assert.ok(text.includes(String(Math.abs(bank.difference))), `${text} doesn't carry the panel's own ${bank.difference}`); } }); }); }); describe("the rows that stay quiet until they have news", () => { test("nobody waiting, no note, no hands played yet", () => { const state = night(); assert.equal(textOf(state, "glanceWaiting"), ""); assert.equal(textOf(state, "glanceHands"), ""); assert.equal(textOf(state, "glanceNotes"), ""); }); test("and speak up the moment they do", () => { const state = night(); state.waitingList = [{ id: "w1", name: "Nadia" }, { id: "w2", name: "Owen" }]; state.handHistory = [{ handNumber: 1, pot: 120, payouts: [], ledger: [] }]; assert.equal(textOf(state, "glanceWaiting"), "2 waiting"); assert.equal(textOf(state, "glanceHands"), "1 hand"); }); test("counting one of something as one of it", () => { const state = night(); state.waitingList = [{ id: "w1", name: "Nadia" }]; state.handHistory = [{ handNumber: 1, pot: 120, payouts: [], ledger: [] }, { handNumber: 2, pot: 80, payouts: [], ledger: [] }]; assert.equal(textOf(state, "glanceWaiting"), "1 waiting"); assert.equal(textOf(state, "glanceHands"), "2 hands"); }); test("and a note shows which note it is, not that there is one", () => { // "Has notes" costs a tap to learn anything, which is the whole failure this // is fixing. The text is what identifies it; the row's own width ellipsises. const state = night(); state.notes = "Sam owes Dee $20 from last night"; assert.equal(textOf(state, "glanceNotes"), "Sam owes Dee $20 from last night"); }); test("reading past the blank lines a note gets typed under", () => { // Notes get written as a list — a heading, a blank line, then the thing. The // first line of the string is regularly not the first line of the note. const state = night(); state.notes = "\n \n Sam owes Dee $20\nand Ted owes Al $5"; assert.equal(textOf(state, "glanceNotes"), "Sam owes Dee $20"); }); test("and a note of nothing but whitespace is no note", () => { const state = night(); state.notes = "\n\n \t \n"; assert.equal(textOf(state, "glanceNotes"), ""); }); }); describe("what the row is allowed to be", () => { const ROWS = ["glanceTableSetup", "glanceStandings", "glanceWaiting", "glanceHands", "glanceChips", "glanceNotes"]; test("every glance the sidebar renders is one this knows about", () => { // The list drives the DOM writes, so a glance computed and never listed is a // row that silently keeps its old text for the rest of the night. const state = night(); assert.deepEqual(Object.keys(glances(state)).sort(), [...ROWS].sort()); assert.deepEqual([...Render.PANEL_GLANCES].sort(), [...ROWS].sort()); }); test("and none of them is a paragraph", () => { // The row is 44px of a 320px column with a label already on it. Anything // long enough to need the panel's room belongs in the panel. const state = night(); state.waitingList = [{ id: "w1", name: "Nadia" }]; state.handHistory = [{ handNumber: 1, pot: 120, payouts: [], ledger: [] }]; ROWS.filter(id => id !== "glanceNotes").forEach(id => { const text = textOf(state, id); assert.ok(text.length <= 24, `${id} is ${text.length} chars: "${text}"`); assert.ok(!text.includes("\n"), `${id} has a line break in it`); }); }); test("including a note, which is trimmed to one line and left to the ellipsis", () => { // The one glance whose length isn't ours to choose. It must still be a single // line — a second one would make the row taller, and the row is what holds the // sidebar to eight equal slabs. const state = night(); state.notes = "x".repeat(400) + "\nsecond line"; const text = textOf(state, "glanceNotes"); assert.ok(!text.includes("\n")); assert.equal(text, "x".repeat(400)); }); test("and a state that isn't there yet asks for nothing", () => { // renderAll runs before the first load has finished on a cold open. assert.deepEqual(Render.panelGlances(null), {}); assert.deepEqual(Render.panelGlances(undefined), {}); }); });