加载中...

加载中...

本文是 matery 主题自动化测试体系的完整教程,与其余 4 篇功能测试文章(内容 tag / 交互视觉 / 布局页面 / Markdown 语法)配套——那 4 篇是"测试靶场",本文是"测试引擎"。权威依据:tools/tests/release-test.sh(137 行纯编排)+ tools/tests/layers/manifest(13 层唯一权威清单)、docs/superpowers/specs/2026-08-06-layout-test-cases.md。

阅读方式:每个命令都先给 输出(真实终端的样貌),再给 对应命令(产出该输出的那一行)。看到输出想复现就抄下面的命令;想查参数直接跳「全部参数」表。

什么时候看这篇?

你是谁你该看什么你能得到什么
普通用户(想知道某个功能是否正常)附录「测试 ↔ 功能对照表」按功能名找到对应测试文件,确认"这个功能有自动验证"
贡献者(想新增/修改测试用例)§12「如何新增测试用例」从零到挂入门禁的完整步骤(含代码模板)
维护者(发版前跑门禁)§2「release-test.sh 用法」本地迭代 / 发版门禁 / 视觉断言的命令
排查问题(门禁报红)§5「外部依赖重试」+ §6「第三方拦截」+ ⚠️ 已知坑区分"环境噪声"和"真 bug"

1. 测试金字塔总览

用途:一眼看清"13 层门禁各自验证什么、依赖什么",决定跑哪一档。

学习路径:看层清单 → 看三种典型输出 → 查层表 → 记住执行顺序的坑

1.1 最小用法

输出(manifest 的原文——这就是金字塔的全部 13 层,行序 = 执行顺序):

# 门禁层清单 —— 唯一权威(行序 = 执行顺序)
# 格式:ID<TAB>脚本文件<TAB>入口函数<TAB>描述
# 消费者:tools/tests/release-test.sh(source + 调用)、tools/tests/gen-tc-index.js(层清单)
L10	L10_smoke.sh	run_L10	发版前 SW 冒烟(远程测试域;--local 跳过)
L01	L01_build.sh	run_L01	构建验证
L02	L02_urls.sh	run_L02	URL 与链接完整性
L03_05	L03_05_static.sh	run_L03_05	静态资源与页面渲染(L3-L6)
L07	L07_core.sh	run_L07	核心功能
L08	L08_enhanced.sh	run_L08	增强功能
L09	L09_sw.sh	run_L09	Service Worker
L11	L11_integration.sh	run_L11	Playwright 集成
L12	L12_local.sh	run_L12	本地化资源 + 多语言一致性
L2i	L2i_seo.sh	run_L2i	SEO 结构化数据
L13	L13_units.sh	run_L13	全量单测门禁
L13a	L13a_cov.sh	run_L13a	覆盖率阈值
L14	L14_visual.sh	run_L14	视觉断言

对应命令:

cat tools/tests/layers/manifest

把上表按"依赖成本"摞起来,就是金字塔的形状:

