import { chromium } from 'playwright-core'; import { PAGE, HAND } from './paths.mjs'; const URL = PAGE('web/index.html'); const VPS = [[1920,1080],[1440,900],[1366,768],[1280,720],[1024,768],[820,1180],[390,844],[360,740]]; const b = await chromium.launch({ executablePath: process.env.CH }); const problems = [], errs = []; const audit = async (p, tag) => p.evaluate((tag) => { const out = [], vw = innerWidth, wideM = vw > 900; if (document.documentElement.scrollWidth > vw + 1) out.push(`${tag}: page scrolls sideways (${document.documentElement.scrollWidth} > ${vw})`); const hero = document.getElementById('hero').getBoundingClientRect(); // every visible control in the hero must be the element actually under its centre for (const el of document.querySelectorAll('#hero button, #hero select, #hero .li, #hero .tabs button')) { const r = el.getBoundingClientRect(); if (!r.width || !r.height) continue; const cx = r.left + r.width / 2, cy = r.top + r.height / 2; if (cy < 0 || cy > innerHeight || cx < 0 || cx > vw) continue; // off-screen (scroll), not covered let clipped = false; // scrolled out of view inside a scrolling box for (let a = el.parentElement; a && a !== document.body; a = a.parentElement) { const o = getComputedStyle(a).overflowY; if (o === 'visible') continue; const q = a.getBoundingClientRect(); if (cy < q.top || cy > q.bottom || cx < q.left || cx > q.right) { clipped = true; break; } } if (clipped) continue; const top = document.elementFromPoint(cx, cy); if (!top || !(el === top || el.contains(top))) out.push(`${tag}: "${(el.textContent||el.getAttribute('aria-label')||'').trim().slice(0,30)}" covered by ${top ? (top.id || top.className || top.tagName) : 'nothing'}`); if (el.scrollWidth > el.clientWidth + 1 && el.tagName === 'BUTTON') out.push(`${tag}: text clipped in button "${el.textContent.trim().slice(0,30)}"`); } for (const el of document.querySelectorAll('#card, #card .body, #card .row, #card .li, .legend, .tools, .models')) if (el.scrollWidth > el.clientWidth + 1) out.push(`${tag}: content wider than its box in ${el.id || el.className} (${el.scrollWidth} > ${el.clientWidth})`); if (wideM) { const boxes = ['.tl', '.bl', '.models', '#card', '#legend', '.tools'].map(s => [s, document.querySelector(s).getBoundingClientRect()]); for (let i = 0; i < boxes.length; i++) { const [n, r] = boxes[i]; if (r.left < hero.left - 1 || r.right > hero.right + 1 || r.top < hero.top - 1 || r.bottom > hero.bottom + 1) out.push(`${tag}: ${n} sticks out of the hero`); for (let j = i + 1; j < boxes.length; j++) { const [m, q] = boxes[j]; const ox = Math.min(r.right, q.right) - Math.max(r.left, q.left), oy = Math.min(r.bottom, q.bottom) - Math.max(r.top, q.top); if (ox > 1 && oy > 1) out.push(`${tag}: ${n} overlaps ${m} (${Math.round(ox)}x${Math.round(oy)})`); } } // the map itself against the HUD const t = T[S.view], cv = document.getElementById('cv').getBoundingClientRect(), cx = cv.left + t.x, cy = cv.top + t.y; const rad = S.view === 'globe' ? t.k : Math.PI * t.k; for (const [n, r] of boxes) { let hit; if (S.view === 'merc') hit = cx - rad < r.right && cx + rad > r.left && cy - rad < r.bottom && cy + rad > r.top; else { const dx = Math.max(r.left - cx, 0, cx - r.right), dy = Math.max(r.top - cy, 0, cy - r.bottom); hit = dx * dx + dy * dy < rad * rad; } if (hit) out.push(`${tag}: map (${S.view}) runs under ${n}`); } if (cy - rad < hero.top - 1 || cy + rad > hero.bottom + 1 || cx - rad < hero.left - 1) out.push(`${tag}: map cut off by the hero edge`); out.push(`INFO ${tag}: map radius ${Math.round(rad)}px of hero ${Math.round(hero.width)}x${Math.round(hero.height)}; card ${Math.round(document.getElementById('card').getBoundingClientRect().height)}px tall, body scrolls: ${document.querySelector('#card .body').scrollHeight > document.querySelector('#card .body').clientHeight}`); } return out; }, tag); for (const [w, h] of VPS) { const p = await b.newPage({ viewport: { width: w, height: h } }); p.on('console', m => { if (m.type() === 'error') errs.push(`${w}x${h} ${m.text()}`); }); p.on('pageerror', e => errs.push(`${w}x${h} pageerror ${e.message}`)); await p.goto(URL, { waitUntil: 'load' }); await p.waitForTimeout(2500); const tag = `${w}x${h}`; problems.push(...await audit(p, `${tag} route/ae`)); await p.screenshot({ path: `out/${tag}_route.png` }); await p.click('#rbig'); await p.waitForTimeout(300); problems.push(...await audit(p, `${tag} list/ae`)); await p.screenshot({ path: `out/${tag}_list.png` }); for (const v of ['globe', 'dp', 'merc']) { await p.click(`.models button[data-v="${v}"]`); await p.waitForTimeout(1200); problems.push(...await audit(p, `${tag} list/${v}`)); } await p.click('#mode button[data-mode="line"]'); await p.waitForTimeout(300); problems.push(...await audit(p, `${tag} list/merc/line`)); await p.click('.models button[data-v="ae"]'); await p.waitForTimeout(1200); // interactions: list row selects, toggles respond const before = await p.evaluate(() => S.sel); const rows = await p.$$('#card .li'); if (rows.length > 2) { await rows[2].click(); await p.waitForTimeout(200); } const after = await p.evaluate(() => S.sel); if (rows.length > 2 && before === after) problems.push(`${tag}: clicking a list row did not select it`); await p.click('#tbad'); await p.click('#tpulse'); await p.waitForTimeout(150); const st = await p.evaluate(() => [S.onlyBad, S.pulses, document.getElementById('rbig').textContent, document.querySelectorAll('#card .li').length]); if (!st[0] || st[1]) problems.push(`${tag}: toggles did not change state ${JSON.stringify(st)}`); problems.push(`INFO ${tag}: counter ${st[2]} vs list rows ${st[3]} (line mode, AE)`); if (w === 1440) await p.screenshot({ path: `out/${tag}_line_onlybad.png` }); await p.close(); } // page body + form at desktop and phone for (const [w, h] of [[1440, 900], [390, 844]]) { const p = await b.newPage({ viewport: { width: w, height: h } }); p.on('pageerror', e => errs.push(`${w} ${e.message}`)); await p.goto(URL, { waitUntil: 'load' }); await p.waitForTimeout(1500); await p.fill('#byoA', 'Perth, AU'); await p.selectOption('#byoB', { index: 0 }); await p.fill('#byoT', '48'); await p.click('button.go'); await p.waitForTimeout(200); problems.push(`INFO ${w} form: ` + (await p.$eval('#byoout .verdict', e => e.textContent)).replace(/\s+/g, ' ')); await p.fill('#byoA', 'Nowhereville'); await p.click('button.go'); await p.waitForTimeout(100); problems.push(`INFO ${w} bad city: ` + (await p.$eval('#byoout', e => e.textContent)).replace(/\s+/g, ' ').slice(0, 90)); const sw = await p.evaluate(() => document.documentElement.scrollWidth); if (sw > w + 1) problems.push(`${w}: page scrolls sideways ${sw}`); await p.screenshot({ path: `out/full_${w}.png`, fullPage: true }); await p.close(); } console.log('ERRORS', JSON.stringify(errs)); for (const x of problems) console.log(x); await b.close();