For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt, and this page is available as Markdown at /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 导入 testexpect,而不是从 @rstest/core 导入,这样可以获得 toHaveTitletoHaveText 等 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');
    });

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

    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 选项

    可以通过 definePlaywrightConfigrstest.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/pfxPathtest.extend。如果多个测试文件需要使用另一组选项,可以在共享模块中通过 test.extend 覆盖 fixture,并让测试文件统一从该模块导入 testexpect

    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 导入 testexpect 不会改变 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 的常见用法。pageserve 会链接到已有示例,避免重复展示相同代码。

    在 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。afterEachbeforeEach 返回的清理函数同样支持这一行为。在一次测试执行内,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}`);
      },
    );

    第二个参数使用命名参数时,仍然可以访问 taskexpect 等内置 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 LocatorPage 时,会提供可重试的 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,
    });

    .notexpect.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.notexpect.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.traceRSTEST_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。完整的 Rsbuild + Playwright 示例可以参考 examples/playwright

    对比其他方案

    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.tsplaywright fixture 覆盖playwright.config.ts
    测试 API@rstest/playwright 导入 testexpect@playwright/test 导入 testexpect

    如果希望 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。