Files
blog/tests/e2e.mjs
T
wait4xandGLM-5.3 7a2880b65d fix(polish): 上线四缺陷修复——明暗委托/灯箱常开/评论站点名/书评宋体
- 明暗切换:document 级委托(软导航换 DOM 后元素级监听全丢);
  E2E 再抓一坑:SVGElement 非 HTMLElement 实例,守卫用 Element 基类
- 图库:#lb 无条件 display:flex 覆盖 UA 隐藏 → 灯箱"永远打开"盖全页;
  改 #lb:not([open]) 显式 none;时间戳标题清空;灯箱打磨(切换钮
  实底阴影/n/21 计数器/空标题隐藏/原图链接暗底亮蓝)
- 评论:site 'wait4x'→'wait4x.online' 对齐 Artalk 服务端(404 根因)
- 书评字体:文楷→思源宋(@fontsource 预分片);文楷降级金句 --font-kai
- 数据:21 张照片组修正 street→landscape(迁移器收到内部组 id 的坑)
- 新增 tests/e2e.mjs:Playwright 30 项全功能 E2E

验证: node tests/e2e.mjs 30/30 PASS(含软导航后明暗切换回归项)

Spec: 2026-10-25_fix-site-polish

Co-authored-by: GLM-5.3
2026-10-25 10:46:03 +08:00

168 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = 'http://127.0.0.1:18742';
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({ 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);
// 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}`);
await page.reload({ 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.keyboard.press('t'); // 回亮色
// 2) 软导航后切换不失效(本次修复的回归项)
await page.click('nav a[href="/archives/"]');
await page.waitForLoadState('networkidle');
await page.click('[data-theme-toggle]');
ok('明暗切换·软导航后仍活', (await page.getAttribute('html', 'data-theme')) === 'dark');
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'));
// 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
await page.keyboard.press('?');
await page.waitForTimeout(300);
ok('键盘 ? 帮助浮层', await page.locator('#kbd-help[open]').count() > 0);
await page.keyboard.press('Escape');
await page.keyboard.press('g');
await page.keyboard.press('g');
await page.waitForLoadState('networkidle');
ok('g-chords 导航到图库', page.url().includes('/gallery'));
// 6) 图库:网格可见(灯箱不再自动打开)+ 灯箱交互
await page.goto(BASE + '/gallery/', { waitUntil: 'networkidle' });
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()} 张`);
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);