Files
ewoooc/scripts/check_v3_responsive_ui.js
OoO 605250619c
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
Frontend V3 responsive production update
2026-05-12 18:27:29 +08:00

160 lines
4.7 KiB
JavaScript

#!/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);
}
})();