react-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Testing

React 测试

For general UI testing patterns (queries, events, async, accessibility), load the
front-end-testing
skill. For TDD workflow, load the
tdd
skill.
如需通用UI测试模式(查询、事件、异步、无障碍),请加载
front-end-testing
技能。如需TDD工作流,请加载
tdd
技能。

Vitest Browser Mode with React (Preferred)

搭配React的Vitest浏览器模式(推荐)

Always prefer
vitest-browser-react
over
@testing-library/react
.
Tests run in a real browser, giving production-accurate rendering, events, and CSS.
优先使用
vitest-browser-react
而非
@testing-library/react
测试在真实浏览器中运行,可提供与生产环境一致的渲染、事件和CSS效果。

Setup

配置步骤

bash
npm install -D vitest @vitest/browser-playwright vitest-browser-react @vitejs/plugin-react
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      headless: true,
      instances: [{ browser: 'chromium' }],
    },
  },
})
bash
npm install -D vitest @vitest/browser-playwright vitest-browser-react @vitejs/plugin-react
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      headless: true,
      instances: [{ browser: 'chromium' }],
    },
  },
})

Component Testing

组件测试

tsx
import { render } from 'vitest-browser-react'
import { expect, test } from 'vitest'

test('should display user name when provided', async () => {
  const screen = await render(<UserProfile name="Alice" email="alice@example.com" />)

  await expect.element(screen.getByText(/alice/i)).toBeVisible()
  await expect.element(screen.getByText(/alice@example.com/i)).toBeVisible()
})
Key differences from
@testing-library/react
:
  • render()
    is async — use
    await
  • Returns a
    screen
    scoped to the rendered component
  • Use
    expect.element()
    for auto-retrying assertions
  • No
    act()
    wrapper needed — CDP events + retry handle timing
  • Auto-cleanup happens before each test (not after), so components stay visible for debugging
tsx
import { render } from 'vitest-browser-react'
import { expect, test } from 'vitest'

test('should display user name when provided', async () => {
  const screen = await render(<UserProfile name="Alice" email="alice@example.com" />)

  await expect.element(screen.getByText(/alice/i)).toBeVisible()
  await expect.element(screen.getByText(/alice@example.com/i)).toBeVisible()
})
@testing-library/react
的主要区别:
  • render()
    为异步方法 —— 需要使用
    await
  • 返回作用于渲染组件的
    screen
    对象
  • 使用
    expect.element()
    实现自动重试断言
  • 无需
    act()
    包装 —— CDP事件和重试机制处理时序问题
  • 自动清理在每个测试前执行(而非测试后),便于调试时查看组件状态

Testing Props and Callbacks

测试Props与回调函数

tsx
test('should call onSubmit when form submitted', async () => {
  const handleSubmit = vi.fn()
  const screen = await render(<LoginForm onSubmit={handleSubmit} />)

  await screen.getByLabelText(/email/i).fill('test@example.com')
  await screen.getByRole('button', { name: /submit/i }).click()

  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
  })
})
tsx
test('should call onSubmit when form submitted', async () => {
  const handleSubmit = vi.fn()
  const screen = await render(<LoginForm onSubmit={handleSubmit} />)

  await screen.getByLabelText(/email/i).fill('test@example.com')
  await screen.getByRole('button', { name: /submit/i }).click()

  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
  })
})

Testing Conditional Rendering

测试条件渲染

tsx
test('should show error message when login fails', async () => {
  server.use(
    http.post('/api/login', () => {
      return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 })
    })
  )

  const screen = await render(<LoginForm />)

  await screen.getByLabelText(/email/i).fill('wrong@example.com')
  await screen.getByRole('button', { name: /submit/i }).click()

  await expect.element(screen.getByText(/invalid credentials/i)).toBeVisible()
})
tsx
test('should show error message when login fails', async () => {
  server.use(
    http.post('/api/login', () => {
      return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 })
    })
  )

  const screen = await render(<LoginForm />)

  await screen.getByLabelText(/email/i).fill('wrong@example.com')
  await screen.getByRole('button', { name: /submit/i }).click()

  await expect.element(screen.getByText(/invalid credentials/i)).toBeVisible()
})

