// The five cards in the middle of the felt, and the sheet of glass over them. // // Two faults, one on top of the other, and the sweep could see neither. // // #seatsLayer is a transparent `inset: 0` sibling of the table that holds the seat // pucks. The pucks ring the felt; the middle of it is empty layer — and an element with // no background still hit-tests. So every tap aimed at the community cards landed on the // layer instead. Measured at 1280x900 with the board on and five cards out: all five // hit-tested to `div#seatsLayer`, on every screen wide enough to keep the board in the // middle rather than on the strip. Each one is a button that opens the picker for that // card, and not one of them could be pressed. #betsLayer, the other sheet over the felt, // has been `pointer-events: none` since it was written. // // Underneath that, the cards were a little over half the size they were meant to be. // `placeBoard` measures how much middle the felt has and writes it to the centre's // `max-width` — but a max-width caps a box, it does not claim anything, and the centre // is a flex column that sizes to its content. So with 463px of measured room it went on // being 136px wide, the board row inside it is `width: 100%` of that, and five cards // sharing 136px come to rest on their own `min-width`: 24px, against a 44px floor this // sweep holds every other control on the felt to. On the strip, where the row simply // gets the width of the page, the same cards have always drawn 44. // // Neither was visible to a scripted probe — `.click()` on a node skips hit-testing // entirely — and the sweep's occlusion check had only ever been shown three selectors' // worth of the page: the action bar, the header, and the seats. 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 CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "holdem.css"), "utf8"); const AUDIT = fs.readFileSync(path.join(__dirname, "uiAudit.js"), "utf8"); const DRIVER = fs.readFileSync(path.join(__dirname, "runUiAudit.js"), "utf8"); const src = name => fs.readFileSync(path.join(ROOT, "wwwroot", "js", "casino", "holdem", `${name}.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("the sheet over the felt", () => { test("it does not take taps meant for what is under it", () => { assert.match(ruleFor("#seatsLayer"), /pointer-events:\s*none/); }); // Which is only safe because the thing in it that *is* a control says so itself. test("but a seat still does", () => { assert.match(ruleFor(".seat"), /pointer-events:\s*auto/); }); // The two chips that want to stay untouchable set their own value and are inside a // seat, so they override what they inherit rather than depending on the layer. test("and the two pucks that are markers rather than controls still are", () => { assert.match(ruleFor(".dealerChip:not(.active)"), /pointer-events:\s*none/); assert.match(ruleFor(".dealerChip.active"), /pointer-events:\s*none/); }); test("the other sheet is unchanged, and was always right", () => { assert.match(ruleFor("#betsLayer"), /pointer-events:\s*none/); }); test("what it measured is written beside the rule", () => { const at = CSS.indexOf("/* A transparent sheet over the whole felt"); assert.ok(at >= 0, "the rule carries no note saying why"); const note = CSS.slice(at, CSS.indexOf("*/", at)); assert.match(note, /1280x900/); assert.match(note, /seatsLayer/); }); }); describe("how wide the middle actually is", () => { test("the room it measured is claimed, not only capped", () => { const render = src("render"); const at = render.indexOf("placeBoard(row, strip) {"); assert.ok(at >= 0, "there is no placeBoard"); const body = render.slice(at, render.indexOf("\n },", at)); assert.match(body, /centre\.style\.width = `\$\{Math\.round\(width\)\}px`/, "the centre is still given a ceiling and no floor, so it shrink-wraps"); assert.match(body, /centre\.style\.maxWidth = `\$\{Math\.round\(width\)\}px`/); }); // Both come off together when the board leaves the felt, or the centre would keep a // width measured for a board that is no longer in it. test("and both are given back when the board moves to the strip", () => { const render = src("render"); const at = render.indexOf("placeBoard(row, strip) {"); const body = render.slice(at, render.indexOf("} else {", at)); assert.match(body, /centre\.style\.width = ""/); assert.match(body, /centre\.style\.maxWidth = ""/); }); // The floor is what it always said it was; what changed is that the cards can now // reach past it to the ceiling. test("a card is still allowed to grow to a thumb", () => { const rule = ruleFor(".boardCard"); assert.match(rule, /max-width:\s*44px/); assert.match(rule, /min-height:\s*44px/); assert.match(rule, /flex:\s*1 1 0/); }); test("what it measured is written beside the code", () => { const render = src("render"); const at = render.indexOf("// Both, and the width is the half that was missing."); assert.ok(at >= 0, "the fix carries no note saying why"); const note = render.slice(at, at + 1400); assert.match(note, /463px/); assert.match(note, /24px/); }); }); describe("why the sweep could not see either of them", () => { // Three selectors: the bar, the header and the seats. Nothing else on the page had // ever been asked whether something was sitting on top of it. test("the occlusion check looks at every control now, not three groups", () => { const at = AUDIT.indexOf("occlusion(selector = null) {"); assert.ok(at >= 0, "occlusion still hard-codes its own list"); assert.match(AUDIT.slice(at, at + 200), /selector = selector \|\| this\.CONTROL_SELECTOR/); }); // Widening it put three kinds of non-fault in front of the rule for the first time. test("a control that opted out of the pointer is not reported as covered", () => { const at = AUDIT.indexOf("occlusion(selector = null) {"); const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at)); assert.match(body, /getComputedStyle\(el\)\.pointerEvents === "none"/); }); test("nor is one inside a disclosure nobody has opened", () => { const at = AUDIT.indexOf("occlusion(selector = null) {"); const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at)); assert.match(body, /el\.checkVisibility/, "a shut
still hands out a real rect, and a size check lets it through"); }); test("and flow content under a fixed layer is said rather than failed", () => { const at = AUDIT.indexOf("occlusion(selector = null) {"); const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at)); assert.match(body, /scrolls clear of it/); }); }); describe("the rest of what the sweep had never run", () => { // It opened five sheets and asked one question of them. test("an overlay gets the whole audit, not the occlusion check alone", () => { const at = DRIVER.indexOf("for (const [name, open] of OVERLAYS) {"); assert.ok(at >= 0); const body = DRIVER.slice(at, DRIVER.indexOf("record(`${name}", at)); assert.match(body, /LayoutAudit\.runAll\(/); assert.ok(!/return LayoutAudit\.occlusion\(/.test(body), "the overlay pass still runs one rule"); }); // Two of the rules that made running only one of them reasonable ask about the // fixed bar, which is not a question while a scrim is covering the page. test("the two questions about the fixed bar stand down while a sheet is open", () => { ["focusHasRoom() {", "furnitureBudget() {"].forEach(signature => { const at = AUDIT.indexOf(signature); assert.ok(at >= 0, `there is no \`${signature}\``); const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at)); assert.match(body, /if \(this\.modalScope\(\)\) \{/, `${signature} still reads a full-screen scrim as furniture`); }); }); // And the third called the bottom four rows of a sheet that scrolls unreachable. test("a dialog's buttons below the fold are only a fault if nothing scrolls to them", () => { const at = AUDIT.indexOf("dialogActionsReachable() {"); const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at)); assert.match(body, /if \(this\.reachableByScrolling\(button\)\) return;/); }); // The helper all three of those lean on had a blind spot of its own: it asked // whether the element had left the box entirely, which is true of something well // below the fold and false of something hanging eight pixels over the edge. test("and reachability counts a control that is only part-way out", () => { const at = AUDIT.indexOf("reachableByScrolling(el) {"); const body = AUDIT.slice(at, AUDIT.indexOf("\n },", at)); assert.match(body, /const below = rect\.bottom - box\.bottom/); assert.match(body, /const above = box\.top - rect\.top/); // The comment above the fix quotes the old test, so the code is read on its own. const code = body.split("\n").filter(line => !/^\s*\/\//.test(line)).join("\n"); assert.ok(!/rect\.top >= box\.bottom/.test(code), "it still asks whether the element has left the box entirely"); }); }); describe("two controls that were a glyph and nothing else", () => { test("the pot wizard's close is named for what leaving does", () => { const markup = fs.readFileSync(path.join(ROOT, "Pages", "Casino", "TexasHoldem.cshtml"), "utf8"); const at = markup.indexOf('id="closePotWizardBtn"'); assert.ok(at >= 0); assert.match(markup.slice(at, markup.indexOf(">", at)), /aria-label="Close without paying the pot"/); }); // Named from the label the dealer typed, and kept up with it — the same shape the // chip legend's remove button already used, in a column of identical crosses. test("and a custom field's remove says which field", () => { const render = src("render"); const at = render.indexOf("renderCustomFields(form, player, index) {"); const body = render.slice(at, render.indexOf("\n },", at)); assert.match(body, /removeBtn\.setAttribute\("aria-label"/); assert.match(body, /labelInput\.value\.trim\(\)/); assert.match(body, /nameRemove\(\);/); }); });