c094eb88fa
Adds src/ (scripts, raw measurement logs, derived data, report source), tests/, LICENSE (MIT, code) and LICENSE-DATA.md (CC BY 4.0 for the survey's data; third-party terms). The report is updated to the current site; the v1 and v2 data hand-off files are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
100 lines
7.0 KiB
JavaScript
100 lines
7.0 KiB
JavaScript
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();
|