Files
JoshHeaps.Net/tests/holdem/theBoardInTheMiddle.test.js
T
jheapsandClaude Opus 5 2e372d4677 Take the glass off the middle of the table
The board — the five community cards a dealer taps to enter the flop, the
turn and the river — could not be pressed at all. #seatsLayer is an
`inset: 0` sibling of the table that holds the seat pucks, and an element
with no background still hit-tests, so the empty middle of that layer took
every tap aimed at what was underneath it. Measured at 1280x900 with five
cards out: all five hit-tested to `div#seatsLayer`, on every screen wide
enough to keep the board in the middle rather than on the strip. #betsLayer,
the other sheet over the felt, has been pointer-events: none since it was
written.

Underneath that, the cards were drawing at a little over half their size.
placeBoard measures how much middle the felt has and writes it to the
centre's max-width — but max-width caps a box and does not claim anything,
and the centre is a flex column that sizes to its content. So with 463px of
measured room it went on being 136px wide, the row inside it is width: 100%
of that, and five cards sharing 136px come to rest on their own min-width:
24px, against the 44px floor every other control on the felt is held to.

Neither was visible to anything. A scripted probe calling .click() on the
node skips hit-testing entirely, and the sweep's occlusion check had only
ever been shown three selectors' worth of the page — the action bar, the
header and the seats. It asks about every control now, which took three
refinements to be honest: a control that set pointer-events: none has opted
out rather than been covered; a shut <details> hands out a real rect for
everything in it, and checkVisibility is the browser's own answer; and flow
content under a fixed layer is a position rather than a fault, so it is said
rather than failed.

The five sheets a dealer spends half the night in were getting one rule of
twenty-five. They get all of them now. Three rules had to learn what a modal
is first: two ask about the fixed bar, which is not a question while a scrim
is covering it, and the third called the bottom four rows of a sheet that
scrolls unreachable buttons. The helper all three lean on had its own blind
spot — it asked whether an element had left its box entirely, which is true
of something well below the fold and false of something hanging eight pixels
over the edge of it.

That turned up two controls that were a glyph and nothing else: the pot
wizard's close, on the one dialog that divides money, and the cross that
removes a custom field, in a column of identical crosses.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-14 00:01:37 -06:00

210 lines
11 KiB
JavaScript

