// 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();