#!/usr/bin/env node /* * Mobile sizing audit — measurement logic. * * Walks the app at Android's viewport and reports layout defects that * are invisible in code review but obvious on a phone: * * 1. Interactive elements below the 44x44px touch floor * (docs/v5-mobile-design.md §4.1). * 2. Nested scroll containers — a scrollable element inside another. * This is the defect class that made the old More sheet unusable. * 3. Horizontal overflow of the document. * * DRIVER-AGNOSTIC BY DESIGN. This module holds the probe and the report * formatting; it does NOT depend on Playwright, because the repo's root * package.json deliberately carries zero devDependencies and a browser * driver would add a ~500MB download to every dev machine and CI runner. * * Two ways to run it: * * a) Inject `PROBE_SOURCE` into any browser automation you already * have (the probe is self-contained — no closures, no imports) and * feed the results to `summarize()`. * * b) Install Playwright and run this file directly: * npm i -D playwright && npx playwright install chromium * node scripts/mobile-audit-probe.mjs * It exits 0 when clean, 1 when there are findings. * * LIMITATION: this measures the BROWSER build at Android's viewport. It * catches layout, sizing and overflow. It does NOT catch WebView-specific * rendering, real touch behaviour, or device-reported safe-area insets. * Confirm on hardware with `adb exec-out screencap -p`. */ import { readFileSync } from "node:fs"; /** Pixel 9 Pro XL: 1344x2992 physical at density 480 → 448x997 CSS px. */ export const VIEWPORT = { width: 448, height: 997 }; /** WCAG 2.5.5 / mobile-design §4.1. */ export const MIN_TARGET = 44; /** Every route reachable from the nav catalogue, plus the tab roots. */ export const ROUTES = [ "/home", "/connection", "/whats-new", "/dashboard", "/more", "/payloads", "/upload", "/install-package", "/saves", "/screenshots", "/videos", "/games", "/installed", "/files", "/search", "/volumes", "/disk-usage", "/console", "/processes", "/profile", "/fan-curve", "/remote-play", "/notifications", "/cheats", "/game-activity", "/sdk-changer", "/tmdb", "/fw-spoof", "/ftp-server", "/smb-browser", "/backup", "/nanodns", "/shell", "/tasks", "/stats", "/logs", "/audit-log", "/bug-report", "/faq", "/settings", "/about", ]; /** * Runs INSIDE the page. Self-contained on purpose so it can be injected * as a string by any driver — do not reference module scope from here. * * Scoped to
so the persistent chrome (bottom nav, top bar) is * measured once on its own rather than re-reported on all 41 routes. */ export function probe(minTarget) { const SEL = 'a, button, [role="button"], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const isVisible = (el) => { const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const s = getComputedStyle(el); return ( s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0" ); }; const labelOf = (el) => ( el.getAttribute("aria-label") || el.textContent || el.getAttribute("placeholder") || "" ) .trim() .replace(/\s+/g, " ") .slice(0, 40); /** Short, reasonably stable key for allowlisting. */ const keyOf = (el) => { const cls = (el.getAttribute("class") || "") .split(/\s+/) .filter(Boolean) .slice(0, 2) .join("."); return el.tagName.toLowerCase() + (cls ? "." + cls : ""); }; /** * Effective tap rect. * * A checkbox or radio is drawn at ~20px on purpose — drawing it at * 44px looks wrong. What the user actually taps is the control PLUS * its associated