本文是 matery 主题自动化测试体系的完整教程,与其余 4 篇功能测试文章(内容 tag / 交互视觉 / 布局页面 / Markdown 语法)配套——那 4 篇是"测试靶场",本文是"测试引擎"。权威依据:
tools/tests/release-test.sh(L1-L14,13 层 156 行纯编排)、docs/superpowers/specs/2026-08-06-layout-test-cases.md。
1. 测试金字塔总览
┌──────────────┐
│ L2 生产 URL │ 发版后:20 页面 HTTP 200 + 功能标记(curl)
│ L9/L10 SW │ 发版前:Vercel 测试域 SW 冒烟(playwright)
├──────────────┤
│ L11 Playwright 集成 │ 真实浏览器:特效/词云/懒加载/阅读模式等 15 项
├──────────────┤
│ L14 视觉断言 │ visual-assert 精确数值:7 套件(生产阻塞,本地默认跳过)
├──────────────┤
│ L13 单测门禁 │ node 直跑:13 个 .test.js + 4 个 shell 单测
├──────────────┤
│ L1 构建验证 │ hexo g Success + 产物完整(index.html > 1000B)
└──────────────┘| 层 | 名称 | 运行方式 | 依赖 |
|---|---|---|---|
| L1 | 构建验证 | hexo generate + 产物检查 | 无 |
| L13 | 全量单测门禁 | node tools/tests/*.test.js + shell 单测 | 无 chromium |
| L11 | Playwright 集成 | 真实浏览器 + 本地 server | chromium + 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(7 套件) | chromium + server |
| L10 | SW 冒烟(测试域) | playwright | Vercel 测试域 blog2.17lai.site |
2. release-test.sh 用法
# 完整跑(构建 + 生产检查 + SW 冒烟 + playwright 集成)——发版门禁
tools/tests/release-test.sh
# 跳过构建(public/ 已就绪)
tools/tests/release-test.sh --skip-build
# 本地迭代(端口自适应探测,跳过远程 SW 冒烟 L10 + playwright 集成 L11)
tools/tests/release-test.sh --local
# 仅跳过远程 SW 冒烟(保留 playwright 集成)
tools/tests/release-test.sh --skip-l10| 参数 | 作用 |
|---|---|
--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 个测试文件自动跟随。
执行顺序(156 行纯编排,非文档顺序):
L0 preflight → 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验证生产环境。
3. L1-L14 各层覆盖内容
| 层 | 覆盖内容 | 断言示例 |
|---|---|---|
| L1 | 构建验证 | hexo g Success + public/index.html > 1000B |
| L2 | 20 个 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 侧栏无 encrypt_ 垃圾标题 / 加密容器完整(hbeForm + storage-key) |
| L2i | SEO 结构化数据 | og:image / JSON-LD / fetchpriority |
| L3 | 评论区检查 | msg/friends/bb 页含 #comments |
| L4 | href=/ 样式错误 | 页面 href=/ 引用数应为 0 |
| L5 | 版本号一致性 | config vs sw.tmp.js 3 处一致 |
| L6 | 关键资源检查 | 404 页面 / 关键 JS 版本 |
| L7 | 核心功能覆盖 | 相册灯箱 / 加密相册 / 搜索 / JS 模块合并 / TOC 折叠 / 暗色 / 评论 / infinite-scroll / wiki 代码块 |
| L8 | 增强功能覆盖 | tag 插件渲染(note/timeline/tabs/label/githubCard/mermaid)/ 加密文章 / Feed / 静态资源 / 分析 |
| L9 | 静态资源 + SW 冒烟 | main.css > 50KB / utils.js > 5KB / 生产域名 7 页无 SW 错误 |
| L10 | SW 冒烟(测试域) | blog2.17lai.site 7 页无 SW 错误 + 交互验证(搜索/打赏/灯箱/TOC/暗色) |
| L11 | Playwright 集成(15 项) | 见下表 |
| L12 | 本地化资源检查 | mermaid / aplayer 资源存在 |
| L13 | 全量单测门禁 | 13 个 *.test.js + 4 个 *.test.sh = 17 文件 |
| L13a | 覆盖率统计 | unit/** 覆盖率扫描(附带执行 unit/** 全部单测) |
| L14 | 视觉断言(7 套件) | 见 §9 |
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 | 阅读模式 / 代码全屏(PASS=14) |
core-events-integration.test.js | 事件总线 / 进度条联动 |
wiki-prism-integration.test.js | Wiki 代码块 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 断言) |
lightbox.test.js | 灯箱(相册/画廊图片放大交互) |
4. 单测清单(tools/tests/)
L13 门禁单测(release-test.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
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 补全 + 本地/网络图片尺寸读取逻辑)
search-integration.test.js
infinite-scroll-aos.test.js
unit-standalone.test.js # 独立页排除法判定 + 语言前缀豁免(22 断言)
# 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 双激活校验)其他单测(原 run-tests.sh L1 / L8 段,已并入 release-test.sh)
unit-encrypt.test.js # 自研加密体系(AES-GCM + PBKDF2 闭环,9 用例)
unit-fontawesome.test.py # FontAwesome 精简脚本5. 外部依赖重试与降级(lib/retry.sh)
第三方网络抖动不可避免(Algolia API / CDN 视频 / 音乐 API),且外部服务不可用不是产品缺陷,不应拦发版。tools/tests/lib/retry.sh 统一处理:
source tools/tests/lib/retry.sh
# exit-code 判定:退避重试 3 次(1s/4s/9s + 抖动)
retry_run "L2 搜索" "node tools/tests/search-integration.test.js"
# 输出模式判定(release-test L11 用)
retry_match "L2d 视频" "node .../videobg-integration.test.js" "PASS=.*FAIL=0"失败分类(全失败时按日志签名区分):
| 签名 | 判定 | 结果 |
|---|---|---|
ERR_NAME_NOT_RESOLVED / ECONNREFUSED / ETIMEDOUT / 429 / 50x 等 | 外部不可用 | ⚠️ 降级 warn(返回 0,不拦发版) |
| 无外部签名 | 业务断言失败 | ❌ fail(返回 1,真 bug) |
6. 第三方拦截 + ERR_ABORTED 排除(lib/browser-env.js)
playwright 测试的共享环境辅助,统一"页面自身错误"归属:
const { interceptThirdParty, collectPageErrors } = require('./lib/browser-env');
const env = collectPageErrors(page, BASE); // 持续累积 JS 异常 + 同源资源失败
await interceptThirdParty(page); // 拦截已知污染源
// 断言:env.jsErrors.length === 0 && env.sameOriginFailures.length === 0| 机制 | 说明 |
|---|---|
interceptThirdParty | 拦截已知污染源(如 webpushr 返回非 JSON → 页面 unhandled rejection),返回合法 JSON 而非 abort(abort 会产生 net::ERR_FAILED 反成新噪声) |
collectPageErrors | 仅收集 JS 异常 + 同源(localhost/生产域)资源失败;第三方 CDN/图床失败为环境噪声不计入 |
ERR_ABORTED 排除 | 导航中断(多次 goto/离开页面取消在途请求)非资源错误,直接跳过 |
7. 压缩开关验证(cicd.sh)
tools/cicd.sh 按模式注入压缩开关(sed 替换根 _config.yml 的 # minify-switch 标记行):
| 模式 | minify 值 | 说明 |
|---|---|---|
-d / --debug | false | 不压缩(本地调试,产物可读) |
-r / -c / -t | true | 压缩(hexo-minify 在 generate 时恒压缩,故需改其 enable) |
验证方法:
# 1. debug 构建(未压缩)
tools/cicd.sh -d
head -c 300 public/css/main.css # 多行、带缩进、有注释
# 2. 压缩构建(-t 本地编译测试,压缩无 CDN)
tools/cicd.sh -t
head -c 300 public/css/main.css # 单行、无注释、体积显著变小构建后 cicd.sh 会还原
_config.yml(仅当改动只涉及 minify-switch 标记行时),避免工作区脏。
8. 环境就绪、单实例锁与基址自适应
门禁前先过 preflight(环境预检,step 0),未就绪则秒级失败并打印修复命令,避免"跑一半才发现 server 挂了":
| 检查项 | 不通过表现 |
|---|---|
| 本地 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 自动跟随。
9. L14 视觉断言层
tools/visual-assert/*.json 共 16 套件,门禁 L14 只跑其中 7 套:layout-checks、dark-mode、search-modal、reward-modal、homepage、about、footer。
| 属性 | 值 |
|---|---|
| 语义 | 生产阻塞(失败即 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 层跑。
10. TC 索引与 git hooks
测试的权威来源是 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 | 校验清单是否落后 |
.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)。
当前索引:13 层 / 33 测试文件 / 3 lib 文件 / 251 断言 / 84 用例名。
11. flake 可见化与基线参考值
flake 可见化:被重试后通过的项目记入 FLAKY_LIST,门禁末尾打印 ⚠️ 本次门禁 flaky: N 项(…);它不改变 PASS/FAIL 计数,只让抖动可见。
| 基线 | 值 | 命令 |
|---|---|---|
| 本地 | PASS=181 FAIL=0 | tools/tests/release-test.sh --local |
| 本地 + L14 | PASS=188 FAIL=0 | RUN_L14=1 tools/tests/release-test.sh |
| 生产 | PASS=207 FAIL=0 | tools/tests/release-test.sh(发版后) |
3 个 lib:
assert.sh(82 行)、retry.sh(114 行,负责 flake 追踪)、preflight.sh(230 行)。browser-env.js为 playwright 共享辅助(见 §6)。
12. 如何新增一个测试用例
步骤
- 确定测试类型:
- 纯单测 / 静态断言(无浏览器)→ 归 L13(node 直跑)
- playwright 集成(需 chromium + server)→ 归 L11
- 创建测试文件
tools/tests/xxx.test.js(node:test 或 playwright) - 外部依赖测试:用
retry_run/retry_match包装(lib/retry.sh),全失败自动分类降级 - playwright 测试:用
lib/browser-env.js的interceptThirdParty+collectPageErrors统一错误归属 - 注册到 release-test.sh 对应层(L11 或 L13 段),输出格式对齐(
PASS=N FAIL=0或N 通过 / 0 失败) - 本地验证:
tools/tests/release-test.sh --local(跑 L13 + curl 断言);发版门禁跑全量 - 同步 TC 索引:新增/改动后必须让 TC 索引同步(
.githooks/pre-commit自动完成;或手动make tc-index)——否则门禁--check报"TC 清单不一致"
已知覆盖缺口:以下 4 个测试文件存在但未挂入任何层——
unit/helpers.test.js、unit/utils.test.js(仅被 L13a 的unit/**覆盖率 glob 附带执行)、wiki-integration.test.js、infinite-scroll-integration.test.js(纯 playwright,从不运行)。新增用例时注意别重蹈覆辙。
断言输出约定
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 覆盖。
⚠️ 已知坑
坑 5:改配置后必须 clean 构建
现象:跑门禁时 L1 构建验证通过但功能断言失败。
原因:本地预览使用旧缓存产物,测试断言的是旧版行为。
正确做法:跑门禁前必须 npm run clean && npm run build,或使用 tools/tests/release-test.sh --prepare 自动就绪。
坑 6:source/_data/* 增量缓存误判
现象:修改友链/相册数据后,L2 URL 断言页面空白或数据不更新。
原因:Hexo 增量缓存误判「已缓存未变化」。
正确做法:修改数据文件后先 rm -f db.json && npm run build 再跑门禁。容器环境用 tools/cicd.sh -d(内置 clean)。
附:测试体系速查表
| 命令 | 用途 |
|---|---|
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 视觉断言(基线 PASS=188) |
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-pwa | Vercel 测试域 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 断点) |

