Files
JoshHeaps.Net/tests/holdem/theWaysControl.test.js
T
jheapsandClaude Opus 5 2d80edd513 Say how many ways once, in the one control that owns it
"Split between" on Quick Split was two controls for one value stacked on
each other: chips 2 3 4 5, and under them a bar reading "2 ways" with a −
and a +. The count was written down three times at once — the lit chip,
the bar, and "Each of 2 players gets" in the panel above — and two of the
three were controls. 134px with its label, against 74px for "Smallest
chip in play" right below it, which offers five choices in one row.

The bar was never meant to be a bar. `.waysStepper` asked for a 150px
min-width inside a wrapping flex row, so on a phone it always wrapped to a
line of its own, and `flex: 1 1 auto` then gave it all 358px of that line:
252px of empty readout with the − and + pushed 304px apart, on a screen
390px wide. Now it is 136px with its ends 121px apart, and it only exists
once you are through the door — a fifth chip reading "6+", behind which
the stepper is where the count is written. Under six ways, which is every
chop a home game will run, the field is one row and 74px.

Two more things went with it:

Neither stepper ever disabled an end, so − at the floor and + at the
ceiling looked live and swallowed the tap. NN/G's stepper guidance asks
for the opposite. Both ends of both steppers now grey out.

The per-pot chop capped its + with `Math.max(contenderCount, MAX_WAYS)`,
which is always 12 — a side pot only P1 and P2 were eligible for would
happily chop ten ways at $12 each. It is `chopCeiling()` now, and a
two-contender pot is offered no stepper at all, that being the common
shape.

The sweep went straight to the all-in mode and audited only that, so the
mode the screen opens in had never been looked at in fourteen rounds,
which is how a 358px bar lasted this long. It audits both now — 428
audits, clean — and the first run of it found the step buttons had been
40px wide rather than 44 since they were written.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-11 22:24:49 -06:00

