在前端开发界,长期存在着一种根深蒂固的偏见: “单元测试(Unit Test)属于后端接口,端到端测试(E2E)属于复杂的大型单页应用(SPA)或电商交易链路;至于纯静态博客或工具站点,HTML 都是构建期直接吐出来的,肉眼看一眼没崩,还需要写什么自动化测试?”

然而,本站(QCSunny Lab)在早期曾遭遇过一起非常尴尬的静默死按键事故: 在一次常规的代码解耦与目录重构中,一个选择器 #calc-panel-basic 发生了残留。构建工具一路绿灯打包通过,生成的 HTML 页面结构完好、排版优雅、SEO 元数据挑不出任何毛病 —— 但在客户端浏览器运行时,底层的绑定脚本因找不到该 DOM 节点直接静默抛出未捕获异常。 全站核心的科学计算器所有按钮整整瘫痪了一周,才被一位热心读者的邮件指出!

静态站由于缺乏服务端动态异常上报通道(尤其是在本站恪守“零外部遥测、零埋点收集”的隐私原则下),一旦在浏览器端发生逻辑断裂,往往比有监控大盘的动态服务更加难以察觉。

为此,我们在持续集成(CI)流水线中植入了目前拥有 150 余个高标准用例的 Playwright 真实浏览器 E2E 自动化质量门禁。本文完整复盘这一工程防线的底层设计考量。


1. 门禁法则一:测真实产物(dist/),绝不测 Dev Server

绝大多数开发者配置 Playwright 时,习惯将 webServer 指向本地的开发服务器(如 astro devvite):

// 典型但脆弱的配置
webServer: {
  command: 'npm run dev',
  port: 3000,
}

为什么这在工程上是不可接受的?

  1. 构建差异(Dev vs Prod Divergence):开发服务器运行的是未压缩的 ESM 模块,CSS 是通过 JS 动态注入的;而上线后的生产产物经过了 Rollup 语法摇树(Tree-shaking)、CSS 文件物理提取、HTML 静态预渲染和 Brotli 极致压缩。在 dev 下正常运行的代码,完全可能在生产打包时因为对象属性可达性误判而产生破坏;
  2. 静态资产脱节:Sitemap 索引、llms.txt、根据标题动态生成的 1200×630 OG 分享图以及离线 Service Worker 缓存,仅在构建期由 Integration 插件写入磁盘。

本站铁律: E2E 必须且只能在执行完 npm run build 之后,基于轻量 HTTP 服务器对 dist/ 物理目录启动测试。线上用户访问的是什么字节,测试套件就验证什么字节。


2. 门禁法则二:不可见缺陷的自动化断言

在现代 Web 标准下,许多致命缺陷根本不会在控制台报错,必须通过无头浏览器的真实排版几何引擎进行捕获:

1. 纯几何防重叠与移动端零横向溢出(375px 铁律)

在 CSS 中,var(--w-page) 变量名一旦拼写错误,整条声明会被浏览器静默丢弃并回退到未定义状态。只断言“属性名是否存在”完全拦不住这种 Bug。

我们在测试中直接通过 getBoundingClientRect() 钉死实际渲染几何:

  • 在 375px 窄屏视口(模拟 iPhone 基础尺寸)下,巡检全站近百个页面,断言页面主体最大宽度绝不超过 375px(彻底杜绝横向滚动条);
  • 验证导航栏 SVG 图标与 H1 字标的渲染垂直中线偏差 1px\le 1\text{px}(拦截 CSS flex 基线对齐因字体 descender 空白导致的细微错位);
  • 针对房贷走势图等脚本动态生成的 SVG,逐个提取 <text> 元素的真实外接矩形,断言文本两两之间重叠面积必须严格 2px2\le 2\text{px}^2

2. 双语视图绝对互斥校验

全站采用 html[data-lang] 属性驱动的中英文纯净双语体系。 如果一位开发者在编写新工具或新文章时,不小心在英文视图中漏掉了一个中文段落,或者在中文视图中硬编码了英文短语,肉眼逐页巡检极其容易眼花。

我们的 E2E 测试会驱动真实页面在两种语言之间来回切换:

  • 切换为英文时,页面可见文本中的汉字密度必须严格低于阈值(允许个别如“市斤”、“等额本息”等作为被解释对象的中文保留);
  • 遍历所有表单生成的下拉框 <option>、按钮提示 title、占位符 placeholder 以及读屏辅助 aria-label,断言它们跟随属性变动即时完成全量热替换。

3. KaTeX 公式防逃逸与零第三方渗透

本站博客大量包含高等数学、算法与密码学公式。为了保证极致性能与网络主权,我们严格锁死 KaTeX 本地依赖,严禁请求任何外部公共 CDN。

门禁中专门设立了针对公式渲染的硬性断言:

  • 拦截页面发出的一切网络请求,一旦出现任何非同源的第三方域名资源,测试立即熔断报警;
  • 字体文件必须且仅允许加载 3 个本地精简子集化 woff2 文件(总大小控制在 48 KB 内);
  • 散落美元号逃逸防护:正文中的货币符号(如 \$0.14)或代码变量(如 \$PATH)若未按规范转义为 \$,会被 KaTeX 误判定为未闭合的数学公式,导致大段正常文字被斜体歪斜排版。测试套件会扫描全站渲染后的 DOM 树,确保没有任何未被转义的美元符号触发意外排版。

3. 门禁法则三:产物级性能预算(Performance Budget)

代码体积的膨胀就像房间里的灰尘,如果不加以强制约束,随着迭代一定会逐渐失控。我们将《静态站的字节账》中推导出的性能指标直接写成测试断言:

// e2e/perf-budget.spec.ts 片段
test('pure content pages must ship 0 bytes of first-party JS', async ({ page }) => {
  const scripts: string[] = [];
  page.on('request', req => {
    if (req.resourceType() === 'script' && req.url().includes('/_astro/')) {
      scripts.push(req.url());
    }
  });

  await page.goto('/blog/static-site-byte-ledger/');
  // 纯文章正文页严禁加载任何第一方客户端 JS 分块!
  expect(scripts).toHaveLength(0);
});

同时,测试套件还会对物理打包产物进行离线测量:

  • 46 个工具页共享的 main 核心分块,经 Brotli 压缩后必须严格 <45 KB< 45\text{ KB}
  • 嵌入 HTML 的首屏轻量内联搜索索引,边际体积必须严格 <12 KB/页< 12\text{ KB/页}。 一旦有开发者引入了臃肿的重量级第三方库,CI 会在合并代码前直接拉响红灯,守死性能底线。

4. 测试不是负担,而是敢于频繁重构的底气

262 个 E2E 测试用例在本地并行执行仅需数分钟,在 GitHub Actions CI 容器中也只需约 2 分钟。

正是这套严丝合缝的自动化测试门禁,让我们在面对底层算法重构、CSS 全局度量衡重写以及高频发布新博文时,能够拥有**“只要 CI 是绿的,我就敢打包上线”**的从容与底气。