返回知识中心
对比

视觉回归测试失败:基线漂移还是界面缺陷?

通过字体、视口、缩放和 CSS 的受控对照定位视觉回归测试失败。先区分逐像素色差容差与差异像素预算,再决定是否更新基线。

BotBrowser Team

文档中心

想直接进入 文档 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

视觉回归测试失败,只说明比较没有通过配置好的规则,并没有确定原因。字体文件、浏览器构建、视口或渲染后端发生变化,即使应用代码不变,也可能移动像素。BotBrowser 提供由配置文件驱动的字体清单和渲染控制,支持跨平台一致性,但不能保证不同渲染后端生成完全相同的字形像素,不能判断应用 CSS 是否正确,也不能替测试选择截图容差。

要点

  • 先比较两张图片的尺寸和渲染输入,再计算或接受像素差异。
  • 色差容差决定哪些像素算不同;差异像素数量或比例决定允许多少已经判为不同的像素。
  • 一次原生 Chrome 受控运行分别改变字体、视口、缩放和 CSS。较大的数量预算与较大的色差容差,都接受了刻意引入的 CSS 颜色变化。
  • 重复相同只能证明已记录条件下的可重复性,不能证明界面、字体选择或基线正确。

截图诊断先检查尺寸与输入,再区分色差容差和差异像素预算,最后检查变化区域后决定是否接受基线。

目录

把比较失败与失败原因分开

先检查两张图片解码后的尺寸。宽和高都变为两倍,像素位置就变为四倍。直接把它与较小的图片比较,得不到通常意义上对应位置的差异像素数。缩小图片又会引入重采样操作,增加另一种差异来源。应先记录尺寸不匹配;只有测试原本就要求归一化时,才采用明确的归一化规则。

再比较截图清单:浏览器可执行文件版本、操作系统、渲染模式、以 CSS 像素表示的 viewport、deviceScaleFactor、可用字体和应用代码修订。容器标签可以帮助定位,但可变标签本身不能证明这些输入保持不变。应把实际解析出的浏览器版本与字体文件身份随基线保存。输入不同时比较失败,与输入相同时比较失败,需要走不同的调查路径。

然后查看差异在哪里。沿文字轮廓出现的变化提示字体或文本排版问题;单个矩形区域提示局部样式或内容变化;大面积变化可能涉及布局、缩放或共同的渲染输入。这些形状只是线索,并不是诊断。修改产品标签也会产生文字形状的差异,而字体回退可能改变远超字形范围的页面布局。

截图准备检查已经覆盖就绪条件与捕获元数据。应把这些输入写清楚,不要把截图断言当作应用就绪探测器。Playwright 的截图断言会等待连续两张截图一致,再把最后一张与基线比较。稳定的两帧仍然可能显示空表格、回退字体或加载错误。应用自己的就绪条件必须明确要求捕获的业务状态。

失败记录要分别回答三个问题:是否使用可比较的输入、哪些图像区域发生变化、变化后的界面是否可以接受。前两项是测量,第三项需要应用契约或设计审查。自动更新基线并没有回答其中任何一项,只是替换了后续比较使用的参考图片。

检查四类基线漂移来源

浏览器构建与截图模式

应固定生成基线的浏览器可执行文件,而不只是自动化库的版本。框架升级可能选择另一个随包浏览器,系统浏览器也可能独立更新。截图时要记录真实运行版本。如果基线来自另一构建,先让旧版应用在新浏览器中运行,再把变化归因于应用代码。

有界面或无界面模式同样属于清单。Playwright 的视觉比较文档明确列出宿主操作系统、版本、设置、硬件、供电方式和无界面模式等影响因素。这个警告支持控制环境,却不能证明某一次失败就是其中某个因素造成的。调查浏览器升级,需要旧构建与新构建的配对实验。下面的受控运行没有改变浏览器构建。

字体文件与加载状态

