浏览器交互
本指南介绍在 Browser Mode 测试中如何模拟用户交互,并帮助你在稳定性、可维护性和控制粒度之间做选择。
在 Browser Mode 下,推荐按以下优先级选择交互方案(仅在需要时再向后降级):
Locator API
Locator API 是 Browser Mode 下的默认选择。它由 Rstest 官方提供:@rstest/browser 提供 page 查询与交互入口,@rstest/core 提供 expect.element 断言能力。
它采用 Playwright 风格的 Locator 写法(page.getBy* + 链式调用 + expect.element),让组件测试和 DOM 测试都能复用同一套交互和断言模式。
推荐优先使用 Locator 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);
});
当前常用查询/组合能力包括:
实践中建议优先使用语义查询(getByRole、getByLabel),仅在语义信息不足时再考虑 getByTestId 或 CSS 选择器。
常用交互与断言
Locator 支持常见交互 API(如 click、fill、check、hover、press、selectOption),并可直接配合 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 匹配到多个元素,click、fill 等操作会抛出错误。请使用 first()、last() 或 nth() 选择特定元素。
Testing library
Testing Library 是一套专注于用户行为的测试工具库,它鼓励你以用户实际操作方式编写测试,而非依赖内部实现细节。在 Browser Mode 中,它更适合作为兼容与迁移方案:
安装
npm add @testing-library/dom @testing-library/user-event -D
yarn add @testing-library/dom @testing-library/user-event -D
pnpm add @testing-library/dom @testing-library/user-event -D
bun add @testing-library/dom @testing-library/user-event -D
deno add npm:@testing-library/dom npm:@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
如果你不想引入额外依赖,或者需要更底层的事件控制(如精确指定 clientX、ctrlKey 等属性),可以直接使用浏览器原生 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);
});