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