┌──────────────┐
│  L2 生产 URL  │  发版后:20 页面 HTTP 200 + 功能标记(curl)
│  L9/L10 SW   │  发版前:Vercel 测试域 SW 冒烟(playwright)
├──────────────┤
│  L11 Playwright 集成 │  真实浏览器:特效/词云/懒加载/阅读模式等 18 项
├──────────────┤
│  L14 视觉断言 │  visual-assert 精确数值:12 套件(生产阻塞,本地默认跳过)
├──────────────┤
│  L13/L13a 单测 │  node 直跑:15 个 .test.js + search 集成 + 4 个 shell 单测 + 教程结构门禁;L13a 再扫 unit/** 覆盖率
├──────────────┤
│  L1 构建验证  │  hexo g Success + 产物完整(index.html > 1000B)
└──────────────┘

1.2 常用参数

① 最上层(构建)的输出现场

输出:

🔨 L1: 构建验证
  ✅ 构建成功 (hexo g Success)
  ✅ public/index.html 存在 (123456 字节)

对应命令:

tools/tests/release-test.sh          # L1 由其内部调用 cicd.sh -d 产出

② 中间层(单测)的输出现场——每个文件一行 ✅

输出:

🧪 L13: 全量单元/集成测试门禁(node tools/tests/*.test.js)
  ✅ 单测 unit-toc.test.js
  ✅ 单测 unit-search.test.js
  ✅ 单测 unit-css-architecture.test.sh(CSS 架构)

对应命令:

node tools/tests/unit/unit-toc.test.js   # 单跑其中一个,看它自己的输出

③ 浏览器层(L11)的输出现场——输出带提取到的 PASS 计数

输出:

✅ 词云/去jQuery/防闪烁集成测试(PASS=8)
✅ 内容宽度统一测试(PASS=5)

对应命令:

node tools/tests/wordcloud.test.js

④ 最底层(视觉断言)的输出现场——按套件汇总

输出:

🎯 L14: 视觉断言(visual-assert)
  ✅ L14 layout-checks:18 项全部通过
  ✅ L14 dark-mode:4 项全部通过
  ⏱️  L14 总计:12 文件通过, 0 文件失败(299s)

对应命令:

node tools/visual-assert.js run --local tools/visual-assert/dark-mode.json

1.3 全部参数

层名称运行方式依赖
L1构建验证hexo generate + 产物检查无
L13全量单测门禁node tools/tests/*.test.js + shell 单测无 chromium
L11Playwright 集成真实浏览器 + 本地 serverchromium + server
L2/L2b/L2x/L2y/L2i生产 URL 断言curl + grep生产/本地 server
L3-L8功能标记检查curl + 静态断言生产/本地 server
L9静态资源 + SW 冒烟curl + playwright生产域名
L13a覆盖率统计node --experimental-test-coverage 扫 unit/**无
L14视觉断言node tools/visual-assert.js run(12 套件)chromium + server
L10SW 冒烟(测试域)playwrightVercel 测试域 blog2.17lai.site

1.4 边界与坑

  • 执行顺序不是文档顺序:manifest 行序 = 执行序,L10 最先跑(在 L1 之前)。旧文档常把 L1 写在第一行——那是文档习惯,不是执行事实。
  • 层脚本本身不能直接执行:layers/*.sh 只定义 run_LXX(),依赖 release-test.sh 先 source lib/assert.sh(pass/fail 是全局计数器)。要单独验证一层,跑它内部对应的测试文件,而不是 bash layers/L13_units.sh。
  • 层脚本缺失即硬失败:manifest 里写了某文件但 layers/ 下没有 → ❌ 层脚本缺失:xxx.sh 直接 exit 1,不会静默跳过。
  • L13 与 L13a 有重叠:L13 是"点名跑"(失败时名字清晰),L13a 是"通配扫 unit/** 并卡覆盖率",同一文件可能被跑两次,这是刻意的双保险。

1.5 小结

层清单只在一个文件里:tools/tests/layers/manifest。想知道"有哪几层、按什么顺序跑",永远看它,不看文档也不看聊天记录。


2. release-test.sh 用法

用途:发版前/后跑全量门禁;本地迭代只跑能跑的那部分。

学习路径:跑一次看骨架 → 按场景挑参数 → 查参数表 → 记住基址与并发限制

2.1 最小用法

输出(一次完整门禁的骨架——首尾固定,中间按层展开):

=============================================
 发版全量自动化测试 (target: https://blog.17lai.site)
=============================================
🔍 [preflight] 环境预检...
  ✅ 构建产物有效 (123456 字节)
  ✅ playwright 可用(NODE_PATH=/home/andy/.local/share/mise/installs/npm-playwright/1.56.0/lib/node_modules)
  ✅ _config.yml 未被污染
  ✅ 网络基址可达(HTTP 200)
✅ [preflight] 环境就绪
🔒 [preflight] 已获取门禁锁(PID 12345)

🌐 L10: 发版前 SW 冒烟(Vercel 测试域 blog2.17lai.site)
...
 测试结果: PASS=<N> FAIL=0
=============================================
🎉 全部通过!

对应命令:

tools/tests/release-test.sh

2.2 常用参数

① --local(本地迭代最常用)

输出(汇总段明确告知跳过了什么):

 ⏭️  已跳过 L10(远程 SW 冒烟)与 L11(playwright 集成)—— 本地迭代模式;
     发版门禁(不带 --local)会跑全量,L11 覆盖内容宽度/灯箱/懒加载/阅读模式等
 测试结果: PASS=<N> FAIL=0
🎉 全部通过!

对应命令:

tools/tests/release-test.sh --local

② --skip-build(public/ 已就绪,只想复跑断言)

输出(L1 两条都变成"跳过",不再调 docker):

🔨 L1: 构建验证
  ✅ 跳过构建(--skip-build)
  ✅ 跳过产物验证(--skip-build)

对应命令:

tools/tests/release-test.sh --skip-build

③ --skip-l10(只跳过远程 SW 冒烟,保留 L11 浏览器集成)

输出:

🌐 L10: 发版前 SW 冒烟(Vercel 测试域 blog2.17lai.site)
  ⏭️ 跳过 L10(--local/--skip-l10:本地迭代不验证远程 SW)

对应命令:

tools/tests/release-test.sh --skip-l10

④ --prepare(环境没就绪时一键拉起)

输出:

🔧 [preflight] 自动就绪环境...
  ✅ 容器已在运行
  ✅ 产物有效 (123456 字节),跳过重建
🔍 [preflight] 复检环境...
✅ [preflight] 环境就绪

对应命令:

tools/tests/release-test.sh --prepare

⑤ RUN_L14=1(本地也跑视觉断言)

输出(原本"skip",现在真跑):

🎯 L14: 视觉断言(visual-assert)
  ✅ L14 layout-checks:18 项全部通过
  ⏱️  L14 总计:12 文件通过, 0 文件失败(299s)

对应命令:

RUN_L14=1 tools/tests/release-test.sh --local

⑥ SKIP_L14=1(强制跳过,优先级高于 RUN_L14)

输出:

🎯 L14: 视觉断言(visual-assert)
  ⏭️  L14 视觉断言(SKIP_L14=1 → 强制跳过)

对应命令:

SKIP_L14=1 RUN_L14=1 tools/tests/release-test.sh

2.3 全部参数

参数作用
--skip-build跳过 L1 构建 + 产物验证(已构建时用)
--local目标改本地基址(端口自适应探测,见下方基址段),跳过 L10(远程 SW 冒烟 5min+)与 L11(playwright 串行 >800s)
--skip-l10仅跳过 L10,保留 L11 playwright 集成
--prepare自动就绪环境:容器未跑则启动、产物无效则重建、最后复检
RUN_L14=1本地模式下启用 L14 视觉断言(默认本地跳过)
SKIP_L14=1强制跳过 L14(即使 RUN_L14=1)

基址:生产固定 https://blog.17lai.site(443,绝无端口);本地 LOCAL_BASE 走端口自适应——探测顺序 BLOG_PORT 环境变量 → TEST_BASE 环境变量 → 根 _config.yml 的 server.port(docker 4000)→ 4100,取第一个 HTTP 200;成功后导出 TEST_BASE/TEST_URL,36 个测试文件自动跟随。

执行顺序(manifest 行序):L10 → L01 → L02 → L03_05 → L07 → L08 → L09 → L11 → L12 → L2i → L13 → L13a → L14 → TC 索引校验 → 汇总。注意 L10 最先跑(在 L01 之前)——旧文档常写反。

发版铁律:tools/cicd.sh -r 发布后必须跑 tools/tests/release-test.sh 验证生产环境。

2.4 边界与坑

① 并发会被直接拒绝

输出:

❌ [preflight] 门禁已在运行(PID 12345),拒绝并发启动
   锁文件:/tmp/opencode/gate.lock
   💡 如确认无其他门禁在跑,可手动删除:rm /tmp/opencode/gate.lock

对应命令:

tools/tests/release-test.sh --local   # 另开一个终端重复执行即可复现

并发会互相 kill 本地 server——这是硬拒绝,不是警告。陈旧锁(持锁进程已死)自动接管并打印 ⚠️ [preflight] 陈旧锁(PID xxxx 已死),自动接管。

② 环境没就绪时秒级失败,并给出修复命令

输出:

❌ 本地服务器不可达(已尝试端口: 4000 4100)

  💡 修复命令(任选其一):
     1. docker 生成: docker compose -f docker-compose.test.yml exec -T hexo bash -c 'tools/cicd.sh -d'
     2. 本机预览:    tools/test.sh server
     3. 恢复服务:    docker compose -f docker-compose.test.yml exec -T hexo bash -c 'pm2 restart all'

对应命令:

tools/tests/release-test.sh --local

③ 失败汇总的形状

输出:

 测试结果: PASS=<N> FAIL=2
=============================================
❌ 存在失败项,请检查!

对应命令:

tools/tests/release-test.sh        # exit code 1
  • 只 --local 不传 RUN_L14 时 L14 必跳——别把"没跑"误读成"通过"。
  • --local 会同时关掉 L10 和 L11,不是只关 L10;只想关 L10 用 --skip-l10。

2.5 小结

三个高频组合:本地迭代 --local、复跑断言 --skip-build、发版 无参数(必须全绿才算发版完成)。


3. L1-L14 各层覆盖内容

用途:知道每一层"断言什么",报红时能立刻定位是哪个功能坏了。

学习路径:看四层典型输出 → 查覆盖表 → 看 L11 清单 → 记住未挂层/重复项

3.1 最小用法

输出(L1 是最小的一层——只有两条断言):

🔨 L1: 构建验证
  ✅ 构建成功 (hexo g Success)
  ✅ public/index.html 存在 (123456 字节)

对应命令:

tools/tests/release-test.sh            # 不带 --skip-build 即跑 L1

3.2 常用参数

① L2:URL 可达性——每条路径一行

输出:

✅ /archives/ → 200
✅ /categories/ → 200
✅ /tags/ → 200

对应命令:

curl -s -o /dev/null -w '%{http_code}\n' https://blog.17lai.site/archives/

② L7:核心功能——功能名 + 实测值

输出:

✅ 搜索入口
✅ TOC 折叠配置(<实际配置值>,0=全部折叠)
✅ 暗色模式 CSS
✅ infinite-scroll 状态元素 + data-loading 文案 (<i18n 文案>)

对应命令:

curl -s https://blog.17lai.site/ | grep -o 'id="searchInput"' | head -1

③ L13:单测门禁——每文件一行

输出:

✅ 单测 unit-wiki-helper.test.js
✅ 单测 search-integration.test.js(PASS=<N> FAIL=0)
✅ 教程文章结构门禁 check-tutorial-articles.js

对应命令:

node tools/tests/unit/unit-wiki-helper.test.js

④ L14:视觉断言——按套件汇总

输出:

✅ L14 homepage:<N> 项全部通过
⚠️  L14 nav-scrolled 第 1 次未通过(疑似负载抖动),重试一次…
✅ L14 nav-scrolled:<N> 项全部通过
⏱️  L14 总计:12 文件通过, 0 文件失败(312s)

对应命令:

node tools/visual-assert.js run --local tools/visual-assert/nav-scrolled.json

3.3 全部参数

层覆盖内容断言示例
L1构建验证hexo g Success + public/index.html > 1000B
L220 个 layout 页面 URL 可达/posts/fc764afd/、/archives/、/categories/、/tags/、/about/、/friends/、/galleries/、/bb/、/musics/、/movies/、/msg/ + 4 篇测试文章 → HTTP 200
L2b多语言验证en 文章真实翻译 / 回落提示条 / 评论 path 归一化 / 默认语言无前缀
L2x统一语言回落MATERY_LANG 注入 / 回落页 / 独立页多语言(含 gallery 子页)/ 聚合页多语言
L2y加密解密刷新wiki 侧栏无元数据/分区键泄漏(structure/i18n/encrypt_)/ 加密容器完整(hbeForm + storage-key)
L2iSEO 结构化数据og:image / JSON-LD / fetchpriority
L3评论区检查msg/friends/bb 页含 #comments
L4href=/ 样式错误页面 href=/ 引用数应为 0
L5版本号一致性config vs sw.tmp.js 3 处一致
L6关键资源检查404 页面 / 关键 JS 版本
L7核心功能覆盖相册灯箱 / 加密相册 / 搜索 / JS 模块合并 / TOC 折叠 / 暗色 / 评论 / infinite-scroll / wiki 代码块 / wiki 组件样式(main 含 post-container / .wiki-content 折行 / admonition .note 规则精确限定)
L8增强功能覆盖tag 插件渲染(note/timeline/tabs/label/githubCard/mermaid)/ 加密文章 / Feed / 静态资源 / 分析
L9静态资源 + SW 冒烟main.css > 50KB / utils.js > 5KB / 生产域名 7 页无 SW 错误
L10SW 冒烟(测试域)blog2.17lai.site 7 页无 SW 错误 + 交互验证(搜索/打赏/灯箱/TOC/暗色)
L11Playwright 集成(18 项)见下表
L12本地化资源检查mermaid / aplayer 资源存在
L13全量单测门禁15 个 *.test.js + search 集成 + 4 个 *.test.sh + 1 个教程结构门禁(详见 §4)
L13a覆盖率统计unit/**/*.test.js 覆盖率扫描(line ≥85 / branch ≥77 / funcs ≥70)
L14视觉断言(12 套件)见 §9

