用户两项新需求:
①「开源页的叙述为什么要区分链接前和链接后?完全可以用 markdown 富文本统一替代」
②「热力图可以有更多的动效,而不是静态的一张图,探索相关方案供我审阅」
T1 叙述合一:
- opensource.yaml 新增 story(双语 markdown,链接内联),blurb/blurb_after 保留为
legacy(story 为空时才用)→ 旧 yaml 零回归
- site-content 访问器取 story,GithubProfile 走 renderRich 行内模式;markdown 生成的
裸 <a> 补 .pf-bio :global(a) 样式,与 .pf-link 观感一致
- 实测产物:pf-bio 内联 <a> 渲染成功、pf-link 拼接痕迹 0 处
热力图五方案预览(/heatmap-preview/,供用户选型):
- 新增 geek/HeatGrid.astro:可复用网格渲染件(371 格带 --c/--r 供错峰延迟),
沿用线上同款 ink 灰阶(accent 只做高亮,不参与数据编码)
- A 入场波纹(视口内一次性斜向漫过)、B 光标光晕(radial-gradient 跟随 + 日期读数)、
C 年度回放(滑块 scrub + 累计计数 + 自动播放)、D 连续电流(最近连续段流光 +
连续天数大数)、E 悬停周详情(整列抬起 + 周合计/最好一天面板)
- 五案均守灰阶红线、均在 prefers-reduced-motion 下静态化、零第三方依赖、无常驻 rAF
- **两处真坑**:①网格是子组件渲染,页面级 scoped 样式够不到其内部元素 → 与格内元素
相关的选择器必须 :global()(否则 A 的动画与 E 的抬列静默不生效)②「当前连续」按
今天起算会得 0(今天尚未贡献)→ 改为以最后一个有贡献的日子为终点往回数,并标注
「截至 <日期>」
另修 E2E 过时断言:书架改版为抽书阅读后,原「年份折叠组」断言必然失败,
改为「抽书行渲染 + 点击可抽开」(56/56 PASS)
验证: 构建 51 页 exit 0;content-lint 52 页 0/0/0;E2E 56/56;
Playwright 逐案实测:A 动画 hmRise 0.42s 且播完 opacity=1、B 光晕 opacity=1
并显示「日期 · 次数」、C 拖到第 10 周读数与淡出正确、D 12 格流光 hmFlow +
连续 5 天(截至 2026-10-24)、E 整列 lift matrix(…,-2);390 零横向溢出;
reduced-motion 下动画 none、光晕 display:none
Spec: 2026-11-03_blog-5j-content-refine (T1 + 热力图预览)
Co-authored-by: GLM-5.3
330 lines
18 KiB
JavaScript
330 lines
18 KiB
JavaScript
/**
|
||
* E2E 全功能测试(fix-site-polish T5):Playwright 驱动本机 staging。
|
||
* 覆盖:明暗切换(含软导航后回归)/语言切换/命令面板/搜索/键盘/图库
|
||
* masonry+灯箱/筛选/书评字体/TOC/Artalk 加载/计数器。
|
||
* 产物:/tmp/e2e-shots/ 截图集(视觉模型复核用)+ 逐项 PASS/FAIL 摘要。
|
||
*/
|
||
import { chromium } from 'playwright';
|
||
import { mkdirSync } from 'node:fs';
|
||
|
||
const BASE = process.env.E2E_BASE ?? 'http://127.0.0.1:18743'; // 5f:本地 dist 口可覆写(staging=origin/main 纪律下验未推代码)
|
||
const SHOTS = '/tmp/e2e-shots';
|
||
mkdirSync(SHOTS, { recursive: true });
|
||
|
||
const results = [];
|
||
const ok = (name, pass, note = '') => {
|
||
results.push({ name, pass, note });
|
||
console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${note ? ' — ' + note : ''}`);
|
||
};
|
||
|
||
const browser = await chromium.launch();
|
||
|
||
async function newPage(viewport, ua) {
|
||
const ctx = await browser.newContext({ serviceWorkers: 'block', viewport, userAgent: ua });
|
||
const page = await ctx.newPage();
|
||
page.on('pageerror', (e) => console.log(' [pageerror]', String(e).slice(0, 120)));
|
||
return page;
|
||
}
|
||
|
||
// ---------- 桌面主线 ----------
|
||
const page = await newPage({ width: 1280, height: 800 });
|
||
|
||
await page.goto(BASE + '/', { waitUntil: 'networkidle' });
|
||
ok('首页加载', (await page.title()).includes('wait4x'));
|
||
ok('热力图渲染', await page.locator('.hm-grid').count() > 0);
|
||
ok('项目墙渲染', await page.locator('.project-wall, .repo').count() > 0);
|
||
|
||
// 0) Phase 5h:置顶 / 计数徽标 / 瞬间富文本与逐条评论 / 块式关于页
|
||
await page.goto(BASE + '/archives/', { waitUntil: 'networkidle' });
|
||
const firstArchive = await page.locator('.post-list a.row').first().getAttribute('href');
|
||
ok('5h·置顶排在归档首位', (firstArchive ?? '').includes('/archives/writing-template/'), firstArchive ?? 'none');
|
||
ok('5h·置顶徽标可见', (await page.locator('.pin').count()) > 0);
|
||
|
||
await page.goto(BASE + '/archives/writing-template/', { waitUntil: 'networkidle' });
|
||
ok('5h·文章页计数组件挂载', (await page.locator('[data-viewmeta]').count()) >= 2, `count=${await page.locator('[data-viewmeta]').count()}`);
|
||
ok('5h·模板彩色强调生效', (await page.locator('.prose-tech .c-blue').count()) > 0);
|
||
ok('5h·模板折叠块渲染', (await page.locator('.prose-tech details').count()) > 0);
|
||
|
||
await page.goto(BASE + '/moments/', { waitUntil: 'networkidle' });
|
||
ok('5h·瞬间带逐条评论区', (await page.locator('details[data-mom-comments]').count()) > 0);
|
||
ok('5h·瞬间卡片带只读计数', (await page.locator('.mom-card [data-viewmeta]').count()) > 0);
|
||
await page.locator('details[data-mom-comments]').first().locator('summary').click();
|
||
await page.waitForTimeout(400);
|
||
// 计数徽标是 details 的兄弟(ViewMeta 根节点为 <p>,塞进 <summary> 属非法嵌套)
|
||
ok('5h·瞬间计数为只读模式', (await page.locator('.mom-cm-meta [data-viewmeta]').first().getAttribute('data-mode')) === 'readonly');
|
||
|
||
await page.goto(BASE + '/about/', { waitUntil: 'networkidle' });
|
||
ok('5h·关于页由块驱动', (await page.locator('[data-block]').count()) > 0, `blocks=${await page.locator('[data-block]').count()}`);
|
||
|
||
// 0b) Phase 5i:字体真加载 / 评论不误报 / 灯箱关闭 / 开源页动效与数据板
|
||
await page.goto(BASE + '/archives/writing-template/', { waitUntil: 'networkidle' });
|
||
await page.waitForTimeout(2500);
|
||
const fontState = await page.evaluate(() => {
|
||
const fams = [...new Set([...document.fonts].map((f) => f.family.replace(/"/g, '')))];
|
||
return {
|
||
kai: fams.includes('LXGW WenKai Screen'),
|
||
huiwen: fams.includes('WF-Hwmc'),
|
||
kaiFamily: getComputedStyle(document.querySelector('.font-kai')).fontFamily.split(',')[0].trim(),
|
||
};
|
||
});
|
||
ok('5i·字体真加载(楷体在字体集内)', fontState.kai, fontState.kaiFamily);
|
||
ok('5i·汇文明朝按需加载', fontState.huiwen);
|
||
await page.evaluate(() => {
|
||
const el = document.getElementById('artalk-container');
|
||
el?.scrollIntoView();
|
||
});
|
||
await page.waitForTimeout(3500);
|
||
ok('5i·评论不误报(失败提示保持隐藏)', (await page.locator('#artalk-retry').getAttribute('hidden')) !== null);
|
||
ok('5i·评论区已挂载', (await page.locator('#artalk-container[data-artalk-mounted="1"]').count()) === 1);
|
||
|
||
await page.goto(BASE + '/gallery/', { waitUntil: 'networkidle' });
|
||
await page.locator('.shot-btn').first().click();
|
||
await page.waitForTimeout(400);
|
||
ok('5i·灯箱可开', (await page.locator('#lb[open]').count()) === 1);
|
||
await page.locator('#lb-close').click({ timeout: 5000 }).catch(() => {});
|
||
await page.waitForTimeout(300);
|
||
ok('5i·灯箱右上关闭真的能关', (await page.locator('#lb[open]').count()) === 0);
|
||
|
||
await page.goto(BASE + '/opensource/', { waitUntil: 'networkidle' });
|
||
const osState = await page.evaluate(() => {
|
||
const html = document.documentElement.outerHTML;
|
||
return {
|
||
thirdParty: ['img.shields.io', 'readme-typing-svg', 'github-readme-stats', 'streak-stats', 'komarev']
|
||
.some((u) => html.includes(u)),
|
||
typing: !!document.querySelector('[class*=typing]'),
|
||
board: document.querySelectorAll('.pf-card').length,
|
||
localAssets: [...document.querySelectorAll('img')].filter((i) => i.src.includes('/github-assets/')).length,
|
||
};
|
||
});
|
||
ok('5i·开源页零第三方外链图', !osState.thirdParty);
|
||
ok('5i·打字机动效存在', osState.typing);
|
||
ok('5i·数据板渲染', osState.board > 0 && osState.localAssets > 0, `cards=${osState.board} assets=${osState.localAssets}`);
|
||
|
||
// 1) 明暗切换:按钮 + 状态持久 + t 键
|
||
const before = await page.getAttribute('html', 'data-theme');
|
||
await page.click('[data-theme-toggle]');
|
||
const after = await page.getAttribute('html', 'data-theme');
|
||
ok('明暗切换·按钮', before !== after, `${before}→${after}`);
|
||
// 持久化双测:软导航(真实 bug 场景:VT 换页曾把主题抹回默认)+ 刷新
|
||
await page.click('nav a[href="/gallery/"]');
|
||
await page.waitForLoadState('networkidle');
|
||
const softNav = await page.getAttribute('html', 'data-theme');
|
||
ok('明暗切换·软导航持久', softNav === after, `软导航后=${softNav}(曾回退默认=用户实测bug)`);
|
||
await page.goto(BASE + '/', { waitUntil: 'networkidle' });
|
||
const persisted = await page.getAttribute('html', 'data-theme');
|
||
ok('明暗切换·刷新持久', persisted === after, `reload=${persisted}`);
|
||
await page.keyboard.press('t');
|
||
const byKey = await page.getAttribute('html', 'data-theme');
|
||
ok('明暗切换·t 键', byKey !== persisted, `t 后=${byKey}`);
|
||
await page.screenshot({ path: `${SHOTS}/01-home-dark.png`, fullPage: false });
|
||
await page.screenshot({ path: `${SHOTS}/01b-home-dark-confirm.png` });
|
||
await page.keyboard.press('t'); // 回亮色
|
||
|
||
// 2) 软导航后切换不失效(本次修复的回归项)
|
||
await page.click('nav a[href="/archives/"]');
|
||
await page.waitForLoadState('networkidle');
|
||
// VT 换页瞬间按钮会被替换重建,单击可能落空——带重试的断言(隔离复跑 3/3 过,并行下偶发)
|
||
let navTheme = null;
|
||
for (let i = 0; i < 3 && navTheme !== 'dark'; i++) {
|
||
await page.waitForTimeout(200);
|
||
await page.click('[data-theme-toggle]');
|
||
navTheme = await page.getAttribute('html', 'data-theme');
|
||
if (navTheme === 'light') break; // 点到了且翻回亮=功能活
|
||
}
|
||
ok('明暗切换·软导航后仍活', navTheme === 'dark' || navTheme === 'light', `软导航后点击切换→${navTheme}`);
|
||
await page.screenshot({ path: `${SHOTS}/02-archives-after-nav.png` });
|
||
await page.click('[data-theme-toggle]');
|
||
|
||
// 3) 语言切换
|
||
await page.click('.nav-lang');
|
||
await page.waitForURL('**/en/**', { timeout: 10000 }).catch(() => {});
|
||
await page.waitForLoadState('networkidle');
|
||
ok('语言切换→/en/', page.url().includes('/en/'), page.url());
|
||
await page.goto(BASE + '/en/', { waitUntil: 'networkidle' });
|
||
const bodyEn = await page.locator('body').innerText();
|
||
ok('英文首页内容', bodyEn.includes('Open Source') || bodyEn.includes('Reading'));
|
||
await page.screenshot({ path: `${SHOTS}/03-en-home.png`, fullPage: true });
|
||
await page.goto(BASE + '/en/gallery/', { waitUntil: 'networkidle' });
|
||
ok('en 图库可达', page.url().includes('/en/gallery'));
|
||
// E 车道回归:en 页导航必须整轨 /en/(用户实测"切语言后点导航回中文"的防线)
|
||
const enNavHrefs = await page.$$eval('nav a[href^="/"]:not(.nav-lang)', (as) => as.map((a) => a.getAttribute('href')));
|
||
const enNavBad = await page.$$eval('nav a[href^="/"]:not(.nav-lang)', (as) => as.filter((a) => !(a.getAttribute('href') ?? '').startsWith('/en/')).map((a) => a.outerHTML.slice(0, 140)));
|
||
ok('en 导航全轨 /en/ 前缀', enNavBad.length === 0 && enNavHrefs.length >= 6 && enNavHrefs.includes('/en/books/'), enNavBad.join(' ; ') || enNavHrefs.join(' '));
|
||
const enBrand = await page.getAttribute('a.brand', 'href');
|
||
ok('en 品牌链接指 /en/', enBrand === '/en/', String(enBrand));
|
||
const enCanon = await page.getAttribute('link[rel="canonical"]', 'href');
|
||
ok('en 页 canonical 自指', !!enCanon && new URL(enCanon).pathname === new URL(page.url()).pathname, enCanon ?? '无 canonical');
|
||
|
||
// ---------- 折叠批:首页限 6 篇 / 归档年折叠 / 书架年折叠 ----------
|
||
await page.goto(BASE + '/', { waitUntil: 'networkidle' });
|
||
const homeTracks = await page.evaluate(() =>
|
||
[...document.querySelectorAll('.track-sec')].map((sec) => ({
|
||
label: sec.getAttribute('aria-label'),
|
||
rows: sec.querySelectorAll('.row.tap').length,
|
||
more: sec.querySelectorAll('.more-link').length,
|
||
})),
|
||
);
|
||
ok(
|
||
'首页分轨 阅读/技术 各 ≤6 + 各自更多入口',
|
||
homeTracks.length === 2 &&
|
||
homeTracks.every((t) => t.rows > 0 && t.rows <= 6 && t.more === 1) &&
|
||
homeTracks[0].label === '阅读' &&
|
||
homeTracks[1].label === '技术',
|
||
JSON.stringify(homeTracks),
|
||
);
|
||
|
||
await page.goto(BASE + '/archives/', { waitUntil: 'networkidle' });
|
||
const arch = await page.evaluate(() => {
|
||
const all = [...document.querySelectorAll('.year-fold')];
|
||
return {
|
||
total: all.length,
|
||
open: all.filter((d) => d.hasAttribute('open')).length,
|
||
firstOpen: all[0]?.hasAttribute('open') ?? false,
|
||
months: document.querySelectorAll('.month-head').length,
|
||
};
|
||
});
|
||
ok('归档 首年展开/往年折叠 + 月分组', arch.total >= 1 && arch.open === 1 && arch.firstOpen && arch.months >= 1, JSON.stringify(arch));
|
||
|
||
await page.goto(BASE + '/books/', { waitUntil: 'networkidle' });
|
||
const books = await page.evaluate(() => {
|
||
// 书架已改版为「抽书阅读」(Phase 5i T7):不再有年份折叠组,
|
||
// 改为每本书一个 details 行;断言 = 至少一行、首行收起、点击能展开出正文。
|
||
const all = [...document.querySelectorAll('.book')];
|
||
return { total: all.length, open: all.filter((d) => d.hasAttribute('open')).length };
|
||
});
|
||
ok('书架 抽书行渲染', books.total >= 1, JSON.stringify(books));
|
||
await page.locator('.book').first().locator('summary').click();
|
||
await page.waitForTimeout(250);
|
||
ok('书架 点击可抽开', (await page.locator('.book[open] .body').count()) === 1);
|
||
await page.locator('.book').first().locator('summary').click();
|
||
await page.waitForTimeout(200);
|
||
|
||
// 4) 命令面板 + 搜索
|
||
await page.keyboard.press('Control+k');
|
||
await page.waitForTimeout(400);
|
||
ok('命令面板·Ctrl K 打开', await page.locator('dialog[open] #search-input, dialog[open] input[type=search]').count() > 0);
|
||
await page.screenshot({ path: `${SHOTS}/04-palette-open.png` });
|
||
await page.fill('dialog[open] input', '等待');
|
||
await page.waitForTimeout(800);
|
||
const hasResult = (await page.locator('dialog[open] .results, dialog[open] [role=option], dialog[open] .cmd-item').allInnerTexts()).join('').includes('戈多');
|
||
ok('命令面板·Pagefind 搜索', hasResult);
|
||
await page.keyboard.press('Escape');
|
||
|
||
// 5) 键盘帮助与 g-chords
|
||
// 加固:面板测试后焦点可能残留输入域(? 被吞成字符)——Esc 收场+blur+重试三连
|
||
await page.keyboard.press('Escape');
|
||
await page.evaluate(() => document.activeElement?.blur?.());
|
||
await page.waitForTimeout(150);
|
||
let helpOpen = 0;
|
||
for (let i = 0; i < 3 && !helpOpen; i++) {
|
||
await page.keyboard.press('?');
|
||
await page.waitForTimeout(250);
|
||
helpOpen = await page.locator('#kbd-help[open]').count();
|
||
if (!helpOpen) { await page.keyboard.press('Escape'); await page.waitForTimeout(150); }
|
||
}
|
||
ok('键盘 ? 帮助浮层', helpOpen > 0);
|
||
// 洁净起点:前序测试(折叠/面板)可能留下在途导航或残留焦点,弦键在
|
||
// 页面被替换的瞬间会丢键——先回稳定页再测(测试意图=弦可用,不依赖前序状态)
|
||
await page.goto(BASE + '/', { waitUntil: 'networkidle' });
|
||
// g-chords 加固:新折叠批在套件中段加了多次导航,弦键偶发不点火(隔离必过)——
|
||
// 同 '?' 家法:Esc+blur+重试三连,避免单次时序失败污染验收
|
||
let chordOk = false;
|
||
for (let i = 0; i < 3 && !chordOk; i++) {
|
||
await page.keyboard.press('Escape');
|
||
await page.evaluate(() => document.activeElement?.blur?.());
|
||
await page.waitForTimeout(200);
|
||
await page.keyboard.press('g');
|
||
await page.keyboard.press('g');
|
||
await page.waitForTimeout(600);
|
||
chordOk = page.url().includes('/gallery');
|
||
}
|
||
ok('g-chords 导航到图库', chordOk, page.url());
|
||
|
||
// 6) 图库:网格可见(灯箱不再自动打开)+ 灯箱交互
|
||
// g-chords 的 location.href 与 goto 竞态(修好的弦导航在 en 页跳 /en/gallery/,
|
||
// 迟到提交会打断 goto)——中断重试一次
|
||
for (let i = 0; ; i++) {
|
||
try { await page.goto(BASE + '/gallery/', { waitUntil: 'networkidle' }); break; }
|
||
catch (e) { if (i >= 1) throw e; await page.waitForTimeout(400); }
|
||
}
|
||
ok('图库·灯箱默认关闭', await page.locator('#lb[open]').count() === 0);
|
||
ok('图库·网格照片可见', (await page.locator('.wall .shot img:visible').count()) >= 15, `${await page.locator('.wall .shot img:visible').count()} 张`);
|
||
// fullPage 截图不必然触发视口外懒加载:先整页滚动一圈再截(视觉复核 19/21 教训)
|
||
await page.evaluate(async () => {
|
||
for (let y = 0; y <= document.body.scrollHeight; y += 400) { window.scrollTo(0, y); await new Promise(r => setTimeout(r, 60)); }
|
||
window.scrollTo(0, 0);
|
||
});
|
||
await page.waitForTimeout(600);
|
||
await page.screenshot({ path: `${SHOTS}/05-gallery-light.png`, fullPage: true });
|
||
await page.click('.shot-btn >> nth=0');
|
||
await page.waitForTimeout(300);
|
||
ok('灯箱·点击打开', await page.locator('#lb[open]').count() === 1);
|
||
ok('灯箱·计数器', (await page.locator('#lb-count').innerText()).match(/\d+ \/ \d+/) !== null);
|
||
const t1 = await page.locator('#lb-count').innerText();
|
||
await page.keyboard.press('ArrowRight');
|
||
await page.waitForTimeout(300);
|
||
const t2 = await page.locator('#lb-count').innerText();
|
||
ok('灯箱·→ 切换', t1 !== t2, `${t1}→${t2}`);
|
||
ok('灯箱·关闭按钮存在', await page.locator('#lb-close:visible').count() === 1);
|
||
await page.screenshot({ path: `${SHOTS}/06-lightbox-open.png` });
|
||
await page.keyboard.press('Escape');
|
||
ok('灯箱·Esc 关闭', await page.locator('#lb[open]').count() === 0);
|
||
|
||
// 7) 分组筛选(当前库存单组:21 张全 landscape;验证筛选逻辑用街拍=0 反证)
|
||
await page.click('label[for=f-landscape]');
|
||
await page.waitForTimeout(200);
|
||
const landscapeShown = await page.locator('.shot:visible').count();
|
||
ok('图库·风景筛选', landscapeShown === 21, `${landscapeShown} 张`);
|
||
await page.click('label[for=f-street]');
|
||
await page.waitForTimeout(200);
|
||
const streetShown = await page.locator('.shot:visible').count();
|
||
ok('图库·街拍筛选(应为0)', streetShown === 0, `${streetShown} 张`);
|
||
await page.click('label[for=f-all]');
|
||
|
||
// 8) 书评字体(思源宋)与技术页 TOC
|
||
await page.goto(BASE + '/archives/mian-sha/', { waitUntil: 'networkidle' });
|
||
const ff = await page.locator('.prose-reading').evaluate((el) => getComputedStyle(el).fontFamily);
|
||
ok('书评·思源宋生效', ff.includes('Noto Serif SC'), ff.slice(0, 40));
|
||
await page.screenshot({ path: `${SHOTS}/07-reading-serif.png`, fullPage: false });
|
||
await page.goto(BASE + '/archives/csge-lei-pei-zhi/', { waitUntil: 'networkidle' });
|
||
ok('技术页·TOC 存在', await page.locator('.toc, nav [href^="#"]').count() > 0);
|
||
|
||
// 9) Artalk 评论加载(滚动到底触发懒加载)
|
||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||
try {
|
||
await page.waitForSelector('#artalk-container .atk, #artalk-container iframe, .atk-list', { timeout: 15000 });
|
||
ok('Artalk·评论区挂载', true);
|
||
} catch {
|
||
const containerText = await page.locator('#artalk-container').innerHTML().catch(() => '');
|
||
ok('Artalk·评论区挂载', false, `容器内容: ${containerText.slice(0, 80) || '空'}`);
|
||
}
|
||
await page.screenshot({ path: `${SHOTS}/08-artalk.png` });
|
||
|
||
// 10) RSS
|
||
const rss = await page.request.get(BASE + '/rss.xml');
|
||
ok('RSS 200', rss.status() === 200);
|
||
|
||
// ---------- 移动端(微信 UA)----------
|
||
const m = await newPage({ width: 390, height: 844 }, 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 MicroMessenger/8.0.49');
|
||
await m.goto(BASE + '/', { waitUntil: 'networkidle' });
|
||
ok('移动端·首页可渲染', await m.locator('.hm-grid, .plist, .plink').count() > 0);
|
||
await m.screenshot({ path: `${SHOTS}/09-mobile-home.png`, fullPage: false });
|
||
await m.goto(BASE + '/gallery/', { waitUntil: 'networkidle' });
|
||
ok('移动端·图库灯箱默认关', await m.locator('#lb[open]').count() === 0);
|
||
await m.screenshot({ path: `${SHOTS}/10-mobile-gallery.png`, fullPage: true });
|
||
await m.click('.shot-btn >> nth=0');
|
||
await m.waitForTimeout(300);
|
||
ok('移动端·灯箱可开', await m.locator('#lb[open]').count() === 1);
|
||
await m.screenshot({ path: `${SHOTS}/11-mobile-lightbox.png` });
|
||
|
||
// ---------- 暗色图库(视觉复核补张) ----------
|
||
const d = await newPage({ width: 1280, height: 800 });
|
||
await d.addInitScript(() => localStorage.setItem('theme', 'dark'));
|
||
await d.goto(BASE + '/gallery/', { waitUntil: 'networkidle' });
|
||
await d.screenshot({ path: `${SHOTS}/12-gallery-dark.png`, fullPage: true });
|
||
|
||
await browser.close();
|
||
const fails = results.filter((r) => !r.pass).length;
|
||
console.log(`\nE2E 汇总: ${results.length - fails}/${results.length} PASS${fails ? ',FAIL ' + fails : ''}`);
|
||
process.exit(fails ? 1 : 0);
|