Testing Hooks with renderHook

使用renderHook测试Hooks

tsx
import { renderHook } from 'vitest-browser-react'

test('should toggle value', async () => {
  const { result } = await renderHook(() => useToggle(false))

  expect(result.current.value).toBe(false)

  await act(() => {
    result.current.toggle()
  })

  expect(result.current.value).toBe(true)
})
tsx
import { renderHook } from 'vitest-browser-react'

test('should toggle value', async () => {
  const { result } = await renderHook(() => useToggle(false))

  expect(result.current.value).toBe(false)

  await act(() => {
    result.current.toggle()
  })

  expect(result.current.value).toBe(true)
})

Testing Context Providers

测试上下文提供者(Context Providers)

tsx
test('should show user menu when authenticated', async () => {
  const screen = await render(
    <AuthProvider initialUser={{ name: 'Alice', role: 'admin' }}>
      <Dashboard />
    </AuthProvider>
  )

  await expect.element(screen.getByRole('button', { name: /user menu/i })).toBeVisible()
})
For hooks that need context:
tsx
const { result } = await renderHook(() => useAuth(), {
  wrapper: ({ children }) => (
    <AuthProvider>{children}</AuthProvider>
  ),
})

tsx
test('should show user menu when authenticated', async () => {
  const screen = await render(
    <AuthProvider initialUser={{ name: 'Alice', role: 'admin' }}>
      <Dashboard />
    </AuthProvider>
  )

  await expect.element(screen.getByRole('button', { name: /user menu/i })).toBeVisible()
})
对于需要上下文的hooks:
tsx
const { result } = await renderHook(() => useAuth(), {
  wrapper: ({ children }) => (
    <AuthProvider>{children}</AuthProvider>
  ),
})

Legacy: @testing-library/react Patterns

旧版方案:@testing-library/react 测试模式

The patterns below apply when using
@testing-library/react
with jsdom. Prefer
vitest-browser-react
for new projects.

以下模式适用于搭配jsdom使用
@testing-library/react
的场景。新项目优先选择
vitest-browser-react

Testing React Components

React组件测试

React components are just functions that return JSX. Test them like functions: inputs (props) → output (rendered DOM).
React组件本质是返回JSX的函数。 像测试函数一样测试它们:输入(props)→ 输出(渲染后的DOM)。

Basic Component Testing

基础组件测试

tsx
// ✅ CORRECT - Test component behavior
it('should display user name when provided', () => {
  render(<UserProfile name="Alice" email="alice@example.com" />);

  expect(screen.getByText(/alice/i)).toBeInTheDocument();
  expect(screen.getByText(/alice@example.com/i)).toBeInTheDocument();
});
tsx
// ❌ WRONG - Testing implementation
it('should set name state', () => {
  const wrapper = mount(<UserProfile name="Alice" />);
  expect(wrapper.state('name')).toBe('Alice'); // Internal state!
});
tsx
// ✅ 正确方式 - 测试组件行为
it('should display user name when provided', () => {
  render(<UserProfile name="Alice" email="alice@example.com" />);

  expect(screen.getByText(/alice/i)).toBeInTheDocument();
  expect(screen.getByText(/alice@example.com/i)).toBeInTheDocument();
});
tsx
// ❌ 错误方式 - 测试内部实现
it('should set name state', () => {
  const wrapper = mount(<UserProfile name="Alice" />);
  expect(wrapper.state('name')).toBe('Alice'); // 内部状态!
});

Testing Props

测试Props

tsx
// ✅ CORRECT - Test how props affect rendered output
it('should call onSubmit when form submitted', async () => {
  const handleSubmit = vi.fn();
  const user = userEvent.setup();

  render(<LoginForm onSubmit={handleSubmit} />);

  await user.type(screen.getByLabelText(/email/i), 'test@example.com');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
  });
});
tsx
// ✅ 正确方式 - 测试props对渲染结果的影响
it('should call onSubmit when form submitted', async () => {
  const handleSubmit = vi.fn();
  const user = userEvent.setup();

  render(<LoginForm onSubmit={handleSubmit} />);

  await user.type(screen.getByLabelText(/email/i), 'test@example.com');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
  });
});

