/* ============================================================ mobile.test.mjs — mobile-viewport regression test. Opens every app route at phone/tablet widths against the running dev server, logs in as the local seeded tester, and fails if any route lets the main pane scroll sideways (the classic clipped-card / blown-out-grid symptom) or throws a runtime error. Intended horizontal scrollers (.table-wrap, .kanban, .tabs, .stepper) are exempt by design: they scroll inside themselves, never the pane. Requirements (not part of the default `npm run verify` chain): - dev server running (npm run dev) and the backend reachable - Chrome installed (CHROME_PATH overrides the default location) - puppeteer-core available: npm i -D puppeteer-core (no download; it drives the installed Chrome) Run: node mobile.test.mjs Env: ATS_BASE_URL, CHROME_PATH, ATS_TEST_EMAIL, ATS_TEST_PASSWORD ============================================================ */ let puppeteer try { puppeteer = (await import('puppeteer-core')).default } catch { console.error('puppeteer-core is not installed. Run: npm i -D puppeteer-core') process.exit(2) } const BASE = process.env.ATS_BASE_URL || 'http://localhost:5173' const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' const EMAIL = process.env.ATS_TEST_EMAIL || 'ats.tester@example.com' const PASSWORD = process.env.ATS_TEST_PASSWORD || 'Test12345!' // Keep in sync with src/app/routes.js (paths only — titles don't matter here). const ROUTES = [ 'dashboard', 'inbox', 'matching', 'jobs', 'candidates', 'cvbank', 'pipeline', 'progress', 'import', 'jobboard', 'recruiterhub', 'talent', 'tasks', 'aiassistant', 'interviews', 'requisitions', 'assessments', 'offers', 'managers', 'calendar', 'reports', 'analytics', 'aistudio', 'notifications', 'rbac', 'settings', 'help', ] const WIDTHS = [320, 375, 390, 430, 768] const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) const browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox'], defaultViewport: { width: 320, height: 800, isMobile: true, hasTouch: true }, }) const failures = [] const runtimeErrors = [] try { const page = await browser.newPage() page.on('pageerror', (e) => runtimeErrors.push(`${page.url()}: ${e.message.split('\n')[0]}`)) await page.goto(`${BASE}/auth/login`, { waitUntil: 'domcontentloaded', timeout: 30000 }) await sleep(1000) if (await page.$('input[type=password]')) { const email = await page.$('input:not([type=password])') await email.type(EMAIL) await page.type('input[type=password]', PASSWORD) await Promise.all([ page.click('button[type=submit]'), page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}), ]) await sleep(1500) } for (const route of ROUTES) { for (const w of WIDTHS) { await page.setViewport({ width: w, height: 800, isMobile: w < 768, hasTouch: w < 768 }) await page.goto(`${BASE}/${route}`, { waitUntil: 'domcontentloaded', timeout: 30000 }) await sleep(1500) let rep try { rep = await page.evaluate(() => { const doc = document.documentElement const content = document.querySelector('.content') return { docOverflow: doc.scrollWidth - doc.clientWidth, contentOverflow: content ? content.scrollWidth - content.clientWidth : 0, } }) } catch { await sleep(1500) continue // page navigated mid-evaluate; the next loop iteration re-covers it } if (rep.docOverflow > 1) failures.push(`${route} @${w}: document scrolls sideways by ${rep.docOverflow}px`) if (rep.contentOverflow > 1) failures.push(`${route} @${w}: .content scrolls sideways by ${rep.contentOverflow}px`) } process.stdout.write('.') } console.log('') } finally { await browser.close() } // 4xx/5xx fetch noise is backend behaviour, not a layout bug — only genuine // script errors (pageerror) fail the run. if (failures.length || runtimeErrors.length) { for (const f of failures) console.error('FAIL ' + f) for (const e of runtimeErrors) console.error('ERROR ' + e) console.error(`\n${failures.length} overflow failure(s), ${runtimeErrors.length} runtime error(s)`) process.exit(1) } console.log(`All ${ROUTES.length} routes clean at ${WIDTHS.join('/')}px — no sideways scroll, no runtime errors`)