Files
novalon-website/docs/acceptance/2026-09-21-iphone-se/iphone-se-audit.mjs
T
zhangxiang a0328a623f chore(qa): 验收台账与证据入库 + 构建/部署配置同步
- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。
- 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。
- 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径;
  next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐
  standalone 产物装配与部署形态。
2026-09-28 10:48:09 +08:00

126 lines
4.5 KiB
JavaScript

import { chromium } from 'playwright';
import { mkdirSync, writeFileSync } from 'fs';
const BASE = process.env.BASE || 'http://localhost:3000';
const OUT = process.env.OUT || '/tmp/iphone-se-evidence';
mkdirSync(OUT, { recursive: true });
const PAGES = [
['home', '/'],
['news-detail', '/news/company-founded'],
['news-list', '/news'],
['contact', '/contact'],
// 边界页:404 的 h1 由 opacity-20 改为 text-brand-ink/80,需在 375px 窄屏(`text-8xl` 档)复核
['not-found', '/no-such-route-404-check'],
];
const MEASURE = () => {
const footer = document.querySelector('footer, [data-testid="footer"]');
let lastTextBottom = 0;
let label = '';
if (footer) {
footer.querySelectorAll('*').forEach((el) => {
if (el.children.length || !el.textContent.trim()) return;
const b = el.getBoundingClientRect();
if (b.height > 0 && b.bottom > lastTextBottom) {
lastTextBottom = b.bottom;
label = el.textContent.trim().slice(0, 24);
}
});
}
const fr = footer ? footer.getBoundingClientRect() : null;
const banner = document.querySelector('[data-testid="cookie-consent-banner"]');
const br = banner ? banner.getBoundingClientRect() : null;
return {
theme: document.documentElement.getAttribute('data-theme'),
footerBottomGap: fr && lastTextBottom ? Math.round(fr.bottom - lastTextBottom) : null,
lastText: label,
footerBottomVsViewport: fr ? Math.round(fr.bottom - innerHeight) : null,
bannerTop: br ? Math.round(br.top) : null,
bannerOverlapsFooter: br && fr ? br.top < fr.bottom : false,
docScrollW: document.documentElement.scrollWidth,
clientW: document.documentElement.clientWidth,
};
};
const browser = await chromium.launch();
const rows = [];
for (const theme of ['dark', 'light']) {
for (const consent of ['dismissed', 'pending']) {
const ctx = await browser.newContext({
viewport: { width: 375, height: 667 },
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
colorScheme: theme,
...(consent === 'dismissed'
? {
storageState: {
cookies: [],
origins: [
{
origin: BASE,
localStorage: [
{
name: 'novalon-cookie-preferences',
value: JSON.stringify({
necessary: true,
analytics: false,
marketing: false,
functionality: true,
timestamp: 1784284000000,
}),
},
],
},
],
},
}
: {}),
});
const page = await ctx.newPage();
for (const [name, path] of PAGES) {
await page.goto(BASE + path, { waitUntil: 'load', timeout: 45000 });
if (consent === 'pending') await page.waitForTimeout(2600);
else await page.waitForTimeout(900);
const tag = `${theme}-${consent}-${name}`;
await page.screenshot({ path: `${OUT}/se-${tag}.png` });
// 逐帧滚到底:首页有入场分级动画,内容高度在滚动后才增长,
// 单次 scrollTo(scrollHeight) 会停在半页,测出的 footer 位置无效。
await page.evaluate(async () => {
let last = -1;
for (let i = 0; i < 12; i += 1) {
window.scrollTo(0, document.body.scrollHeight);
await new Promise((r) => setTimeout(r, 220));
if (Math.round(window.scrollY) === last) break;
last = Math.round(window.scrollY);
}
});
await page.waitForTimeout(700);
await page.screenshot({ path: `${OUT}/se-${tag}-footer.png` });
const m = await page.evaluate(MEASURE);
rows.push({ tag, ...m });
console.log(tag.padEnd(28), JSON.stringify(m));
}
await ctx.close();
}
}
await browser.close();
writeFileSync(
`${OUT}/measurements.json`,
JSON.stringify(
{
generatedAt: new Date().toISOString(),
target: BASE,
device: 'iPhone SE 口径仿真:viewport 375x667 / deviceScaleFactor 2 / isMobile+hasTouch / colorScheme 由 CDP 强制',
caveat: 'Playwright 仿真,非真机;用于复现 R-1/R-3 的布局与叠层判定,不能替代实机验收',
thresholds: { noHorizontalOverflow: 'docScrollW <= clientW', consentClearsFooter: 'bannerOverlapsFooter === false' },
rows,
},
null,
2,
),
);
console.log(`WROTE ${OUT}/measurements.json (${rows.length} rows)`);