Give the sidebar eight names instead of five and a cupboard

"Session" was a container word rather than a name. It held the standings,
a waiting list, the hand log and the save/load/reset buttons — a topic, a
task, a log and three utilities under one label, which is a navigation
layer with more than one logic in it.

It measured 1206px and 103 visible elements, against 555 and 29 for the
next biggest, and "The table itself" sat 1052px down inside it. Reaching
Reset table meant opening a panel called Session and scrolling past the
money and the log to get to it.

It is now Standings, Waiting list, Hands and Save & reset, each named for
the thing a dealer came looking for, in the order of a night: set the
table up, watch the money, fill the seats, look back at a hand — and,
last and apart, the two buttons that end it. Reset table goes from 1052px
inside a panel called Session to 62px inside one whose label says what it
does. The biggest panel drops from 1206px to 607px. The four subheads
that used to carry those names are gone, because the summary says it now.

Every panel name is also a heading. A <summary> is announced as a button,
so the sidebar's names — its whole navigation — were unreachable by
heading, and splitting Session would have removed the only four headings
down there. The outline is now h1 page, h2 per panel, h3 for the two
subheads that remain. Visually identical: the h2 gives up everything a
heading brings with it.

The cost is 216px more collapsed summaries at rest, three more one-line
labels. Audit unchanged at 2 failing configurations and 3 offenders, both
pre-existing. 795 unit tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-05 21:52:34 -06:00
co-authored by Claude Opus 5
parent bd8ee11f77
commit 6eb97c8a35
4 changed files with 242 additions and 33 deletions
+34
View File
@@ -25,6 +25,7 @@ other in the page, in the load order from `TexasHoldem.cshtml`.
| `theRail.test.js` | A phone on its side: which shape the bar is in, what it leaves under the felt, how a name is shortened, how the ring is pulled back onto the table |
| `feltShape.test.js` | What shape the felt is at each phone size, resolved through the media queries in source order (see below) |
| `potBreakdown.test.js` | How each pot says who can win it, and the nesting property that wording rests on (see below) |
| `sidebarPanels.test.js` | What the sidebar's panels are called and what lives in each (see below) |
| `invariants.test.js` | 1000 randomised hands per property (see below) |
## The layout audit
@@ -211,6 +212,39 @@ everything, which is how it survived long enough to be written down. Both readin
silent. Prefer asserting the count you expect over comparing two things whose shapes you
have not checked.
## Testing a fault that broke nothing
`sidebarPanels.test.js` reads `TexasHoldem.cshtml` rather than any JavaScript, because
the fault it covers was not a bug. Every control worked and every id resolved. What was
wrong was which panel a thing was in and what that panel was called.
The sidebar was Table Setup, **Session**, Chip Legend, Keyboard and Table Notes, and
Session held the standings, a waiting list, the hand log and the save/load/reset buttons
— a topic, a task, a log and three utilities under one word. It measured 1206px and 103
visible elements against 555 and 29 for the next biggest, and "The table itself" sat
1052px down inside it, so reaching Reset table meant opening a panel called Session and
scrolling past the money and the log to find it.
Three things are asserted, and each of them is a claim a future edit could quietly undo:
- **The names say what is inside them.** The list is checked literally, because the
failure mode is a word like "Session" that could hold anything.
- **Each name is a heading.** A `<summary>` is announced as a button, so the sidebar's
names — its whole navigation — were not reachable by heading. Splitting Session also
removed the four subheads that had been the only headings down there, so without a
heading in each summary the page would have come out of this with a worse outline than
it went in with.
- **No panel goes back to being a grab-bag.** Not a size limit — Table Setup is
legitimately long. It counts *subjects*: a panel that needs its own headings to list
its parts is holding more than one thing. Two subheads survive, and the test names
them, because Settle up is the standings at the end of the night and Stacks in chips
is the chip legend counted out — the same subject at a different moment, which is what
a subhead is for.
The panels are split on the opening tag rather than parsed, which is sound only because a
sidebar panel never contains another one; Table Setup's sub-panels carry a different
class. That assumption is worth knowing about if one ever does.
## Why the hand evaluator is tested the way it is
`handRank.js` is the only module here that can pay the wrong person without anybody
+124
View File
@@ -0,0 +1,124 @@
// What the sidebar is a list of.
//
// The panels were Table Setup, Session, Chip Legend, Keyboard and Table Notes, and
// "Session" was a container word rather than a name. It held the standings, a waiting
// list, the hand log and the save/load/reset buttons — a topic, a task, a log and three
// utilities under one label, which is the "don't mix apples and oranges" failure: a
// navigation layer with more than one logic in it. Measured at 1206px and 103 visible
// elements, against 555 and 29 for the next biggest, and "The table itself" sat 1052px
// down inside it, so reaching Reset table meant opening a panel called Session and
// scrolling past the money and the log.
//
// This is a markup test because that is where the fault lived. Nothing was broken —
// every control worked and every id resolved — so nothing failed. What was wrong was
// which panel a thing was in and what that panel was called.
const test = require("node:test");
const { describe } = require("node:test");
const assert = require("node:assert");
const fs = require("fs");
const path = require("path");
const PAGE = fs.readFileSync(
path.join(__dirname, "..", "..", "JoshHeaps.Net", "Pages", "Casino", "TexasHoldem.cshtml"), "utf8");
// A sidebar panel never contains another one — Table Setup's sub-panels carry a
// different class — so splitting on the opening tag is enough to say which panel any
// piece of markup is in, without parsing the document.
const panels = () => PAGE
.split(/<details class="sidebarPanel"/)
.slice(1)
.map(chunk => {
const name = chunk.match(/<summary>\s*(?:<h2>)?([^<]+)/);
return { name: name ? name[1].trim() : "(no summary)", markup: chunk };
});
const panelHolding = id => {
const found = panels().filter(p => p.markup.includes(`id="${id}"`));
assert.equal(found.length, 1, `${id} is in ${found.length} panels, expected exactly 1`);
return found[0].name;
};
// ---------------------------------------------------------------------------
describe("the sidebar's names", () => {
test("say what is inside them", () => {
// Every one of these is a thing a dealer goes looking for. None of them is a
// word that could hold anything — which is the test "Session" fails.
assert.deepEqual(panels().map(p => p.name), [
"Table Setup",
"Standings",
"Waiting list",
"Hands",
"Chip Legend",
"Keyboard",
"Table Notes",
"Save &amp; reset",
]);
});
test("and each one is a heading, not only a button", () => {
// A <summary> is announced as a button. The sidebar's names are the whole of its
// navigation, so without a heading inside, none of them is reachable by anyone
// moving through the page by heading — and splitting Session removed the four
// subheads that used to be the only headings down here.
panels().forEach(p => assert.match(p.markup, /<summary><h2>/,
`the ${p.name} panel's name is not a heading`));
});
});
describe("where things live", () => {
test("the money is together, and the warning about losing it is with it", () => {
assert.equal(panelHolding("standingsList"), "Standings");
assert.equal(panelHolding("settleUp"), "Standings");
assert.equal(panelHolding("bankReconcile"), "Standings");
// renderStorageWarning() says this "sits at the top of the panel a dealer opens
// to check the money". That panel is now called Standings.
assert.equal(panelHolding("storageWarning"), "Standings");
});
test("the queue for a seat is its own panel", () => {
assert.equal(panelHolding("waitingList"), "Waiting list");
assert.equal(panelHolding("waitingListForm"), "Waiting list");
});
test("the log and the two ways out of it are together", () => {
assert.equal(panelHolding("handHistoryList"), "Hands");
assert.equal(panelHolding("handHistoryFilter"), "Hands");
assert.equal(panelHolding("exportHistoryBtn"), "Hands");
assert.equal(panelHolding("clearHistoryBtn"), "Hands");
});
test("and the file is last, with the danger on the label", () => {
assert.equal(panelHolding("saveTableBtn"), "Save &amp; reset");
assert.equal(panelHolding("loadTableBtn"), "Save &amp; reset");
assert.equal(panelHolding("resetTableBtn"), "Save &amp; reset");
assert.equal(panels()[panels().length - 1].name, "Save &amp; reset",
"the panel that ends the night belongs at the end, away from the working ones");
});
});
describe("no panel goes back to being a grab-bag", () => {
// Not a size limit — Table Setup is legitimately long and Chip Legend legitimately
// repeats a row per chip. What this counts is *subjects*: a panel that needs its own
// headings to say what its parts are is a panel holding more than one thing, which
// is what Session was and how it got that way.
test("at most one subject needs a heading inside a panel", () => {
panels().forEach(p => {
const subheads = [...p.markup.matchAll(/<h3 class="panelSubhead">([^<]+)</g)]
.map(m => m[1].trim());
assert.ok(subheads.length <= 1,
`${p.name} has ${subheads.length} sections inside it (${subheads.join(", ")}) — ` +
`if a panel needs headings to list its parts, its name is not doing the work`);
});
});
test("and the ones that remain are a detail of their panel, not a second subject", () => {
// Settle up is the standings once the night ends; Stacks in chips is the chip
// legend counted out. Both are the same subject at a different moment, which is
// the case a subhead is for.
const kept = panels().flatMap(p =>
[...p.markup.matchAll(/<h3 class="panelSubhead">([^<]+)</g)]
.map(m => `${p.name}: ${m[1].trim()}`));
assert.deepEqual(kept, ["Standings: Settle up", "Chip Legend: Stacks in chips"]);
});
});