160 lines
4.7 KiB
JavaScript
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);
|
|
}
|
|
})();
|