评论库版本基准(2026-09-24 升级,配置见 userConfig/_config.tmp.yml libs 段):waline v=3.15.2(启用)/ twikoo v=2.0.8(禁用)/ valine v=1.5.3 + av v=5.19.2(禁用)/ gitalk v=1.8.0(废弃);skill matery-libs/references/{waline,twikoo,valine,gitalk}.md 同步记录版本与升级沿革。

L11 Playwright 集成清单(真实浏览器)

测试文件覆盖
effects-integration.test.js页面特效(sakura/snowflake/fireworks 加载)
videobg-integration.test.js视频背景(门槛/亮暗池/webm 契约/禁用态)
wordcloud.test.js词云 / 去 jQuery / TOC 防闪烁 / 明暗切换
i18n.test.js多语言补全(MATERY_I18N 注入 + 按钮文案 + en 翻译)
musics-integration.test.js在线音乐 APlayer(多域名 failover)
lazyload-integration.test.js懒加载 handleCard 三分发(51 断言)
floating-panel-integration.test.js悬浮面板(开关/字体缩放/持久化)
content-width.test.js内容宽度统一(4K/FHD/2K/单栏封顶)
readmode-integration.test.js阅读模式 / 代码全屏 / scroll-down-bar
wiki-styles.test.jsWiki 页 tag 组件样式(note / ::: 容器色 + 居中 + 宽度一致性)
code-widget.test.jscodeWidget 委托幂等(二次调用后单次点击仍生效)
core-events-integration.test.js事件总线 / 进度条联动
wiki-prism-integration.test.jsWiki 代码块 prism + line-numbers
misc-interactions.test.js打赏弹窗 / 繁简转换(G14/G15)
lang-client-strategy.test.js客户端语言策略(全站跟随/爬虫跳过/面板回显,7 断言)
rich-content-integration.test.js富内容渲染(公式 MathJax+KaTeX/mermaid/markmap/打字机/分享/Live2D,11 断言)
wiki-integration.test.js多语言 wiki 可达 / 侧栏章节 / 语言切换器(18 断言)
infinite-scroll-integration.test.jsInfinite Scroll(Home/Archive 真实分页 + 标签页单页栅格)
lightbox.test.js灯箱(相册/画廊图片放大交互)
gallery-hover.test.js图库/卡片 hover 动画(保护 classList 多类名修复)

3.4 边界与坑

  • L11 本地必跳:--local 会整段跳过(⏭️ 跳过 L11 playwright 集成(--local:…)),所以"本地全绿"不等于"浏览器层没坏"。
  • L11 的通过判据不统一:有的文件看 PASS=N FAIL=0,有的看 0 失败,lazyload 干脆没有汇总行(判据是"没有 FAIL/失败/❌")。新增文件时必须对齐既有判据,否则会假绿。
  • L14 失败自动重试一次:⚠️ L14 xxx 第 1 次未通过(疑似负载抖动),重试一次…,重试通过会记入 flaky 列表(见 §11),不改变 PASS/FAIL 计数。
  • 未挂层文件已清零:wiki-integration.test.js、infinite-scroll-integration.test.js 早期存在但没进任何层,现已分别于 2026-09-27 / L11 段接入,有断言兜底。唯一仍未挂层的是 tools/tests/unit-fontawesome.test.py(L13a 的通配只收 .test.js),需手动跑 python3 tools/tests/unit-fontawesome.test.py。

3.5 小结

层数不等于覆盖度:L1/L13 是"能不能跑起来",L2/L7/L8 是"产物里有没有",L11/L14 是"浏览器里对不对"。三层缺一层,故障就会从缺的那层漏出去。


4. 单测清单(tools/tests/)

用途:查"哪个文件测什么",以及为什么有些文件跑两遍。

学习路径:跑一个文件看输出格式 → 认三种汇总行 → 查文件清单 → 记住 mock 的坑

4.1 最小用法

输出(单个测文件自报家门,末尾一定有一行汇总供门禁匹配):

✅ TOC 折叠配置解析(collapseDepth=0 视为全部折叠)
✅ collapseDepth=6 视为全部展开
✅ 未配置时取默认值

结果: PASS=3 FAIL=0

对应命令:

node tools/tests/unit/unit-toc.test.js

4.2 常用参数

① 三种汇总行格式(门禁按正则匹配,格式不能改)

输出:

结果: PASS=3 FAIL=0
结果: 5 通过, 0 失败
unit-i18n-core: 10 pass / 0 fail

对应命令:

grep -n 'console.log' tools/tests/unit/unit-toc.test.js | tail -2

② shell 单测的汇总行最短

输出:

结果: PASS

对应命令:

bash tools/tests/unit/unit-version.test.sh

③ L13a 覆盖率扫描(自带阈值,低于阈值直接 exit 1)

输出:

ℹ all files      | 86.66 | 78.98 | 72.73 |

门禁里被转成:

✅ node:test 单元测试 + 覆盖率达标(86.66/78.98/72.73)

对应命令:

node --test --experimental-test-coverage \
    --test-coverage-lines=85 --test-coverage-branches=77 --test-coverage-functions=70 \
    "tools/tests/unit/**/*.test.js"

4.3 全部参数

L13 门禁直跑清单(L13_units.sh 里逐文件点名,失败时名字清晰):

# node 单测(纯单测/静态,无浏览器依赖)
unit-toc.test.js          # TOC 折叠配置(collapseDepth 0||6 falsy 陷阱)
unit-search.test.js       # 搜索三引擎
unit-multilang.test.js    # 多语言配置
unit-encrypt2.test.js     # 加密扩展(片段 + wiki)
unit-wiki-helper.test.js  # wiki helper(语言解析/侧栏渲染/canonical 回落,10 用例)
helpers.test.js           # 字数统计三 helper(wordcount/min2read/totalcount;加密 origin/wiki 去重/回落/缓存重置,52 断言)
utils.test.js             # 通用工具函数
lang-fallback.test.js     # 回落链 4 级(13 断言)
echarts-charts.test.js    # ECharts 图表
banner-bg.test.js         # banner 与背景偏好联动
homepage.test.js          # 首页结构
imgsize-unit.test.js      # 图片尺寸注入脚本(normalizeImgUrl 协议相对 URL 补全 + 本地/网络图片尺寸读取)
infinite-scroll-aos.test.js # 无限滚动 + AOS 联动
unit-standalone.test.js   # 独立页排除法判定 + 语言前缀豁免(22 断言)
unit-i18n-helper.test.js  # t() helper(fallback_lang + 大小写容错,5 用例)