CSS 字体族名称是请求,不是每个字形实际由哪个文件绘制的证据。请求的字体可能不存在、仍在加载,或缺少某个字符。不同回退字体可能改变字形、宽度和换行。应记录测试提供的字体文件,并在有关加载承诺完成后检查有代表性的文本。等待字体不会让缺失的字形凭空出现。

document.fonts.ready 对应文档的字体加载和布局工作,并不承诺所有未来内容或字体请求已经发生。如果应用随后插入另一块内容,应等待该区域自己的就绪条件。刻意更换字体后,也要让布局重新计算,再截下一张图。把旧图与新字体尚未应用时的截图比较,会把时序问题和字体选择混在一起。

BotBrowser 的字体控制说明描述配置文件支持的字体清单与渲染行为,同时不保证每项文本测量或每个字形都产生相同输出。所选模式允许宿主字体回退,或使用真实系统字体时,宿主可用字体仍然相关。因此,声明字体清单和制定图片比较策略解决的是不同问题,不能互相替代。

视口与设备缩放系数

视口宽度影响响应式布局和文本换行,设备缩放系数影响 CSS 坐标与图片像素的对应关系。两者都要记录,不要用截图宽度代替视口宽度。整页截图还可能因为文档变长而增高,即使视口根本没有变化。

除了自动化请求的配置,也要读取页面实际观察到的值。一个 CSS 宽度下生成的基线,与另一个宽度下生成的当前图片,应先判为几何条件不一致。增加允许变化的像素数,不能恢复原基线的布局条件。如果多个视口是预期的项目划分,应分别保存参考图与项目身份。

图形后端与宿主渲染

声明的 GPU 值不能证明实际由同一个光栅化器绘制图片。软件光栅化器和宿主图形路径,可能为等价绘制操作生成不同像素。BotBrowser 的 Linux 图形后端文档明确区分软件后端及其像素结果。只有渲染器名称字符串,不足以说明精确截图基线的生成环境。

在环境能够提供时保存后端配置,避免混用不同类型执行节点生成的基线。怀疑驱动或后端发生变化时,应固定应用、字体和截图几何条件,做配对实验。下面的输入隔离实验没有修改驱动,也不能证明 GPU 原因。它只演示如何先把已知输入变化分类,再考虑放宽容差。

每次只改变一个输入

使用自有页面、固定内容、两个本地字体文件,不发外部请求。先生成基线,再分别建立只改变字体、视口宽度、设备缩放系数或已知 CSS 颜色的分组。每组使用全新浏览器进程重复运行。CSS 变化是阳性对照,用来确认比较确实能够暴露刻意引入的应用变化。

本次运行共有五组,每组独立启动三次,总计 15 个任务。统一的全局池请求并发上限为 20,因为只有 15 个任务,实际封顶 15。Ubuntu 宿主在启动前有 32 个 CPU 核心和充足可用内存,使用既有的 :88 显示服务。实际浏览器并发峰值达到 15。这是一次有界实验,不是建议所有机器都同时启动 15 个浏览器。

可下载独立复现脚本与已记录的运行 JSON。脚本需要 Linux、Node 20 或更新版本、原生 Chrome、Playwright Core、安装 Pillow 的 Python、两个本地 TTF 文件,以及既有 xvfb-88.service。它不会安装任何依赖。把四个路径变量指向这些依赖后运行:

DISPLAY=:88 node visual-regression-baseline-drift.mjs \
  --browser "$CHROME_BIN" --playwright-root "$PLAYWRIGHT_ROOT" \
  --font-a "$FONT_A" --font-b "$FONT_B" \
  --workers 20 --out /tmp/visual-regression-result.json

脚本通过本机回环接口提供固定页面与字体,记录所有分组,并验证浏览器进程清理。图片和临时数据目录会保留供检查。可以先用 --self-test 验证比较器的边界情况,再启动浏览器。下面的像素数量属于随附的那次运行;使用不同字体文件时,数量不同不代表方法失效。

截图写法要把页面就绪与图片比较分开。应用选择器应换成自有页面真正实现的就绪状态:

