可视化回归测试
Vitest 原生支持可视化回归测试。它会自动截取 UI 组件或页面的截图,并与基准图像对比,以捕捉那些非预期的视觉变化。
与只验证功能逻辑的功能测试不同,可视化测试能发现样式异常、布局偏移和渲染错误——这些问题如果没有细致的人工检查,往往会被忽略。
为什么需要可视化回归测试?
视觉 bug 不会报错,但它们的外观已经改变。这正是可视化测试的意义所在:
- 按钮依然能提交表单,但颜色却变成了亮粉色
- 文本在桌面端显示正常,在移动端却被挤压变形
- 功能没问题,可两个容器已跑出视口
- 精心的 CSS 重构完成了,却破坏了某个无人测试的页面布局
可视化回归测试是 UI 的安全网,确保这些变化在进入生产环境之前就被自动发现并处理。
快速入门
浏览器渲染差异
可视化回归测试对运行环境非常敏感,不同机器生成的截图可能存在差异,常见原因包括:
- 字体渲染差异(最常见,Windows、macOS、Linux 各不相同)
- GPU 驱动与硬件加速
- 是否使用无头模式
- 浏览器版本与设置
- ……甚至偶发的系统差异
因此,Vitest 会在截图文件名中添加浏览器和平台信息(如 button-chromium-darwin.png
),避免不同环境的截图互相覆盖。
要获得稳定结果,应使用相同的测试环境。推荐采用云端服务(如 Microsoft Playwright Testing)或基于 Docker containers 的环境。
在 Vitest 中,可通过 toMatchScreenshot
assertion 断言运行可视化回归测试:
import { page } from '@vitest/browser/context'
import { expect, test } from 'vitest'
test('hero section looks correct', async () => {
// ...the rest of the test
// capture and compare screenshot
await expect(page.getByTestId('hero')).toMatchScreenshot('hero-section')
})
创建基准截图
首次运行可视化测试时, Vitest 会生成一张基准( baseline )截图,并提示如下错误信息使测试失败:
expect(element).toMatchScreenshot()
No existing reference screenshot found; a new one was created. Review it before running tests again.
Reference screenshot:
tests/__screenshots__/hero.test.ts/hero-section-chromium-darwin.png
确认截图正确后再次运行测试,Vitest 会将后续结果与该基准图比较。
TIP
基准截图存放在测试文件所在目录下的 __screenshots__
文件夹中, 请务必提交到版本库。
截图组织方式
Vitest 默认将截图按以下结构保存:
.
├── __screenshots__
│ └── test-file.test.ts
│ ├── test-name-chromium-darwin.png
│ ├── test-name-firefox-linux.png
│ └── test-name-webkit-win32.png
└── test-file.test.ts
文件名由三部分组成:
- 测试名:来自
toMatchScreenshot()
的第一个参数,或自动根据测试用例名生成 - 浏览器名:
chrome
、chromium
、firefox
、webkit
- 平台:如
aix
、darwin
、linux
、win32
等
这种命名方式可避免不同环境生成的截图互相覆盖。
更新基准截图
当你有意修改 UI 时,需要更新基准截图:
$ vitest --update
提交前务必核对更新后的截图,确保改动符合预期。
配置可视化测试
全局配置
可在 Vitest 配置文件 中设定可视化回归测试的默认规则:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
expect: {
toMatchScreenshot: {
comparatorName: 'pixelmatch',
comparatorOptions: {
// 0-1, how different can colors be?
threshold: 0.2,
// 1% of pixels can differ
allowedMismatchedPixelRatio: 0.01,
},
},
},
},
},
})
单测试配置
若某个测试需要不同的比较标准,可在调用时覆盖全局设置:
await expect(element).toMatchScreenshot('button-hover', {
comparatorName: 'pixelmatch',
comparatorOptions: {
// more lax comparison for text-heavy elements
allowedMismatchedPixelRatio: 0.1,
},
})
Best Practices
聚焦测试目标元素
除非确实需要测试整个页面,否则应优先只对目标组件截图,这能显著减少因页面其他部分变化而造成的误报。
// ❌ Captures entire page; prone to unrelated changes
await expect(page).toMatchScreenshot()
// ✅ Captures only the component under test
await expect(page.getByTestId('product-card')).toMatchScreenshot()
处理动态内容
测试中,如果页面包含诸如时间戳、用户信息或随机值等动态内容,往往会导致结果不一致而造成测试失败。 解决方法有两种:一是模拟这些动态数据的生成源; 二是在使用 Playwright 进行截图时,在 screenshotOptions
中启用 mask
选项, 将这些动态区域遮盖,从而确保测试结果的稳定性。
await expect(page.getByTestId('profile')).toMatchScreenshot({
screenshotOptions: {
mask: [page.getByTestId('last-seen')],
},
})
禁用所有动画
动画效果往往会导致测试结果出现波动。为避免这种情况, 可以在测试执行过程中注入一段自定义的 CSS 样式代码,用于禁用所有动画,从而提升测试的稳定性。
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
TIP
在使用 Playwright 作为测试工具时,若执行断言操作,动画会被自动禁用。 具体而言,screenshotOptions
配置中的 animations
选项会默认设为 "disabled"
,从而确保截图与测试结果的稳定一致。
设置合理的阈值
在视觉回归测试中,阈值调整是一项需要权衡的工作——它取决于页面内容、测试环境、 应用所能容忍的差异范围,且可能因具体测试而有所不同。
Vitest 并未为像素差异设定默认阈值,这需要由用户根据实际需求来决定。 官方建议使用 allowedMismatchedPixelRatio
,让阈值按截图的整体尺寸比例计算,而非依赖固定像素数量。
当 allowedMismatchedPixelRatio
与 allowedMismatchedPixels
同时设置时, Vitest 会优先采用二者中限制更严格的那一个,以确保测试结果的准确性与一致性。
保持统一的视口大小
浏览器实例的默认窗口尺寸可能存在差异,这会影响视觉回归测试的稳定性。为避免由于尺寸不一致而产生的截图偏差, 建议在测试脚本或浏览器实例配置中显式指定一个固定的视口大小,从而确保测试结果的可重复性与一致性。
await page.viewport(1280, 720)
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright',
instances: [
{
browser: 'chromium',
viewport: { width: 1280, height: 720 },
},
],
},
},
})
使用 Git LFS 管理基准截图
对于规模较大的视觉回归测试套件,建议将基准截图文件存储在 Git LFS 中。 这样既能避免仓库体积膨胀,又能高效管理和传输这些大尺寸文件,提升团队协作效率。
调试视觉测试失败
当视觉回归测试未能通过时, Vitest 会生成三张关键截图,帮助你分析问题所在:
- 参考截图( Reference screenshot ):测试期望的基准图像
- 实际截图( Actual screenshot ):测试运行过程中截取的画面
- 差异图( Diff image ):用高亮标记出参考图与实际图的差异(有时可能不会生成)
在调试时,你会在输出中看到类似如下的文件列表或路径信息:
expect(element).toMatchScreenshot()
Screenshot does not match the stored reference.
245 pixels (ratio 0.03) differ.
Reference screenshot:
tests/__screenshots__/button.test.ts/button-chromium-darwin.png
Actual screenshot:
tests/.vitest-attachments/button.test.ts/button-chromium-darwin-actual.png
Diff image:
tests/.vitest-attachments/button.test.ts/button-chromium-darwin-diff.png
如何解读差异图
- 红色像素:表示参考截图与实际截图之间存在显著差异的区域
- 黄色像素:由抗锯齿处理带来的细微差异(仅在未忽略抗锯齿时可见)
- 透明或原始图像部分:表示两张截图在该区域完全一致
TIP
如果差异图几乎被红色覆盖,说明测试结果与预期严重不符,需要重点排查。 若只是文字边缘零星出现少量红点,可能只是渲染细节差异,此时适当提高阈值即可解决。
常见问题与解决方案
字体渲染引发的误报
由于不同操作系统在字体可用性与渲染方式上差异明显,视觉回归测试中可能会出现“误报”现象。为降低这种风险,可以考虑以下做法:
使用 Web 字体,并在测试执行前等待字体完全加载;
ts// wait for fonts to load await document.fonts.ready // continue with your tests
对包含大量文字的区域适当提高像素差异的比较阈值,以减少因字体渲染细微差别导致的误报;
tsawait expect(page.getByTestId('article-summary')).toMatchScreenshot({ comparatorName: 'pixelmatch', comparatorOptions: { // 10% of the pixels are allowed to change allowedMismatchedPixelRatio: 0.1, }, })
使用云端服务或容器化测试环境,确保字体渲染效果在各次测试中保持一致,从而减少系统差异带来的影响;
测试不稳定或截图尺寸不一致
如果测试结果出现随机通过或失败,或者在不同运行中生成的截图尺寸不一致,可以采取以下措施:
- 确保页面所有内容均已加载完成,包括加载指示器与动画;
- 明确设置固定的视口大小,例如:
await page.viewport(1920, 1080)
; - 检查页面在视口临界尺寸下的响应式布局表现;
- 排查是否存在非预期的动画或过渡效果干扰截图结果;
- 对体积较大的截图适当延长测试的超时时间;
- 使用云端服务或容器化环境,确保字体渲染、浏览器配置等保持一致。
团队版视觉回归测试方案
视觉回归测试对环境的稳定性要求极高,而本地开发机并不适合担当这一角色。
在团队协作中,常见的三种方案是:
- 自托管运行器:部署过程复杂,日常维护工作量大;
- GitHub Actions:对开源项目免费,可与任何测试框架或服务集成;
- 云服务:如 Microsoft Playwright Testing,专为解决视觉测试环境一致性问题而构建。
我们将重点介绍第 2 和第 3 种方案,因为它们能最快投入使用。
主要权衡点在于:
- GitHub Actions:视觉测试只能在持续集成(CI)环境中运行,开发者无法直接在本地执行;
- Microsoft 云服务:可在任意环境运行,但需额外付费,并且仅支持 Playwright。
要点在于,将视觉回归测试与常规测试分离运行。 否则,你可能会因截图差异引发的失败日志而浪费数小时进行排查。
测试组织建议
首先,应将视觉回归测试与其他测试隔离管理。 建议单独建立一个 visual
文件夹(或根据项目结构选择更合适的目录名称)来存放这些测试用例,以便维护与执行。
{
"scripts": {
"test:unit": "vitest --exclude tests/visual/*.test.ts",
"test:visual": "vitest tests/visual/*.test.ts"
}
}
这样,开发者就能在本地运行 npm run test:unit
,而无需受到视觉回归测试的影响; 视觉测试则放在环境一致的持续集成( CI )平台中运行,以确保结果稳定可靠。
持续集成( CI )环境配置
在 CI 环境中运行视觉回归测试时,需要确保浏览器已正确安装。至于如何安装,则取决于你所使用的 CI 服务提供商及其运行环境。
Playwright 能让浏览器安装与管理变得非常简单。 你只需固定所用的 Playwright 版本,并在运行测试之前加入以下命令或脚本:
# ...the rest of the workflow
- name: Install Playwright Browsers
run: npx --no playwright install --with-deps --only-shell
最后,运行你的视觉回归测试:
# ...the rest of the workflow
# ...browser setup
- name: Visual Regression Testing
run: npm run test:visual
更新工作流程
关键点来了——切勿在每一次 Pull Request 中都自动更新截图, (那只会带来混乱)。更稳妥的方式,是建立一个手动触发的工作流程, 让开发者在有意更改 UI 时主动运行,从而更新基准截图。
该工作流程具备以下特性:
- 仅在功能分支上运行,确保主分支安全不受影响;
- 自动将触发流程的开发者署名为共同作者;
- 阻止同一分支上的并发执行,避免冲突与资源浪费;
- 生成一份清晰美观的执行摘要,便于快速查看结果。
当基准截图发生变动时,系统会列出所有具体的变化项,方便开发者快速了解差异。
当没有任何变化时,系统同样会明确提示,让你一目了然。
TIP
这只是实现的其中一种方式。 有些团队倾向于在 Pull Request 中添加特定评论(如 /update-screenshots
)来触发更新, 也有团队通过添加标签来完成这一操作。 你可以根据自身的开发流程进行调整。
关键在于,必须建立一种可控的机制来更新基准截图, 以避免不必要的混乱和错误。
name: Update Visual Regression Screenshots
on:
workflow_dispatch: # manual trigger only
env:
AUTHOR_NAME: 'github-actions[bot]'
AUTHOR_EMAIL: '41898282+github-actions[bot]@users.noreply.github.com'
COMMIT_MESSAGE: |
test: update visual regression screenshots
Co-authored-by: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
jobs:
update-screenshots:
runs-on: ubuntu-24.04
# safety first: don't run on main
if: github.ref_name != github.event.repository.default_branch
# one at a time per branch
concurrency:
group: visual-regression-screenshots@${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: write # needs to push changes
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
# use PAT if triggering other workflows
# token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git
run: |
git config --global user.name "${{ env.AUTHOR_NAME }}"
git config --global user.email "${{ env.AUTHOR_EMAIL }}"
# your setup steps here (node, pnpm, whatever)
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx --no playwright install --with-deps --only-shell
# the magic happens below 🪄
- name: Update Visual Regression Screenshots
run: npm run test:visual --update
# check what changed
- name: Check for changes
id: check_changes
run: |
CHANGED_FILES=$(git status --porcelain | awk '{print $2}')
if [ "${CHANGED_FILES:+x}" ]; then
echo "changes=true" >> $GITHUB_OUTPUT
echo "Changes detected"
# save the list for the summary
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "changed_count=$(echo "$CHANGED_FILES" | wc -l)" >> $GITHUB_OUTPUT
else
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes detected"
fi
# commit if there are changes
- name: Commit changes
if: steps.check_changes.outputs.changes == 'true'
run: |
git add -A
git commit -m "${{ env.COMMIT_MESSAGE }}"
- name: Push changes
if: steps.check_changes.outputs.changes == 'true'
run: git push origin ${{ github.ref_name }}
# pretty summary for humans
- name: Summary
run: |
if [[ "${{ steps.check_changes.outputs.changes }}" == "true" ]]; then
echo "### 📸 Visual Regression Screenshots Updated" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Successfully updated **${{ steps.check_changes.outputs.changed_count }}** screenshot(s) on \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "#### Changed Files:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.check_changes.outputs.changed_files }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ The updated screenshots have been committed and pushed. Your visual regression baseline is now up to date!" >> $GITHUB_STEP_SUMMARY
else
echo "### ℹ️ No Screenshot Updates Required" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The visual regression test command ran successfully but no screenshots needed updating." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All screenshots are already up to date! 🎉" >> $GITHUB_STEP_SUMMARY
fi
该选哪一个?
两种方案都可行,关键在于团队最在意的痛点是什么。
如果你的团队已经深度依赖 GitHub 生态,那么 GitHub Actions 几乎是无可替代的选择——对开源项目免费、 支持任意浏览器服务商、并且可完全掌控执行流程。
缺点在于:当有人在本地生成的截图与 CI 环境的基准不一致时,就会出现那句熟悉的“在我机器上没问题”。
如果团队需要在本地执行视觉回归测试,那么云服务或许更适合。 这种方式特别适合有设计师参与审核,或开发者希望在推送代码前发现并修复问题的团队, 能够跳过“推送—等待—检查—修改—再推送”的繁琐循环。
如果依然犹豫,不妨先从 GitHub Actions 开始;等到本地测试成为痛点时,再引入云服务也不迟。