Testing Conditional Rendering

测试条件渲染

tsx
// ✅ CORRECT - Test what user sees in different states
it('should show error message when login fails', async () => {
  server.use(
    http.post('/api/login', () => {
      return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
    })
  );

  const user = userEvent.setup();
  render(<LoginForm />);

  await user.type(screen.getByLabelText(/email/i), 'wrong@example.com');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  await screen.findByText(/invalid credentials/i);
});

tsx
// ✅ 正确方式 - 测试用户在不同状态下看到的内容
it('should show error message when login fails', async () => {
  server.use(
    http.post('/api/login', () => {
      return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
    })
  );

  const user = userEvent.setup();
  render(<LoginForm />);

  await user.type(screen.getByLabelText(/email/i), 'wrong@example.com');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  await screen.findByText(/invalid credentials/i);
});

Testing React Hooks

React Hooks测试

Custom Hooks with renderHook

使用renderHook测试自定义Hooks

Built into
@testing-library/react
(import directly, no separate package needed):
tsx
import { renderHook } from '@testing-library/react';

it('should toggle value', () => {
  const { result } = renderHook(() => useToggle(false));

  expect(result.current.value).toBe(false);

  act(() => {
    result.current.toggle();
  });

  expect(result.current.value).toBe(true);
});
Pattern:
  • result.current
    - Current return value of hook
  • act()
    - Wrap state updates
  • rerender()
    - Re-run hook with new props
@testing-library/react
内置该方法
(直接导入,无需额外包):
tsx
import { renderHook } from '@testing-library/react';

it('should toggle value', () => {
  const { result } = renderHook(() => useToggle(false));

  expect(result.current.value).toBe(false);

  act(() => {
    result.current.toggle();
  });

  expect(result.current.value).toBe(true);
});
模式说明:
  • result.current
    - Hook当前的返回值
  • act()
    - 包裹状态更新操作
  • rerender()
    - 使用新props重新运行Hook

Hooks with Props

带Props的Hooks测试

tsx
it('should accept initial value', () => {
  const { result, rerender } = renderHook(
    ({ initialValue }) => useCounter(initialValue),
    { initialProps: { initialValue: 10 } }
  );

  expect(result.current.count).toBe(10);

  // Test with different initial value
  rerender({ initialValue: 20 });
  expect(result.current.count).toBe(20);
});

tsx
it('should accept initial value', () => {
  const { result, rerender } = renderHook(
    ({ initialValue }) => useCounter(initialValue),
    { initialProps: { initialValue: 10 } }
  );

  expect(result.current.count).toBe(10);

  // 使用不同初始值测试
  rerender({ initialValue: 20 });
  expect(result.current.count).toBe(20);
});

Testing Context

上下文(Context)测试

wrapper Option

wrapper选项

For hooks that need context providers:
tsx
const { result } = renderHook(() => useAuth(), {
  wrapper: ({ children }) => (
    <AuthProvider>
      {children}
    </AuthProvider>
  ),
});

expect(result.current.user).toBeNull();

act(() => {
  result.current.login({ email: 'test@example.com' });
});

expect(result.current.user).toEqual({ email: 'test@example.com' });
对于需要上下文提供者的hooks:
tsx
const { result } = renderHook(() => useAuth(), {
  wrapper: ({ children }) => (
    <AuthProvider>
      {children}
    </AuthProvider>
  ),
});

expect(result.current.user).toBeNull();

act(() => {
  result.current.login({ email: 'test@example.com' });
});

expect(result.current.user).toEqual({ email: 'test@example.com' });

Multiple Providers

多提供者嵌套

tsx
const AllProviders = ({ children }) => (
  <AuthProvider>
    <ThemeProvider>
      <RouterProvider>
        {children}
      </RouterProvider>
    </ThemeProvider>
  </AuthProvider>
);

