> For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt.

# E2E 测试


[Added in v0.11.1](https://github.com/web-infra-dev/rstest/releases/tag/v0.11.1)

E2E 测试（端到端测试）从用户的角度验证一个完整的页面或应用：打开页面、点击、输入，然后检查页面上真实呈现的结果。Rstest 通过 [@rstest/playwright](https://github.com/web-infra-dev/rstest/tree/main/packages/playwright) 集成 [Playwright](https://playwright.dev/) fixtures 和 Playwright 风格的断言。

测试代码运行在 Node.js worker 中，由 Playwright 驱动真实浏览器访问本地 dev server、preview server 或线上 URL，并和项目里的其他 Rstest 测试共用同一套 runner、配置和报告。如果要测的是单个组件而不是整个应用，请看 [浏览器模式](/zh/guide/browser-testing.md)。

## 安装

安装两个包：

- [@rstest/playwright](https://www.npmjs.com/package/@rstest/playwright) 提供 Rstest fixtures 和断言。
- [playwright](https://www.npmjs.com/package/playwright) 提供浏览器自动化运行时。


```sh [npm]
npm add @rstest/playwright playwright -D
```

```sh [yarn]
yarn add @rstest/playwright playwright -D
```

```sh [pnpm]
pnpm add @rstest/playwright playwright -D
```

```sh [bun]
bun add @rstest/playwright playwright -D
```

```sh [deno]
deno add npm:@rstest/playwright npm:playwright -D
```

安装 Playwright 使用的 Chromium 浏览器：

```bash
pnpm exec playwright install chromium
```

## 基本用法

推荐从 `@rstest/playwright` 导入 `test` 和 `expect`，而不是从 `@rstest/core` 导入，这样可以获得 `toHaveTitle`、`toHaveText` 等 Playwright 专用断言能力：

```ts
import { expect, test } from '@rstest/playwright';

test('page title', async ({ page }) => {
  await page.goto('https://example.com');

  await expect(page).toHaveTitle(/Example/);
  await expect(page.locator('h1')).toHaveText('Example Domain');
});
```

也可以从 `@rstest/playwright` 直接导入 `describe`、`beforeEach` 等生命周期方法：

```ts
import { beforeEach, describe, test } from '@rstest/playwright';

beforeEach(() => {
  // 准备每个测试的状态。
});

describe('checkout', () => {
  test('opens the checkout page', async ({ page }) => {
    await page.goto('http://localhost:3000/checkout');
  });
});
```

如果测试模块在 Node 侧没有需要隔离的副作用，可以在 `rstest.config.ts` 中设置 `isolate: false`，从而在不同测试文件间复用 worker 的模块缓存，减少重复启动 Playwright 的开销：

```ts title="rstest.config.ts"
import { defineConfig } from '@rstest/core';

export default defineConfig({
  isolate: false,
  testEnvironment: 'node',
});
```

## 配置 Playwright 选项


[Added in v0.12.0](https://github.com/web-infra-dev/rstest/releases/tag/v0.12.0)

可以通过 `definePlaywrightConfig` 在 `rstest.config.ts` 中设置 Playwright 默认选项：

```ts title="rstest.config.ts"
import { defineConfig } from '@rstest/core';
import { definePlaywrightConfig } from '@rstest/playwright/config';

export default defineConfig({
  extends: definePlaywrightConfig({
    contextOptions: {
      viewport: { width: 1440, height: 900 },
    },
  }),
});
```

`definePlaywrightConfig` 会为当前 project 配置默认的 `playwright` fixture。如果使用多 project 配置，请将它添加到每个使用 `@rstest/playwright` 的 Node.js project 的 `extends` 中。配置值必须支持 JSON 序列化，不支持 `launchOptions.logger` 等函数、类实例、客户端证书中直接传入的 `Buffer`，以及依赖当前测试或重试上下文的值。此类配置请改用 `certPath`/`keyPath`/`pfxPath` 或 `test.extend`。如果多个测试文件需要使用另一组选项，可以在共享模块中通过 `test.extend` 覆盖 fixture，并让测试文件统一从该模块导入 `test` 和 `expect`：

```ts title="tests/e2e.ts"
import { expect, test as base } from '@rstest/playwright';
import type { PlaywrightOptions } from '@rstest/playwright';

export { expect };
export const test = base.extend({
  playwright: {
    contextOptions: {
      viewport: { width: 390, height: 844 },
    },
  } satisfies PlaywrightOptions,
});
```

这里的 `export { expect }` 只是从共享模块重新导出带有 Playwright 断言的 `expect`，方便测试文件和共享的 `test` 一起导入。`test.extend` 本身不要求重新导出 `expect`。

```ts title="tests/home.test.ts"
import { expect, test } from './e2e';

test('mobile page', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await expect(page.locator('main')).toBeAttached();
});
```

如果只有少数测试需要不同的选项，可以在对应测试文件中基于共享的 `test` 再次调用 `test.extend`。再次覆盖 `playwright` fixture 会替换而不是合并共享选项，因此新的值必须包含该测试需要保留的所有共享选项。

`playwright` fixture 支持以下选项：

| 选项               | 说明                             |
| ---------------- | ------------------------------ |
| `browserName`    | 要启动的浏览器引擎。目前只支持 `chromium`。    |
| `launchOptions`  | 传给 `browserType.launch()` 的选项。 |
| `contextOptions` | 传给 `browser.newContext()` 的选项。 |
| `requestOptions` | 传给 `request.newContext()` 的选项。 |
| `debug`          | 用于本地 headed 调试的便捷选项。           |
| `trace`          | 捕获用于调试的 Playwright trace 产物。   |

### E2E 默认值 \{#e2e-defaults}


[Added in v0.12.0](https://github.com/web-infra-dev/rstest/releases/tag/v0.12.0)

使用 `extends: definePlaywrightConfig({})` 时，还会为当前 project 提供以下默认值：

| 配置                           | 默认值                                           |
| ---------------------------- | --------------------------------------------- |
| `testTimeout`                | `30_000ms`                                    |
| `hookTimeout`                | `30_000ms`                                    |
| Playwright Locator/Page 断言超时 | `expect.poll.timeout`（使用此 helper 时为 `5000ms`） |
| Rstest `expect.poll.timeout` | `5000ms`                                      |

显式的 Rstest 配置会覆盖继承的默认值。例如：

```ts title="rstest.config.ts"
import { defineConfig } from '@rstest/core';
import { definePlaywrightConfig } from '@rstest/playwright/config';

export default defineConfig({
  extends: definePlaywrightConfig({}),
  testTimeout: 60_000,
  hookTimeout: 45_000,
  expect: {
    poll: { timeout: 2000 },
  },
});
```

这里的 Playwright Locator/Page 断言和 `expect.poll()` 都使用 `2000ms`。两类 API 都可以通过单次调用的 timeout 进一步覆盖默认值。

这些默认值只在使用 `definePlaywrightConfig` 时生效。仅从 `@rstest/playwright` 导入 `test` 或 `expect` 不会改变 Rstest 的运行默认值。单元测试与 E2E 混用的 workspace 应只在 E2E project 中使用此 helper。

对齐超时数值不会改变 Rstest 的 hook/fixture 计时机制，它们仍与 Playwright Test 共用测试时间额度的机制有差异。轮询仍使用 Rstest 的固定间隔（默认 `50ms`），不采用 Playwright Test 的退避间隔。worker 数量和 `isolate` 也保留 Rstest 的默认值，需要调整时请在根配置中显式设置。

## Fixtures

`@rstest/playwright` 提供以下 fixtures：

| Fixture   | 说明                                                    |
| --------- | ----------------------------------------------------- |
| `browser` | 当前 worker 内测试共享的 Chromium `Browser`。                  |
| `context` | 每个使用它的测试都会创建一个新的 `BrowserContext`，并在测试结束后关闭。          |
| `page`    | 每个使用它的测试都会创建一个新的 `Page`，并在测试结束后关闭。                    |
| `request` | 每个使用它的测试都会创建一个新的 `APIRequestContext`，并在测试结束后 dispose。 |
| `serve`   | 在测试内启动静态 server，并在测试结束后自动清理。                          |

下面说明每个 fixture 的常见用法。`page` 和 `serve` 会链接到已有示例，避免重复展示相同代码。

### 在 hook 中使用 fixture


[Added in v0.11.4](https://github.com/web-infra-dev/rstest/releases/tag/v0.11.4)

Suite 级 hook 可以请求该 suite 内测试所提供的 fixture。需要显式指定 hook 的 fixture context 类型；只在 hook 中使用的 fixture 不需要设置 `auto: true`：

```ts
import {
  beforeEach,
  describe,
  expect,
  test,
  type PlaywrightFixture,
} from '@rstest/playwright';

type DashboardFixtures = PlaywrightFixture & {
  route: string;
};

const dashboardTest = test.extend<{ route: string }>({
  route: '/dashboard',
});

describe('dashboard', () => {
  beforeEach<DashboardFixtures>(async ({ page, route }) => {
    await page.goto(`http://localhost:3000${route}`);
  });

  dashboardTest('shows the dashboard', async ({ page }) => {
    await expect(page.locator('h1')).toHaveText('Dashboard');
  });
});
```

Hook 的作用域是所在的 `describe` 块，而不是某个扩展后的 test 对象。因此，该块内的每个测试都必须提供 hook 请求的 fixture；否则，Rstest 会在调用 hook 前让该测试失败，并报告缺失的 fixture。`afterEach` 和 `beforeEach` 返回的清理函数同样支持这一行为。在一次测试执行内，hook 与测试函数共享 fixture 实例，最后按 setup 的反序执行 teardown。

请在 hook 参数中通过直接对象解构声明 fixture 依赖。在 hook 函数体内再解构 context 对象不会请求 fixture。使用 fixture 的回调不支持剩余属性（rest property）和默认值。

### 在 `test.for` 中使用 fixture

使用 `test.for` 时，请直接从 callback 的第二个参数解构 fixture：

```ts
test.for([{ path: '/dashboard' }])(
  'opens $path',
  async ({ path }, { page }) => {
    await page.goto(`http://localhost:3000${path}`);
  },
);
```

第二个参数使用命名参数时，仍然可以访问 `task`、`expect` 等内置 `TestContext` API，但通过这个参数访问或解构属性不会请求 fixture。

### `browser`

当你需要自己创建自定义 browser context 时，可以使用 `browser`：

```ts
import { expect, test } from '@rstest/playwright';

test('custom browser context', async ({ browser }) => {
  const context = await browser.newContext({ locale: 'en-US' });
  const page = await context.newPage();

  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);

  await context.close();
});
```

### `context`

当一个测试需要多个共享同一个 browser context 的页面时，可以使用 `context`：

```ts
import { expect, test } from '@rstest/playwright';

test('multiple pages', async ({ context }) => {
  const page = await context.newPage();
  const popup = await context.newPage();

  await page.goto('https://example.com');
  await popup.goto('https://example.com');

  await expect(page).toHaveTitle(/Example/);
  await expect(popup).toHaveTitle(/Example/);
});
```

### `page`

常见的 E2E 页面访问和断言流程见[基本用法](#基本用法)。

### `request`

当你只需要 Playwright 的 API 客户端，而不需要启动浏览器时，可以使用 `request`：

```ts
import { expect, test } from '@rstest/playwright';

test('health check', async ({ request }) => {
  const response = await request.get('http://localhost:3000/health');

  expect(response.ok()).toBe(true);
});
```

### `serve`

从本地文件启动静态应用的示例见[本地应用 server](#本地应用-server)。

## 断言

`expect` 保留常规 Rstest 断言；当传入 Playwright `Locator` 或 `Page` 时，会提供可重试的 Playwright 风格异步断言。

Locator 断言面向 Playwright 的 [`Locator`](https://playwright.dev/docs/api/class-locator)，并尽量参考 [Playwright Locator assertions](https://playwright.dev/docs/test-assertions#locator-assertions) 的命名。当前会优先对齐 `@rstest/browser` 已支持的元素断言能力：

- `toBeVisible(options?)`
- `toBeHidden(options?)`
- `toBeEnabled(options?)`
- `toBeDisabled(options?)`
- `toBeChecked(options?)`
- `toBeUnchecked(options?)`
- `toBeAttached(options?)`
- `toBeDetached(options?)`
- `toBeEditable(options?)`
- `toBeFocused(options?)`
- `toBeEmpty(options?)`
- `toBeInViewport(options?)`
- `toContainText(expected, options?)`
- `toHaveAttribute(name, expected?, options?)`
- `toHaveClass(expected, options?)`
- `toHaveCSS(propertyName, expected, options?)`
- `toHaveCount(expected, options?)`
- `toHaveId(expected, options?)`
- `toHaveJSProperty(name, expected, options?)`
- `toHaveText(expected, options?)`
- `toHaveValue(expected, options?)`

Page 断言：

- `toHaveTitle(expected, options?)`
- `toHaveURL(expected, options?)`

字符串文本断言会规范化空白字符。Playwright 风格断言会持续重试，直到断言通过或达到 `timeout` 选项。默认 timeout 取自 `expect.poll.timeout`。

```ts
await expect(page.locator('.message')).toContainText('Saved', {
  timeout: 10_000,
});
```

`.not` 和 `expect.soft` 也受支持：

```ts
await expect(page.locator('.error')).not.toBeAttached();
await expect.soft(page).toHaveTitle(/Dashboard/);
```

### 断言超时 \{#assertion-timeout}


[Added in v0.12.0](https://github.com/web-infra-dev/rstest/releases/tag/v0.12.0)

Playwright Locator 和 Page 断言读取 Rstest 的 [`expect.poll.timeout`](/zh/config/test/expect.md#expectpolltimeout)。配置一次即可同时控制这些断言和 `expect.poll()`：

```ts title="rstest.config.ts"
import { defineConfig } from '@rstest/core';
import { definePlaywrightConfig } from '@rstest/playwright/config';

export default defineConfig({
  extends: definePlaywrightConfig({}),
  expect: {
    poll: { timeout: 10_000 },
  },
});
```

matcher 的 `{ timeout }` 优先于 `expect.poll.timeout`，`.not` 和 `expect.soft` 也遵循这一规则。helper 将 `expect.poll.timeout` 的默认值设为 `5000ms`；未使用 helper 且未显式配置时，Node 模式默认使用 `1000ms`。即使不使用 helper，显式配置也能生效；组合多个 helper 时，也不再有另一套断言超时需要合并。

外层 test 或 hook 的超时仍可能提前结束断言。Playwright 断言保留固定的 `50ms` 重试间隔，`expect.poll.interval` 只控制 `expect.poll()`。`page.setDefaultTimeout()` 控制 Playwright 操作的超时，不控制这些断言的超时。Rstest 不读取 `playwright.config.ts`；迁移时请将 Playwright Test 的 `expect.timeout` 改为 Rstest 的 `expect.poll.timeout`。

## Trace 调试


[Added in v0.11.2](https://github.com/web-infra-dev/rstest/releases/tag/v0.11.2)

设置 `playwright.trace` 或 `RSTEST_PLAYWRIGHT_TRACE` 后，`context` fixture 会捕获 Playwright 官方的 `trace.zip` 产物。这个 trace 覆盖默认的 `page` fixture，也覆盖通过 `context.newPage()` 创建的页面。优先级是 fixture 配置高于环境变量，最后默认关闭。

```ts
import { expect, test } from '@rstest/playwright';
import type { PlaywrightOptions } from '@rstest/playwright';

const e2e = test.extend({
  playwright: {
    trace: process.env.CI ? 'on-first-retry' : 'off',
  } satisfies PlaywrightOptions,
});

e2e('checkout', async ({ page }) => {
  await page.goto('http://localhost:3000/checkout');
  await expect(page.locator('main')).toBeAttached();
});
```

如果只是临时用命令行风格打开调试，不想修改测试代码，可以设置 `RSTEST_PLAYWRIGHT_TRACE`：

```bash
RSTEST_PLAYWRIGHT_TRACE=retain-on-failure rstest
```

当 trace 由环境变量开启时，可以用 `RSTEST_PLAYWRIGHT_TRACE_OUTPUT_DIR` 覆盖默认输出目录：

```bash
RSTEST_PLAYWRIGHT_TRACE=on RSTEST_PLAYWRIGHT_TRACE_OUTPUT_DIR=.rstest/playwright-traces rstest
```

`trace` 支持 `'off'`、`'on'`、`'retain-on-failure'`、`'on-first-retry'`、`'on-all-retries'`，也支持传入 options 对象：


[Added in v0.11.6](https://github.com/web-infra-dev/rstest/releases/tag/v0.11.6)

`on-first-retry` 只记录并保留第一次 retry 的 trace。`on-all-retries` 会记录并保留每次 retry 的 trace。两种模式在初次尝试时都不会启动 tracing，因此通过的测试不会产生 trace 启动和临时产物开销。

```ts
const e2e = test.extend({
  playwright: {
    trace: {
      mode: 'retain-on-failure',
      outputDir: '.rstest/playwright-traces',
      screenshots: true,
      snapshots: true,
      sources: true,
    },
  } satisfies PlaywrightOptions,
});
```

默认情况下，trace 会写入 `.rstest/playwright-traces/<test-name>-<hash>/`。如果同一个测试保存了多份 trace，例如 retry 产生的多次尝试，后续 trace 会使用数字后缀避免覆盖之前的产物。每次保存都会包含：

- `trace.zip`：Playwright 官方 trace 产物。可以用 `npx playwright show-trace <path-to-trace.zip>` 打开。

启用 `summary`（默认行为）时，该目录还会包含：

- `trace-summary.json`：面向工具和 AI 助手的 Rstest 测试元信息、产物路径和错误堆栈。
- `debug.md`：面向开发者阅读的调试报告。

`trace.zip` 不是通用的 Chrome/Perfetto trace，而是 Playwright 自己的 trace 格式，推荐使用 Playwright Trace Viewer 查看。

## 本地应用 server

当测试需要访问已构建的本地应用时，可以使用 `serve` fixture。它会为入口文件启动静态 server，并在测试结束后自动停止 server。

```ts
import { expect, test } from '@rstest/playwright';

test('home page', async ({ page, serve }) => {
  const { url } = await serve('./dist/index.html');

  await page.goto(url);
  await expect(page.locator('h1')).toHaveText('Home');
});
```

启用 `PWDEBUG=1` 时，`serve` 默认会保留 server，避免已打开页面失去可访问的应用服务。在非 watch 运行中，这可能会让 Rstest 进程保持运行，直到你手动停止。如果即使在 debug 模式下也希望关闭 server，可以设置 `keepAliveOnDebug: false`。

## Headed 调试

设置 `PWDEBUG=1` 可以在本地调试时以 headed 模式启动 Chromium：

```bash
PWDEBUG=1 rstest watch
```

这个环境变量不需要修改测试代码，并会应用以下默认值：

- `headless: false`
- `slowMo: 100`
- `devtools: true`

你也可以在测试里覆盖 debug 默认值：

```ts
import { test } from '@rstest/playwright';
import type { PlaywrightOptions } from '@rstest/playwright';

const e2e = test.extend({
  playwright: {
    debug: {
      enabled: true,
      slowMo: 100,
      devtools: false,
    },
  } satisfies PlaywrightOptions,
});

e2e('debug page', async ({ page }) => {
  await page.goto('http://localhost:3000');
});
```

如果需要在调试时停在当前页面，可以配合零测试超时使用 Playwright 的 `page.pause()`：

```ts
test('debug page state', { timeout: 0 }, async ({ page, serve }) => {
  const { url } = await serve('./dist/index.html');

  await page.goto(url);
  await page.pause();
});
```

debug 模式下，失败测试会在关闭 page 和 context 前自动调用 `page.pause()`。如果不需要这个行为，可以在 `debug` 选项中设置 `pauseOnFailure: false`，或设置 `RSTEST_PLAYWRIGHT_PAUSE=false`。

在 CI 或本地非交互式调试时，推荐在测试失败时截图：

```ts
import { test } from '@rstest/playwright';

test('home page', async ({ onTestFailed, page, serve }) => {
  onTestFailed(async ({ task }) => {
    await page.screenshot({
      fullPage: true,
      path: `${task.id}-failed.png`,
    });
  });

  const { url } = await serve('./dist/index.html');

  await page.goto(url);
});
```

这种方式不会阻塞测试运行，同时可以把失败时的页面状态保留下来作为 artifact。完整的示例可以参考 [Rstest + Playwright 示例](https://github.com/rstackjs/rstack-examples/tree/main/rstest/playwright)，它使用 `@rstest/playwright` 测试一个 Rsbuild + React 项目。

## 对比其他方案

### Rstest 浏览器模式

Rstest 浏览器模式会把测试模块打包后放进浏览器运行时执行，适合测试组件；`@rstest/playwright` 控制的是一个已经由应用或 server 准备好的页面，适合测试完整应用。

| 场景                                     | 推荐方案                                                             |
| -------------------------------------- | ---------------------------------------------------------------- |
| 测试组件，并使用 Rstest 的 web 打包和浏览器运行时        | [Rstest 浏览器模式](/zh/guide/browser-testing.md) 和 `@rstest/browser` |
| 通过 `page.goto()` 测试完整页面或应用             | `@rstest/playwright`                                             |
| 驱动已有 dev server、preview server 或线上 URL | `@rstest/playwright`                                             |
| 需要浏览器内组件测试能力                           | [Rstest 浏览器模式](/zh/guide/browser-testing.md)                     |

因为 `@rstest/playwright` 控制的是外部页面，不在 Rstest 的浏览器 runner 里执行测试，所以它不走 Browser UI 的预览 iframe。需要看着浏览器调试时，使用上面介绍的 headed 模式。

### 原生 Playwright

`@rstest/playwright` 和原生 Playwright 的区别主要在 runner 和配置方式：

| 项目     | `@rstest/playwright`                         | 原生 Playwright                             |
| ------ | -------------------------------------------- | ----------------------------------------- |
| Runner | Rstest runner                                | Playwright Test runner                    |
| 配置方式   | `rstest.config.ts` 和 `playwright` fixture 覆盖 | `playwright.config.ts`                    |
| 测试 API | 从 `@rstest/playwright` 导入 `test` 和 `expect`  | 从 `@playwright/test` 导入 `test` 和 `expect` |

如果希望 Playwright E2E 测试和其他 Rstest 测试使用同一套 Rstest 工作流，可以使用 `@rstest/playwright`。如果希望使用完整的 Playwright Test runner 工作流和配置模型，可以使用原生 Playwright。

## 从 Playwright 迁移

已有的 Playwright Test 项目可以交给支持 Skills 的 Coding Agent 迁移，使用 [migrate-to-rstest](https://github.com/rstackjs/agent-skills#migrate-to-rstest) skill。Agent Skills 是安装到 Coding Agent 里的领域知识包，让 Agent 在特定场景下给出更准确的建议或直接执行操作，[skills](https://www.npmjs.com/package/skills) 包负责安装它们。`migrate-to-rstest` skill 包含 Playwright 配置、fixtures 和行为一致性的迁移指南。

先安装该 skill：


```sh [npx]
npx skills add rstackjs/agent-skills --skill migrate-to-rstest
```

```sh [yarn]
yarn dlx skills add rstackjs/agent-skills --skill migrate-to-rstest
```

```sh [pnpm]
pnpm dlx skills add rstackjs/agent-skills --skill migrate-to-rstest
```

```sh [bunx]
bunx skills add rstackjs/agent-skills --skill migrate-to-rstest
```

```sh [deno]
deno run -A npm:skills add rstackjs/agent-skills --skill migrate-to-rstest
```

然后复制下面的 prompt 并发送给你的 Coding Agent：


For your Agent

将 Playwright 迁移到 @rstest/playwright

复制这个 prompt 并发送给你的 Coding Agent。

复制 Prompt

Migrate this Playwright Test project to @rstest/playwright using the migrate-to-rstest skill. Follow its Playwright migration reference, preserve existing behavior and coverage, and report unsupported configuration or fixture features instead of silently dropping them.