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

# 浏览器交互

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

在 Browser Mode 下，推荐按以下优先级选择交互方案（仅在需要时再向后降级）：

- **Locator API（首选）**：使用 [page.getBy\*](/zh/api/runtime-api/browser-mode/locator.md#page) + [expect.element](/zh/api/runtime-api/browser-mode/assertion.md) 做语义化查询、交互与断言，适合绝大多数交互测试
- **Testing Library**：适合迁移存量用例或复用既有 [Testing Library](https://testing-library.com/) 工具链；新测试一般不作为首选
- **原生 DOM API（兜底）**：更轻量、可精确控制事件属性，但需要手动拼装事件序列，适合验证底层事件逻辑或特殊交互细节

## Locator API

Locator API 是 Browser Mode 下的默认选择。它由 Rstest 官方提供：`@rstest/browser` 提供 [page](/zh/api/runtime-api/browser-mode/locator.md#page) 查询与交互入口，`@rstest/core` 提供 [expect.element](/zh/api/runtime-api/browser-mode/assertion.md) 断言能力。

它采用 Playwright 风格的 Locator 写法（[page.getBy\*](/zh/api/runtime-api/browser-mode/locator.md#page) + 链式调用 + [expect.element](/zh/api/runtime-api/browser-mode/assertion.md)），让组件测试和 DOM 测试都能复用同一套交互和断言模式。

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

- 查询更稳定：优先按 `role`、`label`、`text` 等用户可感知语义定位
- 交互更直接：[click](/zh/api/runtime-api/browser-mode/locator.md#click)、[fill](/zh/api/runtime-api/browser-mode/locator.md#fill)、[check](/zh/api/runtime-api/browser-mode/locator.md#check)、[press](/zh/api/runtime-api/browser-mode/locator.md#press) 等动作直接挂在 Locator 上
- 断言更自然：和 [expect.element](/zh/api/runtime-api/browser-mode/assertion.md) 配合，等待与断言语义一致
- 一致性更高：同一套 API 覆盖查询、操作、断言，减少在多套工具间切换

### 示例

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

```ts
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，把范围逐步收窄到目标元素：

```ts
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);
});
```

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

- 语义与属性查询：[getByRole](/zh/api/runtime-api/browser-mode/locator.md#getbyrole)、[getByText](/zh/api/runtime-api/browser-mode/locator.md#getbytext)、[getByLabel](/zh/api/runtime-api/browser-mode/locator.md#getbylabel)、[getByPlaceholder](/zh/api/runtime-api/browser-mode/locator.md#getbyplaceholder)、[getByAltText](/zh/api/runtime-api/browser-mode/locator.md#getbyalttext)、[getByTitle](/zh/api/runtime-api/browser-mode/locator.md#getbytitle)、[getByTestId](/zh/api/runtime-api/browser-mode/locator.md#getbytestid)
- 基础选择与过滤：[locator](/zh/api/runtime-api/browser-mode/locator.md#locator)、[filter](/zh/api/runtime-api/browser-mode/locator.md#filter)
- 集合组合与定位：[and](/zh/api/runtime-api/browser-mode/locator.md#and--or)、[or](/zh/api/runtime-api/browser-mode/locator.md#and--or)、[nth](/zh/api/runtime-api/browser-mode/locator.md#nth--first--last)、[first](/zh/api/runtime-api/browser-mode/locator.md#nth--first--last)、[last](/zh/api/runtime-api/browser-mode/locator.md#nth--first--last)

实践中建议优先使用语义查询（[getByRole](/zh/api/runtime-api/browser-mode/locator.md#getbyrole)、[getByLabel](/zh/api/runtime-api/browser-mode/locator.md#getbylabel)），仅在语义信息不足时再考虑 [getByTestId](/zh/api/runtime-api/browser-mode/locator.md#getbytestid) 或 CSS 选择器。

### 常用交互与断言

Locator 支持常见交互 API（如 [click](/zh/api/runtime-api/browser-mode/locator.md#click)、[fill](/zh/api/runtime-api/browser-mode/locator.md#fill)、[check](/zh/api/runtime-api/browser-mode/locator.md#check)、[hover](/zh/api/runtime-api/browser-mode/locator.md#hover)、[press](/zh/api/runtime-api/browser-mode/locator.md#press)、[selectOption](/zh/api/runtime-api/browser-mode/locator.md#selectoption)），并可直接配合 [expect.element](/zh/api/runtime-api/browser-mode/assertion.md) 断言：

- 状态断言：[toBeVisible](/zh/api/runtime-api/browser-mode/assertion.md#tobevisible)、[toBeHidden](/zh/api/runtime-api/browser-mode/assertion.md#tobehidden)、[toBeEnabled](/zh/api/runtime-api/browser-mode/assertion.md#tobeenabled)、[toBeDisabled](/zh/api/runtime-api/browser-mode/assertion.md#tobedisabled)
- 表单/结构断言：[toBeChecked](/zh/api/runtime-api/browser-mode/assertion.md#tobechecked)、[toBeFocused](/zh/api/runtime-api/browser-mode/assertion.md#tobefocused)、[toBeEmpty](/zh/api/runtime-api/browser-mode/assertion.md#tobeempty)
- 文本和值断言：[toHaveText](/zh/api/runtime-api/browser-mode/assertion.md#tohavetext)、[toContainText](/zh/api/runtime-api/browser-mode/assertion.md#tocontaintext)、[toHaveValue](/zh/api/runtime-api/browser-mode/assertion.md#tohavevalue)、[toHaveCount](/zh/api/runtime-api/browser-mode/assertion.md#tohavecount)
- 属性断言：[toHaveAttribute](/zh/api/runtime-api/browser-mode/assertion.md#tohaveattribute)、[toHaveClass](/zh/api/runtime-api/browser-mode/assertion.md#tohaveclass)、[toHaveCSS](/zh/api/runtime-api/browser-mode/assertion.md#tohavecss)、[toHaveJSProperty](/zh/api/runtime-api/browser-mode/assertion.md#tohavejsproperty)

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

:::info 自动等待 vs 自动重试
Locator API 中有两种不同的等待机制：

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

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

你也可以链式使用 [not](/zh/api/runtime-api/browser-mode/assertion.md#not) 和可选 `timeout`：

```ts
await expect
  .element(page.getByRole('button', { name: 'Save' }))
  .not.toBeDisabled({ timeout: 1000 });
```

:::warning 严格模式
Locator 的交互操作是严格的：如果一个 locator 匹配到多个元素，`click`、`fill` 等操作会抛出错误。请使用 `first()`、`last()` 或 `nth()` 选择特定元素。
:::

## Testing library

[Testing Library](https://testing-library.com/) 是一套专注于用户行为的测试工具库，它鼓励你以用户实际操作方式编写测试，而非依赖内部实现细节。在 Browser Mode 中，它更适合作为兼容与迁移方案：

- [@testing-library/dom](https://testing-library.com/docs/dom-testing-library/intro)：负责查询，提供 `getByRole`、`getByText`、`getByLabelText` 等方法，让你按用户可感知语义查找元素
- [@testing-library/user-event](https://testing-library.com/docs/user-event/intro)：负责交互，提供更完整的事件模拟流程；在 Browser Mode 中，新测试仍建议优先 Locator API

### 安装


```sh [npm]
npm add @testing-library/dom @testing-library/user-event -D
```

```sh [yarn]
yarn add @testing-library/dom @testing-library/user-event -D
```

```sh [pnpm]
pnpm add @testing-library/dom @testing-library/user-event -D
```

```sh [bun]
bun add @testing-library/dom @testing-library/user-event -D
```

```sh [deno]
deno add npm:@testing-library/dom npm:@testing-library/user-event -D
```

### 示例

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

```tsx title="src/LoginForm.test.tsx"
import { expect, rs, 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 = rs.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 官方文档](https://testing-library.com/docs/user-event/intro)。

## 原生 DOM API

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

### 示例

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

```ts title="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);
});
```
