/* * The 393px check from AGENTS.md §5, run over the Motion routes. * * node scripts/motion-overflow-check.mjs # PIG_WEB_URL=http://127.0.0.1:8975 * * `scripts/screenshots.mjs` already asserts zero horizontal overflow, but only * on the ten pages it photographs, and Motion added five it does not know * about. The rule it enforces has been fixed at individual call sites three * separate times, so a new page group that nobody measures is exactly how it * comes back. * * Checked per route, in light and dark, at 393 and 1440: * - scrollWidth === clientWidth on the document (the sideways-scroll failure) * - no element wider than the viewport (which one, if so — the message has to * name the offender or nobody can act on it) * - nothing thrown to the console, and no failed request */ import { chromium } from 'playwright'; const BASE = process.env.PIG_WEB_URL ?? 'http://127.0.0.1:8975'; const ROUTES = [ { slug: 'motion', path: '/motion' }, { slug: 'library', path: '/motion/library' }, { slug: 'engagements', path: '/motion/engagements' }, ]; const VIEWPORTS = [ { name: 'mobile', width: 393, height: 852 }, { name: 'desktop', width: 1440, height: 900 }, ]; const browser = await chromium.launch({ channel: 'chrome' }); let failures = 0; /** The detail pages need a real id, so they are discovered rather than typed. */ const api = await fetch(`${BASE}/api/motion/templates`).then((r) => r.json()); const template = api.templates?.[0]; if (template) ROUTES.push({ slug: 'template', path: `/motion/library/${template.id}` }); const engagements = await fetch(`${BASE}/api/motion/engagements`).then((r) => r.json()); const engagement = engagements.engagements?.[0]; if (engagement) ROUTES.push({ slug: 'engagement', path: `/motion/engagements/${engagement.id}` }); for (const theme of ['light', 'dark']) { for (const vp of VIEWPORTS) { const ctx = await browser.newContext({ viewport: { width: vp.width, height: vp.height }, colorScheme: theme, isMobile: vp.name === 'mobile', hasTouch: vp.name === 'mobile', }); // The sidebar and the dock are per-device states in localStorage; left to // whatever a human last set, the measurement is not reproducible. await ctx.addInitScript(` localStorage.setItem('pig.sidebar', 'expanded'); localStorage.setItem('pig.piggy.dock', 'closed'); `); for (const route of ROUTES) { const page = await ctx.newPage(); const problems = []; page.on('console', (m) => { if (m.type() !== 'error') return; if (m.text().includes('/api/piggy/') || m.text().includes('Failed to load resource')) return; problems.push(`console: ${m.text()}`); }); page.on('pageerror', (e) => problems.push(`threw: ${e.message}`)); page.on('response', (r) => { // Piggy is a separate container and is normally down on a dev host, so // its 503 is a fact about the environment rather than about the page. // Every other status belongs to the route under test. if (r.status() >= 400 && !r.url().includes('/api/piggy/')) { problems.push(`${r.status()} ${r.url().replace(BASE, '')}`); } }); await page.goto(`${BASE}${route.path}`, { waitUntil: 'networkidle' }); // The pages are lazy chunks behind a Suspense boundary; a measurement // taken before the fallback resolves measures the spinner. await page.waitForTimeout(1200); const measured = await page.evaluate(() => { const doc = document.documentElement; const overflow = doc.scrollWidth - doc.clientWidth; const wide = [...document.querySelectorAll('*')] .filter((el) => el.getBoundingClientRect().width > doc.clientWidth + 1) .slice(0, 3) .map((el) => `${el.tagName.toLowerCase()}.${(el.className?.baseVal ?? el.className ?? '') .toString() .split(' ') .slice(0, 4) .join('.')}`); return { overflow, wide, text: document.body.innerText.slice(0, 120) }; }); const label = `${route.slug} ${vp.name} ${theme}`; if (measured.overflow !== 0) { failures++; console.log(`FAIL ${label}: overflows by ${measured.overflow}px — ${measured.wide.join(', ')}`); } else if (problems.length) { failures++; console.log(`FAIL ${label}: ${[...new Set(problems)].slice(0, 4).join(' | ')}`); } else { console.log(`ok ${label}`); } await page.close(); } await ctx.close(); } } await browser.close(); console.log(failures === 0 ? '\nevery Motion route: no overflow, no error' : `\n${failures} failed`); process.exit(failures === 0 ? 0 : 1);