// 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/); }); });