From de4331de72ac241741c8550e7276432931c9dae4 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Fri, 11 Sep 2026 20:38:03 -0600 Subject: [PATCH] Make forty-four pixels mean forty-four pixels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every input and select a dealer touches has a rule asking for min-height: 44px, and not one of them was 44px tall. Form controls are content-box by default, so 44 was being read as the height of the text inside, and 8px of padding top and bottom plus a 1px border each side went on the outside of it. Measured on an iPhone: all fifteen settings fields in the sidebar stood at 62px, and the raise pad's amount box at 66 with the text turned up. Nothing looked wrong, which is why it lasted. 62px clears a 44px floor, so the sweep — which asks whether a target is big enough, never whether it is the size it was asked to be — had nothing to say, and one field is not obviously too tall. The cost is only visible in the aggregate: Table Setup 941px to 869, the waiting list 212 to 194, every panel in the sidebar open 2,993 to 2,813. The raise pad's field was worth measuring rather than assuming, because it is not only a field: it sets the action bar's height, the bar's height is published into --feltRoomPx, and the felt is laid out from what is left. It takes 10px off the bar and hands them to the table. On a Galaxy S8 twelve-handed at 20px text the bar goes 248px to 238 and seat 7's stack comes out from behind it — with the taller field the sweep reported "$1,100 is 7/7 under div#actionBar", a figure a dealer cannot read at all. Written against the element types rather than the surfaces they sit on. The surface version was written first and had already missed the waiting list's name box, two hundred lines from anything it named. This is the third time this stylesheet has been caught by content-box; the panel headers and the rulings summaries are the other two. The new test holds every field given a 44px floor to being measured the way that floor assumes. 1167 tests, 418 audits clean. --- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 37 +++++++ tests/holdem/theFieldsHeight.test.js | 111 ++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 tests/holdem/theFieldsHeight.test.js diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index c7ed550..4b2d82f 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -207,6 +207,43 @@ a { touch-action: manipulation; } +/* A field is as tall as it says it is. + * + * Form controls are content-box by default, and this stylesheet has been caught by that + * twice already — the sidebar's panel headers and the rulings sheet's summaries both + * carry a note about it. It was doing the same thing to the fields, and nobody had + * measured them: the touch rule near the bottom of this file asks for `min-height: 44px` + * on every input and select in a panel, 44 was being read as the *content* height, and + * 8px of padding top and bottom plus a 1px border each side went on the outside of it. + * Measured on an iPhone, all fifteen settings fields in the sidebar stood at 62px — 41% + * over the number the rule that sizes them is written in terms of. + * + * Nothing looked wrong, which is why it lasted. 62px clears a 44px floor, so the sweep + * had nothing to say, and one field is not obviously too tall. Fifteen of them are 180px + * of scroll on a page whose whole argument is that a dealer is holding a phone in one + * hand: Table Setup 941 to 869, the waiting list 212 to 194, and every panel in the + * sidebar open 2,993 to 2,813. + * + * The raise pad's field is the one worth measuring rather than assuming, because it is + * not only a field: its height is the action bar's height, the action bar's height is + * published into --feltRoomPx, and the felt is laid out from what is left. Correcting it + * takes 10px off the bar and hands them to the table. Measured on a Galaxy S8 + * twelve-handed at 20px text, which is where that matters most: the bar goes 248px to + * 238, and seat 7's stack comes out from behind it — with the taller field the sweep + * reported "$1,100 is 7/7 under div#actionBar", which is a figure a dealer cannot read + * at all. + * + * Said about every field rather than about the surfaces they sit on, because a list of + * surfaces is a list that goes stale. Written that way first, it already missed one: the + * waiting list's name box asks for the same 44px two hundred lines from anything the + * list named. The rule is identical either way and there is one fewer thing to keep up + * to date. */ +input, +select, +textarea { + box-sizing: border-box; +} + /* The grey rectangle goes from every control, fields included — a tapped input already shows a caret and a focus ring, which say the same thing more quietly. */ .holdemBtn, diff --git a/tests/holdem/theFieldsHeight.test.js b/tests/holdem/theFieldsHeight.test.js new file mode 100644 index 0000000..40b2bbf --- /dev/null +++ b/tests/holdem/theFieldsHeight.test.js @@ -0,0 +1,111 @@ +// Forty-four pixels, meaning forty-four pixels. +// +// Every input and select a dealer touches has a rule asking for `min-height: 44px`, and +// not one of them was 44px tall. Form controls are content-box by default, so 44 was +// being read as the height of the text inside and 8px of padding top and bottom plus a +// 1px border each side went on the outside of it. Measured on an iPhone: all fifteen +// settings fields in the sidebar stood at 62px, and the raise pad's amount box at 66px +// with the text turned up. +// +// Nothing looked wrong, which is why it lasted eleven rounds. 62px clears a 44px floor, +// so the sweep — which asks whether a target is big enough, never whether it is the size +// it was asked to be — had nothing to say about it. The cost is only visible in the +// aggregate: fifteen fields at 18px over is 180px of sidebar, and the raise field's 10px +// is 10px of felt, because the action bar's height is published into --feltRoomPx and +// the table is laid out from what is left. +// +// This is the third time this stylesheet has been caught by content-box — the sidebar's +// panel headers and the rulings sheet's summaries are the other two, and both carry a +// note saying so. The note is in the stylesheet; the list of what it covers is here. +const test = require("node:test"); +const { describe } = require("node:test"); +const assert = require("node:assert"); +const fs = require("fs"); +const path = require("path"); + +const CSS = fs.readFileSync(path.join( + __dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8"); + +// The rule, the selectors it is written against, and the note above it saying why. +const boxSizing = () => { + const note = CSS.indexOf("/* A field is as tall as it says it is."); + assert.ok(note >= 0, "nothing in the stylesheet asks a field to be the height it states"); + const at = CSS.indexOf("box-sizing: border-box;", note); + assert.ok(at >= 0); + return CSS.slice(note, CSS.indexOf("}", at)); +}; + +// Every selector anywhere in the stylesheet that gives a typing field a 44px floor, +// wherever the media query that holds it happens to live. Checkboxes and colour swatches +// are left out: they are sized by an explicit width and height rather than by a floor +// their padding can push past, which is the whole of the bug this guards. +const touchSized = () => { + const found = []; + let from = 0; + for (;;) { + const hit = CSS.indexOf("min-height: 44px", from); + if (hit === -1) break; + from = hit + 1; + const opens = CSS.lastIndexOf("{", hit); + const prior = Math.max(CSS.lastIndexOf("}", opens), CSS.lastIndexOf("{", opens - 1), + CSS.lastIndexOf("*/", opens)); + CSS.slice(prior + 1, opens).split(",") + .map(line => line.trim()) + .filter(Boolean) + .forEach(selector => found.push(selector)); + } + return found.filter(selector => + /\b(input|select|textarea)\b|\.raiseInput/.test(selector) && + !/\[type="(checkbox|color)"\]/.test(selector)); +}; + +// --------------------------------------------------------------------------- + +describe("a field is the height it says it is", () => { + const rule = boxSizing(); + + // Every field, not a list of the surfaces fields sit on. The list version of this + // rule was written first and had already missed one — see the note. + test("said about the elements, so nothing can sit outside the list", () => { + assert.match(rule, /(^|\n)input,\s*\n\s*select,\s*\n\s*textarea \{/); + }); + + // The one field whose height is not only its own: it sets the action bar's height, + // the bar's height is published into --feltRoomPx, and the felt is laid out from + // what is left. Taking 10px off the bar gave them to the table. + test("the felt's stake in it is written down", () => { + assert.match(rule, /--feltRoomPx/); + assert.match(rule, /248px to[\s*]*238/); + }); + + test("and so is the measurement that found it", () => { + assert.match(rule, /62px/); + assert.match(rule, /fifteen settings fields/); + }); + + // Two other rules in this file already carry a content-box note; this is the third + // place it has bitten, and the note is what stops it being a fourth. + test("and what it is about, so the next one is recognised", () => { + assert.match(rule, /content-box/); + assert.match(rule, /padding/); + }); +}); + +describe("nothing asks for a height it will not get", () => { + // A durable guard rather than a snapshot. If a later round gives a field a 44px + // floor, that floor is only the height the author meant while this rule reaches the + // field — and nothing else in the suite would notice if it stopped. + test("every field given a touch height is measured the way that height assumes", () => { + const fields = touchSized(); + assert.ok(fields.length >= 4, `only ${fields.length} field rules found — the scrape has drifted`); + // The rule names the element types themselves, so the only way a field escapes + // is a later round narrowing it back to a list of places. + const rule = boxSizing(); + fields.forEach(selector => { + const kind = (selector.match(/\b(input|select|textarea)\b/) || [])[1] + || (/\.raiseInput/.test(selector) ? "input" : null); + assert.ok(kind && new RegExp(`(^|\\n|,\\s*)${kind}\\b`).test(rule), + `\`${selector}\` is given a touch height that box-sizing no longer reaches`); + }); + }); +});