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 产物装配与部署形态。
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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)`);
|
||||
Reference in New Issue
Block a user