# 带外部依赖的集成(retry_match 包装,判据 PASS=N FAIL=0)
search-integration.test.js  # 搜索集成(Algolia 分页需真实 API)

# shell 单测
unit-css-architecture.test.sh   # CSS 架构(编译产物关键选择器)
unit-version.test.sh            # 版本递增进位(防 12.99→12.100)
unit-retry.test.sh              # 退避重试库
unit-deploy-target.test.sh      # 部署目标切换(hexo/hexoback 双激活校验)

L13a 通配覆盖(unit/**/*.test.js,含上面部分文件):

unit-i18n-core.test.js    # i18n 核心回落语义(resolveI18n 四级链 + cfg 防御,10 用例)
unit-sidebar-i18n.test.js # 侧栏 i18n 查表 + 回退链(当前→fallback→default→key,5 用例)
unit-sidebar-keys.test.js # 门禁校验器 sidebar-keys.js 行为(4 wiki × 9 语言覆盖,4 用例)
unit-encrypt.test.js      # 自研加密体系(AES-GCM + PBKDF2 闭环,9 用例)
unit-markmap-prune.test.js # markmap 无脑图页面资源拆除(6 用例)
unit-updated.test.js      # 「更新于」取 git 最后提交时间(4 用例)

未挂层(需手动跑):

unit-fontawesome.test.py  # FontAwesome 精简脚本(L13a 通配只收 .test.js,不含 .py)

4.4 边界与坑

mock 必须模拟真实 Hexo 对象(Query 而非普通数组)

  • site.posts / site.pages 是 Hexo 的 Query(类数组对象),.filter() 返回的仍是 Query 而非数组,且不能 for...of——必须先 .toArray() 取真数组。
  • helpers.test.js(字数统计)最初用普通数组 mock,单测全绿;真实构建却 wikiPages is not iterable → 全部 HTML 输出 0 字节。补上 toArray() 后才覆盖到真实路径。

输出格式不能随手改

输出(门禁匹配不到汇总行时的真实下场):

⏳ 搜索集成 无输出(疑似超时),退避 1s 重试...
❌ 搜索集成 业务断言失败(真 bug)

对应命令:

tools/tests/release-test.sh --local   # 改动末尾 console.log 后复跑即可复现

教训:mock 越接近真实对象,单测才越能拦住构建级故障;汇总行越稳定,门禁才越能认出"通过"。

4.5 小结

一个测试文件只有一个契约:末尾输出可被正则匹配的汇总行 + 失败时 exit 非 0。其余输出随便写,这两条不能动。


5. 外部依赖重试与降级(lib/retry.sh)

用途:把"外部服务抖动"和"自己的 bug"分开,前者不拦发版。

学习路径:看一次重试输出 → 看降级/失败两种结局 → 查分类表 → 知道何时不该包

5.1 最小用法

输出(首次即通过——最理想,只有一行):

✅ L2 搜索 通过

对应命令:

source tools/tests/lib/retry.sh
retry_run "L2 搜索" "node tools/tests/search-integration.test.js"

5.2 常用参数

① retry_run:按 exit code 判定(3 次退避 1s/4s/9s + 抖动)

输出(抖动场景——重试后通过,并记入 flaky):

⏳ L2 搜索 第 1 次失败,退避 1s 重试...
⚠️  L2 搜索 第 2 次通过(前 1 次 flaky,需关注)

对应命令:

retry_run "L2 搜索" "node tools/tests/search-integration.test.js"

② retry_match:按输出模式判定(L11 段主用)

输出(匹配 PASS=.*FAIL=0 才算通过):

⏳ L2d 视频 第 1 次未通过,退避 1s 重试...
✅ L2d 视频 通过

对应命令:

retry_match "L2d 视频" "timeout 180 node tools/tests/videobg-integration.test.js" "0 失败|fail 0|失败, 0"

③ 全失败 → 外部不可用(降级 warn,返回 0)

输出:

⚠️  L2d 视频 外部依赖不可用(降级 warn,非产品缺陷)
📄 日志: /tmp/opencode/test-L2d_视频.log

对应命令:

retry_match "L2d 视频" "timeout 180 node tools/tests/videobg-integration.test.js" "0 失败"

④ 全失败 → 业务断言失败(真 bug,返回 1)

输出:

❌ L2d 视频 业务断言失败(真 bug)
📄 日志: /tmp/opencode/test-L2d_视频.log

对应命令:

retry_match "L2d 视频" "timeout 180 node tools/tests/videobg-integration.test.js" "0 失败"

5.3 全部参数

失败分类(全失败时按日志签名区分):

签名判定结果
ERR_NAME_NOT_RESOLVED / ECONNREFUSED / ETIMEDOUT / 429 / 50x 等外部不可用⚠️ 降级 warn(返回 0,不拦发版)
无外部签名业务断言失败❌ fail(返回 1,真 bug)

5.4 边界与坑

  • 重试次数/退避是固定的:3 次、1s/4s/9s + 抖动。不要为了"让它绿"而调大——unit-retry.test.sh 会校验退避库行为。
  • 不要给纯静态断言套 retry_match:本地文件读不到不是网络抖动,重试只会把真 bug 拖成 3 倍耗时。
  • retry_match 的模式必须锚定失败数为 0:只匹配 PASS= 会把"4/24 通过"判成绿(2026-10-07 已因此收紧判据)。
  • 日志落在 /tmp/opencode/:报红先看 📄 日志: 那一行给出的文件,比翻终端滚动区快。

5.5 小结

retry_run 看退出码,retry_match 看输出;两者都只在"全失败"时才分类——中间成功就是 flaky,不算失败但会被看见。


6. 第三方拦截 + ERR_ABORTED 排除(lib/browser-env.js)

用途:让 playwright 测试只对"页面自身的错误"负责,第三方 CDN 的噪声不算账。

学习路径:看一次断言输出 → 看两个 API → 查机制表 → 记住 abort 的坑

6.1 最小用法

输出(用共享环境后,测试只需断言两个数组为空):

✅ 页面无 JS 异常
✅ 无同源资源失败

对应命令:

node tools/tests/content-width.test.js    # 任一 playwright 测试的断言现场

6.2 常用参数

① interceptThirdParty(page):拦截已知污染源

输出(不拦截时,页面自身的 unhandled rejection 会混进 JS 异常):

❌ JS errors [ 'Failed to parse JSON from webpushr' ]

对应命令:

node tools/tests/effects-integration.test.js

② collectPageErrors(page, BASE):只收 JS 异常 + 同源资源失败

输出(第三方图床 404 不计入,同源 404 才计入):

jsErrors: []
sameOriginFailures: []

对应命令:

NODE_PATH=$(npm root) node tools/tests/lightbox.test.js

6.3 全部参数

机制说明
interceptThirdParty拦截已知污染源(如 webpushr 返回非 JSON → 页面 unhandled rejection),返回合法 JSON 而非 abort(abort 会产生 net::ERR_FAILED 反成新噪声)
collectPageErrors仅收集 JS 异常 + 同源(localhost/生产域)资源失败;第三方 CDN/图床失败为环境噪声不计入
ERR_ABORTED 排除导航中断(多次 goto/离开页面取消在途请求)非资源错误,直接跳过

