视觉回归工程化:稳定基线、像素差异与可审查更新
CI 红灯不是一句“截图不一致”
一次常见事故发生在样式修复合并之后:开发者本机重新运行用例,所有截图都通过;Linux CI 却连续报告登录卡片有两千多个差异像素。把 CI 生成的 actual 图下载下来,肉眼看不到布局变化,放大后才发现文字边缘和图标轮廓全部偏了一圈。有人准备提高阈值,有人准备直接更新全部基线,还有人认为这是浏览器偶发抖动。三种动作都可能让流水线变绿,却没有回答真正的问题:基线由什么环境产生,当前截图和它是否属于同一个可比较空间,差异来自业务变更、字体栅格化还是动态内容。
视觉回归比较的不是“页面大概长得一样”,而是两个确定像素矩阵在约定容差下是否一致。操作系统字体、浏览器构建、无头模式、设备像素比、视口、缩放、语言、时区、颜色主题、动画时刻、网络字体和测试数据都会进入矩阵。只保存一张 PNG 而没有保存这些条件,基线就只是作者机器的一次偶然截图。
先建立三个判断。功能断言仍负责按钮能否点击、订单是否创建、URL 是否跳转;视觉断言负责布局、颜色、字体、图标和资源是否偏离已审基线;人工设计审核负责变化是否符合产品意图。截图通过不能证明功能正确,截图失败也不自动等于产品错误。CI 的任务是保存 expected、actual、diff 和运行环境,让审查者能把差异归因,而不是只给一个红叉。
安装后先生成一张可解释的基线
新项目使用 Playwright Test,因为 expect(page).toHaveScreenshot() 与 expect(locator).toHaveScreenshot() 属于测试运行器的截图断言能力。官方 安装指南 给出了 npm init playwright@latest 和包管理器安装入口,并列出当前发行线支持的 Node.js LTS 主线;执行时应以项目锁文件和该页列出的运行时为准,不要把 npm 包中宽松的 engines 下限当作团队支持承诺。包安装和浏览器安装是两步,浏览器安装说明要求显式执行 npx playwright install;Linux CI 还可用 --with-deps 补齐系统库。
# 在已有 Node 项目中安装;提交 package-lock.json 锁住解析结果
npm install --save-dev @playwright/test
# 本地只跑 Chromium 时可以先安装一个浏览器
npx playwright install chromium
# Linux CI 通常同时安装浏览器和系统依赖
npx playwright install --with-deps chromium
# 留下实际版本证据
node --version
npx playwright --version版本不能只写在文档里。CI 应从 lockfile 安装,Playwright 官方 Docker 镜像也要与项目中的 @playwright/test 使用同一版本标签;Docker 指南明确提醒,镜像版本不匹配可能导致浏览器可执行文件找不到,并建议固定镜像版本。系统 Chrome 可以用于本机诊断,却不应悄悄替代 Playwright 管理的浏览器生成共享基线,因为系统自动升级会改变渲染结果。
最小用例只截一个稳定组件,先避免全页滚动、广告位和实时数据扩大变量。第一次运行没有 expected 文件时,Playwright 会写入基线;第二次在同一环境运行应直接通过。官方 Visual comparisons 说明,截图会落在测试文件旁的 snapshot 目录中,文件名包含平台信息;实际路径还会受到项目名和 snapshotPathTemplate 影响。
// tests/visual/account-card.spec.ts
import { expect, test } from '@playwright/test';
test('account card keeps the approved layout', async ({ page }) => {
await page.goto('/account');
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
await expect(page.locator('[data-testid="account-card"]'))
.toHaveScreenshot('account-card.png');
});# 第一次由开发者在受控环境生成,随后人工查看新增 PNG
npx playwright test tests/visual/account-card.spec.ts --update-snapshots
# 不带更新参数再跑一次,证明基线能够复现
npx playwright test tests/visual/account-card.spec.ts
# 查看测试文件、基线和配置是否都进入预期变更集
git status --short预期是首次日志出现“snapshot doesn't exist, writing actual”一类信息,第二次退出码为零。若第二次立即失败,不要提交基线:页面尚未稳定,或者测试在两次运行间消费了不同数据。CI 不仅不能带 --update-snapshots,还应把 updateSnapshots 显式设为 none;当前配置默认值是 missing,会为缺失项创建快照。ignoreSnapshots 也必须保持 false,因为它会跳过 toMatchSnapshot() 和 toHaveScreenshot() 等全部快照期望。两项边界见官方 TestConfig API。比较作业没有写回基线的权限,基线生成与更新只在受审分支中执行。
基线身份必须同时包含页面与渲染环境
基线名称需要让人回答“哪个页面、哪个状态、哪个项目、哪个平台”。只叫 home.png,半年后无法区分游客首页、登录首页和移动首页。推荐把业务状态放进快照名,把浏览器、平台、项目交给模板生成,例如 checkout-empty.png、checkout-error.png;不要把随机订单号或执行时间放进名称。
snapshotPathTemplate 决定 expected 文件如何映射。官方 TestConfig API列出了 {testDir}、{testFilePath}、{arg}、{projectName}、{platform} 等 token,并说明未提供值的 token 前缀可被移除。团队应先决定是否跨项目共享基线:若 Chromium、Firefox 与 WebKit 都要比较,就保留 {projectName};若 Windows 和 Linux 都是正式环境,就保留 {platform};只有经过金丝雀正反样例证明渲染一致,才合并某个维度。模板没有浏览器版本、channel、headless/headed、CPU 架构、镜像摘要和字体清单这些 token;只要这些维度可能并存,就要把长期并存的身份编码进项目名,把具体版本和摘要保存到基线清单,不能指望 {platform} 自动区分。
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
snapshotPathTemplate:
'{testDir}/__screenshots__/{projectName}/{platform}/{testFilePath}/{arg}{ext}',
projects: [
{
name: 'desktop-chromium-headless',
use: { ...devices['Desktop Chrome'], headless: true },
},
{
name: 'mobile-chromium-headless',
use: { ...devices['Pixel 7'], headless: true },
},
],
});生成后目录应能直接看出两个项目的基线分开。若项目名未进入路径,移动项目可能覆盖桌面 expected,或在同名文件上反复失败。把 platform 去掉则意味着 Windows 作者生成的字体像素要在 Linux CI 复现,这通常不成立。更稳的做法是在和 CI 相同的容器、虚拟机镜像或专用基线作业中生成共享基线,本机只预览差异。
基线是代码评审资产。它应和触发变化的 CSS、组件、字体或图片放在同一个变更中,不能从聊天附件复制;二进制 diff 查看器要能展示旧图、新图和叠加差异。每套基线旁的清单至少保存 Playwright 与 browser revision、project/channel、headless/headed、runner OS 与架构、镜像摘要、字体包或字体清单摘要、locale、browser timezone、runner TZ、viewport、device scale factor 和生成作业。仓库启用 Git LFS 时,还要确认 CI checkout 得到的是 PNG 本体而不是 LFS pointer。缺失 expected、损坏图片、环境身份不匹配和真正像素差异必须产生不同错误,否则排障会把存储或环境问题误判为 UI 回归。
先稳定环境,再谈阈值
视觉用例应固定视口、设备像素比、颜色主题、语言和时区。Playwright 的 browser context 天然隔离 cookie 与存储,但不会替你固定业务数据、字体服务和第三方图片。页面在截图前至少达到业务稳定点:关键接口已完成、骨架屏已消失、字体已加载、目标元素可见且布局不再变化。不要用固定 waitForTimeout(3000) 代替这些条件;网络慢时三秒不够,网络快时三秒只是增加成本。
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
updateSnapshots: process.env.CI ? 'none' : 'missing',
ignoreSnapshots: false,
use: {
viewport: { width: 1440, height: 900 },
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
colorScheme: 'light',
reducedMotion: 'reduce',
deviceScaleFactor: 1,
},
expect: {
toHaveScreenshot: {
animations: 'disabled',
caret: 'hide',
scale: 'css',
},
},
});animations: 'disabled' 会快进有限动画并取消无限动画,默认即为 disabled;它可能触发 transitionend,所以截图前后若有业务监听器改变 DOM,要显式等待最终状态。caret: 'hide' 去掉输入光标,scale: 'css' 让高 DPI 设备按 CSS 像素截图,减小平台差异和文件体积。字段的完整语义以 PageAssertions.toHaveScreenshot 为准。
配置层与单次断言层不是同一组字段。expect.toHaveScreenshot 可统一配置 animations、caret、两种面积容差、scale、stylePath、threshold 和 pathTemplate;mask、maskColor、omitBackground 与单次 timeout 只属于具体断言。clip 和 fullPage 更只存在于 page 断言,locator 断言按元素边界截图。全局路径字段叫 snapshotPathTemplate,截图断言自己的路径字段则是 expect.toHaveScreenshot.pathTemplate。把单次字段塞进全局配置应让 TypeScript 配置检查失败,而不是靠类型断言绕过。具体字段分别以 page 截图断言 和 locator 截图断言 为准。
字体是最常见的隐性依赖。自托管字体应被构建制品包含,测试等待 document.fonts.ready 后再断言;依赖系统字体时,镜像升级、Windows ClearType 与 Linux FreeType 都可能改变边缘像素。不要把缺失字体用大阈值吞掉,先在失败 trace 的 Network 中确认字体请求状态,再从 document.fonts.check() 验证目标字族。对许可证受限的商业字体,CI 镜像还需要合法分发方式;无法分发时,应选用可在目标环境稳定安装的替代字体并由设计方批准。
动态数据要在数据层固定。价格、余额、头像、推荐列表、相对时间和随机 ID 可以由测试 API 创建确定数据,或用路由拦截返回受控 fixture。只有确实不属于判断对象的局部内容才 mask。把整张页面遮掉虽然稳定,却也让回归检测失去价值。
阈值、mask、样式覆盖各自解决不同问题
截图比较有三个容易混淆的容差。threshold 是同一像素在 YIQ 感知颜色空间中的可接受差异,取值从 0 到 1;maxDiffPixels 是允许的差异像素绝对数;maxDiffPixelRatio 是差异像素占总像素的比例。官方 API 给出的 threshold 默认值是 0.2,但这不表示“允许页面变化 20%”。它控制单像素颜色距离,另外两项才控制差异面积。
选择时先从严格值开始运行固定正反样例。图标边缘有稳定的少量抗锯齿差异,可以用小的 maxDiffPixels;同一组件有多种尺寸,比例可能比绝对数更可比;业务颜色本身需要精确时,不应提高 threshold。全局放宽会让小组件的大变化被整页面积稀释,优先把容差放在具体断言上并附原因。阈值不是一次拍定的团队常量:先收集固定环境下正例的差异分布,再用必须失败的颜色、尺寸和位移反例确认检测下限。任何阈值上调都按基线变更审核,附前后 diff 与反例结果,不能由失败作业自动学习或自动批准。
await expect(page.locator('[data-testid="price-card"]')).toHaveScreenshot(
'price-card.png',
{
animations: 'disabled',
maxDiffPixels: 40,
threshold: 0.12,
mask: [page.locator('[data-testid="live-clock"]')],
maskColor: '#FF00FF',
stylePath: './tests/visual/screenshot.css',
},
);mask 默认用纯色矩形覆盖定位器边界,且对不可见元素也可能应用,因此定位器必须足够窄并在截图前验证数量。mask 不是脱敏保证:元素边界变化、弹层、浏览器自动填充、canvas 或背景图都可能让真实内容出现在矩形之外;actual、trace 和 video 也可能保留未遮挡画面。敏感页面仍需使用合成数据和受控账号。
stylePath 适合隐藏不可控光标、暂停第三方轮播或固定测试专用样式,并能穿透 Shadow DOM 与内部 frame 应用;它不应修补产品 CSS。若测试样式把溢出、焦点环或错误布局改正确,expected 记录的就不是用户看到的页面。每条规则都应说明被稳定化的对象,升级 Playwright 后重新验证样式是否仍能到达目标 frame。
一组正反实验把“稳定”和“敏感”同时证明
稳定性只靠正例不能证明。一个合格的金丝雀实验要先在相同页面连续通过,再制造一个明确变化并稳定失败。下面的页面将卡片宽度和字体固定,环境变量只改变按钮宽度;基线生成后,正常运行应通过,FAIL_VARIANT=1 应产生差异附件。
// tests/visual/canary.spec.ts
import { expect, test } from '@playwright/test';
test('visual canary', async ({ page }) => {
const buttonWidth = process.env.FAIL_VARIANT === '1' ? 176 : 160;
await page.setContent(`
<style>
* { box-sizing: border-box }
body { margin: 0; font-family: Arial, sans-serif; background: #f4f6f8 }
.card { width: 360px; margin: 80px; padding: 24px; background: white;
border: 1px solid #ccd2d8 }
h1 { margin: 0 0 20px; font-size: 24px }
button { width: ${buttonWidth}px; height: 40px; border: 0;
background: #146c43; color: white }
</style>
<main class="card"><h1>Account</h1><button>Continue</button></main>
`);
await expect(page.locator('.card')).toHaveScreenshot('account-card.png', {
maxDiffPixels: 0,
});
});npx playwright test tests/visual/canary.spec.ts --update-snapshots
npx playwright test tests/visual/canary.spec.ts
$env:FAIL_VARIANT = '1'
npx playwright test tests/visual/canary.spec.ts
Remove-Item Env:FAIL_VARIANT在锁定 Node、Playwright、浏览器和操作系统镜像后,基线创建与第二次比较都应以退出码 0 结束;反向运行应以非零退出码结束,并在当前 outputDir 下留下 expected、actual 与 diff 附件。差异像素数、ratio、附件前缀和截图重试间隔会受浏览器构建、平台、图片边界与 Playwright 版本影响,不应把某次运行的精确数字写成通用预期。可移植的判据只有两项:相同输入可重复通过,16px 宽度变化在当前零面积容差下稳定失败。
如果反例竟然通过,依次检查:断言是否截错元素,按钮是否在 clip 外,maxDiffPixels 或 ratio 是否过宽,mask 是否覆盖了按钮,环境变量是否传入 worker。若正例偶发失败,则收集多次 actual 的哈希和 diff 分布;差异位置每次随机散布通常指向动画或字体,固定在实时文案区域指向测试数据,整图平移则优先检查视口、滚动条和页面缩放。
实验结束删除临时测试产物,不删除经过批准并进入仓库的 expected:
Remove-Item -LiteralPath test-results -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath playwright-report -Recurse -Force -ErrorAction SilentlyContinue
git status --short清理命令应限定在仓库约定的产物目录。不要用模糊通配符删除所有 *.png,它会把基线一并移除。共享 runner 还要清理临时下载、用户数据目录和认证状态,避免下一作业继承上一页面的数据。
接入真实项目时把数据、服务与 worker 一起设计
真实项目通常通过 webServer 启动构建或预览服务器,视觉用例再用 baseURL 访问。开发服务器的热更新、错误覆盖层和按需编译会制造额外变化,CI 更适合测试生产构建后的预览入口。服务 readiness 应检查稳定 URL,而不是看到进程启动就截图。
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/visual',
outputDir: 'test-results/visual',
workers: process.env.CI ? 2 : undefined,
retries: process.env.CI ? 1 : 0,
failOnFlakyTests: !!process.env.CI,
updateSnapshots: process.env.CI ? 'none' : 'missing',
ignoreSnapshots: false,
webServer: {
command: 'npm run preview -- --host 127.0.0.1 --port 4173',
url: 'http://127.0.0.1:4173/health',
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: 'http://127.0.0.1:4173',
trace: 'retain-on-first-failure',
screenshot: 'only-on-failure',
},
});worker 并发会同时访问后端。每个 worker 应拥有独立账号或数据命名空间,例如把 testInfo.workerIndex 写入测试对象标识;用例结束通过 API 删除数据。若所有 worker 修改同一个“默认头像”或“主题设置”,截图将随执行顺序变化。重试会重新执行 beforeEach 和测试体,也必须使用幂等种子与清理。当前 Playwright 提供 failOnFlakyTests,CI 开启后可让“首轮失败、重试通过”的用例仍产生非零退出;若团队暂不阻断,也必须单独统计这类结果,不能把绿色最终状态当成稳定。
登录页面不要使用个人账号。setup project 可以用测试租户的服务账号生成 storage state,文件保存在忽略提交的临时目录,并按角色分开;视觉用例只读取所需角色。认证文件含 cookie 与 localStorage,等同凭证,应在作业结束删除。登录后的页面截图还要用合成姓名、订单和头像,不把生产数据复制到测试环境。
CI 分层能控制等待时间。每次提交跑 Chromium 核心组件与关键页面;合并队列或夜间任务再扩展 Firefox、WebKit、移动视口、深色主题和更多语言。不同项目保留独立基线。若一个产品正式支持 WebKit,就不能永远只在低频任务里检查支付或登录关键路径;矩阵频率应由用户风险和历史回归率决定。
从 expected、actual、diff 反推故障层
看到失败先打开三张图,不先更新。expected 是已批准状态,actual 是当前运行结果,diff 标出比较器认定的像素变化。再结合 trace 的网络、DOM snapshot、console 和测试步骤判断差异从哪里进入。
差异只围绕文字边缘出现:核对操作系统、浏览器构建、字体文件响应、document.fonts 和设备像素比。整张页面颜色偏移:核对 colorScheme、强制色彩、ICC、透明背景和浏览器版本。某个区域每次内容不同:核对账号、种子数据、相对时间、随机数、第三方请求和缓存。页面整体下移:核对未加载图片尺寸、顶部 banner、滚动条、cookie 弹窗和错误覆盖层。diff 为空但断言超时:页面可能一直未达到“两张连续截图一致”,应看动画、canvas、视频和持续重排。
定位器截图还可能因元素边界变化扩大画布。此时差异像素比例不只反映内部变化,还包含新增区域;比较 expected/actual 尺寸比单看 ratio 更重要。全页截图则会受懒加载、固定定位元素和滚动触发动画影响。需要覆盖长页面时,优先按稳定业务区域拆断言;只有页面整体版式本身是契约时才使用 fullPage。
浏览器崩溃、字体下载失败、expected 缺失和像素不一致要在报告中分开统计。前两者属于环境或依赖故障,第三项属于基线资产问题,第四项才进入视觉审查。若团队把它们都标为“visual flaky”,长期会用阈值掩盖基础设施问题。
基线更新必须能审核,也必须能撤回
确认变化符合需求后,只更新相关用例,不执行无过滤的全仓库更新。更新前记录需求或设计变更,更新后查看 PNG 与源码 diff,再运行一次不带更新参数的比较。一个 PR 若改了按钮颜色,却同时更新几十张无关页面,说明环境或数据已经漂移,不能合并。
# 只更新一个用例或一组明确标签
npx playwright test tests/visual/checkout.spec.ts --grep "empty cart" --update-snapshots
# 重新比较,确保新基线可复现
npx playwright test tests/visual/checkout.spec.ts --grep "empty cart"
# 审查变更集合;PNG 需在代码托管平台或本地图像查看器中逐张比较
git diff -- tests/visual/checkout.spec.ts
git status --short基线提交者不能单独批准自己的 expected。业务或设计 owner 确认变化意图,视觉基线 owner 确认环境身份、diff、mask 与阈值,敏感页面还需数据 owner 确认产物不含真实数据;小团队可以由同一审查者兼任多个 owner,但仍需与提交者分离。批准记录必须关联需求、生成作业和基线清单,普通比较作业保持只读,机器人不得因测试转绿自动接受差异。
审查者要确认变化与需求一致,diff 没有夹带字体、channel、时区或数据漂移,mask 没有扩大,阈值没有被顺手放宽,反向金丝雀仍会失败。若错误基线已经进入主分支,回滚应恢复前一批准版本的 expected、基线清单以及触发它的配置或样式,而不是再生成第三份图片覆盖:
# 在修复分支中从上一批准提交恢复指定基线,再运行比较确认
git restore --source=<approved-commit> -- tests/__screenshots__/desktop-chromium/linux/checkout.spec.ts/empty-cart.png
npx playwright test tests/visual/checkout.spec.ts --grep "empty cart"回滚后如果当前产品代码确实不同,测试应失败,这正是证据:需要继续回滚代码,或重新走变化审批。不要为了得到绿色结果同时恢复 expected 和 actual 对应代码却不确认部署版本。发布制品、镜像、字体包和浏览器镜像也可能独立升级,恢复仓库 PNG 并不一定恢复渲染环境。
截图断言底层怎样得到结论
toHaveScreenshot 不是调用一次 page.screenshot() 后立即做二进制比较。官方 API 说明它会先截图,等待短间隔,再截图;当连续两张截图一致时,才把最后一张与 expected 比较。这个运行模型能过滤正在收敛的布局,却无法让永不停止的 canvas、视频、时钟或随机内容自动稳定。超时意味着“没有得到稳定候选”或“持续比较不通过”,调用日志能区分具体阶段。
截图前,Playwright 根据断言参数处理动画、光标、遮罩、CSS 覆盖、缩放和背景,再按页面或 locator 边界生成像素。比较器对同位置像素计算感知颜色差异,threshold 决定单像素是否算不同,随后聚合差异数量并应用 maxDiffPixels 或 maxDiffPixelRatio。因此先提高 threshold 和先提高面积容差会掩盖不同故障:前者可能忽略颜色偏移,后者可能允许一块真实布局变化。
这条链还解释了为何 expected 不能跨环境随意复用。DOM 相同不保证栅格相同,字体 hinting、GPU 路径、浏览器截图实现都会改变像素。容器能固定 OS 包和浏览器,却仍需固定字体、locale、时区、CPU 架构与浏览器镜像;在 ARM 与 x64 runner 间迁移时,要跑金丝雀正反样例后再决定是否共用基线。
选型要看变化审核和环境控制能力
仓库内 Playwright 基线适合页面数量可控、团队已使用 Playwright、希望把截图与测试代码同 PR 审核的项目。优点是运行链透明、能与 locator、登录态和 trace 共用;代价是二进制仓库增长、跨平台基线维护和审图体验需要自行建设。
组件级视觉测试适合设计系统和高复用组件。它把数据与布局压缩到较小状态空间,运行快、差异定位直接,但不能发现路由、真实 CSS 级联、页面容器和后端数据组合造成的回归。页面级视觉测试适合关键业务状态和集成布局,数量不应等于所有 E2E 步骤。两层通常是互补关系:组件层覆盖状态矩阵,页面层覆盖少量高价值旅程。
托管视觉审核服务适合需要浏览器矩阵、集中审图、历史对比和跨团队批准的组织,但会把截图、DOM 或构建资产发送到第三方,必须先评估数据区域、访问控制、保留、删除、合同和费用。不能上传客户数据的系统可保留自托管基线与内部 artifact。选型不以“能截图”为标准,而看环境是否可固定、差异是否容易审查、敏感数据能否受控、基线能否迁移和退出。
权限、敏感数据与产物容量一起治理
截图会复制屏幕上的一切:姓名、邮箱、余额、内部域名、头像、会话提示和调试 banner。mask 只影响指定截图,不会自动处理 trace、video、console、HAR 与 HTML report。优先使用合成数据和专用测试租户;其次在页面准备阶段隐藏不属于断言对象的数据;最后才用 mask。任何会进入第三方服务或公开 artifact 的产物都要按真实数据副本管理。
CI 最小权限包括只读代码、读取指定测试凭证、写入当前作业 artifact;基线更新权限只给受审分支和明确 owner,普通比较作业不能写回仓库。fork PR 不应获得环境 secret,来自不受信代码的测试也不应在带生产网络权限的 runner 上运行。下载 diff 的人需要项目成员权限,artifact 链接不能长期公开。
容量由“基线数量 × 平均图片大小 × 环境矩阵”与“失败次数 × expected/actual/diff/trace/video 大小 × 保留周期”共同决定。全页 PNG、设备像素比 2、多浏览器和多语言会乘法放大。可以在 CI 记录基线目录总字节、单 PR 新增图片数、失败 artifact 大小和下载频率;对成功运行不保存重复 actual,对失败运行保留足够定位的附件,过期后按策略删除。删除 CI artifact 不等于删除 Git 历史中的基线,敏感图片一旦提交还要执行仓库历史处置和凭证轮换。
长期门禁关注漂移,不只关注通过率
团队应维护一组视觉金丝雀:一个严格通过样例、一个明确失败样例、一个字体样例、一个 animation/mask 样例。升级 Node、Playwright、浏览器、Docker 镜像、字体包或 runner 架构时先跑这组用例,再跑关键业务页面。若反例不再失败,说明阈值、断言边界或工具行为已经漂移;若正例不再稳定,先修环境,不批量更新业务基线。
持续指标至少包含首轮失败率、重试转绿率、按原因分类的失败数、基线更新数量、无关联需求的更新数、单套运行时间、artifact 字节和过期例外。flaky 用例要有 owner、问题单和退出期限;隔离只能暂时阻止噪声,不能成为永久目录。阈值、mask、跳过项目和平台专用基线都应能追溯到原因。
当页面重构使旧用例失去业务价值,先删除测试引用,再删除对应 expected,并确认 CI 与报告平台没有继续寻找旧路径。更换工具时导出基线、差异记录、owner 与例外,保留一段双跑窗口:旧链和新链对同一金丝雀给出一致结论后再停旧链。这样视觉回归的可信度来自可复现环境、稳定正反样例和可撤回审批,而不是某一批永远不变的 PNG。
