// What the tool says out loud between taps. // // The action bar carried `aria-live="polite"` on the element whose entire contents // render() rebuilds every action, so a screen reader read the whole bar after every // single tap. Measured, after one: // // "PRE-FLOP Pot $15 9 players Bob · $250 behind 0:00 ↶ dealing the hand U // Fold Call $10 Raise" // // Nothing in that is wrong. It is just unusable at the speed a hand is dealt, and it // is the documented anti-pattern: a live region has to be a stable element whose text // changes, not a container that gets replaced. // // What a dealer running this by ear needs is who is on the clock and what it costs // them. These are the sentences. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { ActionBar, Betting, HoldemState, makeTable } = require("./load"); const barOn = (state) => { ActionBar.state = state; ActionBar.lastAnnouncement = undefined; return ActionBar; }; describe("before the cards are out", () => { test("a table that could be dealt says so", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] }); assert.equal(barOn(state).announcement(), "Ready to deal."); }); test("and an empty table says what it is, rather than nothing", () => { // This used to announce "" — silence being the honest answer when the bar was // hidden on a table nobody was sitting at. The bar isn't hidden any more: an // empty table is the one state a dealer meets before they know how any of this // works, so it says so and offers the next step. const state = HoldemState.createDefault(); assert.equal(barOn(state).announcement(), "Nobody's at the table yet."); }); test("and a table with one named player has moved on from that", () => { const state = HoldemState.createDefault(); state.seats[0].name = "Ann"; assert.notEqual(barOn(state).announcement(), "Nobody's at the table yet."); }); }); describe("while a hand is running", () => { const dealt = () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 }); state.seats.forEach((p, i) => { p.name = ["Ann", "Ben", "Cal", "Dee"][i] || ""; }); Betting.beginHand(state); return state; }; test("names who is on the clock, what it costs, and the pot", () => { const state = dealt(); const said = barOn(state).announcement(); assert.match(said, /to act/); assert.match(said, /\$2 to call/); assert.match(said, /Pot \$3\./); }); test("it is the seat that is actually acting, not the one after it", () => { const state = dealt(); const name = HoldemState.displayName(state.seats[state.actingSeatIndex], state.actingSeatIndex); assert.ok(barOn(state).announcement().startsWith(name + " to act")); }); test("says checked to them rather than $0 to call", () => { // A number nobody owes is a number a dealer has to stop and parse. const state = dealt(); state.currentBet = 0; assert.match(barOn(state).announcement(), /checked to them/); assert.doesNotMatch(barOn(state).announcement(), /\$0 to call/); }); test("a street with nobody left to act says the street is done", () => { const state = dealt(); state.actingSeatIndex = null; state.street = "flop"; assert.equal(barOn(state).announcement(), "Flop betting is done."); }); test("and a finished hand points at the pot", () => { const state = dealt(); state.seats.forEach((p, i) => { if (i !== 0) p.folded = true; }); assert.equal(Betting.isHandOver(state), true); assert.equal(barOn(state).announcement(), "Hand over. Resolve the pot."); }); }); describe("how often it speaks", () => { // A live region that is handed the same sentence twice reads it out twice, and // render() runs for a great many reasons that have nothing to do with whose turn // it is — a resize, a panel opening, a note being typed. const stubTarget = () => { const el = { textContent: "" }; global.document = { getElementById: id => (id === "actionAnnouncer" ? el : null) }; return el; }; test("says nothing new when nothing changed", () => { const el = stubTarget(); const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 }); Betting.beginHand(state); const bar = barOn(state); bar.announce(); const first = el.textContent; assert.ok(first.length > 0); el.textContent = "CLEARED"; // if announce() writes again, this comes back bar.announce(); assert.equal(el.textContent, "CLEARED", "the same sentence was not written twice"); delete global.document; }); test("but does when the action moves", () => { const el = stubTarget(); const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 }); Betting.beginHand(state); const bar = barOn(state); bar.announce(); const first = el.textContent; Betting.call(state, state.actingSeatIndex); bar.announce(); assert.notEqual(el.textContent, first); assert.match(el.textContent, /to act/); delete global.document; }); });