6.4 边界与坑

  • 别用 route.abort() 拦第三方:abort 会把请求变成 net::ERR_FAILED,反而新增一条同源失败——所以用 fulfill 返回合法 JSON。
  • 别把基线域写死成生产域名:collectPageErrors 的第二参数是当前 BASE,本地跑却传生产域时,本地资源失败会被当成第三方漏掉。
  • 多次 goto 的测试必须容忍 ERR_ABORTED:否则翻页测试会稳定假红。

6.5 小结

一句话:同源算账,第三方不计;abort 制造噪声,fulfill 消除噪声。


7. 压缩开关验证(cicd.sh)

用途:确认当前构建产物到底是压缩的还是可读的——调试要可读,发版要压缩。

学习路径:看开关原文 → 看两种产物的头部 → 查模式表 → 记住别手工改

7.1 最小用法

输出(开关只有 3 行,用同一个注释标记串起来):

js:
  enable: true # minify-switch
css:
  enable: true # minify-switch
html:
  enable: true # minify-switch

对应命令:

grep -n 'minify-switch' _config.yml

7.2 常用参数

① -d / --debug:关压缩,产物可读

输出(public/css/main.css 开头——多行、带缩进):

.anchorjs-link {
  text-decoration: none !important;
  transition: opacity 0.2s ease-in-out;
}
.markdown-body h1:hover > .anchorjs-link,
h2:hover > .anchorjs-link,

对应命令:

tools/cicd.sh -d
head -c 200 public/css/main.css

② -r/-c/-t:开压缩,单行无注释

输出(同样位置变成一行、体积显著变小):

.anchorjs-link{text-decoration:none!important;transition:opacity .2s ease-in-out}.markdown-body h1:hover>.anchorjs-link,...

对应命令:

tools/cicd.sh -t
head -c 200 public/css/main.css

7.3 全部参数

模式minify 值说明
-d / --debugfalse不压缩(本地调试,产物可读)
-r / -c / -ttrue压缩(hexo-minify 在 generate 时恒压缩,故需改其 enable)

7.4 边界与坑

构建后 cicd.sh 会还原 _config.yml(仅当改动只涉及 minify-switch 标记行时),避免工作区脏。

  • 别手工改 _config.yml 的 minify 来测压缩:下次 cicd.sh 会按模式覆盖,测完的手改会变成"怎么回事又变了"。走参数。
  • preview: false 时本地 hexo s 不压缩:想验证压缩效果必须走 npm run build 或 cicd.sh -t,不能看本地预览。
  • 压缩开关只有 3 行:js / css / html。漏改一行会出现"JS 压了 CSS 没压"的半压缩产物。

7.5 小结

压缩与否由构建模式决定,不由配置文件决定;验证方式就是 head -c 200 public/css/main.css 看第一行是不是单行。


8. 环境就绪、单实例锁与基址自适应

用途:门禁跑之前的"体检"——不健康就秒级失败并告诉你怎么修。

学习路径:看一次成功体检 → 看一次失败体检 → 查检查项表 → 记住锁

8.1 最小用法

输出(健康环境的完整体检,5 行内结束):

🔍 [preflight] 环境预检...
  ✅ 构建产物有效 (123456 字节)
  ✅ playwright 可用(NODE_PATH=/home/andy/.local/share/mise/installs/npm-playwright/1.56.0/lib/node_modules)
  ✅ _config.yml 未被污染
  ✅ 网络基址可达(HTTP 200)
✅ [preflight] 环境就绪

对应命令:

tools/tests/release-test.sh --local

8.2 常用参数

① 产物缺失/过小

输出:

❌ 构建产物不存在(public/index.html)
💡 修复命令:docker compose -f docker-compose.test.yml exec -T hexo bash -c 'tools/cicd.sh -d'

对应命令:

tools/tests/release-test.sh --skip-build   # 跳过构建但产物不存在时即可见

② _config.yml 被构建污染

输出(自动恢复,不会拦你):

⚠️  themes/matery/_config.yml 被构建污染(有未提交修改)
🔧 自动恢复:git -C themes/matery checkout -- _config.yml
✅ _config.yml 已自动恢复

对应命令:

tools/tests/release-test.sh --local

③ 本地端口自适应(不写死 4000)

输出:

✓ 本地基址: http://localhost:4100(端口 4100)
  ✅ 已导出 TEST_BASE=http://localhost:4100 TEST_URL=http://localhost:4100

对应命令:

tools/tests/release-test.sh --local

8.3 全部参数

检查项不通过表现
本地 server 可达(HTTP 200)打印修复命令后退出
构建产物有效(public/index.html > 1000B)打印重建命令
playwright 可用跳过依赖浏览器的层
themes/matery/_config.yml 未被构建污染自动 git checkout 精确恢复
网络基址可达(仅发版模式)提示基址不可达
# 未就绪时打印的 3 条修复命令(任选其一)
docker compose -f docker-compose.test.yml exec -T hexo bash -c 'tools/cicd.sh -d'
tools/test.sh server
docker compose -f docker-compose.test.yml exec -T hexo bash -c 'pm2 restart all'

--prepare 自动就绪:容器未跑则启动、产物无效则重建、最后复检。

单实例锁:门禁启动即在 ${TMPDIR:-/tmp}/opencode/gate.lock 取锁,并发运行被直接拒绝(打印持锁 PID);陈旧锁自动接管,trap EXIT 释放。禁止并发门禁——并发会互相 kill 本地 server。

基址与端口自适应:见 §2 的基址段(生产 443 固定 / 本地端口探测)——36 个测试文件靠导出的 TEST_BASE/TEST_URL 自动跟随。

8.4 边界与坑

  • preflight 在网络基址不可达时重试 3 次(❌ 网络基址不可达(HTTP 000,期望 200/301,已重试 3 次)),随后给出 💡 检查代理/DNS:curl -sI https://blog.17lai.site——先修网络,别改断言。
  • 本地模式不做网络基址检查(只查本地 200),所以本地全绿不代表生产可达。
  • 锁文件残留:非正常退出后可能留下 gate.lock,脚本会校验 PID 是否存活并自动接管;只有"锁里有活着的 PID"才算真并发。
  • 别用 rm -rf public 代替 --prepare:产物没了 --prepare 会重建,手删只会让下一次门禁在 L1 前就失败。

8.5 小结

preflight 的职责是"要么秒级告诉我怎么修,要么放行",绝不允许跑到第 15 分钟才发现 server 没起。


9. L14 视觉断言层

用途:用 playwright 读取真实 computed style,把"位置/尺寸/颜色"变成数值断言——CSS 改动后的硬门槛。

学习路径:跑单套件 → 看重试与失败输出 → 查套件清单 → 记住 make 目标的坑

9.1 最小用法

输出(单套件通过时只有一行汇总):

结果: 18 通过, 0 失败

对应命令:

node tools/visual-assert.js run --local tools/visual-assert/layout-checks.json

9.2 常用参数

① --local:断言本地基址(生产模式则不带)

输出:

✅ L14 layout-checks:18 项全部通过

对应命令:

node tools/visual-assert.js run --local tools/visual-assert/layout-checks.json

② 失败明细(只打前 3 条 ❌,避免刷屏)

输出:

❌ L14 footer:<M> 失败(共 <N> 项)|明细:❌ footer-margin 期望 24px 实际 16px  ❌ footer-padding ...

对应命令:

node tools/visual-assert.js run --local tools/visual-assert/footer.json

③ 单文件超时(默认 600s,可用 L14_TIMEOUT 覆盖)

输出:

❌ L14 post-layout:超时或异常(exit 124)

对应命令:

L14_TIMEOUT=900 node tools/visual-assert.js run --local tools/visual-assert/layout-checks.json

9.3 全部参数

