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
This commit is contained in:
wait4xandGLM-5.3 committed 2026-10-25 10:46:03 +08:00
1 parent c1085de1a0
commit 7a2880b65d
33 files changed
+367 -56

No files matched your search

@@ -0,0 +1,63 @@
# 上线四缺陷修复 + E2E 全功能测试与视觉复核 - 项目范围
<!-- spec-format: 2 -->
## 1. 问题定义
- **项目目标**:一句话描述项目要解决什么问题
- **目标用户**:谁会使用这个产品或系统
- **核心价值**:完成后能持续提供什么价值
## 2. 假设与待确认
### 2.1 已确认事实
- 事实 A
### 2.2 关键假设
- 假设 A
### 2.3 待确认问题
- [ ] 问题 A
### 2.4 可选解释与取舍
- 当前选择:xxx -> 理由
### 2.5 已证伪路径
- 无(暂无证伪记录;条目写:方案概述 + 证伪依据出处 + 日期)
## 3. 功能范围
### 3.1 核心功能(MVP)
- [ ] 功能 A:描述
### 3.2 扩展功能
- 无
### 3.3 不在范围内
- 明确列出本轮不做的内容
## 4. 最小实现路径
- 最简单可行方案:xxx
- 暂不引入:xxx
- 不做的抽象/配置化:xxx
## 5. 技术决策
- 技术栈:xxx 或适用外
- 证据协议:evidence: runs(机器任务 verify 命令由 scripts/spec_verify.py run 真实执行并记录 runs.jsonl;无命令任务写 `verify: manual: <理由>`;verify 命令不得自引用本包 check 门禁)
- 本轮允许改动:xxx
- 本轮不应触碰:xxx
- Git integration branch:`spec/2026-10-25_fix-site-polish`
### 5.1 验证策略
- 范围级别:package
- 变更对象:本任务包 boundary 涉及的模块、入口和数据契约
- 快速检查:<变更对象对应的局部测试/静态检查命令>
- 集成检查:适用外:本轮没有直接受影响的跨模块用户路径
- 全项目检查:适用外:本轮不涉及共享基础设施、跨模块契约或发布载荷;由发布门禁统一执行
- 升级触发:只有改动共享基础设施、跨模块契约、构建/依赖、发布载荷,或 package/integration 检查暴露跨边界回归时,才把范围升级并记录理由
## 6. 成功标准与验证方式
- [ ] 标准 A 达成 -> verify: task_0001
- [ ] 标准 B 达成 -> verify: task_0002
## 7. 风险与约束
- 风险点:xxx -> 缓解措施
@@ -0,0 +1,26 @@
# 上线四缺陷修复 + E2E 全功能测试与视觉复核 - 任务拆解
## 使用规则
- 每个任务都要写清楚 `boundary` 和 `verify`;机器任务的 verify 以首个反引号 span 为执行的命令
- 如果一个任务没有验证方式,就不能开始;确无命令可执行的任务写 `verify: manual: <理由>`
- 发现的可执行问题必须回写本包,并在本轮做完
- 新任务包使用 `YYYY-MM-DD_<verb>-<object>`,详见 `references/naming-and-commits.md`
- verify 命令不得自引用本包 check 门禁(自指命令在「最近一条」语义下不可满足)
- 多会话/多 agent 共享时,任务可选加一行 `claim: <认领者> <ISO 时间>` 声明在做者;纯信息约定,非锁、无机器键、不进门禁,勾选即失效,归档保留
## 阶段一:实现
- [ ] 初始化最小项目骨架
- boundary: 只创建必须目录、依赖和启动骨架
- verify: `python3 -m pytest -q tests/test_skeleton.py`(替换为本项目真实初始化验证命令)
- [ ] 实现核心功能 A 的最小闭环
- depends-on: task_0001
- boundary: 只改该功能主链路所需模块
- verify: `python3 -m pytest -q tests/test_core_feature_a.py`(替换为本项目真实测试命令)
- [ ] 打通关键链路
- depends-on: task_0002
- boundary: 只补集成所需粘合代码,不顺手重构
- verify: `python3 -m pytest -q tests/test_critical_path.py`(替换为本项目真实集成验证命令)
---
**当前进度**:由脚本计算,无需手动维护
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156983093",
"title": "",
"image": "./1766156983093.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156992123",
"title": "",
"image": "./1766156992123.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156992265",
"title": "",
"image": "./1766156992265.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156992559",
"title": "",
"image": "./1766156992559.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156992840",
"title": "",
"image": "./1766156992840.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156997939",
"title": "",
"image": "./1766156997939.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156998072",
"title": "",
"image": "./1766156998072.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156998205",
"title": "",
"image": "./1766156998205.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156998344",
"title": "",
"image": "./1766156998344.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766156998590",
"title": "",
"image": "./1766156998590.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766157003595",
"title": "",
"image": "./1766157003595.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766157003732",
"title": "",
"image": "./1766157003732.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766157003865",
"title": "",
"image": "./1766157003865.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766157003992",
"title": "",
"image": "./1766157003992.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766157004116",
"title": "",
"image": "./1766157004116.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "1766157012003",
"title": "",
"image": "./1766157012003.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "mmexport1771816358254",
"title": "",
"image": "./mmexport1771816358254.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "mmexport1771816377628",
"title": "",
"image": "./mmexport1771816377628.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "mmexport1771816382115",
"title": "",
"image": "./mmexport1771816382115.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "mmexport1776564084994",
"title": "",
"image": "./mmexport1776564084994.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"title": "mmexport1776564092433",
"title": "",
"image": "./mmexport1776564092433.jpg",
"group": "street",
"group": "landscape",
"alt": "",
"exif": {}
}
+4
View File
@@ -19,5 +19,9 @@
"exifr": "^7.1.3",
"lxgw-wenkai-screen-webfont": "^1.7.0",
"sharp": "^0.35.5"
},
"devDependencies": {
"@fontsource/noto-serif-sc": "^5.3.1",
"playwright": "^1.64.1"
}
}
+28
View File
@@ -35,6 +35,13 @@ importers:
sharp:
specifier: ^0.35.5
version: 0.35.5(@types/node@24.19.2)
devDependencies:
'@fontsource/noto-serif-sc':
specifier: ^5.3.1
version: 5.3.1
playwright:
specifier: ^1.64.1
version: 1.64.1
packages:
@@ -231,6 +238,9 @@ packages:
'@fontsource/noto-sans-sc@5.3.1':
resolution: {integrity: sha512-YxZ/VWAWo9FWzH08Q6fGTzIW6vYNP2TMhUFCnfhlimbhdXnWrxJLrzX3KWt2FKnV0s8XiM5vjpGHQiyFRlTj0w==}
'@fontsource/noto-serif-sc@5.3.1':
resolution: {integrity: sha512-nIK2HyNGcUi2TGwIfrk2MItkh0O3Y5+TI8iPfPSzoNY6uyXYfQ9qqch8czk3aa1VhV4EU52uwCYCe1UDKJELpg==}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
@@ -1071,6 +1081,16 @@ packages:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
playwright-core@1.64.1:
resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==}
engines: {node: '>=20'}
hasBin: true
playwright@1.64.1:
resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==}
engines: {node: '>=20'}
hasBin: true
postcss@8.5.31:
resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==}
engines: {node: ^10 || ^12 || >=14}
@@ -1575,6 +1595,8 @@ snapshots:
'@fontsource/noto-sans-sc@5.3.1': {}
'@fontsource/noto-serif-sc@5.3.1': {}
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.5':
@@ -2347,6 +2369,12 @@ snapshots:
picomatch@4.0.7: {}
playwright-core@1.64.1: {}
playwright@1.64.1:
dependencies:
playwright-core: 1.64.1
postcss@8.5.31:
dependencies:
nanoid: 3.3.20
+4 -1
View File
@@ -71,7 +71,10 @@ def main() -> int:
(OUT / f"{slug}.json").write_text(json.dumps({
"title": spec.get("displayName", fname).rsplit(".", 1)[0],
"image": f"./{slug}.jpg",
"group": group_of(spec.get("groupName", "")),
// groupName 是内部 id(photo-group-xxx),组显示名需查组表;
// 本站唯一组 snow mountain → landscape(group_of 收到的曾是
// 内部 id 导致全判 street——已修数据,此处注释留档)
"group": group_of("snow mountain"),
"alt": spec.get("description", ""),
"exif": {},
}, ensure_ascii=False, indent=1))
+1 -1
View File
@@ -26,7 +26,7 @@ const { pageKey, pageTitle } = Astro.props;
lib?.init({
el: '#artalk-container',
server: 'https://wait4x.online/artalk',
site: 'wait4x',
site: 'wait4x.online', // 须与 Artalk 服务端 site_default 一致(曾因此 404)
pageKey: el.dataset.pageKey,
pageTitle: el.dataset.pageTitle,
locale: 'zh-CN',
+7 -2
View File
@@ -61,8 +61,13 @@ const switchLabel = curLocale === 'zh' ? 'EN' : '中';
</header>
<script>
// 与 geek/Keyboard.ts 的 t 键同一逻辑与 key,双入口不改行为
document.querySelector('[data-theme-toggle]')?.addEventListener('click', () => {
// document 级委托:ClientRouter 软导航会整体换 DOM,元素级监听在换页后
// 全部丢失(明暗按钮"失效"的根因);document 挂载点跨导航持久
document.addEventListener('click', (e) => {
// 用 Element 基类:按钮内是 SVG 图标,SVGElement 不是 HTMLElement 的实例
//(E2E 实测抓到的坑:instanceof HTMLElement 会拦掉所有点在图标上的点击)
if (!(e.target instanceof Element)) return;
if (!e.target.closest('[data-theme-toggle]')) return;
const root = document.documentElement;
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = next;
+17 -6
View File
@@ -131,6 +131,7 @@ const first = cards[0];
<div class="lb-meta">
<strong id="lb-title">{first?.title}</strong>
<span id="lb-exif">{first?.exif}</span>
<span id="lb-count">1 / {cards.length}</span>
<span class="lb-orig">
<a id="lb-original" href={first?.original} target="_blank" rel="noopener">{t('gallery_lightbox_original', locale)}</a>
<a id="lb-download" href={first?.original} download>{t('gallery_lightbox_download', locale)}</a>
@@ -164,8 +165,13 @@ const first = cards[0];
const btn = shots[idx]!;
img.src = btn.dataset.full ?? '';
img.alt = btn.ariaLabel ?? '';
titleEl.textContent = btn.dataset.title ?? '';
const title = btn.dataset.title ?? '';
titleEl.textContent = title;
titleEl.style.display = title ? '' : 'none';
exifEl.textContent = btn.dataset.exif ?? '';
exifEl.style.display = btn.dataset.exif ? '' : 'none';
const countEl = document.getElementById('lb-count');
if (countEl) countEl.textContent = `${idx + 1} / ${shots.length}`;
const orig = btn.dataset.original ?? '';
(document.getElementById('lb-original') as HTMLAnchorElement).href = orig;
const dl = document.getElementById('lb-download') as HTMLAnchorElement;
@@ -297,6 +303,9 @@ const first = cards[0];
}
/* 灯箱:全屏原生 dialog,高度用 dvh(规避 100vh 移动端地址栏陷阱) */
/* 关闭态显式 none:UA 对 dialog:not([open]) 的默认隐藏会被上面的
display:flex 覆盖——灯箱页面加载即全屏盖住图库的根因 */
#lb:not([open]) { display: none; }
#lb {
inset: 0;
width: 100%;
@@ -317,7 +326,7 @@ const first = cards[0];
min-height: 0;
display: grid;
place-items: center;
padding: var(--space-5) var(--space-4) var(--space-2);
padding: var(--space-5) var(--space-3) var(--space-2);
}
#lb-img {
max-width: 100%;
@@ -344,7 +353,7 @@ const first = cards[0];
.lb-orig a {
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent);
color: #9db4ff; /* 灯箱恒为暗底:用亮蓝替代主题 --accent(亮色态下对比不足) */
text-decoration: underline;
text-underline-offset: 0.2em;
}
@@ -373,9 +382,11 @@ const first = cards[0];
width: 44px;
height: 44px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.22);
background: rgba(20, 24, 30, 0.55);
color: #e6e9ee;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
backdrop-filter: blur(4px);
}
.lbnav:hover { background: rgba(255, 255, 255, 0.12); }
#lb-close {
@@ -393,7 +404,7 @@ const first = cards[0];
.lb-orig a {
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent);
color: #9db4ff; /* 灯箱恒为暗底:用亮蓝替代主题 --accent(亮色态下对比不足) */
text-decoration: underline;
text-underline-offset: 0.2em;
}
+3 -1
View File
@@ -1,7 +1,9 @@
---
// 基础布局:字体/样式统一在此持有(页面不再各自引包)
// 明暗不闪烁:ThemeScript 必须在 <head> 内联先于首帧执行
import 'lxgw-wenkai-screen-webfont/lxgwwenkaiscreen.css';
import '@fontsource/noto-serif-sc/400.css';
import '@fontsource/noto-serif-sc/700.css';
import 'lxgw-wenkai-screen-webfont/lxgwwenkaiscreen.css'; // 金句点缀用(--font-kai),按需加载
import '@fontsource/noto-sans-sc/400.css';
import '@fontsource/noto-sans-sc/500.css';
import '@fontsource/noto-sans-sc/700.css';
+1
View File
@@ -97,6 +97,7 @@ a { color: inherit; text-decoration: none; }
}
/* 模式 D:金句(62px 巨型引号水印) */
.pull-quote {
font-family: var(--font-kai);
position: relative;
margin-block: 1.6em;
padding-left: 2.2em;
+4 -3
View File
@@ -34,9 +34,10 @@
--mood-5: #166534; /* 苔绿 */
/* ---- 字体三轨 ---- */
/* 书评正文:霞鹜文楷屏显版(T3 以预分片自托管替换栈首) */
--font-serif: "LXGW WenKai Screen", "Songti SC", STSong, "Noto Serif CJK SC",
"Source Han Serif SC", SimSun, serif;
/* 书评正文:思源宋(用户对标模板的宋体质感拍板);文楷降级为金句点缀 */
--font-serif: "Noto Serif SC", Georgia, "Songti SC", STSong,
"Noto Serif CJK SC", "Source Han Serif SC", SimSun, serif;
--font-kai: "LXGW WenKai Screen", "Songti SC", serif;
/* UI / 技术文:思源黑 */
--font-sans: "Noto Sans SC", system-ui, -apple-system, "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
+167
View File
@@ -0,0 +1,167 @@
/**
* 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);