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