tools/visual-assert/*.json 共 14 套件,门禁 L14 跑其中 12 套:

layout-checks  reward-modal  search-modal  dark-mode  homepage  about
footer         404           i18n          tags       nav-scrolled  toc-expanded

刻意不激活(需手动跑):mobile-menu(与 layout-checks 的 mobile-menu-functional 重复)、post-layout(与 layout-checks 部分重叠)。

属性值
语义生产阻塞(失败即 FAIL);本地默认跳过
本地启用RUN_L14=1 tools/tests/release-test.sh
强制跳过SKIP_L14=1
失败处理某套件失败自动重试一次(负载抖动),仍失败才 FAIL,并打印细节
耗时约 299s(重)

⚠️ make visual-assert 陷阱:该 make 目标依赖 build,build 会原地改写 themes/matery/_config.yml 占位符 → 触发 L5 版本一致性 4 项失败。正确用法:node tools/visual-assert.js run --local [tools/visual-assert/<suite>.json],或交给 L14 层跑。

9.4 边界与坑

  • 必须 --local:不带时断言的是生产基址,本地改动永远测不到。
  • playwright 目录找不到就整层 skip:⏭️ L14 visual-assert(playwright 不可用)——看到 skip 别当成通过。
  • 本地 server 不可达也 skip:⏭️ L14 视觉断言(localhost:4000 不可达 → HTTP 000)。
  • 别用 make visual-assert 做验证:污染 _config.yml 的代价是后面 L5 四项全红,排查方向会被彻底带偏。

9.5 小结

L14 是唯一"能证明像素没错"的层;它慢、它重、它默认不在本地跑——正因如此,CSS 改动后才必须手动补上这一层。


10. TC 索引与 git hooks

用途:让"测试文件清单"和"用例文档"永不脱节。

学习路径:看扫描输出 → 看生成/校验两条命令 → 查机制表 → 记住 hook 要启用一次

10.1 最小用法

输出(生成时打印扫描统计,末尾写文件):

📊 扫描结果:13 层 / 42 测试文件 / 3 lib 文件 / 284 断言 / 406 用例名
✅ TC 索引已写入(13 层 / 284 断言)

对应命令:

node tools/tests/gen-tc-index.js

10.2 常用参数

① 校验(门禁调用的就是这条)

输出(一致时一行):

✅ TC 索引与测试文件一致

对应命令:

node tools/tests/gen-tc-index.js --check

② 不一致时(exit 1)

输出(门禁汇总段的那条 ❌):

❌ TC 清单不一致(运行 node tools/tests/gen-tc-index.js 更新)

对应命令:

make tc-index-check

③ 提交时自动重生成(hook)

输出(pre-commit 自动 stage,提交照常成功):

🔧 检测到 tools/tests/ 变更,重新生成 TC 清单...
✅ TC 索引已写入(13 层 / 284 断言)

对应命令:

git config core.hooksPath .githooks    # 新克隆执行一次

10.3 全部参数

测试的权威来源是 tools/tests/**;生成的 TC 表位于 docs/superpowers/specs/2026-08-06-layout-test-cases.md 的 <!-- GENERATED:START --> 与 <!-- GENERATED:END --> 之间。

机制说明
node tools/tests/gen-tc-index.js / make tc-index重新生成 TC 清单
make tc-index-check校验清单是否落后(不一致 → exit 1)
.githooks/pre-commit提交涉及 tools/tests/ 时自动重生成并 stage 该文档 → 提交陈旧索引在结构上不可能
门禁 --check汇总前跑 gen-tc-index.js --check(覆盖 --no-verify 等旁路)

首次克隆需启用一次:git config core.hooksPath .githooks。门禁报"TC 清单不一致"时,跑 make tc-index 再提交(旁路 git commit --no-verify)。

扫描统计里的具体数字(层/文件/断言/用例名)随测试增长而变,以实际输出为准,文档不写死。

10.4 边界与坑

  • make tc-index 会改文档:跑完记得一起提交,否则下一次 --check 照样红。
  • --no-verify 会绕过 hook 但绕不过门禁:门禁里那条 --check 是最后一道网。
  • GENERATED 区间外的手写内容不会被覆盖:可以放心在表外补说明,生成器只重写标记之间。

10.5 小结

测试文件是源,TC 文档是镜像;hook 保证镜像实时,门禁 --check 保证镜像可信。


11. flake 可见化与基线参考值

用途:把"重试才通过"的项目显式列出来——不拦发版,但不许悄悄消失。

学习路径:看一次 flaky 汇总 → 看三档基线 → 查基线表 → 记住数字会变

11.1 最小用法

输出(干净的一轮——没有 flaky 段):

 测试结果: PASS=<N> FAIL=0
=============================================
🎉 全部通过!

对应命令:

tools/tests/release-test.sh

11.2 常用参数

① 有 flaky 时(多一行 ⚠️,PASS/FAIL 不变)

输出:

 ⚠️  本次门禁 flaky: 1 项(L14 nav-scrolled)
 测试结果: PASS=<N> FAIL=0
🎉 全部通过!

对应命令:

tools/tests/release-test.sh

② 三档基线

输出(本地档的固定标志):

 ⏭️  已跳过 L10(远程 SW 冒烟)与 L11(playwright 集成)—— 本地迭代模式;
 测试结果: PASS=<N> FAIL=0
🎉 全部通过!

对应命令:

tools/tests/release-test.sh --local

11.3 全部参数

基线预期命令
本地全部通过(0 失败)tools/tests/release-test.sh --local
本地 + L14全部通过(0 失败,含视觉断言)RUN_L14=1 tools/tests/release-test.sh
生产全部通过(0 失败)tools/tests/release-test.sh(发版后)

具体 PASS 条数随版本增长(每新增一条断言即变化),以实际输出为准,不写死数字。

3 个 lib:assert.sh(82 行)、retry.sh(114 行,负责 flake 追踪)、preflight.sh(230 行)。browser-env.js 为 playwright 共享辅助(见 §6)。

11.4 边界与坑

  • flaky 不是通过:它只说明"重试后过了",同一项连续两轮出现 flaky 就该当 bug 查。
  • 不同档位 PASS 数不可比:--local 少跑 L10/L11/L14,PASS 自然低;拿数字对比毫无意义,只看 FAIL=0。
  • 基线要重跑不要引用:发版后的生产门禁必须现场跑一次,旧日志里的 PASS 不构成证据。

11.5 小结

看门禁结果只看两件事:FAIL=0 和有没有 flaky 段。前者是门禁,后者是技术债提醒。


12. 如何新增一个测试用例

用途:从"写一个测试文件"到"挂进门禁并被 TC 索引收录"的完整路径。

学习路径:看新测试自己的输出 → 看挂入后的门禁输出 → 用模板 → 记住 mock 与判据

12.1 最小用法

输出(新文件写完,先自己跑通——这一行就是门禁要匹配的契约):

✅ should render expected element

结果: PASS=1 FAIL=0

对应命令:

node tools/tests/my-feature.test.js

门禁里复用同一契约(run_test 用法):

run_test "my-feature" "node tools/tests/my-feature.test.js"

run_test 是 release-test.sh 内的辅助函数:跑命令、捕获输出、按 PASS=N FAIL=0 判据决定该层是否通过。

12.2 常用参数

① 归 L13(纯单测/静态,无浏览器)

输出(挂入后门禁多一行 ✅):

✅ 单测 my-feature.test.js

对应命令:

# 在 tools/tests/layers/L13_units.sh 的 for 清单里追加文件名即可(unit/ 前缀自动判断)
    my-feature.test.js \

② 归 L11(需 chromium + server)

输出(判据是 PASS=N FAIL=0):

✅ my-interaction 通过

对应命令:

retry_match "my-interaction" "node tools/tests/my-interaction.test.js" "PASS=.*FAIL=0"   # 写进 layers/L11_integration.sh

③ 本地验证(只跑 L13 + curl 断言,最快闭环)

输出:

 测试结果: PASS=<N> FAIL=0
🎉 全部通过!

对应命令:

tools/tests/release-test.sh --local

④ 同步 TC 索引

输出:

✅ TC 索引与测试文件一致

对应命令:

make tc-index

12.3 全部参数

步骤

  1. 确定测试类型:
    • 纯单测 / 静态断言(无浏览器)→ 归 L13(node 直跑)
    • playwright 集成(需 chromium + server)→ 归 L11
  2. 创建测试文件 tools/tests/xxx.test.js(node:test 或 playwright)
  3. 外部依赖测试:用 retry_run / retry_match 包装(lib/retry.sh),全失败自动分类降级
  4. playwright 测试:用 lib/browser-env.js 的 interceptThirdParty + collectPageErrors 统一错误归属
  5. 注册到对应层脚本(tools/tests/layers/L11_integration.sh 或 L13_units.sh),输出格式对齐(PASS=N FAIL=0 或 N 通过 / 0 失败)
  6. 本地验证:tools/tests/release-test.sh --local(跑 L13 + curl 断言);发版门禁跑全量
  7. 同步 TC 索引:新增/改动后必须让 TC 索引同步(.githooks/pre-commit 自动完成;或手动 make tc-index)——否则门禁 --check 报"TC 清单不一致"

代码模板

L13 单测模板(node:test,无浏览器依赖):

// tools/tests/my-feature.test.js
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');

const PUBLIC = path.resolve(__dirname, '../../public');

describe('my-feature', () => {
  it('should render expected element', () => {
    const html = fs.readFileSync(path.join(PUBLIC, 'index.html'), 'utf8');
    assert.ok(html.includes('expected-marker'), 'missing expected marker');
  });
});

// 末尾输出(release-test.sh 通过此行判定)
const pass = 1, fail = 0;
console.log(`\n结果: PASS=${pass} FAIL=${fail}`);
process.exit(fail > 0 ? 1 : 0);

L11 playwright 模板(需 chromium + server):

// tools/tests/my-interaction.test.js
const { chromium } = require('playwright');
const { interceptThirdParty, collectPageErrors } = require('./lib/browser-env');

const BASE = process.env.TEST_BASE || 'http://localhost:4000';
let browser, page;
let pass = 0, fail = 0;

async function setup() {
  browser = await chromium.launch();
  page = await browser.newPage();
  await interceptThirdParty(page);
}

async function testFeature() {
  const env = collectPageErrors(page, BASE);
  await page.goto(`${BASE}/posts/xxxxx/`, { waitUntil: 'networkidle' });
  const el = await page.$('.my-feature');
  if (el) { pass++; } else { fail++; console.error('FAIL: .my-feature not found'); }
  // 检查无 JS 异常
  if (env.jsErrors.length > 0) { fail++; console.error('FAIL: JS errors', env.jsErrors); }
  else { pass++; }
}

(async () => {
  await setup();
  await testFeature();
  await browser.close();
  console.log(`\n结果: PASS=${pass} FAIL=${fail}`);
  process.exit(fail > 0 ? 1 : 0);
})();

挂入门禁:在哪里加一行

L13(node 单测):在 tools/tests/layers/L13_units.sh 的 for t in \ 清单里追加一行文件名(目录前缀会自动判断 unit/ 或根目录)。

L11(playwright 集成):在 tools/tests/layers/L11_integration.sh 追加:

retry_match "my-interaction" "node tools/tests/my-interaction.test.js" "PASS=.*FAIL=0"

retry_match 包装外部依赖测试——网络抖动时自动重试 3 次,全失败降级为 warn 而非 fail(见 §5)。

完整示例:新增一个「侧边栏折叠」测试

  1. 需求:验证文章页侧边栏在窄屏下自动折叠
  2. 类型:需浏览器 → L11 playwright
  3. 创建 tools/tests/sidebar-collapse.test.js(用上面 L11 模板)
  4. 挂入:在 layers/L11_integration.sh 加 retry_match 行
  5. 验证:tools/tests/release-test.sh --local?——不行,本地跳过 L11,改为直接 node tools/tests/sidebar-collapse.test.js,或 --skip-l10 保留 L11
  6. TC 同步:make tc-index(或等 pre-commit hook 自动完成)

断言输出约定

release-test.sh 按输出模式判定通过/失败,测试文件末尾必须输出可匹配的汇总行:

console.log(`\n结果: PASS=${pass} FAIL=${fail}`);
process.exit(fail > 0 ? 1 : 0);

测试用例权威清单

docs/superpowers/specs/2026-08-06-layout-test-cases.md(全量 layout 测试用例)+ docs/testing/TESTING-PYRAMID-GUIDE.md(测试方法论)——新增功能后同步更新用例清单与 release-test.sh 覆盖。

12.4 边界与坑

mock 必须贴合真实 Hexo 对象(同 §4.4):普通数组 mock 会让单测全绿而构建 0 字节,toArray() 缺失测不出来。

新增后最容易忘的一步:

输出(漏了 TC 同步,门禁最后一条会红):

❌ TC 清单不一致(运行 node tools/tests/gen-tc-index.js 更新)

对应命令:

node tools/tests/gen-tc-index.js --check

别把 playwright 测试塞进 L13:无浏览器环境下必失败,会拖垮整层。

12.5 小结

新增一个测试 = 一个文件 + 一处挂载 + 一次 TC 同步;三件事少做一件,门禁就会以"不一致"或"必失败"的方式提醒你。


13. 性能验证(LCP 基线,手工)

用途:LCP 不在门禁里,但必须有可复现的对照基线。

学习路径:看基线结论 → 看复现命令 → 查手段与门槛 → 记住 noscript 陷阱

13.1 最小用法

输出(基线文档给出的移动端结论——这就是"优化前"的对照值):

**移动端 LCP 4.6 s / 5.3 s 均远超 2.5 s 门槛**,与 Search Console「40 个 URL 的 LCP > 2.5s」的字段结论方向一致。

对应命令:

grep -n '移动端 LCP' docs/analysis/LCP-V2-BASELINE-2026-09-18.md

13.2 常用参数

① 看基线表结构(页面 / LCP / FCP / TBT / CLS / 性能分 / LCP 元素)

输出:

| 模式 | 页面 | LCP | FCP | TBT | CLS | 性能分 | LCP 元素 |

对应命令:

grep -n '| 模式 | 页面 | LCP' docs/analysis/LCP-V2-BASELINE-2026-09-18.md

② 看运行间方差(决定验收口径)

输出:

- mobile LCP 两组分别为 **4.6/5.3 s**(§2.2)与 **3.7/3.6 s**(本组)——**运行间方差约 ±0.9 s**,与 mobile 节流下的固有噪声一致。**B 验收必须用多组采样的中位数,勿用单次值**。

对应命令:

grep -n '运行间方差' docs/analysis/LCP-V2-BASELINE-2026-09-18.md

13.3 全部参数

  • 基线文档:docs/analysis/LCP-V2-BASELINE-2026-09-18.md(资源计数 / Lighthouse desktop+mobile / LCP 元素身份 / §5 复现命令 / §7 优化实施)
  • 优化四手段(2026-09-18~20):关键 CSS preload · highlight 按 layout 条件加载 · markmap 本地化+异步 · 文章页首屏图 preload
  • 门槛:mobile LCP ≤ 2.5s(Search Console「40 个 URL 超标」为观测口径)
  • 触发条件:改动 head 内资源加载顺序/条件(CSS/JS/图片)后必须复测

13.4 边界与坑

  • 陷阱:noscript 不剥离会让资源计数虚高 4 条/页(基线文档 §1.3),任何复测前必须先剥离。
  • 单次采样不可作为验收值:mobile 节流下运行间方差约 ±0.9 s,必须多组取中位数。
  • LCP 元素身份不稳定:首页 LCP 元素在运行间会漂移,比较时对齐"元素身份 + 数值"两项,别只看数字。
  • LCP 不进自动化门禁是刻意的:它需要 Lighthouse 真机采样 + 网络条件控制,断言型测试表达不了。

13.5 小结

性能验证 = 基线文档 + 复现命令 + 中位数口径;三者缺一,得到的"优化结果"都不可信。

⚠️ 已知坑

坑 5:改配置后必须 clean 构建

输出(旧缓存下的典型症状:L1 绿,功能断言红):

🔨 L1: 构建验证
  ✅ 构建成功 (hexo g Success)
...
  ❌ 搜索入口

对应命令:

npm run clean && npm run build        # 或 tools/tests/release-test.sh --prepare

原因:本地预览使用旧缓存产物,测试断言的是旧版行为。

坑 6:source/_data/* 增量缓存误判

输出(L2 断言页面对不上):

❌ /friends/ → 200 但页面无友链内容

对应命令:

rm -f db.json && npm run build        # 容器环境用 tools/cicd.sh -d(内置 clean)

原因:Hexo 增量缓存误判「已缓存未变化」。


附:测试 ↔ 功能对照表

用途:想知道"某个功能有没有被自动测试覆盖"?在这里按功能名查找。

功能测试文件层验证什么
页面特效(樱花/雪花/烟花)effects-integration.test.jsL11特效脚本按门控加载,不报错
视频背景videobg-integration.test.jsL11门槛/明暗池/webm 契约/禁用态
词云wordcloud.test.jsL11词云渲染 / 去 jQuery / TOC 防闪烁 / 明暗切换
多语言i18n.test.jsL11MATERY_I18N 注入 + 按钮文案 + en 翻译
音乐 APlayermusics-integration.test.jsL11多域名 failover
懒加载lazyload-integration.test.jsL11handleCard 三分发(51 断言)
悬浮面板floating-panel-integration.test.jsL11开关 / 字体缩放 / 持久化
内容宽度content-width.test.jsL114K/FHD/2K/单栏封顶
阅读模式readmode-integration.test.jsL11全屏进入/退出 / 悬浮球 / 代码全屏 / 展开
Wiki 样式wiki-styles.test.jsL11note / ::: 容器色 + 居中 + 宽度一致
codeWidget 幂等code-widget.test.jsL11二次调用后单次点击仍生效
事件总线core-events-integration.test.jsL11事件总线 / 进度条联动
Wiki prismwiki-prism-integration.test.jsL11代码块 prism + line-numbers
打赏 / 繁简misc-interactions.test.jsL11打赏弹窗 / 繁简转换
客户端语言策略lang-client-strategy.test.jsL11全站跟随 / 爬虫跳过 / 面板回显
富内容渲染rich-content-integration.test.jsL11公式 / mermaid / markmap / 打字机 / 分享 / Live2D
Wiki 多语言wiki-integration.test.jsL11多语言 wiki 可达 / 侧栏 / 语言切换
灯箱lightbox.test.jsL11相册/画廊图片放大交互
卡片 hovergallery-hover.test.jsL11图库/卡片 hover 动画
TOC 折叠unit-toc.test.jsL13collapseDepth 0||6 falsy 陷阱
搜索unit-search.test.jsL13三引擎配置
多语言配置unit-multilang.test.jsL13语言配置解析
加密扩展unit-encrypt2.test.jsL13片段 + wiki 加密
wiki helperunit-wiki-helper.test.jsL13语言解析 / 侧栏渲染 / canonical 回落
侧栏 i18nunit-sidebar-i18n.test.jsL13a查表 + 回退链
侧栏 keyunit-sidebar-keys.test.jsL13a门禁校验器 4 wiki × 9 语言
回落链lang-fallback.test.jsL134 级回落(13 断言)
i18n 核心unit-i18n-core.test.jsL13aresolveI18n 四级链 + cfg 防御
i18n helperunit-i18n-helper.test.jsL13t() helper fallback
EChartsecharts-charts.test.jsL13图表渲染
Banner 背景banner-bg.test.jsL13banner 与背景偏好联动
首页结构homepage.test.jsL13首页 DOM 结构
图片尺寸imgsize-unit.test.jsL13normalizeImgUrl + 尺寸读取逻辑
独立页unit-standalone.test.jsL13排除法判定 + 语言前缀豁免
markmap 拆除unit-markmap-prune.test.jsL13a无脑图页面拆插件注入(6 用例)
「更新于」unit-updated.test.jsL13a取 git 最后提交时间(4 用例)
加密体系unit-encrypt.test.jsL13aAES-GCM + PBKDF2 闭环(9 用例)
CSS 架构unit-css-architecture.test.shL13编译产物关键选择器
版本号unit-version.test.shL13递增进位
重试库unit-retry.test.shL13退避重试
部署目标unit-deploy-target.test.shL13hexo/hoback 双激活

附:三重定位自查清单

新增/修改一篇「hexo主题功能测试」系列文章时,检查以下三项:

#检查项通过标准
①可验证每个功能项有明确的输入 → 期望输出(自动化断言标记或人工核对标准)
②有效果每个功能项描述了渲染后的实际效果(文字描述、示例渲染、或截图)
③会使用每个功能项有语法/配置步骤 + 注意事项(参数顺序、前置条件、常见坑)

快速判断:如果一个功能"只列了名字没说怎么验证"→ 缺 ①;"只列语法不给效果"→ 缺 ②;"没说怎么用或有歧义"→ 缺 ③。

附:测试体系速查表

命令用途
tools/tests/release-test.sh发版全量门禁(L1-L14)
tools/tests/release-test.sh --local本地迭代(端口自适应,跳 L10/L11/L14)
tools/tests/release-test.sh --prepare自动就绪环境(容器 + 构建 + 复检)
RUN_L14=1 tools/tests/release-test.sh --local本地启用 L14 视觉断言
make tc-index / make tc-index-check生成 / 校验 TC 清单(.githooks/pre-commit 自动同步)
tools/tests/run-tests.sh已于 2026-09-13 删除(与 L11 功能重叠);等价能力见 tools/tests/release-test.sh
node tools/tests/gen-tc-index.js生成/校验 TC 清单(make tc-index)
make test-pwaVercel 测试域 SW 冒烟(7 页)+ 交互验证
make visual-compare / visual-assert视觉回归(playwright 数值断言);⚠️ make visual-assert 含 build 依赖会污染 _config.yml,优先 node tools/visual-assert.js run --local
tools/visual-regression.sh report视觉对比报告(20 页面 × 4 断点)

参考&致谢

系列教程

全部文章RSS订阅

Hexo 主题功能测试系列


Hexo主题功能测试-自动化测试篇
发布于
2026年9月11日
许可协议。转载请注明来源
赏
评论
数据加载中 ...
 本篇

阅读全文

Hexo主题功能测试-自动化测试篇
Hexo主题功能测试-自动化测试篇 Hexo主题功能测试-自动化测试篇
主题自动化测试体系完整教程:测试金字塔(L1 构建 / L13 单测门禁 / L11 Playwright 集成 / 生产 release-test)、release-test.sh 用法、L1-L14 各层覆盖、单测清单、外部依赖重试与降
2026-09-11
下一篇 

阅读全文

Hexo主题功能测试-Markdown语法篇
Hexo主题功能测试-Markdown语法篇 Hexo主题功能测试-Markdown语法篇
主题 Markdown 语法完整教程与测试:16 个 markdown-it 插件全覆盖(emoji/缩写/脚注/插入/上下标/高亮/任务列表/表格增强/图片尺寸/容器/定义列表/数学公式/中文排版)。每个插件按「最小示例 → 常用变体 →
2026-08-08