const { result } = renderHook(() => useMyHook(), {
  wrapper: AllProviders,
});
tsx
const AllProviders = ({ children }) => (
  <AuthProvider>
    <ThemeProvider>
      <RouterProvider>
        {children}
      </RouterProvider>
    </ThemeProvider>
  </AuthProvider>
);

const { result } = renderHook(() => useMyHook(), {
  wrapper: AllProviders,
});

Testing Components with Context

测试带上下文的组件

tsx
// ✅ CORRECT - Wrap component in provider
const renderWithAuth = (ui, { user = null, ...options } = {}) => {
  return render(
    <AuthProvider initialUser={user}>
      {ui}
    </AuthProvider>,
    options
  );
};

it('should show user menu when authenticated', () => {
  renderWithAuth(<Dashboard />, {
    user: { name: 'Alice', role: 'admin' },
  });

  expect(screen.getByRole('button', { name: /user menu/i })).toBeInTheDocument();
});

tsx
// ✅ 正确方式 - 将组件包裹在提供者中
const renderWithAuth = (ui, { user = null, ...options } = {}) => {
  return render(
    <AuthProvider initialUser={user}>
      {ui}
    </AuthProvider>,
    options
  );
};

it('should show user menu when authenticated', () => {
  renderWithAuth(<Dashboard />, {
    user: { name: 'Alice', role: 'admin' },
  });

  expect(screen.getByRole('button', { name: /user menu/i })).toBeInTheDocument();
});

Testing Forms

表单测试

Controlled Inputs

受控输入框

tsx
it('should update input value as user types', async () => {
  const user = userEvent.setup();

  render(<SearchInput />);

  const input = screen.getByLabelText(/search/i);

  await user.type(input, 'react');

  expect(input).toHaveValue('react');
});
tsx
it('should update input value as user types', async () => {
  const user = userEvent.setup();

  render(<SearchInput />);

  const input = screen.getByLabelText(/search/i);

  await user.type(input, 'react');

  expect(input).toHaveValue('react');
});

Form Submissions

表单提交

tsx
it('should submit form with user input', async () => {
  const handleSubmit = vi.fn();
  const user = userEvent.setup();

  render(<RegistrationForm onSubmit={handleSubmit} />);

  await user.type(screen.getByLabelText(/name/i), 'Alice');
  await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
  await user.type(screen.getByLabelText(/password/i), 'password123');
  await user.click(screen.getByRole('button', { name: /sign up/i }));

  expect(handleSubmit).toHaveBeenCalledWith({
    name: 'Alice',
    email: 'alice@example.com',
    password: 'password123',
  });
});
tsx
it('should submit form with user input', async () => {
  const handleSubmit = vi.fn();
  const user = userEvent.setup();

  render(<RegistrationForm onSubmit={handleSubmit} />);

  await user.type(screen.getByLabelText(/name/i), 'Alice');
  await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
  await user.type(screen.getByLabelText(/password/i), 'password123');
  await user.click(screen.getByRole('button', { name: /sign up/i }));

  expect(handleSubmit).toHaveBeenCalledWith({
    name: 'Alice',
    email: 'alice@example.com',
    password: 'password123',
  });
});

Form Validation

表单验证

tsx
it('should show validation errors for invalid input', async () => {
  const user = userEvent.setup();

  render(<RegistrationForm />);

  // Submit empty form
  await user.click(screen.getByRole('button', { name: /sign up/i }));

  // Validation errors appear
  expect(screen.getByText(/name is required/i)).toBeInTheDocument();
  expect(screen.getByText(/email is required/i)).toBeInTheDocument();
  expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});

tsx
it('should show validation errors for invalid input', async () => {
  const user = userEvent.setup();

  render(<RegistrationForm />);

  // 提交空表单
  await user.click(screen.getByRole('button', { name: /sign up/i }));

  // 显示验证错误
  expect(screen.getByText(/name is required/i)).toBeInTheDocument();
  expect(screen.getByText(/email is required/i)).toBeInTheDocument();
  expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});

React-Specific Anti-Patterns

React专属反模式

