Add a phone-width audit script for the web app

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
simonandClaude Opus 5.5 committed 2026-11-13 00:00:07 +01:00
1 parent b7b7f2f648
commit 4bd4e5b2db
2 files changed
+75 -1

No files matched your search

+3 -1
View File
@@ -71,7 +71,9 @@ a permalink shows the same to everyone.
from the home page's "Entdecken" cards.
4. Page modules may only export Next.js page fields (`default`, `generateMetadata`, …); keep helpers and
constants module-local or in `src/components`.
5. Check it at 390 px and in both themes (design-system.md §7).
5. Check it at 390 px and in both themes (design-system.md §7). `node web/scripts/mobile-audit.mjs` emulates a
phone, reports pages that scroll sideways (and which elements cause it) and saves screenshots; the
script's header shows how to call it. All pages passed on 2026-11-12.
## Showing a new claim type
+72
View File
@@ -0,0 +1,72 @@
// Phone-width audit: emulate a 390x844 phone through the DevTools protocol (a plain headless window can't go
// below ~500 px and fakes overflow), then for each page report horizontal overflow, the elements causing it and
// small tap targets, and save a full-page screenshot. Needs Node 22+ and any Chromium browser (Chrome, Edge).
// Usage: node web/scripts/mobile-audit.mjs <browser executable> <outdir> <baseUrl> <path>...
// Example: node web/scripts/mobile-audit.mjs "/usr/bin/chromium" /tmp/audit http://localhost:3000 / /personen /geld
// From Git Bash, prefix MSYS_NO_PATHCONV=1, or it rewrites "/personen" into a Windows path.
import {spawn} from 'node:child_process';
import {mkdirSync, writeFileSync} from 'node:fs';
const [edge, outDir, base, ...paths] = process.argv.slice(2);
mkdirSync(outDir, {recursive: true});
const port = 9333;
const proc = spawn(edge, ['--headless=new', `--remote-debugging-port=${port}`, '--disable-gpu', '--no-first-run',
`--user-data-dir=${outDir}/profile`, 'about:blank'], {stdio: 'ignore'});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let target;
for (let i = 0; i < 40 && !target; i++) {
try { target = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()).find((t) => t.type === 'page'); }
catch { await sleep(250); }
}
const ws = new WebSocket(target.webSocketDebuggerUrl);
await new Promise((r) => ws.addEventListener('open', r, {once: true}));
let seq = 0; const pending = new Map();
ws.addEventListener('message', (ev) => {
const msg = JSON.parse(ev.data);
if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); }
});
const send = (method, params = {}) => new Promise((resolve) => {
const id = ++seq; pending.set(id, resolve); ws.send(JSON.stringify({id, method, params}));
});
const evaluate = async (expression) => (await send('Runtime.evaluate', {expression, returnByValue: true})).result?.result?.value;
await send('Page.enable');
await send('Emulation.setDeviceMetricsOverride', {width: 390, height: 844, deviceScaleFactor: 1, mobile: true});
await send('Emulation.setTouchEmulationEnabled', {enabled: true});
await send('Network.enable');
await send('Network.setUserAgentOverride', {userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148'});
const report = [];
for (const path of paths) {
await send('Page.navigate', {url: base + path});
await sleep(4000);
const info = await evaluate(`(() => {
const vw = document.documentElement.clientWidth;
const offenders = [];
for (const el of document.querySelectorAll('body *')) {
const r = el.getBoundingClientRect();
if (r.width > 0 && r.right > vw + 1) {
const style = getComputedStyle(el);
let p = el.parentElement, clipped = false;
while (p) { const s = getComputedStyle(p); if (/(auto|scroll|hidden|clip)/.test(s.overflowX)) { clipped = true; break; } p = p.parentElement; }
if (!clipped) offenders.push({tag: el.tagName.toLowerCase(), cls: String(el.className).slice(0, 60), right: Math.round(r.right), text: (el.textContent || '').trim().slice(0, 50)});
}
}
// keep only the outermost offenders
return {vw, scrollWidth: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight,
offenders: offenders.slice(0, 12), tapTargetsTooSmall: [...document.querySelectorAll('a,button,input,select,summary')]
.filter((e) => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && (r.height < 24 || r.width < 24); }).length};
})()`);
const name = path.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '') || 'home';
const height = Math.min(info?.height ?? 2000, 4000);
await send('Emulation.setDeviceMetricsOverride', {width: 390, height, deviceScaleFactor: 1, mobile: true});
await sleep(500);
const shot = await send('Page.captureScreenshot', {format: 'png'});
writeFileSync(`${outDir}/${name}.png`, Buffer.from(shot.result.data, 'base64'));
await send('Emulation.setDeviceMetricsOverride', {width: 390, height: 844, deviceScaleFactor: 1, mobile: true});
report.push({path, ...info});
}
console.log(JSON.stringify(report, null, 1));
ws.close(); proc.kill();
process.exit(0);