Vitest 组件测试实战:Browser Mode 与 Playwright 集成
说实话,我第一次测试 Canvas 组件时踩了个大坑。
那天凌晨两点,盯着测试报告上的绿色”PASS”,我信心满满提交了代码。第二天,同事在真实浏览器里一打开页面——Canvas 根本没渲染出来。测试明明通过了啊?!
后来才明白:jsdom 只是个”假”浏览器,它模拟了 DOM API,但 Canvas 的真实渲染行为、CSS 计算样式、Web Components 的生命周期——这些它都测不了。我用了半年的单元测试配置,原来只测了个皮毛。
这就是为什么 Vitest 在 3.0 版本推出了 Browser Mode——直接在真实浏览器里跑测试。不像 jsdom 在 Node.js 里模拟 DOM,Browser Mode 启动 Chromium/Firefox/Safari,把你的组件真实渲染出来,再用 Playwright 的 API 去交互。测试通过?那就真通过了。
这篇文章,我会带你从零配置 Browser Mode,到 React/Vue 组件实战,再到 CI 环境覆盖率门禁。这是 Vitest 测试指南系列的第三篇,前面两篇讲了单元测试配置和 TDD 流程,这次我们补上组件测试这块拼图。
为什么需要 Browser Mode?
你可能想问:jsdom 不够用吗?
说实话,对于纯逻辑组件——比如一个计算器、一个表单验证器——jsdom 完全够了。它是 Node.js 环境里的 DOM 模拟器,速度飞快,配置简单。我前两篇文章讲的单元测试,全是在 jsdom 环境里跑的,测响应式数据、测事件触发,没问题。
但碰到这些场景,jsdom 就怂了:
- Canvas 绘图:jsdom 有 Canvas API,但不会真的画出来。你测
ctx.fillRect()调用了几次,没问题;测画出来的图形对不对?测不了。 - CSS 计算样式:
getComputedStyle()在 jsdom 里返回的是空对象。真实浏览器里,元素宽度受父容器、padding、border 影响——jsdom 根本算不出来。 - Web Components:自定义元素的
connectedCallback、disconnectedCallback,jsdom 有模拟,但生命周期触发时机和真实浏览器不一样。 - 异步渲染:动画帧、requestIdleCallback、IntersectionObserver——这些 API jsdom 要么没实现,要么实现得不完整。
举个我踩过的坑。去年做项目,有个组件用 CSS 动画控制按钮展开/收起。jsdom 测试里,transitionend 事件永远不会触发——因为根本没有真正的 transition。测试写成了 mock 事件,结果真实浏览器里动画时长改了,测试还”通过”,但组件已经崩了。
Browser Mode 就是来解决这些问题的。它在真实浏览器里渲染组件,你写测试代码,它启动 Chromium(或 Firefox/Safari),把组件挂载到页面,然后用 Playwright 的 API 去点击、去输入、去等待。真实浏览器里发生什么,测试就测什么。
有个数据值得说下:Vitest 3.0 的 Browser Mode 共享 Chromium 上下文,测试启动只开一次浏览器,所有测试共享同一个实例。官方说比传统 Playwright E2E 快 30%。你测 50 个组件,不用等浏览器反复启动关闭。
还有一点,测试金字塔在组件测试这块有争议。传统的金字塔:单元测试占大头,E2E 最少。但 Vue 官方博客有个反金字塔观点——alexop.dev 的测试金字塔反转:集成测试 70%,单元测试 20%,E2E 10%。理由是:组件本身就是集成单位,它组合了模板、样式、逻辑,用 jsdom 单测只能测逻辑部分,集成测试才能验证整体行为。Browser Mode 算是填补了这块空白——比 jsdom 更真实,比 Playwright E2E 更轻量。
Browser Mode 配置实战
配置 Browser Mode,其实没那么复杂。但也有几个坑我踩过,提前说下。
安装依赖
首先安装 Vitest 和 Browser Mode 提供器。官方推荐用 Playwright:
npm install -D vitest @vitest/browser-playwright
Playwright 会顺便安装 Chromium、Firefox、WebKit 三个浏览器。如果你只想测 Chromium(大多数情况够用),可以在安装时指定:
npx playwright install chromium
这一步有点慢,Chromium 包大概 170MB。等你下载完,基本就准备好了。
vitest.config.ts 配置
配置文件很简单,但有个细节要注意:
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
provider: playwright(),
enabled: true,
instances: [{ browser: 'chromium' }],
},
},
})
这里 instances 配置,决定了你在哪个浏览器里跑测试。如果你想测多浏览器兼容性,可以加 Firefox 和 WebKit:
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' }, // Safari
]
但我一般只开 Chromium——多浏览器测试慢,而且大多数前端 bug 在 Chromium 里测出来就够了。Safari 的兼容问题,我靠 Playwright E2E 去覆盖关键路径。
Headless 模式 vs UI 模式
这个选择有点微妙。
- Headless 模式:浏览器不打开窗口,后台跑测试。适合 CI 环境,速度快,但你看不到组件渲染过程。
- UI 模式:浏览器打开窗口,你能看到组件被渲染、被点击、被输入。适合开发阶段调试,写测试时很有用。
我开发时用 UI 模式,跑命令:
npx vitest --browser.ui
Vitest 会打开一个测试工作台,左边是测试列表,右边是浏览器窗口。你点击测试文件,浏览器里就渲染出组件,测试代码执行的过程你能看到。有个按钮点击没响应?直接在浏览器里调试。
CI 环境用 Headless 模式,配置加一行:
browser: {
provider: playwright(),
enabled: true,
headless: true, // CI 强制 headless
instances: [{ browser: 'chromium' }],
}
测试文件命名约定
官方建议用 .browser.test.ts 命名 Browser Mode 测试文件,和普通的 .test.ts 区分开。我试了下,好处是:
- 你可以同时跑 jsdom 单元测试和 Browser Mode 组件测试,不会混在一起。
- CI 莫名失败时,看文件名就知道是浏览器测试,排查方向明确。
但 Vitest 不强制这个命名,你用 .test.ts 也行。关键是配置里要指定包含 Browser Mode 测试的目录,或者直接把所有测试都跑在 Browser Mode 里。我习惯分开,单元测试用 jsdom,组件测试用 Browser Mode。
React/Vue 组件测试实战
这块是 Browser Mode 的核心用法。说实话,和传统 Testing Library 写法有点不一样,但熟悉后挺顺手。
React 组件测试
先安装 React 适配器:
npm install -D @vitest/browser-react
然后写测试。假设有个 Counter 组件,点击按钮计数加 1:
// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter'
test('按钮点击计数', async () => {
// 渲染组件到浏览器
await page.mount(<Counter />)
// 找到按钮元素
const button = page.getByRole('button', { name: 'Count: 0' })
// 点击按钮
await userEvent.click(button)
// 验证文本变化
await expect.element(button).toHaveTextContent('Count: 1')
})
和 Testing Library 的写法对比一下:Testing Library 用 render(),Browser Mode 用 page.mount()。Testing Library 用 screen.getByRole(),Browser Mode 用 page.getByRole()。API 很像,只是 page 对象是 Vitest Browser Mode 的上下文。
有个细节:await expect.element(button)。这是 Vitest 的 Web Testing API,它会自动等待元素状态变化。不用你手动 await waitFor(),测试框架帮你处理了异步等待。这点比 Testing Library 简洁。
Vue 组件测试
Vue 的写法也类似,但需要 Vue 适配器:
npm install -D @vitest/browser-vue
测试 Vue Counter 组件:
// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter.vue'
test('按钮点击计数', async () => {
// 渲染 Vue 组件
await page.mount(Counter)
// 找按钮,点击,验证
const button = page.getByRole('button', { name: 'Count: 0' })
await userEvent.click(button)
await expect.element(button).toHaveTextContent('Count: 1')
})
Vue 2 用 @vue/test-utils 的写法,Browser Mode 不直接支持。但 Vue 3 项目,用 @vitest/browser-vue 就够了。
一个实际案例
我项目里有这个场景:一个拖拽排序组件,用 HTML5 Drag & Drop API 实现。jsdom 环境里,dragstart、drop 事件模拟不了——得用 mock,测出来的只是”事件被触发”,但排序逻辑是否正确?不知道。
换成 Browser Mode,测试直接写:
test('拖拽排序', async () => {
await page.mount(<SortableList items={['A', 'B', 'C']} />)
const itemA = page.getByText('A')
const itemC = page.getByText('C')
// 拖拽 A 到 C 后面
await userEvent.dragTo(itemA, itemC)
// 验证顺序变化
const items = page.getByRole('listitem')
await expect.element(items.nth(2)).toHaveTextContent('A')
})
真实浏览器里,Drag & Drop API 真的触发,排序逻辑真的执行,元素顺序真的变化。测试通过了?那就真的没问题。
这就是 Browser Mode 的价值——它测的是真实行为,不是模拟行为。
Playwright vs Browser Mode 选择指南
你可能有点困惑:Browser Mode 和 Playwright,不都是浏览器测试吗?区别在哪?
简单说:Browser Mode 测组件,Playwright 测流程。
核心区别
| 特性 | Browser Mode | Playwright |
|---|---|---|
| 测试范围 | 单组件隔离测试 | 多页面流程测试 |
| 执行速度 | 200ms 左右/测试 | 2-5s/测试 |
| 启动成本 | 共享浏览器实例 | 每个测试独立启动 |
| 配置复杂度 | 低,集成在 Vitest | 高,独立项目配置 |
| 适用场景 | 开发阶段快速迭代 | 发布前关键路径验证 |
Browser Mode 的定位是组件测试——它把组件挂载到浏览器,但只测这个组件本身的行为。Playwright 的定位是 E2E——它打开你的完整应用,导航、登录、提交表单,测的是整个流程。
打个比方:Browser Mode 像是外科医生用显微镜看单个细胞,Playwright 像是体检医生看整个器官系统。各有用途,互不替代。
组合策略
我实际项目里是这样用的:
- Browser Mode:测所有 UI 组件——按钮、表单、卡片、弹窗。开发时写,随代码提交。测试快,反馈即时。
- Playwright E2E:测 3-5 个关键路径——登录后看首页、搜索后看结果、提交后看订单。只在发布前跑,或者在 CI 每日构建时跑。
这样组合的好处:
- Browser Mode 覆盖大部分 UI bug,开发阶段就发现。
- Playwright 覆盖跨页面集成 bug,发布前验证。
- 维护成本可控——组件测试 50 个,E2E 测试 5 个,不会写太多测试拖慢 CI。
什么时候选哪个?
简单判断:
- 选 Browser Mode:你要测单个组件的 UI 行为——点击、输入、渲染、样式。组件代码改了,测试要即时反馈。
- 选 Playwright:你要测多页面流程——登录 -> 导航 -> 操作 -> 验证。或者测跨系统集成——前端 + 后端 API + 数据库。
举个例子:测一个日期选择器组件,日期范围限制、禁用日期、格式显示——用 Browser Mode。测用户在预订页面选日期、提交订单、跳转到支付页面——用 Playwright。
还有一点:Browser Mode 只测前端,Playwright 可以测全栈。如果你的应用有后端 API,Playwright E2E 能测前端 + 后端联调。Browser Mode 只测前端组件,后端得 mock。
CI 环境覆盖率门禁
覆盖率配置是 CI 环境的最后一环。说实话,这块我之前也没重视,直到某次代码重构,测试覆盖率从 80% 跌到 60%,线上出了 bug,才发现覆盖率门禁的重要性。
覆盖率配置
在 vitest.config.ts 加覆盖率阈值:
test: {
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80
}
}
}
阈值设多少合适?我的经验:
- 新项目:从 50% 开始,慢慢提升。一开始设太高,写代码压力大。
- 成熟项目:80% 是个合理值。核心模块可以更高,90% 或 95%。
- 不要追求 100%:边界分支、异常处理,有些场景测不到,强行写测试反而浪费时间。
运行覆盖率测试:
npx vitest run --coverage
覆盖率低于阈值?Vitest 会报错,CI 构建失败。这就是门禁——低于阈值的代码,不能合并到主干。
GitHub Actions 集成
CI 工作流里加两步:跑测试 + 报告覆盖率。
# .github/workflows/test.yml
name: Test
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install chromium --with-deps
- name: Run tests with coverage
run: npx vitest run --coverage
- name: Report coverage
uses: davelosert/vitest-coverage-report-action@v2
with:
json-summary-path: './coverage/coverage-summary.json'
这个 Action 会自动在 PR 评论里显示覆盖率变化:
- 覆盖率变化百分比(比如从 80% 到 79%,显示 -1%)
- 哪些文件覆盖率下降
- 哪些新增代码未覆盖
PR 作者一眼就能看到:“哦,我新写的代码覆盖率不够,得补测试。“
CI 注意事项
Browser Mode 在 CI 环境有几个坑:
- Playwright 浏览器安装:加
--with-deps参数,不然 Chromium 启动不了。 - Headless 模式:配置里强制
headless: true,CI 环境没显示器。 - 超时设置:Browser Mode 测试比 jsdom 慢,CI 超时阈值调大点,我设 30 秒。
- 并行控制:Browser Mode 共享浏览器实例,并行测试数别设太高,我限制
maxWorkers: 4。
我 CI 工作流完整配置:
- name: Run browser tests
run: npx vitest run --coverage --browser.headless
env:
CI: true
--browser.headless 确保浏览器不打开窗口,CI: true 环境变量 Vitest 会自动调整一些行为(比如禁用颜色输出)。
总结
说了这么多,核心其实很简单:jsdom 测不了真实浏览器行为,Browser Mode 才能测。Canvas、CSS 计算样式、Web Components、拖拽、动画——这些场景,Browser Mode 是唯一选择。
配置不难,安装 @vitest/browser-playwright,改几行 vitest.config.ts,就能跑起来。React 和 Vue 组件测试,API 和 Testing Library 差不多,学起来没门槛。
但 Browser Mode 不是万能药。它测单组件,Playwright 测流程。组合起来用:开发阶段用 Browser Mode 覆盖所有 UI 组件,发布前用 Playwright E2E 验证关键路径。这样测试覆盖全面,CI 跑得也快。
覆盖率门禁是最后一环。设个阈值,低于阈值的 PR 不能合并,防止覆盖率慢慢下滑。GitHub Actions 里加个 coverage-report-action,PR 评论里自动显示覆盖率变化,一眼就能看出问题。
如果你还没试过 Browser Mode,建议从简单组件开始。比如一个按钮组件、一个输入框组件,先配置 Browser Mode,验证配置正确,再逐步扩展到复杂组件。踩坑是肯定的,但坑踩完了,测试写顺手了,开发效率真的会提升。
配置 Vitest Browser Mode 进行组件测试
从零开始配置 Browser Mode,运行 React/Vue 组件测试,集成 CI 覆盖率门禁
⏱️ 预计耗时: 20 分钟
- 1
步骤1: 安装 Playwright 提供器
运行 npm install -D vitest @vitest/browser-playwright 安装依赖,执行 npx playwright install chromium 下载浏览器。 - 2
步骤2: 配置 vitest.config.ts
在 test.browser 中设置 provider: playwright()、enabled: true、instances: [{ browser: 'chromium' }],CI 环境加 headless: true。 - 3
步骤3: 编写组件测试
使用 page.mount() 渲染组件,page.getByRole() 查询元素,userEvent.click() 模拟交互,expect.element() 断言结果。 - 4
步骤4: 配置覆盖率门禁
在 vitest.config.ts 的 coverage.thresholds 设置阈值(lines: 80),GitHub Actions 集成 vitest-coverage-report-action 显示 PR 覆盖率变化。
常见问题
Browser Mode 和 jsdom 有什么区别?
Browser Mode 和 Playwright E2E 如何选择?
覆盖率阈值设多少合适?
Browser Mode 支持 Vue 2 吗?
CI 环境 Browser Mode 有什么注意事项?
11 分钟阅读 · 发布于: 2026年5月17日 · 修改于: 2026年5月17日
Vitest 测试指南
你正在阅读这个系列的开篇,读完后可以直接继续下一篇,或者先进入系列页查看完整目录。
上一篇
你正在读系列开篇。
下一篇
这已经是系列当前最后一篇。
相关文章
GitHub Actions 安全实践:从 tj-actions 事件学到的 3 个关键防护
GitHub Actions 安全实践:从 tj-actions 事件学到的 3 个关键防护
Nginx 性能调优实战:gzip、缓存与连接池配置
Nginx 性能调优实战:gzip、缓存与连接池配置
Docker 网络模式选择实战:bridge、host 与 overlay 的决策指南
评论
使用 GitHub 账号登录后即可评论