From b4f1136457a08c6ad6ef52d9dc1f83fef050d87c Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Fri, 18 Sep 2026 08:27:06 +0800 Subject: test(frontend): 真浏览器 UI 溢出与图表渲染验证脚本 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [变更性质] 新增验证脚本,非产品代码。 [测试目标] Leader 要求每个页面在真浏览器 375/768/1440 三档下零横向溢出、零 pageerror,且迁移后的 layerchart 图表须在含数据状态下渲染并遵循 同一溢出约束(空态验证无法覆盖)。 [测试方案] scripts/ui-verify.mjs:静态服务 dist + Playwright chromium,按路由× 宽度断言 document overflow=0、pageerror=0、bg/fg 主题成立,输出 overflow-probe.json 与全页截图。scripts/chart-overflow-probe.mjs:以 page.route mock /api(含 /api/auth/me、runs 列表带 result、结果单), 勾选两个已完成回测触发对比曲线,真实渲染 svgs 并断言宽度内渲染。 证据存 artifacts/ui-shadcn/(30 组合全 OK,图表 6 组合全 OK)。 [影响范围] 仅验证工具链,node frontend/scripts/... 手动运行;不影响构建产物。 --- frontend/scripts/ui-verify.mjs | 112 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 frontend/scripts/ui-verify.mjs (limited to 'frontend/scripts/ui-verify.mjs') diff --git a/frontend/scripts/ui-verify.mjs b/frontend/scripts/ui-verify.mjs new file mode 100644 index 0000000..1f53e99 --- /dev/null +++ b/frontend/scripts/ui-verify.mjs @@ -0,0 +1,112 @@ +// Playwright visual/overflow verification: real browser, three widths, +// zero pageerror + zero document horizontal overflow asserted per page. +// Usage: node frontend/scripts/ui-verify.mjs [routesCsv] +import { chromium } from 'playwright'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import http from 'node:http'; +import { createReadStream } from 'node:fs'; + +const dist = path.resolve(process.argv[2] || 'dist'); +const outDir = path.resolve(process.argv[3] || '../artifacts/ui-shadcn'); +const routes = (process.argv[4] || 'login,register,reset') + .split(',') + .map(function (s) { return s.trim(); }) + .filter(Boolean); + +mkdirSync(outDir, { recursive: true }); + +const MIME = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.woff2': 'font/woff2', + '.json': 'application/json' +}; + +// tiny static server for the built dist (hash router friendly, /api 404s) +const server = http.createServer(function (req, res) { + const url = req.url || '/'; + if (url.startsWith('/api')) { + res.writeHead(404, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ error: { code: 'not_found', message: 'no backend in visual probe' } })); + return; + } + let file = url.split('?')[0].split('#')[0]; + if (file === '/' || file === '') file = '/index.html'; + var responded = false; + const send = function (fp, isFallback) { + const stream = createReadStream(fp); + stream.on('open', function () { + if (responded) return; + responded = true; + res.writeHead(200, { 'content-type': MIME[path.extname(fp)] || 'application/octet-stream' }); + stream.pipe(res); + }); + stream.on('error', function () { + if (isFallback) { + if (!responded) { + responded = true; + res.writeHead(404); + res.end(); + } + return; + } + send(path.join(dist, 'index.html'), true); + }); + }; + send(path.join(dist, file)); +}); +await new Promise(function (res) { server.listen(0, '127.0.0.1', res); }); +const port = server.address().port; + +const widths = [375, 768, 1440]; +const results = {}; +const browser = await chromium.launch(); + +for (const route of routes) { + const url = 'http://127.0.0.1:' + port + '/#' + route; + for (const w of widths) { + const context = await browser.newContext({ viewport: { width: w, height: Math.round(w * 2) } }); + const page = await context.newPage(); + const pageErrors = []; + page.on('pageerror', function (e) { pageErrors.push(String(e)); }); + await page.goto(url, { waitUntil: 'networkidle' }); + await page.waitForTimeout(400); + const m = await page.evaluate(function () { + return { + scrollWidth: document.documentElement.scrollWidth, + clientWidth: document.documentElement.clientWidth + }; + }); + const overflow = Math.max(0, m.scrollWidth - m.clientWidth); + const bg = await page.evaluate(function () { return getComputedStyle(document.body).backgroundColor; }); + const fg = await page.evaluate(function () { return getComputedStyle(document.body).color; }); + const shot = path.join(outDir, route + '-' + w + '.png'); + await page.screenshot({ path: shot, fullPage: true }); + results[route + '@' + w] = { + overflow: overflow, + clientWidth: m.clientWidth, + scrollWidth: m.scrollWidth, + pageErrors: pageErrors, + bg: bg, + fg: fg, + shot: path.relative(process.cwd(), shot) + }; + await context.close(); + } +} + +await browser.close(); +server.close(); +writeFileSync(path.join(outDir, 'overflow-probe.json'), JSON.stringify(results, null, 2)); +let bad = 0; +for (const k of Object.keys(results)) { + const r = results[k]; + const ok = r.overflow === 0 && r.pageErrors.length === 0 && r.bg !== r.fg; + console.log((ok ? 'OK ' : 'FAIL ') + k + ': overflow=' + r.overflow + ' pageErrors=' + r.pageErrors.length + ' bg=' + r.bg + ' fg=' + r.fg); + if (!ok) bad++; +} +process.exit(bad === 0 ? 0 : 1); -- cgit v1.2.3