- 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 产物装配与部署形态。
126 lines
4.5 KiB
JavaScript
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)`);
|