close

浏览器交互

本指南介绍在 Browser Mode 测试中如何模拟用户交互,并帮助你在稳定性、可维护性和控制粒度之间做选择。

在 Browser Mode 下,推荐按以下优先级选择交互方案(仅在需要时再向后降级):

  • Locator API(首选):使用 page.getBy* + expect.element 做语义化查询、交互与断言,适合绝大多数交互测试
  • Testing Library:适合迁移存量用例或复用既有 Testing Library 工具链;新测试一般不作为首选
  • 原生 DOM API(兜底):更轻量、可精确控制事件属性,但需要手动拼装事件序列,适合验证底层事件逻辑或特殊交互细节

Locator API

Locator API 是 Browser Mode 下的默认选择。它由 Rstest 官方提供:@rstest/browser 提供 page 查询与交互入口,@rstest/core 提供 expect.element 断言能力。

它采用 Playwright 风格的 Locator 写法(page.getBy* + 链式调用 + expect.element),让组件测试和 DOM 测试都能复用同一套交互和断言模式。

推荐优先使用 Locator API 的原因:

  • 查询更稳定:优先按 rolelabeltext 等用户可感知语义定位
  • 交互更直接:clickfillcheckpress 等动作直接挂在 Locator 上
  • 断言更自然:和 expect.element 配合,等待与断言语义一致
  • 一致性更高:同一套 API 覆盖查询、操作、断言,减少在多套工具间切换

示例

下面示例聚焦最常见链路:填表、点击、断言。

import { page } from '@rstest/browser';
import { expect, test } from '@rstest/core';

test('interacts with form using locator api', async () => {
  document.body.innerHTML = `
    <form aria-label="Login form">
      <label for="username">Username</label>
      <input id="username" />

      <label for="password">Password</label>
      <input id="password" type="password" />

      <label>
        <input id="remember" type="checkbox" />
        Remember me
      </label>

      <button type="button">Login</button>
    </form>
  `;

  await page.getByLabel('Username').fill('alice');
  await page.getByLabel('Password').fill('secret123');
  await page.getByLabel('Remember me').check();
  await page.getByRole('button', { name: 'Login' }).click();

  await expect.element(page.getByLabel('Username')).toHaveValue('alice');
  await expect.element(page.getByLabel('Remember me')).toBeChecked();
});

常用查询与组合

你可以像 Playwright 一样组合 Locator,把范围逐步收窄到目标元素:

import { page } from '@rstest/browser';
import { expect, test } from '@rstest/core';

test('composes locators', async () => {
  document.body.innerHTML = `
    <section>
      <h2>Home</h2>
      <button>Save</button>
    </section>
    <section>
      <h2>Profile</h2>
      <button>Save</button>
    </section>
  `;

  const saveInProfileSection = page
    .locator('section')
    .filter({ has: page.getByRole('heading', { name: 'Profile' }) })
    .getByRole('button', { name: 'Save' });

  await expect.element(saveInProfileSection).toHaveCount(1);
});

当前常用查询/组合能力包括:

实践中建议优先使用语义查询(getByRolegetByLabel),仅在语义信息不足时再考虑 getByTestId 或 CSS 选择器。

常用交互与断言

Locator 支持常见交互 API(如 clickfillcheckhoverpressselectOption),并可直接配合 expect.element 断言:

建议在关键交互后立即断言可观察结果(例如状态文案、按钮状态、字段值),这样失败信息更聚焦、调试成本更低。

自动等待 vs 自动重试

Locator API 中有两种不同的等待机制:

  • 自动等待(交互)click()fill()check() 等方法会自动等待目标元素可见、启用、稳定后再执行操作。
  • 自动重试(断言)expect.element 的 matcher 会在超时时间内持续重试,直到断言通过,适合处理异步渲染场景。

大多数情况下,你只需要 await 每个调用——框架会在内部处理所有等待和重试。

你也可以链式使用 not 和可选 timeout

await expect
  .element(page.getByRole('button', { name: 'Save' }))
  .not.toBeDisabled({ timeout: 1000 });
严格模式

Locator 的交互操作是严格的:如果一个 locator 匹配到多个元素,clickfill 等操作会抛出错误。请使用 first()last()nth() 选择特定元素。

Testing library

Testing Library 是一套专注于用户行为的测试工具库,它鼓励你以用户实际操作方式编写测试,而非依赖内部实现细节。在 Browser Mode 中,它更适合作为兼容与迁移方案:

  • @testing-library/dom:负责查询,提供 getByRolegetByTextgetByLabelText 等方法,让你按用户可感知语义查找元素
  • @testing-library/user-event:负责交互,提供更完整的事件模拟流程;在 Browser Mode 中,新测试仍建议优先 Locator API

安装

npm
yarn
pnpm
bun
deno
npm add @testing-library/dom @testing-library/user-event -D

示例

以下是一个完整的表单提交测试示例,展示了常用的用户交互方法:

src/LoginForm.test.tsx
import { expect, test } from '@rstest/core';
import { render } from '@rstest/browser-react';
import { getByLabelText, getByRole } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

test('submits login form with user credentials', async () => {
  const user = userEvent.setup();
  const onSubmit = rstest.fn();

  const { container } = await render(<LoginForm onSubmit={onSubmit} />);

  // Type into input fields
  await user.type(getByLabelText(container, 'Username'), 'alice');
  await user.type(getByLabelText(container, 'Password'), 'secret123');

  // Check the "remember me" checkbox
  await user.click(getByLabelText(container, 'Remember me'));

  // Submit the form
  await user.click(getByRole(container, 'button', { name: 'Login' }));

  // Assert the form was submitted with correct data
  expect(onSubmit).toHaveBeenCalledWith({
    username: 'alice',
    password: 'secret123',
    rememberMe: true,
  });
});

如果你的项目已经大量使用 Testing Library,可以继续复用它的点击、文本输入、键盘事件、下拉选择、拖拽等能力。详细用法请参考 user-event 官方文档

原生 DOM API

如果你不想引入额外依赖,或者需要更底层的事件控制(如精确指定 clientXctrlKey 等属性),可以直接使用浏览器原生 DOM API。通常将它作为兜底方案,仅在你需要精确控制事件参数时使用。

示例

以下示例展示了常见的原生事件操作,包括点击、输入和键盘事件:

src/native-events.test.ts
import { expect, test } from '@rstest/core';

test('handles click and input events', () => {
  // Create elements
  const button = document.createElement('button');
  const input = document.createElement('input');
  document.body.append(button, input);

  // Click event
  let clicked = false;
  button.addEventListener('click', () => (clicked = true));
  button.click();
  expect(clicked).toBe(true);

  // Input event
  input.focus();
  input.value = 'hello';
  input.dispatchEvent(new InputEvent('input', { bubbles: true }));
  expect(input.value).toBe('hello');
});

test('handles keyboard shortcuts', () => {
  let shortcutTriggered = false;

  document.addEventListener('keydown', (e) => {
    // Detect Ctrl+S shortcut
    if (e.ctrlKey && e.key === 's') {
      e.preventDefault();
      shortcutTriggered = true;
    }
  });

  document.dispatchEvent(
    new KeyboardEvent('keydown', {
      key: 's',
      code: 'KeyS',
      ctrlKey: true,
      bubbles: true,
    }),
  );

  expect(shortcutTriggered).toBe(true);
});