225 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// One number, one control, and a ceiling that matches the table.
//
// "Split between" on Quick Split was two controls for one value sitting on top of each
// other: a row of chips 2 3 4 5, and under it a bar reading "2 ways" with a − and a +.
// Measured on an iPhone: 134px with its label, against 74px for "Smallest chip in play"
// right below it, which offers five choices in one row. And the number was written down
// three times at once — chip "2" lit gold, "2 ways" in the bar, and "Each of 2 players
// gets" in the result panel above. Two of those three were controls.
//
// The bar was never meant to be a bar. `.waysStepper` asked for `min-width: 150px` in a
// wrapping flex row, so on any phone it wrapped to a line of its own, and `flex: 1 1
// auto` then gave it all 358px of that line: 252px of empty readout with the − and the
// + pushed 304px apart. One-handed, on a screen 390px wide, they cannot both be reached.
//
// So the chips get a fifth chip. It reads "6+" whatever is behind it, because behind it
// is where the count is written, and the stepper only exists once you are through the
// door. Under six ways — every chop a home game will ever run — the field is one row.
//
// Two more things went with it. Neither stepper on this screen ever disabled an end, so
// − at the floor and + at the ceiling looked live and swallowed the tap; NN/G's stepper
// guidelines ask for exactly the opposite. And the per-pot chop capped its + with
// `Math.max(contenderCount, MAX_WAYS)`, which is always 12: a side pot that only P1 and
// P2 were eligible for would happily be chopped ten ways at $12 each.
const test = require("node:test");
const { describe } = require("node:test");
const assert = require("node:assert");
const fs = require("fs");
const path = require("path");
const { QuickSplit } = require("./load");
const ROOT = path.join(__dirname, "..", "..", "JoshHeaps.Net");
const CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "quicksplit.css"), "utf8");
const SRC = fs.readFileSync(path.join(ROOT, "wwwroot", "js", "casino", "holdem", "quickSplit.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("where the chips stop and the stepper starts", () => {
test("the door is the first count the chips don't cover", () => {
assert.deepEqual(QuickSplit.WAY_CHOICES, [2, 3, 4, 5]);
assert.equal(QuickSplit.overflowFrom(), 6);
});
// Derived rather than written down twice: adding a chip has to move the door.
test("and it moves if a chip is added or taken away", () => {
const was = QuickSplit.WAY_CHOICES;
try {
QuickSplit.WAY_CHOICES = [2, 3];
assert.equal(QuickSplit.overflowFrom(), 4);
} finally {
QuickSplit.WAY_CHOICES = was;
}
});
test("the stepper's own range is what the door promised", () => {
assert.equal(QuickSplit.clampWays(QuickSplit.overflowFrom() - 1), 5);
assert.equal(QuickSplit.clampWays(QuickSplit.MAX_WAYS + 4), QuickSplit.MAX_WAYS);
assert.equal(QuickSplit.clampWays(0), QuickSplit.WAY_CHOICES[0]);
assert.equal(QuickSplit.clampWays(-3), 2);
});
test("the door is drawn once, from the same number", () => {
const at = SRC.indexOf("renderWayChoices() {");
const body = SRC.slice(at, SRC.indexOf("\n },", at));
assert.match(body, /`\$\{this\.overflowFrom\(\)\}\+`/,
"the chip's label is typed in rather than taken from where the chips stop");
assert.match(body, /aria-label/, "\"6+\" is not a name a screen reader can read out");
});
// The chips and the stepper are one group: when the count is past the chips, no
// chip is lit, and the door is.
test("nothing is lit in two places at once", () => {
const at = SRC.indexOf("renderWayChoices() {");
const body = SRC.slice(at, SRC.indexOf("\n },", at));
assert.match(body, /const beyond = this\.ways >= this\.overflowFrom\(\)/);
assert.match(body, /!beyond && this\.ways === count/,
"a preset chip can still light up while the stepper holds a different number");
assert.match(body, /if \(beyond\) row\.appendChild\(this\.buildWaysStepper\(\)\)/,
"the stepper is drawn even when the chips are the control");
});
test("and the count is written in one place, without a unit the label already gave", () => {
const at = SRC.indexOf("buildWaysStepper() {");
assert.ok(at >= 0);
const body = SRC.slice(at, SRC.indexOf("\n },", at));
assert.match(body, /readout\.textContent = String\(this\.ways\)/);
assert.ok(!/\$\{this\.ways\} ways/.test(body), "the stepper is spelling the count out again");
});
});
describe("both ends of both steppers", () => {
test("a step that cannot happen says so", () => {
const at = SRC.indexOf("makeStepButton(label, onPick, options) {");
assert.ok(at >= 0, "the step buttons still take no disabled state");
assert.match(SRC.slice(at, SRC.indexOf("\n },", at)), /btn\.disabled = /);
});
test("the ways stepper disables its floor and its ceiling", () => {
const at = SRC.indexOf("buildWaysStepper() {");
const body = SRC.slice(at, SRC.indexOf("\n },", at));
assert.match(body, /disabled: this\.ways <= this\.overflowFrom\(\)/);
assert.match(body, /disabled: this\.ways >= this\.MAX_WAYS/);
});
test("so does the per-pot chop", () => {
const at = SRC.indexOf("addChopControl(row, amount, contenderCount) {");
const body = SRC.slice(at, SRC.indexOf("\n updateUneven()", at));
assert.match(body, /disabled: ways <= 2/);
assert.match(body, /disabled: ways >= ceiling/);
});
// 40x44 until the sweep first opened the even mode and read back `hittable 39x43`
// on every touch viewport there is.
test("and both ends are a fingertip wide, not just a fingertip tall", () => {
const rule = ruleFor(".stepBtn");
const px = axis => Number((rule.match(new RegExp(`${axis}:\\s*(\\d+)px`)) || [])[1]);
assert.ok(px("width") >= 44, `a step button is ${px("width")}px wide`);
assert.ok(px("height") >= 44, `a step button is ${px("height")}px tall`);
});
// Disabled has to look disabled, or it is the same silent tap with extra steps.
test("and a disabled end is painted as one", () => {
assert.match(ruleFor(".stepBtn:disabled"), /opacity:\s*0\.\d+/);
assert.match(CSS, /\.stepBtn:not\(:disabled\):hover/,
"a dead button still lights up under the mouse");
});
});
describe("how many ways a pot can go", () => {
// The whole of the bug: `Math.max` where `Math.min` was meant.
test("never more ways than there are players who can win it", () => {
assert.equal(QuickSplit.chopCeiling(2), 2);
assert.equal(QuickSplit.chopCeiling(3), 3);
assert.equal(QuickSplit.chopCeiling(9), 9);
});
test("and never past the screen's own ceiling", () => {
assert.equal(QuickSplit.chopCeiling(20), QuickSplit.MAX_WAYS);
});
// A pot is only offered a chop at two contenders or more, but the floor is stated
// here too so that a caller that gets it wrong can't produce a one-way split.
test("a chop is at least two ways", () => {
assert.equal(QuickSplit.chopCeiling(1), 2);
assert.equal(QuickSplit.chopCeiling(0), 2);
});
test("the chop's + is held to it", () => {
const at = SRC.indexOf("addChopControl(row, amount, contenderCount) {");
const body = SRC.slice(at, SRC.indexOf("\n updateUneven()", at));
assert.match(body, /const ceiling = this\.chopCeiling\(contenderCount\)/);
assert.match(body, /if \(ways < ceiling\)/);
assert.ok(!/Math\.max\(contenderCount/.test(body), "the old ceiling is still there");
});
// Two eligible players is two ways and nothing else. A stepper with both ends dead
// is furniture.
test("a heads-up pot is offered no stepper at all", () => {
const at = SRC.indexOf("addChopControl(row, amount, contenderCount) {");
const body = SRC.slice(at, SRC.indexOf("\n updateUneven()", at));
assert.match(body, /if \(ceiling > 2\) \{/);
assert.ok(body.indexOf("if (ceiling > 2) {") < body.indexOf('className = "unevenChopWays"'));
});
});
describe("the bar that was never meant to be a bar", () => {
test("the stepper is as wide as what is in it", () => {
const rule = ruleFor(".waysStepper");
assert.match(rule, /flex:\s*0 0 auto/);
assert.ok(!/min-width/.test(rule),
"a min-width in a wrapping row is what handed it the whole second line");
});
test("and the readout no longer pushes the two buttons apart", () => {
const rule = ruleFor(".waysReadout");
assert.ok(!/flex:\s*1(\s|;)/.test(rule));
assert.match(rule, /min-width:\s*[\d.]+ch/);
assert.match(rule, /tabular-nums/);
});
// It is standing in for a selected chip, so it is painted like one.
test("the count is painted like the chip it replaces", () => {
const chip = ruleFor(".choiceChip.selected");
// Not `border-color`, which comes first in the rule and is a different gold.
const colour = (chip.match(/(?:^|[^-\w])color:\s*(#[0-9a-f]{3,6})/i) || [])[1];
assert.ok(colour, "the selected chip has no colour to match");
assert.match(ruleFor(".waysReadout"), new RegExp(`color:\\s*${colour}`, "i"));
});
// The measurement that found it, kept where the next round will read it.
test("what it measured is written beside the rule", () => {
const at = CSS.indexOf("/* As wide as the three things in it");
assert.ok(at >= 0, "the rule carries no note saying why");
const note = CSS.slice(at, CSS.indexOf("*/", at));
assert.match(note, /304px/);
assert.match(note, /252px/);
});
});
describe("what the sweep looks at", () => {
const DRIVER = fs.readFileSync(path.join(__dirname, "runUiAudit.js"), "utf8");
// It went straight to the all-in mode, so the mode the screen opens in was audited
// zero times in fourteen rounds.
test("both modes of the screen, not just the second one", () => {
const at = DRIVER.indexOf("const modes = [");
assert.ok(at >= 0, "the quick split pass still audits one mode");
const body = DRIVER.slice(at, DRIVER.indexOf("];", at));
assert.match(body, /"even"/);
assert.match(body, /"all-in"/);
assert.match(DRIVER, /quick split · \$\{mode\}/, "a failure won't say which mode it was in");
});
test("with the stepper open, since it isn't there otherwise", () => {
const at = DRIVER.indexOf('["even", () => {');
assert.ok(at >= 0);
assert.match(DRIVER.slice(at, DRIVER.indexOf("}]", at)), /#waysChoices \.choiceChip/);
});
});