// The five cards in the middle of the felt, and the sheet of glass over them.
//
// Two faults, one on top of the other, and the sweep could see neither.
//
// #seatsLayer is a transparent `inset: 0` sibling of the table that holds the seat
// pucks. The pucks ring the felt; the middle of it is empty layer — and an element with
// no background still hit-tests. So every tap aimed at the community cards landed on the
// layer instead. Measured at 1280x900 with the board on and five cards out: all five
// hit-tested to `div#seatsLayer`, on every screen wide enough to keep the board in the
// middle rather than on the strip. Each one is a button that opens the picker for that
// card, and not one of them could be pressed. #betsLayer, the other sheet over the felt,
// has been `pointer-events: none` since it was written.
//
// Underneath that, the cards were a little over half the size they were meant to be.
// `placeBoard` measures how much middle the felt has and writes it to the centre's
// `max-width` — but a max-width caps a box, it does not claim anything, and the centre
// is a flex column that sizes to its content. So with 463px of measured room it went on
// being 136px wide, the board row inside it is `width: 100%` of that, and five cards
// sharing 136px come to rest on their own `min-width`: 24px, against a 44px floor this
// sweep holds every other control on the felt to. On the strip, where the row simply
// gets the width of the page, the same cards have always drawn 44.
//
// Neither was visible to a scripted probe — `.click()` on a node skips hit-testing
// entirely — and the sweep's occlusion check had only ever been shown three selectors'
// worth of the page: the action bar, the header, and the seats.
const test = require("node:test");
const { describe } = require("node:test");
const assert = require("node:assert");
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "..", "JoshHeaps.Net");
const CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "holdem.css"), "utf8");
const AUDIT = fs.readFileSync(path.join(__dirname, "uiAudit.js"), "utf8");
const DRIVER = fs.readFileSync(path.join(__dirname, "runUiAudit.js"), "utf8");
const src = name => fs.readFileSync(path.join(ROOT, "wwwroot", "js", "casino", "holdem", `${name}.js`), "utf8");
const ruleFor = selector => {
const at = CSS.indexOf(`\n${selector} {`);
assert.ok(at >= 0, `there is no \`${selector}\` rule`);
return CSS.slice(at, CSS.indexOf("}", at));
};
// ---------------------------------------------------------------------------
describe("the sheet over the felt", () => {
test("it does not take taps meant for what is under it", () => {
assert.match(ruleFor("#seatsLayer"), /pointer-events:\s*none/);
});
// Which is only safe because the thing in it that *is* a control says so itself.
test("but a seat still does", () => {
assert.match(ruleFor(".seat"), /pointer-events:\s*auto/);
});
// The two chips that want to stay untouchable set their own value and are inside a
// seat, so they override what they inherit rather than depending on the layer.
test("and the two pucks that are markers rather than controls still are", () => {
assert.match(ruleFor(".dealerChip:not(.active)"), /pointer-events:\s*none/);
assert.match(ruleFor(".dealerChip.active"), /pointer-events:\s*none/);
});
test("the other sheet is unchanged, and was always right", () => {
assert.match(ruleFor("#betsLayer"), /pointer-events:\s*none/);
});
test("what it measured is written beside the rule", () => {
const at = CSS.indexOf("/* A transparent sheet over the whole felt");
assert.ok(at >= 0, "the rule carries no note saying why");
const note = CSS.slice(at, CSS.indexOf("*/", at));
assert.match(note, /1280x900/);
assert.match(note, /seatsLayer/);
});
});
describe("how wide the middle actually is", () => {
test("the room it measured is claimed, not only capped", () => {
const render = src("render");
const at = render.indexOf("placeBoard(row, strip) {");
assert.ok(at >= 0, "there is no placeBoard");
const body = render.slice(at, render.indexOf("\n },", at));
assert.match(body, /centre\.style\.width = `\$\{Math\.round\(width\)\}px`/,
"the centre is still given a ceiling and no floor, so it shrink-wraps");
assert.match(body, /centre\.style\.maxWidth = `\$\{Math\.round\(width\)\}px`/);
});
// Both come off together when the board leaves the felt, or the centre would keep a
// width measured for a board that is no longer in it.
test("and both are given back when the board moves to the strip", () => {
const render = src("render");
const at = render.indexOf("placeBoard(row, strip) {");
const body = render.slice(at, render.indexOf("} else {", at));
assert.match(body, /centre\.style\.width = ""/);
assert.match(body, /centre\.style\.maxWidth = ""/);
});
// The floor is what it always said it was; what changed is that the cards can now
// reach past it to the ceiling.
test("a card is still allowed to grow to a thumb", () => {
const rule = ruleFor(".boardCard");
assert.match(rule, /max-width:\s*44px/);
assert.match(rule, /min-height:\s*44px/);
assert.match(rule, /flex:\s*1 1 0/);
});
test("what it measured is written beside the code", () => {
const render = src("render");
const at = render.indexOf("// Both, and the width is the half that was missing.");
assert.ok(at >= 0, "the fix carries no note saying why");
const note = render.slice(at, at + 1400);
assert.match(note, /463px/);
assert.match(note, /24px/);
});
});
describe("why the sweep could not see either of them", () => {
// Three selectors: the bar, the header and the seats. Nothing else on the page had
// ever been asked whether something was sitting on top of it.
test("the occlusion check looks at every control now, not three groups", () => {
const at = AUDIT.indexOf("occlusion(selector = null) {");
assert.ok(at >= 0, "occlusion still hard-codes its own list");
assert.match(AUDIT.slice(at, at + 200), /selector = selector \|\| this\.CONTROL_SELECTOR/);
});
// Widening it put three kinds of non-fault in front of the rule for the first time.
test("a control that opted out of the pointer is not reported as covered", () => {
const at = AUDIT.indexOf("occlusion(selector = null) {");
const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at));
assert.match(body, /getComputedStyle\(el\)\.pointerEvents === "none"/);
});
test("nor is one inside a disclosure nobody has opened", () => {
const at = AUDIT.indexOf("occlusion(selector = null) {");
const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at));
assert.match(body, /el\.checkVisibility/,
"a shut <details> still hands out a real rect, and a size check lets it through");
});
test("and flow content under a fixed layer is said rather than failed", () => {
const at = AUDIT.indexOf("occlusion(selector = null) {");
const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at));
assert.match(body, /scrolls clear of it/);
});
});
describe("the rest of what the sweep had never run", () => {
// It opened five sheets and asked one question of them.
test("an overlay gets the whole audit, not the occlusion check alone", () => {
const at = DRIVER.indexOf("for (const [name, open] of OVERLAYS) {");
assert.ok(at >= 0);
const body = DRIVER.slice(at, DRIVER.indexOf("record(`${name}", at));
assert.match(body, /LayoutAudit\.runAll\(/);
assert.ok(!/return LayoutAudit\.occlusion\(/.test(body),
"the overlay pass still runs one rule");
});
// Two of the rules that made running only one of them reasonable ask about the
// fixed bar, which is not a question while a scrim is covering the page.
test("the two questions about the fixed bar stand down while a sheet is open", () => {
["focusHasRoom() {", "furnitureBudget() {"].forEach(signature => {
const at = AUDIT.indexOf(signature);
assert.ok(at >= 0, `there is no \`${signature}\``);
const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at));
assert.match(body, /if \(this\.modalScope\(\)\) \{/,
`${signature} still reads a full-screen scrim as furniture`);
});
});
// And the third called the bottom four rows of a sheet that scrolls unreachable.
test("a dialog's buttons below the fold are only a fault if nothing scrolls to them", () => {
const at = AUDIT.indexOf("dialogActionsReachable() {");
const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at));
assert.match(body, /if \(this\.reachableByScrolling\(button\)\) return;/);
});
// The helper all three of those lean on had a blind spot of its own: it asked
// whether the element had left the box entirely, which is true of something well
// below the fold and false of something hanging eight pixels over the edge.
test("and reachability counts a control that is only part-way out", () => {
const at = AUDIT.indexOf("reachableByScrolling(el) {");
const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at));
assert.match(body, /const below = rect\.bottom - box\.bottom/);
assert.match(body, /const above = box\.top - rect\.top/);
// The comment above the fix quotes the old test, so the code is read on its own.
const code = body.split("\n").filter(line => !/^\s*\/\//.test(line)).join("\n");
assert.ok(!/rect\.top >= box\.bottom/.test(code),
"it still asks whether the element has left the box entirely");
});
});
describe("two controls that were a glyph and nothing else", () => {
test("the pot wizard's close is named for what leaving does", () => {
const markup = fs.readFileSync(path.join(ROOT, "Pages", "Casino", "TexasHoldem.cshtml"), "utf8");
const at = markup.indexOf('id="closePotWizardBtn"');
assert.ok(at >= 0);
assert.match(markup.slice(at, markup.indexOf(">", at)), /aria-label="Close without paying the pot"/);
});
// Named from the label the dealer typed, and kept up with it — the same shape the
// chip legend's remove button already used, in a column of identical crosses.
test("and a custom field's remove says which field", () => {
const render = src("render");
const at = render.indexOf("renderCustomFields(form, player, index) {");
const body = render.slice(at, render.indexOf("\n },", at));
assert.match(body, /removeBtn\.setAttribute\("aria-label"/);
assert.match(body, /labelInput\.value\.trim\(\)/);
assert.match(body, /nameRemove\(\);/);
});
});