#!/usr/bin/env node /** * V3 responsive UI smoke. * * Checks the main Flask pages at desktop and mobile widths: * - route renders without 4xx/5xx * - route does not fall back to the login form * - document body does not create page-level horizontal overflow * * Usage: * LOGIN_PASSWORD=... BASE_URL=http://127.0.0.1:5003 \ * NODE_PATH=/path/to/node_modules node scripts/check_v3_responsive_ui.js */ const { chromium } = require('playwright'); const BASE_URL = process.env.BASE_URL || 'http://127.0.0.1:5003'; const LOGIN_PASSWORD = process.env.LOGIN_PASSWORD || 'codex-local-password'; const CHROME_PATH = process.env.PLAYWRIGHT_CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; const ROUTES = [ '/', '/edm', '/sales_analysis', '/daily_sales', '/monthly_summary_analysis', '/growth_analysis', '/ai_recommend', '/auto_import', '/vendor-stockout/', '/vendor-stockout/import', '/vendor-stockout/list', '/settings', '/logs', '/observability/overview', '/observability/agent_orchestration', '/observability/business_intel', '/observability/host_health', '/observability/ai_calls', '/observability/budget', '/observability/promotion_review', '/observability/rag_queries', '/observability/quality_trend', '/observability/ppt_audit_history', ]; const VIEWPORTS = [ { name: 'desktop', width: 1440, height: 1000 }, { name: 'mobile', width: 390, height: 900 }, ]; async function login(context) { const page = await context.newPage(); await page.goto(`${BASE_URL}/login`, { waitUntil: 'domcontentloaded', timeout: 20000 }); const passwordInput = page.locator('input[name="password"]'); if ((await passwordInput.count()) === 0) { await page.close(); return; } await passwordInput.fill(LOGIN_PASSWORD); await Promise.all([ page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => null), page.locator('button[type="submit"]').click(), ]); await page.close(); } async function inspectRoute(page, route) { const response = await page.goto(`${BASE_URL}${route}`, { waitUntil: 'domcontentloaded', timeout: 25000, }); await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => null); const metrics = await page.evaluate(() => { const maxScroll = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth); const client = document.documentElement.clientWidth; const localWide = Array.from(document.querySelectorAll('body *')) .filter((element) => { const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); return rect.width > client + 1 && style.position !== 'fixed' && style.position !== 'absolute'; }) .slice(0, 4) .map((element) => ({ tag: element.tagName, className: String(element.className || ''), width: Math.round(element.getBoundingClientRect().width), })); return { title: document.title, finalPath: `${location.pathname}${location.search}`, login: Boolean(document.querySelector('form[action="/login"]')), clientWidth: client, scrollWidth: maxScroll, overflow: maxScroll > client + 1, localWide, }; }); return { route, status: response ? response.status() : null, ...metrics, }; } (async () => { const browser = await chromium.launch({ headless: true, executablePath: CHROME_PATH, }); const failures = []; for (const viewport of VIEWPORTS) { const context = await browser.newContext({ viewport: { width: viewport.width, height: viewport.height }, }); await login(context); for (const route of ROUTES) { const page = await context.newPage(); try { const result = await inspectRoute(page, route); const failed = result.status >= 400 || result.login || result.overflow; const line = [ failed ? 'FAIL' : 'PASS', viewport.name, route, `status=${result.status}`, `overflow=${result.overflow}`, `scroll=${result.scrollWidth}`, `client=${result.clientWidth}`, `title=${result.title}`, ].join(' '); console.log(line); if (failed) failures.push({ viewport: viewport.name, ...result }); } catch (error) { const failure = { viewport: viewport.name, route, error: error.message.split('\n')[0] }; failures.push(failure); console.log(`FAIL ${viewport.name} ${route} error=${failure.error}`); } finally { await page.close(); } } await context.close(); } await browser.close(); if (failures.length > 0) { console.error(JSON.stringify({ failures }, null, 2)); process.exit(1); } })();