1. Unnecessary act() wrapping

1. 不必要的act()包裹

WRONG - Manual act() everywhere
tsx
act(() => {
  render(<MyComponent />);
});

await act(async () => {
  await user.click(button);
});
CORRECT - RTL handles it
tsx
render(<MyComponent />);
await user.click(button);
Modern RTL auto-wraps:
  • render()
  • userEvent
    methods
  • fireEvent
  • waitFor
    ,
    findBy
When you DO need manual
act()
:
  • Custom hook state updates (
    renderHook
    )
  • Direct state mutations (rare, usually bad practice)

错误方式 - 手动到处使用act()
tsx
act(() => {
  render(<MyComponent />);
});

await act(async () => {
  await user.click(button);
});
正确方式 - RTL自动处理
tsx
render(<MyComponent />);
await user.click(button);
现代RTL自动包裹以下操作:
  • render()
  • userEvent
    方法
  • fireEvent
  • waitFor
    ,
    findBy
    系列方法
需要手动使用
act()
的场景:
  • 自定义Hook的状态更新(
    renderHook
    中)
  • 直接修改状态(罕见,通常是不良实践)

2. Manual cleanup() calls

2. 手动调用cleanup()

WRONG - Manual cleanup
tsx
afterEach(() => {
  cleanup(); // Automatic since RTL 9!
});
CORRECT - No cleanup needed
tsx
// Cleanup happens automatically after each test

错误方式 - 手动清理
tsx
afterEach(() => {
  cleanup(); // RTL 9版本后已自动执行!
});
正确方式 - 无需手动清理
tsx
// 每个测试后自动执行清理

3. beforeEach render pattern

3. beforeEach中渲染组件的模式

WRONG - Shared render in beforeEach
tsx
let button;
beforeEach(() => {
  render(<MyComponent />);
  button = screen.getByRole('button'); // Shared state across tests
});

it('test 1', () => {
  // Uses shared button from beforeEach
});
CORRECT - Factory function per test
tsx
const renderComponent = () => {
  render(<MyComponent />);
  return {
    button: screen.getByRole('button'),
  };
};

it('test 1', () => {
  const { button } = renderComponent(); // Fresh state
});
For factory patterns, see
testing
skill.

错误方式 - 在beforeEach中共享渲染
tsx
let button;
beforeEach(() => {
  render(<MyComponent />);
  button = screen.getByRole('button'); // 测试间共享状态
});

it('test 1', () => {
  // 使用beforeEach中共享的button
});
正确方式 - 每个测试使用工厂函数
tsx
const renderComponent = () => {
  render(<MyComponent />);
  return {
    button: screen.getByRole('button'),
  };
};

it('test 1', () => {
  const { button } = renderComponent(); // 全新状态
});
关于工厂模式,请查看
testing
技能。

4. Testing component internals

4. 测试组件内部实现

WRONG - Accessing component internals
tsx
const wrapper = shallow(<MyComponent />);
expect(wrapper.state('isOpen')).toBe(true); // Internal state
expect(wrapper.instance().handleClick).toBeDefined(); // Internal method
CORRECT - Test rendered output
tsx
render(<MyComponent />);
expect(screen.getByRole('dialog')).toBeInTheDocument(); // What user sees

错误方式 - 访问组件内部
tsx
const wrapper = shallow(<MyComponent />);
expect(wrapper.state('isOpen')).toBe(true); // 内部状态
expect(wrapper.instance().handleClick).toBeDefined(); // 内部方法
正确方式 - 测试渲染输出
tsx
render(<MyComponent />);
expect(screen.getByRole('dialog')).toBeInTheDocument(); // 用户可见内容

5. Shallow rendering

5. 浅渲染(Shallow rendering)

WRONG - Shallow rendering
tsx
const wrapper = shallow(<MyComponent />);
// Child components not rendered - incomplete test
CORRECT - Full rendering
tsx
render(<MyComponent />);
// Full component tree rendered - realistic test
Why: Shallow rendering hides integration bugs between parent/child components.

