close
  • 简体中文
  • Playwright

    @rstest/playwright 为运行在 Node.js worker 中的 Rstest 测试提供 Playwright fixtures 和 Playwright 风格的断言。它适合对一个完整页面或应用做 E2E 测试,例如本地 dev server、preview server 或线上 URL。测试代码运行在 Node.js 中,并通过 Playwright 控制页面。

    与 Rstest browser mode 的区别

    两者的主要区别是适用场景:Rstest browser mode 会对测试模块进行 web 打包,并在浏览器运行时中执行测试代码;@rstest/playwright 会控制一个由应用或服务端准备好的页面。

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

    由于 @rstest/playwright 控制的是外部页面,而不是 Rstest browser mode 的 runner iframe,因此它不走 Browser UI 的预览 iframe。需要本地可视化调试时,可以使用 PWDEBUG=1 开启 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。

    安装

    安装两个包:

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

    安装 Playwright 使用的 Chromium browser binary:

    pnpm exec playwright install chromium

    基本用法

    @rstest/playwright 导入 testexpect,而不是从 @rstest/core 导入:

    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 直接导入常规 lifecycle helpers:

    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 module cache,减少重复启动 Playwright 的开销:

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

    Fixtures

    @rstest/playwright 提供以下 fixtures:

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

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

    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 client,而不需要启动 browser 时,可以使用 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 是 5000 毫秒。

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

    暂不支持在 rstest.config.ts 中添加全局 playwright 配置项。当某个测试文件需要自定义 Playwright 选项时,可以覆盖 playwright fixture:

    import { expect, test } from '@rstest/playwright';
    import type { PlaywrightOptions } from '@rstest/playwright';
    
    const e2e = test.extend({
      playwright: {
        contextOptions: {
          viewport: { width: 390, height: 844 },
        },
      } satisfies PlaywrightOptions,
    });
    
    e2e('mobile page', async ({ page }) => {
      await page.goto('http://localhost:3000');
      await expect(page.locator('main')).toBeAttached();
    });

    playwright fixture 支持以下选项:

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

    本地应用 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