---
url: /guide/browser.md
---
# 浏览器模式 {#browser-mode}
本页提供有关 Vitest API 中浏览器模式功能的信息,该功能允许你原生地在浏览器中运行测试,从而可以访问诸如 window 和 document 等浏览器全局对象。
::: tip
如果你正在寻找 `expect`、`vi` 或任何通用 API(如测试项目或类型测试)的文档,请参阅 ["入门" 指南](/guide/)。
:::
## 安装
为了更轻松地设置,你可以使用 `vitest init browser` 命令来安装所需的依赖项并创建浏览器配置。
::: code-group
```bash [npm]
npx vitest init browser
```
```bash [yarn]
yarn exec vitest init browser
```
```bash [pnpm]
pnpx vitest init browser
```
```bash [bun]
bunx vitest init browser
```
:::
### 手动安装
你也可以手动安装包。Vitest 始终需要定义一个提供者。你可以选择 [`preview`](/config/browser/preview)、[`playwright`](/config/browser/playwright) 或 [`webdriverio`](/config/browser/webdriverio)。
如果你只想预览测试的样子,可以使用 `preview` 提供者:
::: code-group
```bash [npm]
npm install -D vitest @vitest/browser-preview
```
```bash [yarn]
yarn add -D vitest vite @vitest/browser-preview
```
```bash [pnpm]
pnpm add -D vitest @vitest/browser-preview
```
```bash [bun]
bun add -D vitest @vitest/browser-preview
```
:::
::: warning
但是,要在 CI 中运行测试,你需要安装 [`playwright`](https://npmx.dev/package/playwright) 或 [`webdriverio`](https://npmx.dev/package/webdriverio)。我们也建议切换到其中任何一个来进行本地测试,而不是使用默认的 `preview` 提供者,因为它依赖于模拟事件而不是使用 Chrome DevTools Protocol。
如果你还没有使用这些工具之一,我们建议从 Playwright 开始,因为它支持并行执行,这会使你的测试运行得更快。
::: tabs key:provider
\== Playwright
[Playwright](https://npmx.dev/package/playwright) 是一个用于 Web 测试和自动化的框架。
::: code-group
```bash [npm]
npm install -D vitest @vitest/browser-playwright
```
```bash [yarn]
yarn add -D vitest vite @vitest/browser-playwright
```
```bash [pnpm]
pnpm add -D vitest @vitest/browser-playwright
```
```bash [bun]
bun add -D vitest @vitest/browser-playwright
```
\== WebdriverIO
[WebdriverIO](https://npmx.dev/package/webdriverio) 允许你使用 WebDriver 协议在本地运行测试。
::: code-group
```bash [npm]
npm install -D vitest @vitest/browser-webdriverio
```
```bash [yarn]
yarn add -D vitest vite @vitest/browser-webdriverio
```
```bash [pnpm]
pnpm add -D vitest @vitest/browser-webdriverio
```
```bash [bun]
bun add -D vitest @vitest/browser-webdriverio
```
:::
## 配置
要在 Vitest 配置中激活浏览器模式,请在你的 Vitest 配置文件中将 `browser.enabled` 字段设置为 `true`。以下是使用 browser 字段的配置示例:
```ts [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' },
],
},
}
})
```
::: info
Vitest 会分配端口 `63315`,以避免与开发服务器冲突,从而允许两者并行运行。你可以通过 [`api`](/config/api) 选项更改端口。端口会在首次启动浏览器时绑定;如果启用了 `api` 或 `ui`,则会在启动时绑定。
:::
如果你之前没有使用过 Vite,请确保已安装并在配置中指定了你的框架插件。某些框架可能需要额外配置才能工作 - 请查看它们的 Vite 相关文档以确保无误。
::: code-group
```ts [react]
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [vue]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [svelte]
import { defineConfig } from 'vitest/config'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [svelte()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [solid]
import { defineConfig } from 'vitest/config'
import solidPlugin from 'vite-plugin-solid'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [solidPlugin()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [marko]
import { defineConfig } from 'vitest/config'
import marko from '@marko/vite'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [marko()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [qwik]
import { defineConfig } from 'vitest/config'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { playwright } from '@vitest/browser-playwright'
// 可选,在 SSR 模式下运行测试
import { testSSR } from 'vitest-browser-qwik/ssr-plugin'
export default defineConfig({
plugins: [testSSR(), qwikVite()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }]
},
},
})
```
:::
如果你需要使用基于 Node 的运行器运行某些测试,你可以定义一个 [`projects`](/guide/projects) 选项,为不同的测试策略提供单独的配置:
{#projects-config}
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
projects: [
{
test: {
// 基于文件的约定示例,
// 你不必遵循它
include: [
'tests/unit/**/*.{test,spec}.ts',
'tests/**/*.unit.{test,spec}.ts',
],
name: 'unit',
environment: 'node',
},
},
{
test: {
// 基于文件的约定示例,
// 你不必遵循它
include: [
'tests/browser/**/*.{test,spec}.ts',
'tests/**/*.browser.{test,spec}.ts',
],
name: 'browser',
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
},
},
},
],
},
})
```
## 浏览器选项类型
Vitest 中的浏览器选项取决于提供者。如果你传递 `--browser` 但未在配置文件中指定其名称,Vitest 将失败。可用选项:
* `webdriverio` 支持这些浏览器:
* `firefox`
* `chrome`
* `edge`
* `safari`
* `playwright` 支持这些浏览器:
* `firefox`
* `webkit`
* `chromium`
## 浏览器兼容性
Vitest 使用 [Vite 开发服务器](https://vitejs.dev/guide/#browser-support) 来运行测试,因此我们只支持 [`esbuild.target`](https://vitejs.dev/config/shared-options.html#esbuild) 选项中指定的功能(默认为 `esnext`)。
默认情况下,Vite 面向支持原生 [ES 模块](https://caniuse.com/es6-module)、原生 [ESM 动态导入](https://caniuse.com/es6-module-dynamic-import)和 [`import.meta`](https://caniuse.com/mdn-javascript_operators_import_meta) 的浏览器。此外,我们使用 [`BroadcastChannel`](https://caniuse.com/?search=BroadcastChannel) 在 iframe 之间通信,并使用 `WeakRef` 和 `FinalizationRegistry` 跟踪模拟对象,同时不阻止垃圾回收:
* Chrome >=87
* Firefox >=79
* Safari >=15.4
* Edge >=88
## 运行测试
当你在浏览器选项中指定浏览器名称时,Vitest 将尝试默认使用 `preview` 运行指定的浏览器,然后在那里运行测试。如果你不想使用 `preview`,可以通过使用 `browser.provider` 选项配置自定义浏览器提供者。
要使用 CLI 指定浏览器,请使用 `--browser` 标志后跟浏览器名称,如下所示:
```sh
npx vitest --browser=chromium
```
或者你可以使用点表示法向 CLI 提供浏览器选项:
```sh
npx vitest --browser.headless
```
::: warning
自 Vitest 3.2 以来,如果你配置中没有 `browser` 选项但指定了 `--browser` 标志,Vitest 将失败,因为它无法假设该配置是用于浏览器测试而不是 Node.js 测试。
:::
默认情况下,Vitest 将自动打开浏览器 UI 进行开发。你的测试将在中心的 iframe 内运行。你可以通过选择首选尺寸、在测试中调用 `page.viewport` 或在 [配置](/config/browser/viewport) 中设置默认值来配置视口。
对于另一种调试模型——它会为每个测试捕获 DOM 快照,而不是显示实时 iframe——请参阅 [Trace View](/guide/browser/trace-view)。
## 无头模式
无头模式是浏览器模式中的另一个可用选项。在无头模式下,浏览器在没有用户界面的情况下在后台运行,这使得它对于运行自动化测试非常有用。Vitest 中的 headless 选项可以设置为布尔值以启用或禁用无头模式。
使用无头模式时,Vitest 不会自动打开 UI。如果你想继续使用 UI 但让测试无头运行,你可以安装 [`@vitest/ui`](/guide/ui) 包并在运行 Vitest 时传递 `--ui` 标志。
以下是启用无头模式的配置示例:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
provider: playwright(),
enabled: true,
headless: true,
},
}
})
```
你也可以使用 CLI 中的 `--browser.headless` 标志设置无头模式,如下所示:
```sh
npx vitest --browser.headless
```
在这种情况下,Vitest 将使用 Chrome 浏览器以无头模式运行。
::: warning
默认情况下不提供无头模式。你需要使用 [`playwright`](https://npmx.dev/package/playwright) 或 [`webdriverio`](https://npmx.dev/package/webdriverio) 提供者来启用此功能。
:::
## 示例
默认情况下,使用浏览器模式不需要任何外部包:
```js [example.test.js]
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
import { render } from './my-render-function.js'
test('properly handles form inputs', async () => {
render() // 挂载 DOM 元素
// 断言初始状态。
await expect.element(page.getByText('Hi, my name is Alice')).toBeInTheDocument()
// 通过查询关联的 label 获取 input DOM 节点。
const usernameInput = page.getByLabelText(/username/i)
// 将名称输入到 input 中。这已经验证了 input
// 是否填写正确,无需手动检查值。
await usernameInput.fill('Bob')
await expect.element(page.getByText('Hi, my name is Bob')).toBeInTheDocument()
})
```
然而,Vitest 还提供了开箱即用的包来渲染几个流行框架的组件:
* [`vitest-browser-vue`](https://github.com/vitest-dev/vitest-browser-vue) 用于渲染 [vue](https://vuejs.org) 组件
* [`vitest-browser-svelte`](https://github.com/vitest-dev/vitest-browser-svelte) 用于渲染 [svelte](https://svelte.dev) 组件
* [`vitest-browser-react`](https://github.com/vitest-dev/vitest-browser-react) 用于渲染 [react](https://react.dev) 组件
* [`vitest-browser-angular`](https://github.com/vitest-community/vitest-browser-angular) 用于渲染 [Angular](https://angular.dev) 组件
其他框架也有社区包可用:
* [`vitest-browser-lit`](https://github.com/EskiMojo14/vitest-browser-lit) 用于渲染 [lit](https://lit.dev) 组件
* [`vitest-browser-preact`](https://github.com/JoviDeCroock/vitest-browser-preact) 用于渲染 [preact](https://preactjs.com) 组件
* [`vitest-browser-qwik`](https://github.com/QwikDev/vitest-browser-qwik) 用于渲染 [qwik](https://qwik.dev) 组件
如果你的框架没有代表,随时可以创建自己的包——它只是框架渲染器和 `page.elementLocator` API 的简单封装。我们会在此页面上添加指向它的链接。确保它的名称以 `vitest-browser-` 开头。
除了渲染组件和定位元素,你还需要进行断言。Vitest fork 了 [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) 库,以提供广泛的开箱即用的 DOM 断言。请在 [断言 API](/api/browser/assertions) 阅读更多内容。
```ts
import { expect } from 'vitest'
import { page } from 'vitest/browser'
// 元素渲染正确
await expect.element(page.getByText('Hello World')).toBeInTheDocument()
```
Vitest 暴露了一个 [上下文 API](/api/browser/context),包含一组可能在测试中有用的工具。例如,如果你需要进行交互,比如点击元素或在输入框中输入文本,你可以使用来自 `vitest/browser` 的 `userEvent`。请在 [交互 API](/api/browser/interactivity) 阅读更多内容。
```ts
import { page, userEvent } from 'vitest/browser'
await userEvent.fill(page.getByLabelText(/username/i), 'Alice')
// 或直接使用 locator.fill
await page.getByLabelText(/username/i).fill('Alice')
```
::: code-group
```ts [vue]
import { render } from 'vitest-browser-vue'
import Component from './Component.vue'
test('properly handles v-model', async () => {
const screen = await render(Component)
// 断言初始状态。
await expect.element(screen.getByText('Hi, my name is Alice')).toBeInTheDocument()
// 通过查询关联的 label 获取 input DOM 节点。
const usernameInput = screen.getByLabelText(/username/i)
// 将名称输入到 input 中。这已经验证了 input
// 是否填写正确,无需手动检查值。
await usernameInput.fill('Bob')
await expect.element(screen.getByText('Hi, my name is Bob')).toBeInTheDocument()
})
```
```ts [svelte]
import { render } from 'vitest-browser-svelte'
import { expect, test } from 'vitest'
import Greeter from './greeter.svelte'
test('greeting appears on click', async () => {
const screen = await render(Greeter, { name: 'World' })
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
```tsx [react]
import { render } from 'vitest-browser-react'
import Fetch from './fetch'
test('loads and displays greeting', async () => {
// 将 React 元素渲染到 DOM 中
const screen = render()
await screen.getByText('Load Greeting').click()
// 在找不到元素时抛出错误之前等待
const heading = screen.getByRole('heading')
// 断言警报消息正确
await expect.element(heading).toHaveTextContent('hello there')
await expect.element(screen.getByRole('button')).toBeDisabled()
})
```
```ts [lit]
import { render } from 'vitest-browser-lit'
import { html } from 'lit'
import './greeter-button'
test('greeting appears on click', async () => {
const screen = render(html``)
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
```tsx [preact]
import { render } from 'vitest-browser-preact'
import { createElement } from 'preact'
import Greeting from '.Greeting'
test('greeting appears on click', async () => {
const screen = render()
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
```tsx [qwik]
import { render } from 'vitest-browser-qwik'
import Greeting from './greeting'
test('greeting appears on click', async () => {
// renderSSR 和 renderHook 也可用
const screen = render()
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
:::
Vitest 并不开箱即用支持所有框架,但你可以使用外部工具来运行这些框架的测试。我们也鼓励社区创建他们自己的 `vitest-browser` 封装——如果你有一个,随时可以将其添加到上面的示例中。
对于不支持的框架,我们推荐使用 `testing-library` 包:
* [`@solidjs/testing-library`](https://testing-library.com/docs/solid-testing-library/intro) 用于渲染 [solid](https://www.solidjs.com) 组件
* [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) 用于渲染 [marko](https://markojs.com) 组件
你也可以在 [`browser-examples`](https://github.com/vitest-tests/browser-examples) 仓库中看到更多示例。
::: warning
`testing-library` 提供了一个包 `@testing-library/user-event`。我们不推荐直接使用它,因为它模拟事件而不是实际触发它们——相反,请使用从 `vitest/browser` 导入的 [`userEvent`](/api/browser/interactivity),它在底层使用 Chrome DevTools Protocol 或 Webdriver(取决于提供商)。
:::
::: code-group
```tsx [solid]
// 基于 @testing-library/solid API
// https://testing-library.com/docs/solid-testing-library/api
import { render } from '@testing-library/solid'
it('uses params', async () => {
const App = () => (
<>
(
Id:
{useParams()?.id}
)}
/>
Start
} />
>
)
const { baseElement } = render(() => , { location: 'ids/1234' })
const screen = page.elementLocator(baseElement)
await expect.screen(screen.getByText('Id: 1234')).toBeInTheDocument()
})
```
```ts [marko]
// 基于 @testing-library/marko API
// https://testing-library.com/docs/marko-testing-library/api
import { render, screen } from '@marko/testing-library'
import Greeting from './greeting.marko'
test('renders a message', async () => {
const { baseElement } = await render(Greeting, { name: 'Marko' })
const screen = page.elementLocator(baseElement)
await expect.element(screen.getByText(/Marko/)).toBeInTheDocument()
expect(container.firstChild).toMatchInlineSnapshot(`
Hello, Marko!
`)
})
```
:::
## 限制
### 线程阻塞对话框
使用 Vitest Browser 时,需要注意的是,像 `alert`、`confirm` 或 `print` 这样的线程阻塞对话框无法原生使用。这是因为它们会阻塞网页,导致 Vitest 无法继续与网页通信,从而使执行挂起。
在这种情况下,Vitest 为这些 API 提供了带有默认返回值的默认模拟。这确保了即使用户不小心使用了同步弹出网页 API,执行也不会挂起。然而,仍然建议用户模拟这些网页 API 以获得更好的体验。请在 [模拟](/guide/mocking) 阅读更多内容。
### 监听模块导出
浏览器模式使用浏览器的原生 ESM 支持来提供模块。模块命名空间对象是密封的且无法重新配置,这与 Node.js 测试不同,在 Node.js 测试中 Vitest 可以修补 Module Runner。这意味着你不能在导入的对象上调用 `vi.spyOn`:
```ts
import { vi } from 'vitest'
import * as module from './module.js'
vi.spyOn(module, 'method') // ❌ 抛出错误
```
为了绕过这个限制,Vitest 在 `vi.mock('./module.js')` 中支持 `{ spy: true }` 选项。这将自动监听模块中的每个导出,而不会用假的实现替换它们。
```ts
import { vi } from 'vitest'
import * as module from './module.js'
vi.mock('./module.js', { spy: true })
vi.mocked(module.method).mockImplementation(() => {
// ...
})
```
然而,模拟导出\_变量\_的唯一方法是导出一个方法来改变内部值:
::: code-group
```js [module.js]
export let MODE = 'test'
export function changeMode(newMode) {
MODE = newMode
}
```
```js [module.test.ts]
import { expect } from 'vitest'
import { changeMode, MODE } from './module.js'
changeMode('production')
expect(MODE).toBe('production')
```
:::