错误方式 - 浅渲染
tsx
const wrapper = shallow(<MyComponent />);
// 子组件未渲染 - 测试不完整
正确方式 - 完整渲染
tsx
render(<MyComponent />);
// 完整组件树渲染 - 更贴近真实场景的测试
原因: 浅渲染会隐藏父子组件间的集成bug。

Testing Loading States

加载状态测试

tsx
it('should show loading then data', async () => {
  render(<UserList />);

  // Initially loading
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // Wait for data
  await screen.findByText(/alice/i);

  // Loading gone
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

tsx
it('should show loading then data', async () => {
  render(<UserList />);

  // 初始状态为加载中
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // 等待数据加载完成
  await screen.findByText(/alice/i);

  // 加载状态消失
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

Testing Error Boundaries

错误边界(Error Boundaries)测试

tsx
it('should catch errors with error boundary', () => {
  // Suppress console.error for this test
  const spy = vi.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      <ThrowsError />
    </ErrorBoundary>
  );

  expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();

  spy.mockRestore();
});

tsx
it('should catch errors with error boundary', () => {
  // 抑制本次测试的console.error输出
  const spy = vi.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      <ThrowsError />
    </ErrorBoundary>
  );

  expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();

  spy.mockRestore();
});

Testing Portals

门户(Portals)测试

tsx
it('should render modal in portal', () => {
  render(<Modal isOpen={true}>Modal content</Modal>);

  // Portal renders outside root, but Testing Library finds it
  expect(screen.getByText(/modal content/i)).toBeInTheDocument();
});
Testing Library queries the entire document, so portals work automatically.

tsx
it('should render modal in portal', () => {
  render(<Modal isOpen={true}>Modal content</Modal>);

  // 门户渲染在根节点外,但Testing Library仍能找到
  expect(screen.getByText(/modal content/i)).toBeInTheDocument();
});
Testing Library会查询整个文档, 因此门户可自动被检测到。

Testing Suspense

Suspense测试

tsx
it('should show fallback then content', async () => {
  render(
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );

  // Initially fallback
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // Wait for component
  await screen.findByText(/lazy content/i);
});

tsx
it('should show fallback then content', async () => {
  render(
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );

  // 初始显示fallback
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // 等待组件加载完成
  await screen.findByText(/lazy content/i);
});

Summary Checklist

检查清单总结

React-specific checks:
  • Preferred: Using
    vitest-browser-react
    with Vitest Browser Mode (real browser)
  • Fallback: Using
    @testing-library/react
    if Browser Mode not yet configured
  • All Playwright/Browser Mode tests are idempotent (no shared state between tests)
  • Using
    renderHook()
    for custom hooks
  • Using
    wrapper
    option for context providers
  • No manual
    act()
    calls (handled automatically)
  • No manual
    cleanup()
    calls (automatic)
  • Testing component output, not internal state
  • Using factory functions, not
    beforeEach
    render
  • Using
    expect.element()
    for auto-retrying assertions (Browser Mode)
  • Following TDD workflow (see
    tdd
    skill)
  • Using general UI testing patterns (see
    front-end-testing
    skill)
  • Using test factories for data (see
    testing
    skill)
React专属检查项:
  • 推荐方案:使用搭配Vitest浏览器模式的
    vitest-browser-react
    (真实浏览器环境)
  • 备选方案:若未配置浏览器模式,使用
    @testing-library/react
  • 所有Playwright/浏览器模式测试均为幂等(测试间无共享状态)
  • 使用
    renderHook()
    测试自定义Hooks
  • 使用
    wrapper
    选项配置上下文提供者
  • 无手动
    act()
    调用(由框架自动处理)
  • 无手动
    cleanup()
    调用(自动执行)
  • 测试组件输出,而非内部状态
  • 使用工厂函数,而非
    beforeEach
    中渲染组件
  • 使用
    expect.element()
    实现自动重试断言(浏览器模式)
  • 遵循TDD工作流(查看
    tdd
    技能)
  • 使用通用UI测试模式(查看
    front-end-testing
    技能)
  • 使用测试工厂生成数据(查看
    testing
    技能)