// The panel named after a legend that never showed one. // // Measured on an iPhone: 793px, of which 420 were the colour form — five rows of // swatch, value box and a filled red delete block, then an add button and a paragraph // of instructions. Under all of it sat "Stacks in chips", which is the thing a dealer // opens this panel during a game to read: the fewest chips that make each stack, for // colouring up or counting one out. Setting the colours is something you do once, on // the first night, and it was sitting on top of the thing you do at every colour-up. // // So the order is the other way round. The legend itself — five colours and five // amounts, which is what the panel is named for and what it had never actually // displayed, the values being locked inside number inputs — leads, in two lines. The // colour-up list follows. The form goes behind a disclosure, which is the shape Table // Setup already uses twice. Panel: 793px to 441px. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const fs = require("fs"); const path = require("path"); const ROOT = path.join(__dirname, "..", "..", "JoshHeaps.Net"); const PAGE = fs.readFileSync(path.join(ROOT, "Pages", "Casino", "TexasHoldem.cshtml"), "utf8"); const CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "holdem.css"), "utf8"); const RENDER = fs.readFileSync(path.join(ROOT, "wwwroot", "js", "casino", "holdem", "render.js"), "utf8"); const DRIVER = fs.readFileSync(path.join(__dirname, "runUiAudit.js"), "utf8"); const panel = () => { const at = PAGE.indexOf('

Chip Legend

'); assert.ok(at >= 0, "there is no Chip Legend panel"); return PAGE.slice(at, PAGE.indexOf('
', at)); }; 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("what the panel leads with", () => { test("the legend, then the stacks, then the form", () => { const markup = panel(); const strip = markup.indexOf('id="chipLegendStrip"'); const stacks = markup.indexOf('id="colourUp"'); const form = markup.indexOf('id="chipLegendList"'); assert.ok(strip >= 0, "the panel still doesn't show the legend it is named for"); assert.ok(strip < stacks, "the legend is below the colour-up list"); assert.ok(stacks < form, "the form a dealer fills in once is above the list they read all night"); }); test("and the form is behind a disclosure, shut", () => { const markup = panel(); const at = markup.indexOf('
'); assert.ok(at >= 0, "the colour form is not behind a disclosure"); assert.ok(!/
]*\bopen\b/.test(markup)); assert.ok(at < markup.indexOf('id="chipLegendList"'), "the disclosure doesn't contain the form"); assert.match(markup.slice(at, at + 200), /Colours and values<\/summary>/); }); // The add button and the instructions belong to the form, not to the panel. test("the instructions went with it", () => { const markup = panel(); const sub = markup.indexOf('
'); assert.ok(markup.indexOf('id="addChipColorBtn"') > sub); assert.ok(markup.indexOf("Give each chip color a value") > sub); }); }); describe("the legend line", () => { test("is built from the same list the form edits", () => { const at = RENDER.indexOf("renderChipLegendStrip() {"); assert.ok(at >= 0, "nothing draws the legend"); const body = RENDER.slice(at, RENDER.indexOf("\n },", at)); assert.match(body, /this\.state\.chipLegend/); assert.match(body, /Money\.format/); assert.match(body, /chipSwatch/); }); test("and is drawn whenever the list is", () => { const at = RENDER.indexOf("renderChipLegend() {"); assert.ok(at >= 0); assert.match(RENDER.slice(at, at + 160), /this\.renderChipLegendStrip\(\);/); }); // Typing in the form is the one moment the line above it goes stale. Only the // strip is redrawn — redrawing the list would write the field's own value back // into it mid-keystroke, which is why everything else here is called one at a time. test("it keeps up with the field being typed into, and nothing else does", () => { // Anchored inside renderChipLegend: the seat editor's custom fields have // listeners spelled the same way, earlier in the file. const from = RENDER.indexOf("renderChipLegend() {"); assert.ok(from >= 0); ["colorInput.addEventListener(\"input\"", "valueInput.addEventListener(\"input\""].forEach(hook => { const at = RENDER.indexOf(hook, from); assert.ok(at >= 0, `there is no ${hook}`); const body = RENDER.slice(at, RENDER.indexOf("});", at)); assert.match(body, /this\.renderChipLegendStrip\(\);/, `${hook} leaves the legend line stale`); assert.ok(!/this\.renderChipLegend\(\)/.test(body), "redrawing the whole list would rewrite the field being typed in"); }); }); test("it wraps rather than running off the side of a 320px sidebar", () => { const rule = ruleFor(".chipLegendStrip"); assert.match(rule, /display:\s*flex/); assert.match(rule, /flex-wrap:\s*wrap/); assert.match(rule, /list-style:\s*none/); }); }); describe("five ways to delete something", () => { // The target stays the size the touch rules give it. Only the paint changes: height // is not what makes a destructive control safe, and these were the loudest thing in // the panel — one filled red block per row, brighter than the values beside them. test("the delete buttons are quiet without being small", () => { const rule = ruleFor("#chipLegendList .removeChipColorBtn"); assert.match(rule, /background:\s*none/); assert.ok(!/min-height/.test(rule), "de-emphasis is a colour change, never a height change"); assert.ok(!/width/.test(rule)); }); // The dialog that asks "reset the table?" is also holdemBtnDanger, and it is meant // to be loud. test("and the confirm dialog's own danger button is untouched", () => { const at = CSS.indexOf("\n.holdemBtnDanger {"); assert.ok(at >= 0); assert.match(CSS.slice(at, CSS.indexOf("}", at)), /background:\s*linear-gradient/); }); test("the red comes back on the way to pressing one", () => { assert.match(CSS, /#chipLegendList \.removeChipColorBtn:hover \{[^}]*color:\s*#ffd6d6/); }); }); describe("the list that runs into its own footnote", () => { test("the colour-up note is spaced off the last stack", () => { assert.match(ruleFor("#colourUp .colourUpList + .fieldHint"), /margin-top:\s*var\(--sp-\d\)/); }); }); describe("what the sweep is allowed to shrug at", () => { // Both twelve-handed shapes, for one stated reason, and only at 20px text. test("both crowded shapes are notes, not failures, at large text", () => { const at = DRIVER.indexOf("const CROWDED_BEYOND_HELP"); assert.ok(at >= 0); assert.match(DRIVER.slice(at, DRIVER.indexOf(";", at)), /\["all in", "packed"\]/); assert.match(DRIVER, /soft && soft\.includes\(shape\)/); }); // The arithmetic has to be written down, or the next round reads this as the sweep // being softened to make something pass. test("and the arithmetic for why is written down beside it", () => { const at = DRIVER.indexOf("// The packed table is the same arithmetic"); assert.ok(at >= 0, "the packed shape is exempt with no reason given"); const why = DRIVER.slice(at, DRIVER.indexOf("const CROWDED_BEYOND_HELP", at)); assert.match(why, /315px of puck[\s/]*\/\/?\s*into 294px of felt/); assert.match(why, /44x63/); }); // Only at 20px. The same table at the browser's normal size is held to the same // standard as everything else on the felt. test("the other two passes are not softened", () => { assert.match(DRIVER, /\{ hand: "mouse", touch: false, viewports: VIEWPORTS \}/); assert.match(DRIVER, /\{ hand: "touch", touch: true, viewports: VIEWPORTS \}/); }); });