Last round I found a regression I'd shipped two rounds earlier: the standings table's player-name column was zero pixels wide, at every viewport, and nobody noticed because the audit that would have caught it only ever existed in a previous session's scrollback. These checks have been re-written by hand at least six times, and each time they covered only whatever had just changed. So they're a file now. uiAudit.js runs in the page — no build step, no package.json, no dependency — and checks touch-action and tap highlights, that fields stay selectable and are big enough not to trigger iOS zoom, that nothing important is covered or stranded off-screen, that no grid column has collapsed to nothing, and that no text is painted outside its box. runUiAudit.js drives it over the table, all five overlays and Quick Split at thirteen viewports. Deliberately outside CI: the unit suite is dependency-free and runs in a second, this one needs a browser and a running site. Three real faults it found on its first run Fields were 13.3px on every iPad. The 16px minimum that stops iOS zooming the page on focus went in inside a max-width:700px block, and every iPad is wider than that — so a dealer on a tablet, which is the device this is most likely to be propped up on, got the table zoomed every time they touched the blind amount. The reason for that rule was never "narrow screen", it was "iOS". Font size applies at all widths now, and the 44px touch targets key off hover:none, which is what actually distinguishes a finger from a mouse. The seat menu couldn't be scrolled. It had no height limit and no overflow, so on a phone held sideways its first two actions — Fold and All-in — sat below the bottom of the screen with no way to reach them. A dealer in landscape could long-press a seat and then not fold it. The pot wizard sat under the action bar. Every other overlay is z-index 90-98; the wizard alone was at 50, below both the action bar at 55 and the toast at 80. In landscape one of its inputs was completely covered by the Fold button, and a toast could sit over "Confirm Winner(s)". It was the odd one out rather than a decision. Three faults in the audit itself, found by checking before acting Worth recording, because each would have had me "fix" something that wasn't broken: - an ellipsised name was reported as painting outside its seat. A Range measures laid-out text, which runs past the element while overflow:hidden quietly clips it. Only overflow that is actually visible can paint over a neighbour. - every link below the fold was reported as off-screen. Something under the fold on a page that scrolls is reachable; the check now asks whether scrolling can bring it into view, which is the thing that actually matters. - a button in the newly-scrollable seat menu came back "covered by null". elementFromPoint returns null for a point outside a scrollable ancestor's clip — scrolled out of view, not covered by anything. A toast partly over a control is a warning rather than a failure now: it dismisses itself and carries a close button, and on a desktop it necessarily lands over a table that fills the content column. Fully covered by a toast is still a failure, because that was a real bug once — a seat sat exactly under the Undo button. Final sweep: 13 viewports, 65 overlay checks, 13 full-page audits, 4 Quick Split audits, zero failures, one warning. 137 unit tests still passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
135 lines
5.8 KiB
JavaScript
135 lines
5.8 KiB
JavaScript
// Drives uiAudit.js over the dealer's surfaces at every viewport worth caring about.
|
|
//
|
|
// node tests/holdem/runUiAudit.js [baseUrl]
|
|
//
|
|
// Needs Playwright available (`npx playwright install chromium` once). It is
|
|
// deliberately NOT wired into the unit-test suite or CI, because that suite is
|
|
// dependency-free and runs in a second — this one needs a browser and a running
|
|
// site. Run it after any change that adds or resizes a control.
|
|
//
|
|
// No Playwright to hand? Open the page, paste tests/holdem/uiAudit.js into devtools,
|
|
// and call LayoutAudit.runAll(). Same checks, one viewport.
|
|
const path = require("path");
|
|
|
|
const BASE = process.argv[2] || "http://localhost:5299";
|
|
const AUDIT = path.join(__dirname, "uiAudit.js");
|
|
|
|
// Portrait phones, tablets, desktops, then the same phones on their side — landscape
|
|
// is where this layout has broken most often, because the table is sized from width
|
|
// and the action bar is fixed to a very short viewport.
|
|
const VIEWPORTS = [
|
|
[360, 640, "Galaxy S8"], [375, 667, "iPhone SE"], [390, 844, "iPhone 14"],
|
|
[412, 915, "Pixel 7"], [430, 932, "iPhone Pro Max"],
|
|
[768, 1024, "iPad"], [834, 1112, "iPad Air"], [1024, 1366, "iPad Pro"],
|
|
[1280, 900, "desktop"], [1600, 1000, "wide desktop"],
|
|
[640, 360, "S8 landscape"], [667, 375, "SE landscape"], [844, 390, "iPhone landscape"],
|
|
];
|
|
|
|
const OVERLAYS = [
|
|
["rulings", () => document.getElementById("openRulingsBtn").click()],
|
|
["seat menu", () => SeatMenu.open(HoldemState.load(), {}, 0)],
|
|
["card picker", () => { document.getElementById("cardPickerOverlay").hidden = false; }],
|
|
["add chips", () => AddChips.open(HoldemState.load(), 0, () => {})],
|
|
["pot wizard", () => document.getElementById("resolvePotBtn").click()],
|
|
];
|
|
|
|
// A table part-way through a real night: long names, uneven stacks, a hand log.
|
|
function seedTable() {
|
|
const state = HoldemState.createDefault();
|
|
HoldemState.resizeSeats(state, 9);
|
|
[["Christopher", 1200], ["Mike from work", 800], ["Alexandra", 1500], ["Bob", 250], ["Dee", 600]]
|
|
.forEach(([name, cash], i) => {
|
|
state.seats[i].name = name;
|
|
state.seats[i].inHand = true;
|
|
state.seats[i].buyIns = cash;
|
|
state.seats[i].stack = cash;
|
|
});
|
|
state.smallBlind = 5;
|
|
state.bigBlind = 10;
|
|
for (let hand = 1; hand <= 4; hand++) {
|
|
state.handHistory.push({
|
|
handNumber: hand, timestamp: Date.now(), pot: 120,
|
|
payouts: [{ id: state.seats[hand % 5].id, label: "P", amount: 120 }],
|
|
ledger: state.seats.slice(0, 5).map(p => ({ id: p.id, name: p.name, in: 30, won: 0 })),
|
|
});
|
|
}
|
|
state.handNumber = 5;
|
|
HoldemState.save(state);
|
|
}
|
|
|
|
async function main() {
|
|
let chromium;
|
|
try {
|
|
({ chromium } = require("playwright"));
|
|
} catch {
|
|
console.error("This script needs Playwright: npm i -D playwright && npx playwright install chromium");
|
|
console.error("Or paste tests/holdem/uiAudit.js into devtools and call LayoutAudit.runAll().");
|
|
process.exit(2);
|
|
}
|
|
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newPage();
|
|
const failures = [];
|
|
const warnings = [];
|
|
let checks = 0;
|
|
|
|
for (const [width, height, label] of VIEWPORTS) {
|
|
await page.setViewportSize({ width, height });
|
|
|
|
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
|
await page.evaluate(seedTable);
|
|
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
|
await page.click("#nextHandBtn");
|
|
await page.waitForTimeout(300);
|
|
await page.evaluate(() => document.querySelectorAll("details.sidebarPanel").forEach(d => { d.open = true; }));
|
|
await page.addScriptTag({ path: AUDIT });
|
|
|
|
const table = await page.evaluate(() => LayoutAudit.runAll());
|
|
checks++;
|
|
if (!table.clean) failures.push({ where: `table · ${label} ${width}x${height}`, offenders: table.failed.flatMap(f => f.offenders) });
|
|
table.warnings.forEach(w => warnings.push(`${label}: ${w}`));
|
|
|
|
for (const [name, open] of OVERLAYS) {
|
|
await page.evaluate(open);
|
|
await page.waitForTimeout(180);
|
|
checks++;
|
|
const result = await page.evaluate(() => {
|
|
const overlay = [...document.querySelectorAll("[id$='Overlay']")].find(o => !o.hidden);
|
|
if (!overlay) return { offenders: [] };
|
|
return LayoutAudit.occlusion(`#${overlay.id} button, #${overlay.id} a, #${overlay.id} input`);
|
|
});
|
|
await page.evaluate(() => document.querySelectorAll("[id$='Overlay']").forEach(o => { o.hidden = true; }));
|
|
if (result.offenders.length) failures.push({ where: `${name} · ${label} ${width}x${height}`, offenders: result.offenders });
|
|
}
|
|
}
|
|
|
|
for (const [width, height, label] of VIEWPORTS.filter((_, i) => i % 4 === 0)) {
|
|
await page.setViewportSize({ width, height });
|
|
await page.goto(`${BASE}/Casino/TexasHoldem/QuickSplit`);
|
|
await page.evaluate(() => {
|
|
const button = [...document.querySelectorAll("button")].find(b => /all.?in/i.test(b.textContent));
|
|
if (button) button.click();
|
|
});
|
|
await page.addScriptTag({ path: AUDIT });
|
|
checks++;
|
|
const result = await page.evaluate(() => LayoutAudit.runAll({ occlusionSelector: "main button, main a, main input" }));
|
|
if (!result.clean) failures.push({ where: `quick split · ${label} ${width}x${height}`, offenders: result.failed.flatMap(f => f.offenders) });
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
console.log(`${checks} audits across ${VIEWPORTS.length} viewports`);
|
|
warnings.forEach(w => console.log(` note: ${w}`));
|
|
if (failures.length === 0) {
|
|
console.log("clean");
|
|
return;
|
|
}
|
|
failures.forEach(f => {
|
|
console.log(`\nFAIL ${f.where}`);
|
|
f.offenders.forEach(o => console.log(` ${o}`));
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
main();
|