import { chromium } from 'playwright';
import { writeFile } from 'node:fs/promises';

const browser = await chromium.launch({ headless: false });
try {
  const context = await browser.newContext({
    viewport: { width: 1280, height: 900 },
    deviceScaleFactor: 1,
  });
  const page = await context.newPage();
  await page.goto('http://127.0.0.1:4173/fixture');
  await page.locator('[data-ready="true"]').waitFor();
  await page.evaluate(async () => {
    await document.fonts.ready;
    await new Promise(resolve =>
      requestAnimationFrame(() => {
        requestAnimationFrame(resolve);
      })
    );
  });
  const manifest = await page.evaluate(() => ({
    viewport: [innerWidth, innerHeight],
    devicePixelRatio,
    fontStatus: document.fonts.status,
  }));
  manifest.browserVersion = browser.version();
  await page.screenshot({ path: 'capture.png', animations: 'disabled' });
  await writeFile('capture.json', JSON.stringify(manifest, null, 2));
} finally {
  await browser.close();
}

这个例子捕获证据,不负责比较图片,也没有定义测试服务器。仅凭截图无法还原浏览器版本、所提供的字体文件或业务就绪条件。应把这些输入加入基线记录,而不是从像素猜测。禁用动画只排除特定的运动来源,并不会冻结实时数据、变化的时钟或应用计时器。

字体实验固定文本、宽度和浏览器,只切换所提供的字体;几何实验固定字体与内容,只改视口或缩放选项;CSS 对照只改已知颜色声明。如果多个输入一起改变,应保留这个事实,并把观察称为配置比较,不能当成单一因素的因果测量。

解读受控截图结果

15 个任务中的原生 Chrome 都报告运行版本 154.0.8037.57。基线图片为 1280 × 900,各分组三次启动的结果一致。比较器将 PNG 解码为 RGBA 字节;当某个像素的最大通道绝对差超过选定整数容差时,该像素才计为变化。这是用于说明问题的精确字节规则,不是 Playwright 所用的 pixelmatch 感知比较规则。

分组图片尺寸精确比较下的差异像素含义
基线重复1280 × 9000本次运行的受控输入能够重复。
切换所选字体1280 × 90026,962,约 2.34%差异限于文字相关区域。
改变视口宽度1024 × 900不计算先分类为尺寸不匹配。
改变设备缩放系数2560 × 1800不计算图片采样尺寸不同。
改变已知 CSS 颜色1280 × 9009,600,约 0.83%差异限于预定色块。

CSS 色块占 120 × 80 像素,面积就是 9,600 像素。刻意修改的通道值相差 8。通道容差为 0 时,这些像素都算不同;数量预算设为 10,000,却会接受已知 CSS 变化。把示例通道容差提高到 16,差异像素数又会变成零。这些数字说明两条不同的接受路径,不是建议用于生产环境的参数。

字体分组在记录的文本区域内改变了 26,962 个像素,区域外没有变化。这支持在刻意受限的页面内做归因。真实应用更换字体后可能大幅改变布局,所以“只在文本区域变化”不是通用字体检测器。CSS 对照也特意保持简单,若改的是布局属性,受影响范围可能接近整页,而不是一个矩形。

所有任务都只加载自有页面与本地字体资源。记录保留了实际几何值、字体就绪状态、浏览器来源与比较结果。清理检查没有发现残留的任务浏览器或工作进程。这证明有界运行已经完成,不证明读者机器上也有相同字体或图形环境。复现脚本与输出记录随实验证据一起保存。

分别选择色差容差与差异预算

Playwright 的图片比较选项文档把对应像素间可接受的感知色差,与总差异额度分开。maxDiffPixels 表示允许多少已经判为不同的像素,maxDiffPixelRatio 则表示相对于整张图面积的比例。它们作用于比较过程的不同阶段,改一个不等于改另一个,也不能直接套用其他算法的参数数值。

