For AI agents: the complete documentation index is available at https://rstest.rs/zh/llms.txt, the full documentation bundle is available at https://rstest.rs/zh/llms-full.txt, and this page is available as Markdown at https://rstest.rs/zh/guide/basic/e2e-testing.md.
close
  • 简体中文
  • E2E 测试

    E2E 测试(端到端测试)从用户的角度验证一个完整的页面或应用:打开页面、点击、输入,然后检查页面上真实呈现的结果。Rstest 通过 @rstest/playwright 集成 Playwright fixtures 和 Playwright 风格的断言。

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

    安装

    安装两个包:

    npm
    yarn
    pnpm
    bun
    deno
    npm add @rstest/playwright playwright -D

    安装 Playwright 使用的 Chromium 浏览器:

    pnpm exec playwright install chromium

    基本用法

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

    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');
    });

    Playwright 类型

    @rstest/playwright 会重新导出常用的 Playwright 类型,因此测试辅助函数可以和 test、expect 使用同一个入口:

    import { expect, test, type Locator, type Page } from '@rstest/playwright';
    
    const getHeading = (page: Page): Locator => page.locator('h1');
    
    test('page heading', async ({ page }) => {
      await expect(getHeading(page)).toHaveText('Example Domain');
    });

    这些仅类型导出包括 APIRequestContext、APIResponse、Browser、BrowserContext、BrowserContextOptions、BrowserType、Frame、LaunchOptions、Locator、Page、Request、Response 和 Route。

    @rstest/playwright 是 Rstest 的集成层,而不是 playwright 包的替代品。它不会 re-export chromium、firefox、webkit 或 request 等运行时 API。如果确实需要这些 API,请直接从 playwright 导入:

    import { chromium, request } from 'playwright';

    一般的 E2E 测试直接使用 browser、context、page 和 request fixtures 即可,Rstest 会按照 fixture 生命周期创建并清理这些资源。

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

    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 的开销:

    rstest.config.ts
    import { defineConfig } from '@rstest/core';
    
    export default defineConfig({
      isolate: false,
      testEnvironment: 'node',
    });

    配置 Playwright 选项

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

    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:

    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。

    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 默认值

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

    配置默认值
    testTimeout30_000ms
    hookTimeout30_000ms
    Playwright Locator/Page 断言超时expect.poll.timeout(使用此 helper 时为 5000ms)
    Rstest expect.poll.timeout5000ms

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

    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

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

    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:

    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:

    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:

    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:

    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。

    断言

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

    Locator 断言面向 Playwright 的 Locator,并尽量参考 Playwright 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。

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

    .not 和 expect.soft 也受支持:

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

    断言超时

    Playwright Locator 和 Page 断言读取 Rstest 的 expect.poll.timeout。配置一次即可同时控制这些断言和 expect.poll():

    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 调试

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

    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:

    RSTEST_PLAYWRIGHT_TRACE=retain-on-failure rstest

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

    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 对象:

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

    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。

    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:

    PWDEBUG=1 rstest watch

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

    • headless: false
    • slowMo: 100
    • devtools: true

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

    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():

    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 或本地非交互式调试时,推荐在测试失败时截图:

    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 示例,它使用 @rstest/playwright 测试一个 Rsbuild + React 项目。

    对比其他方案

    Rstest 浏览器模式

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

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

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

    原生 Playwright

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

    项目@rstest/playwright原生 Playwright
    RunnerRstest runnerPlaywright 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 skill。Agent Skills 是安装到 Coding Agent 里的领域知识包,让 Agent 在特定场景下给出更准确的建议或直接执行操作,skills 包负责安装它们。migrate-to-rstest skill 包含 Playwright 配置、fixtures 和行为一致性的迁移指南。

    先安装该 skill:

    npm
    yarn
    pnpm
    bun
    deno
    npx skills add rstackjs/agent-skills --skill migrate-to-rstest

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

    For your Agent
    将 Playwright 迁移到 @rstest/playwright

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