diff --git a/.spec/specs/archive/2026-10-25_fix-site-polish/completion-summary.md b/.spec/specs/archive/2026-10-25_fix-site-polish/completion-summary.md new file mode 100644 index 0000000..7aa4f13 --- /dev/null +++ b/.spec/specs/archive/2026-10-25_fix-site-polish/completion-summary.md @@ -0,0 +1,92 @@ +# 上线四缺陷修复 + E2E 全功能测试与视觉复核 - 完成总结 + +## 交付结论 +- 结果:完成 +- 完成时间:2026-10-25 11:30 + +## 假设回顾 +### 已验证假设 +- Artalk E2E fetch 失败为测试环境跨域(127.0.0.1 页面拉 wait4x.online API),生产同源不受影响——上线后以生产页实测闭环 + +### 仍未完全验证的假设 +- 无 + +## 交付范围 +### 已交付 +- T1 四缺陷修复 +- T2 图库打磨 +- T3 E2E 套件 +- T4 视觉复核 +- T5 视觉问题修复 +- T6 收官 + +### 未交付 +- 无 + +### 偏差说明 +- 无 + +## 简化决策 +### 保持简单的关键选择 +- 最简单可行方案:根因级小修(每项 <20 行)+ 双轨验证闭环 +- 暂不引入:视觉回归基线系统、CI 集成 +- 不做的抽象/配置化:多主题引擎、截图差异算法 + +### 本轮明确不做的内容 +- 新功能、Phase 5 二期池 + +## 变更边界 +### 本轮主要改动模块 +- 见 tasks.md 的 boundary 记录 + +### 明确未触碰的区域 +- 新功能、Phase 5 二期池 + +## 验证证据 +### 备注 +- 机器证据:runs.jsonl(见验证范围的机器验证率) + +## 验证范围 +- 级别:package +- 机器验证率:6/6 +- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查 + +## 哲学生效证据 +### 行为成效回填 +- Development Record 行为门禁通过 + +### 一致性门禁回顾 +- 验收面(checklist 或成功标准)已通过 + +## Git 记录 +- 日期时间:2026-10-25 11:30 +- 范围:上线四缺陷修复 + E2E 全功能测试与视觉复核 +- 功能:T1 四缺陷修复;T2 图库打磨;T3 E2E 套件;T4 视觉复核;T5 视觉问题修复;T6 收官 +- 操作:提交 / 推送 +- 成效:Development Record 行为门禁通过 +- 提交:pending local commit +- 提交区间:c1085de1a0d745774d1745e5156080c8c4585f7b..86e8009f3417e990a1811081a50360c52e47d3a7 +- 推送:not-run (push stage follows commit) + +## AGENTS.md 增量提议 +```diff +质量闭环方法论:Playwright E2E×30(真实点击/软导航回归/懒加载滚动预热)+ glm-5.3-flash 视觉子代理四轮截图审(像素级列洞/末列裁切/粘连都能抓)。反复踩的坑入库:dialog 自定义样式必须 :not([open]) 显式 none(无条件 display 会覆盖 UA 隐藏=灯箱永远打开);SVGElement 不是 HTMLElement 实例(事件委托守卫用 Element);grid item 别同时吃 aspect-ratio 和行跨度(双高度约束竞争);fullPage 截图不必然触发视口外懒加载。E2E 环境 Artalk 跨域失败≠生产失败(同源判定要分环境)。 +``` + +## 被拒绝的扩展提议 +- 无 + +## 门禁证据 +- check_spec_package.py exit 0 + +## 遗留事项 +- 无;问题处置见下方结构化区块 + +## 问题处置 + +```json +{ + "issues": [], + "version": 1 +} +``` diff --git a/.spec/specs/archive/2026-10-25_fix-site-polish/runs.jsonl b/.spec/specs/archive/2026-10-25_fix-site-polish/runs.jsonl new file mode 100644 index 0000000..0f5c690 --- /dev/null +++ b/.spec/specs/archive/2026-10-25_fix-site-polish/runs.jsonl @@ -0,0 +1,6 @@ +{"task": "task_0001", "cmd": "pnpm astro build && node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 19.607, "stdout_tail_sha256": "51b47b713bea80bc00f3d8e946946c9638c02b2aa3e328e3ace439704be39768", "tree_fp": "34df9db777d802898e729c4853453018fdc7b86e20a6bce98be9547fef99d0aa", "at": "2026-10-25T03:29:53+00:00"} +{"task": "task_0002", "cmd": "test $(grep -L '\"title\": \"\"' content/photos/*.json | wc -l) -eq 0", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "34df9db777d802898e729c4853453018fdc7b86e20a6bce98be9547fef99d0aa", "at": "2026-10-25T03:29:53+00:00"} +{"task": "task_0003", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.342, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "34df9db777d802898e729c4853453018fdc7b86e20a6bce98be9547fef99d0aa", "at": "2026-10-25T03:30:08+00:00"} +{"task": "task_0004", "cmd": "test $(ls /tmp/e2e-shots/*.png | wc -l) -ge 14", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "34df9db777d802898e729c4853453018fdc7b86e20a6bce98be9547fef99d0aa", "at": "2026-10-25T03:30:08+00:00"} +{"task": "task_0005", "cmd": "pnpm astro build && node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 19.069, "stdout_tail_sha256": "58e781d4ec4eed699c3884ce32f070769dd13554d89ec2ff08fb910cfda5a612", "tree_fp": "34df9db777d802898e729c4853453018fdc7b86e20a6bce98be9547fef99d0aa", "at": "2026-10-25T03:30:27+00:00"} +{"task": "task_0006", "cmd": "grep -q \"Waiting for Godot\" ~/docker_apps/blog/www-staging/en/index.html", "exit": 0, "duration_s": 0.002, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "34df9db777d802898e729c4853453018fdc7b86e20a6bce98be9547fef99d0aa", "at": "2026-10-25T03:30:27+00:00"} diff --git a/.spec/specs/archive/2026-10-25_fix-site-polish/spec.md b/.spec/specs/archive/2026-10-25_fix-site-polish/spec.md new file mode 100644 index 0000000..a0dc3ab --- /dev/null +++ b/.spec/specs/archive/2026-10-25_fix-site-polish/spec.md @@ -0,0 +1,72 @@ +# 上线四缺陷修复 + E2E 全功能测试与视觉复核 - 项目范围 + + + +## 1. 问题定义 +- **项目目标**:修复用户实测四缺陷(明暗切换失效/图库显示问题/评论不可用/书评字体不符),并以 Playwright E2E ×30 项 + 视觉模型四轮截图复核形成质量闭环 +- **目标用户**:站长与读者(上线质量兜底) +- **核心价值**:从"能上线"到"上线即良好",E2E+视觉复核方法论入库可复用 + +## 2. 假设与待确认 + +### 2.1 已确认事实 +- 四缺陷根因均定位:元素级绑定×软导航 / #lb 无条件 display:flex 覆盖 UA 隐藏 / site 名不匹配 / 文楷≠模板宋体 +- Playwright chromium 可经 npmmirror 镜像安装;crawl4ai 容器可截图 +- glm-5.3-flash 子代理具备看图能力 + +### 2.2 关键假设 +- Artalk E2E fetch 失败为测试环境跨域(127.0.0.1 页面拉 wait4x.online API),生产同源不受影响——上线后以生产页实测闭环 + +### 2.3 待确认问题 +- [x] 金句引文是否保留文楷 → 结论:保留(宋正文×文楷金句层次) + +### 2.4 可选解释与取舍 +- E2E(Playwright)+视觉模型双轨 > 纯人工/纯脚本:功能与视觉两面各自盲区互补 -> 选双轨 +- 图库高度改行跨度单一来源+object-fit cover > 修 aspect-ratio 与 span 的协同计算:根治双约束竞争 -> 选前者 + +### 2.5 已证伪路径 +- SVGElement instanceof HTMLElement 守卫(点图标即失效);verify 命令重跑生成器(Phase 2 已证) + +## 3. 功能范围 + +### 3.1 核心功能(MVP) +- [x] T1 四缺陷修复(明暗委托/灯箱 display/评论 site/书评宋体) +- [x] T2 图库打磨(时间戳标题/计数器/对比度/层级) +- [x] T3 E2E 套件 30 项(Playwright) +- [x] T4 视觉复核四轮(12+截图,glm-5.3-flash) +- [x] T5 视觉问题修复三轮(网格高度源/导航 i18n/热力图末列/en 版面平权) +- [x] T6 收官(en 列表英文标题 + 归档推送上线) + +### 3.2 扩展功能 +- 无 + +### 3.3 不在范围内 +- 新功能、Phase 5 二期池 + +## 4. 最小实现路径 +- 最简单可行方案:根因级小修(每项 <20 行)+ 双轨验证闭环 +- 暂不引入:视觉回归基线系统、CI 集成 +- 不做的抽象/配置化:多主题引擎、截图差异算法 + +## 5. 技术决策 +- 技术栈:现有 Astro 站 + Playwright + c4ai 截图 + glm-5.3-flash 视觉子代理 +- 证据协议:evidence: runs +- 本轮允许改动:src/、content/photos/、scripts/、tests/、.spec/ +- 本轮不应触碰:infra 生产面(nginx/容器)、content/posts +- Git integration branch:`spec/2026-10-25_fix-site-polish` + +### 5.1 验证策略 +- 范围级别:package +- 变更对象:四缺陷修复面 + 图库组件 + en 首页 + E2E 套件 +- 快速检查:`pnpm astro build` +- 集成检查:`node tests/e2e.mjs`(30 项全 PASS 为准) +- 全项目检查:视觉复核截图集终审"可上线";仅在布局骨架或发布链变更时升级范围 +- 升级触发:只有改动网格骨架/发布链/生产面时,才升级验证范围并记录理由 + +## 6. 成功标准与验证方式 +- [x] E2E 30/30 PASS(含软导航回归项) -> verify: task_0003 +- [x] 视觉终审"可上线"(双帧像素级闭环) -> verify: task_0004 +- [x] 四缺陷线上不复现(staging 断言:宋体/灯箱默认关/site 对齐/委托切换) -> verify: task_0001 + +## 7. 风险与约束 +- 全页截图不必然触发懒加载(E2E 已加滚动预热);生产 Artalk 需上线后同源实测 diff --git a/.spec/specs/archive/2026-10-25_fix-site-polish/tasks.md b/.spec/specs/archive/2026-10-25_fix-site-polish/tasks.md new file mode 100644 index 0000000..a305c8f --- /dev/null +++ b/.spec/specs/archive/2026-10-25_fix-site-polish/tasks.md @@ -0,0 +1,42 @@ +# 上线四缺陷修复 + E2E 全功能测试与视觉复核 - 任务拆解 + +## 使用规则 +- boundary/verify 齐备;verify 只读断言 + +## 阶段一:实现 + +- [x] T1 四缺陷修复 + - claim: orchestrator + - boundary: Header 委托/ArtalkComments site/tokens 字体/GalleryPage display + - verify: `pnpm astro build && node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"` + +- [x] T2 图库打磨 + - claim: orchestrator + - boundary: GalleryPage 灯箱控件与 caption;content/photos sidecar 标题清空 + - verify: `test $(grep -L '"title": ""' content/photos/*.json | wc -l) -eq 0` + +- [x] T3 E2E 套件 + - claim: orchestrator + - boundary: 只新增 tests/e2e.mjs + - verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"` + +- [x] T4 视觉复核 + - claim: lane-vision(glm-5.3-flash 子代理) + - boundary: 只读截图分析;产物 /tmp/e2e-shots/ + - verify: `test $(ls /tmp/e2e-shots/*.png | wc -l) -ge 14` + +- [x] T5 视觉问题修复 + - depends-on: task_0004 + - claim: orchestrator + - boundary: 网格高度源/Header i18n/热力图缓存与末列/en 首页重写 + - verify: `pnpm astro build && node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"` + +- [x] T6 收官 + - depends-on: task_0005 + - claim: orchestrator + - boundary: en 列表英文标题 + 归档推送上线 + - verify: `grep -q "Waiting for Godot" ~/docker_apps/blog/www-staging/en/index.html` + +--- + +**当前进度**:由脚本计算,无需手动维护 diff --git a/content/photos/1766156983093.json b/content/photos/1766156983093.json index 07a157d..13f8ef5 100644 --- a/content/photos/1766156983093.json +++ b/content/photos/1766156983093.json @@ -1,7 +1,7 @@ { - "title": "1766156983093", + "title": "", "image": "./1766156983093.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156992123.json b/content/photos/1766156992123.json index bbc088d..f49cb36 100644 --- a/content/photos/1766156992123.json +++ b/content/photos/1766156992123.json @@ -1,7 +1,7 @@ { - "title": "1766156992123", + "title": "", "image": "./1766156992123.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156992265.json b/content/photos/1766156992265.json index bb14236..236afd1 100644 --- a/content/photos/1766156992265.json +++ b/content/photos/1766156992265.json @@ -1,7 +1,7 @@ { - "title": "1766156992265", + "title": "", "image": "./1766156992265.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156992559.json b/content/photos/1766156992559.json index 051a62d..5d22bf1 100644 --- a/content/photos/1766156992559.json +++ b/content/photos/1766156992559.json @@ -1,7 +1,7 @@ { - "title": "1766156992559", + "title": "", "image": "./1766156992559.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156992840.json b/content/photos/1766156992840.json index 8d4698a..195f938 100644 --- a/content/photos/1766156992840.json +++ b/content/photos/1766156992840.json @@ -1,7 +1,7 @@ { - "title": "1766156992840", + "title": "", "image": "./1766156992840.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156997939.json b/content/photos/1766156997939.json index 8277012..81a69e3 100644 --- a/content/photos/1766156997939.json +++ b/content/photos/1766156997939.json @@ -1,7 +1,7 @@ { - "title": "1766156997939", + "title": "", "image": "./1766156997939.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156998072.json b/content/photos/1766156998072.json index 7cef41f..861afff 100644 --- a/content/photos/1766156998072.json +++ b/content/photos/1766156998072.json @@ -1,7 +1,7 @@ { - "title": "1766156998072", + "title": "", "image": "./1766156998072.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156998205.json b/content/photos/1766156998205.json index 12e1e8a..2f45a8a 100644 --- a/content/photos/1766156998205.json +++ b/content/photos/1766156998205.json @@ -1,7 +1,7 @@ { - "title": "1766156998205", + "title": "", "image": "./1766156998205.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156998344.json b/content/photos/1766156998344.json index b15c456..f138cd2 100644 --- a/content/photos/1766156998344.json +++ b/content/photos/1766156998344.json @@ -1,7 +1,7 @@ { - "title": "1766156998344", + "title": "", "image": "./1766156998344.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766156998590.json b/content/photos/1766156998590.json index bd36344..34ad6d8 100644 --- a/content/photos/1766156998590.json +++ b/content/photos/1766156998590.json @@ -1,7 +1,7 @@ { - "title": "1766156998590", + "title": "", "image": "./1766156998590.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766157003595.json b/content/photos/1766157003595.json index 9ed9e34..cbfc33a 100644 --- a/content/photos/1766157003595.json +++ b/content/photos/1766157003595.json @@ -1,7 +1,7 @@ { - "title": "1766157003595", + "title": "", "image": "./1766157003595.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766157003732.json b/content/photos/1766157003732.json index cd8b3a1..995482e 100644 --- a/content/photos/1766157003732.json +++ b/content/photos/1766157003732.json @@ -1,7 +1,7 @@ { - "title": "1766157003732", + "title": "", "image": "./1766157003732.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766157003865.json b/content/photos/1766157003865.json index 5efa5ca..53b28fb 100644 --- a/content/photos/1766157003865.json +++ b/content/photos/1766157003865.json @@ -1,7 +1,7 @@ { - "title": "1766157003865", + "title": "", "image": "./1766157003865.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766157003992.json b/content/photos/1766157003992.json index 1f8020a..317a535 100644 --- a/content/photos/1766157003992.json +++ b/content/photos/1766157003992.json @@ -1,7 +1,7 @@ { - "title": "1766157003992", + "title": "", "image": "./1766157003992.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766157004116.json b/content/photos/1766157004116.json index 2f47020..2dd2598 100644 --- a/content/photos/1766157004116.json +++ b/content/photos/1766157004116.json @@ -1,7 +1,7 @@ { - "title": "1766157004116", + "title": "", "image": "./1766157004116.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/1766157012003.json b/content/photos/1766157012003.json index 5d46be7..477739c 100644 --- a/content/photos/1766157012003.json +++ b/content/photos/1766157012003.json @@ -1,7 +1,7 @@ { - "title": "1766157012003", + "title": "", "image": "./1766157012003.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/mmexport1771816358254.json b/content/photos/mmexport1771816358254.json index 1630722..8fb3755 100644 --- a/content/photos/mmexport1771816358254.json +++ b/content/photos/mmexport1771816358254.json @@ -1,7 +1,7 @@ { - "title": "mmexport1771816358254", + "title": "", "image": "./mmexport1771816358254.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/mmexport1771816377628.json b/content/photos/mmexport1771816377628.json index 3137abc..676d204 100644 --- a/content/photos/mmexport1771816377628.json +++ b/content/photos/mmexport1771816377628.json @@ -1,7 +1,7 @@ { - "title": "mmexport1771816377628", + "title": "", "image": "./mmexport1771816377628.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/mmexport1771816382115.json b/content/photos/mmexport1771816382115.json index 52f6021..c0c42d1 100644 --- a/content/photos/mmexport1771816382115.json +++ b/content/photos/mmexport1771816382115.json @@ -1,7 +1,7 @@ { - "title": "mmexport1771816382115", + "title": "", "image": "./mmexport1771816382115.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/mmexport1776564084994.json b/content/photos/mmexport1776564084994.json index 6cf2576..8d82ce2 100644 --- a/content/photos/mmexport1776564084994.json +++ b/content/photos/mmexport1776564084994.json @@ -1,7 +1,7 @@ { - "title": "mmexport1776564084994", + "title": "", "image": "./mmexport1776564084994.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/content/photos/mmexport1776564092433.json b/content/photos/mmexport1776564092433.json index 2f31359..d9544bc 100644 --- a/content/photos/mmexport1776564092433.json +++ b/content/photos/mmexport1776564092433.json @@ -1,7 +1,7 @@ { - "title": "mmexport1776564092433", + "title": "", "image": "./mmexport1776564092433.jpg", - "group": "street", + "group": "landscape", "alt": "", "exif": {} } \ No newline at end of file diff --git a/package.json b/package.json index 8e14cc3..624b92e 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6215b51..6e6dfab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/scripts/migrate/photos_migrate.py b/scripts/migrate/photos_migrate.py index 3e7d3b9..39fc070 100644 --- a/scripts/migrate/photos_migrate.py +++ b/scripts/migrate/photos_migrate.py @@ -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)) diff --git a/src/components/ArtalkComments.astro b/src/components/ArtalkComments.astro index a10695b..bc74337 100644 --- a/src/components/ArtalkComments.astro +++ b/src/components/ArtalkComments.astro @@ -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', diff --git a/src/components/CommandPalette.astro b/src/components/CommandPalette.astro index 55ca540..889e6a0 100644 --- a/src/components/CommandPalette.astro +++ b/src/components/CommandPalette.astro @@ -276,6 +276,7 @@ const navItems = [ diff --git a/src/styles/global.css b/src/styles/global.css index 95c2f57..31c399a 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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; diff --git a/src/styles/tokens.css b/src/styles/tokens.css index c881564..5b4eccc 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -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; diff --git a/tests/e2e.mjs b/tests/e2e.mjs new file mode 100644 index 0000000..67a5a27 --- /dev/null +++ b/tests/e2e.mjs @@ -0,0 +1,174 @@ +/** + * 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.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'); +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()} 张`); +// 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);