runAll() did not include the contrast check — it simply wasn't in the list — and
touch targets sat behind an opt-in flag nothing passed. So "the audit is clean"
has meant nine checks rather than eleven for a long run of rounds, and the two
left out are the two whose faults a dealer meets in a dim room with their thumb.
Both were written, tested, and then never called; the contrast checker was built
across two rounds and every sweep since has been silent about colour.
The standalone runner had a second version of the same hole: the 44px rule
stands itself down where `hover: hover` says there's a pointer, and a headless
desktop browser is a pointer, so a single pass had never measured a touch target
at any viewport. It now runs every viewport twice, once with a mouse and once
with a thumb.
A check that decides it doesn't apply now says so, rather than reporting an empty
offenders list — which reads exactly like a pass.
Faded text is measured instead of filed as unchecked. `opacity` on a box fades
the box and its text together onto whatever is behind it, so a dimmed seat is not
grey text on a dark puck: it is a faded puck and faded text both landing on the
page. Both halves are composited that way now. Two exemptions remain and both say
so out loud: a disabled control, which WCAG itself excuses, and a link inside a
sentence, whose size is set by the line it sits in.
What running them found, and this round fixes:
- the chair number on every seat: #8a8a8a at 9px, 3.39:1 once the empty seat's
fade is composited, on the label that answers "which seat is that?";
- Quick Split's amount field at 37px tall and its delete button at 30px across,
on a page whose targets had never been measured;
- and on a 360x640 phone with the bar grown by a side pot, the bottom seat
sitting 38px inside it.
That last one brings back a cap this project measured and rejected once: capping
the felt to the room left fixed the six-seat ring and broke the nine-seat one.
That trade was real then and isn't now — seats are spaced by distance around the
rim rather than by angle, so a shorter felt no longer piles them up. Re-measured
across 30 combinations of five phone sizes and 2 to 12 seats: every one clean
except twelve seats at 360x640, where a 240px felt and a 64px puck cannot both be
true. Where the rim can't give each seat a puck's width, the pucks come down a
size, measured from the rim itself rather than from a media query.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
99 lines
5.2 KiB
Markdown
99 lines
5.2 KiB
Markdown
# Hold'em dealer tests
|
|
|
|
Unit tests for the money logic behind `/Casino/TexasHoldem` — the betting engine, the
|
|
side-pot calculator, the split calculator and the table state.
|
|
|
|
```bash
|
|
node --test tests/holdem/*.test.js
|
|
```
|
|
|
|
No install step, no `package.json`, no dependencies. The browser modules are plain
|
|
script-tag files that each end with a `module.exports` guard, so Node can require them
|
|
as-is; `load.js` puts them on `globalThis` under the same names they use to find each
|
|
other in the page, in the load order from `TexasHoldem.cshtml`.
|
|
|
|
## What's covered
|
|
|
|
| File | Covers |
|
|
| --- | --- |
|
|
| `potCalculator.test.js` | Main and side pots, dead money, uncalled bets, odd-cent distribution |
|
|
| `betting.test.js` | Blinds, street completion, raises reopening the action, all-in run-outs |
|
|
| `state.test.js` | Button rotation, which seats post blinds, session standings |
|
|
| `splitCalculator.test.js` | Chopped pots, odd chips, unmakeable denominations |
|
|
| `invariants.test.js` | 1000 randomised hands per property (see below) |
|
|
|
|
## The layout audit
|
|
|
|
The unit tests above cover the money. `uiAudit.js` covers the thing you can only see
|
|
in a browser: whether a dealer can actually reach every control.
|
|
|
|
```bash
|
|
node tests/holdem/runUiAudit.js # needs Playwright and the site running
|
|
```
|
|
|
|
It checks WCAG AA contrast, touch target sizes, touch-action and tap highlights, that fields stay selectable and are large
|
|
enough not to trigger iOS zoom, that nothing important is covered or off-screen with
|
|
no way to scroll to it, that no grid column has collapsed to nothing, and that no
|
|
text is painted outside its box. It runs over the table, all five overlays and Quick
|
|
Split, at thirteen viewports from a 360px phone to a 1600px desktop and the same
|
|
phones on their side — each viewport twice, once with a mouse and once with a thumb.
|
|
|
|
That last part, and two of those checks, are newer than this paragraph claiming them.
|
|
`runAll()` did not include the contrast check at all, and touch targets sat behind an
|
|
opt-in flag nothing passed, so "the audit is clean" meant nine checks rather than
|
|
eleven for a long run of rounds. Both were written and tested and simply never called.
|
|
The mouse/thumb pair matters for the same reason: the 44px rule stands itself down
|
|
where `hover: hover` says there is a pointer, and a headless desktop browser is a
|
|
pointer, so a single pass had never measured a touch target at any viewport. Between
|
|
them they found a 9px seat label at 3.39:1, a 37px-tall amount field and a 30px delete
|
|
button on Quick Split.
|
|
|
|
A check that decides it doesn't apply says so in the output rather than reporting no
|
|
offenders, which reads identically to a pass. The same goes for the two exemptions
|
|
that exist: a disabled control, which WCAG itself excuses, and a link inside a
|
|
sentence, whose size is set by the line it sits in.
|
|
|
|
Deliberately not in CI: the unit suite is dependency-free and runs in a second, while
|
|
this one needs a browser and a running site. Run it after any change that adds or
|
|
resizes a control. With no Playwright to hand, paste `uiAudit.js` into devtools and
|
|
call `LayoutAudit.runAll()`.
|
|
|
|
The contrast check is built so it cannot report a false positive: backgrounds
|
|
composite through alpha, and where a gradient is involved every colour stop is tried
|
|
and the most flattering one used. A finding means the text fails even under the
|
|
reading most generous to it. Two earlier attempts at this got it wrong in both
|
|
directions — one read only `background-color` and scored text on a gradient against
|
|
nothing, the other treated low-alpha stops as opaque — so `uiAudit.js` is checked
|
|
against known-answer cases before it is believed.
|
|
|
|
Touch targets are measured by probing outward with `elementFromPoint` rather than by
|
|
reading the element box, because the box lies in both directions: a small control can
|
|
be padded out with a transparent `::after` and respond well beyond its painted size,
|
|
and a checkbox inside a label is hit by tapping the label. The 44px rule is only
|
|
enforced where `hover: none` says there's no mouse.
|
|
|
|
It exists because these checks were being re-typed into a console every time
|
|
something changed, and each time they only covered whatever had just been touched.
|
|
That is how the standings table shipped with a player-name column zero pixels wide
|
|
and kept it for two rounds.
|
|
|
|
## Why the randomised suite exists
|
|
|
|
Every correctness bug found in this tool so far has been shape-dependent: blinds posted
|
|
to empty chairs, the button skipping past players, heads-up acting out of order, an
|
|
overbet announced as a side pot. None of them show up on a full six-handed table with
|
|
equal stacks, which is the table anyone writes a fixed example for.
|
|
|
|
`invariants.test.js` deals a thousand hands across random seat counts, random empty
|
|
chairs, random stack depths and random dealer input, and checks the things a dealer
|
|
would notice being wrong:
|
|
|
|
- chips are never created or destroyed during play
|
|
- the pot breakdown plus the uncalled returns always equal the money that went in
|
|
- paying the pot out settles it to the cent
|
|
- once uncalled chips are handed back, nobody is still betting above the whole table
|
|
- the button and both blinds always land on players who are actually in the hand
|
|
|
|
The generator is seeded, so a failure reproduces from the seed printed in the message
|
|
rather than being a flake to re-run.
|