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, 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 官方文档

    原生 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);
    });