设置或操作回答的问题可能掩盖的失败
逐像素色差容差这个位置的颜色差是否大到需要计数?重要组件上幅度很小、但确实被修改的颜色。
最大差异像素数一张图允许出现多少个已分类差异?面积仍在预算内的图标缺失或控件变化。
最大差异比例整张图允许多大比例发生变化?同一个缺陷在更长整页图中占比变小。
新的基线图片未来比较使用哪个参照?未经检查就接受的任何意外变化。

比例依赖图像面积。同一个缺失控件,在很长的整页截图中所占比例,小于在组件裁剪图中的比例。数量预算没有这个分母,却仍然不理解变化区域的意义。任何一种像素指标都不知道差异来自装饰阴影、价格,还是被禁用的支付按钮。比较区域应在定义测试时选择,不能失败后才为通过断言临时缩小。

容差策略应同时依据目标环境下的重复运行,以及必须持续发现的已知应用变化,并保留两类例子。如果某种设置接受了已知 CSS 缺陷,它能压低无害噪声,并不代表它适合这个断言。应检查差异图,再决定比较更小且稳定的组件、稳定另一项输入,还是保留更严格的设置。

不要把实验中的整数通道容差 16 直接填入 Playwright 的感知色差选项,两者刻度和语义不同。10,000 像素预算也不是示范配置;它是在运行前选定,用于展示允许额度如何吞掉已知的 9,600 像素对照。合理预算必须绑定图像尺寸、区域和测试必须拒绝的具体变化。

决定恢复调查还是更新

如果图片尺寸意外不同,先恢复预期 viewport、deviceScaleFactor与捕获区域。若字体或浏览器输入不同,先判断环境变化是否有意。意外漂移应恢复;有意升级则先让旧版应用在两个环境中对照,再审查新的参考图。条件允许时,将环境迁移与应用功能变化拆开。

输入一致而稳定区域仍然不同,就调查应用修订与捕获状态,包括样式、文本、数据和加载结果。Grid 配置与像素差异的判断负责分布式节点间的归因;本地截图也应保持配置证据与图像证据的区别,不能因为清单相同,就认定渲染一定相同。

接受变化区域并记录生成环境之后,再更新基线。应在版本历史中保留旧图、当前差异、接受原因,以及生成新图的输入。更新基线的命令是一项写出操作,不是审阅者。不要把它放进无人值守的失败恢复流程,否则持续存在的缺陷可能成为明天认可的基线。

有意升级浏览器时,除了汇总差异,还要检查代表性文本、控件和布局。差异比例小可能掩盖重要局部缺陷,比例大也可能只是无害的字体边缘变化。容差是在理解这些区别后表达测试策略的工具,不能替产品决定是否应该接受变化。

视觉回归测试常见问题

截图哈希相同能证明没有缺陷吗?

不能。字节相同只确定该次捕获相等,两张图可能共享同一个错误字体、缺失内容或过期数据。视觉断言应配合对应的语义检查,例如控件状态或应用数据加载条件。

所有执行节点都应该共享一份基线吗?

只有预期渲染契约如此、相关输入也受控时才适合。不同浏览器项目、平台渲染路径或视口尺寸可能需要独立参考图。但为每次意外差异都建一份基线会隐藏漂移,应先分类再增加参考图族。

声明式配置文件可以替代字体和 GPU 记录吗?

不可以。配置文件控制的是所选浏览器表面,截图仍取决于实际文本和渲染路径。应记录字体模式与后端假设,不能把配置文件名称视为跨机器像素完全一致的保证。

来源

实用结论

截图失败后,先确保尺寸可比较、输入已记录、差异经过检查,再考虑放宽容差。把逐像素分类容差与总差异预算分开,并用必须拒绝的已知变化验证两者。只有界面变化及其生成环境都通过审查,才接受新的基线。

#视觉回归测试#截图#Playwright#渲染

在 BotBrowser 里跑一遍同样的检查

加载 profile,让刚读到的这些信号在浏览器内部保持一致,并在正式上线前先在自己的机器上确认结果。