---
name: react-testing-patterns
description: Testing patterns cho React với Vitest, React Testing Library, và Playwright. Use for unit tests, component tests, E2E tests, và Storybook integration testing.
compatibility: "vitest>=4, @testing-library/react>=14, playwright>=1"
metadata:
  author: Velik Ho
  version: "1.0"
---

# React Testing Patterns / Mẫu Testing React

## When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:
- Writing unit tests / Viết unit tests
- Testing React components / Testing React components
- Creating E2E tests with Playwright / Tạo E2E tests với Playwright
- Testing Storybook stories / Testing Storybook stories
- Mocking APIs and modules / Mocking APIs và modules

## Testing Pyramid / Tháp Testing

```
        ┌───────────┐
        │   E2E     │  Playwright (few, slow, high confidence)
        ├───────────┤
        │Integration│  RTL + Vitest (moderate)
        ├───────────┤
        │   Unit    │  Vitest (many, fast, focused)
        └───────────┘
```

## Key Patterns / Mẫu Chính

### Test File Organization

```
src/
├── features/auth/
│   ├── components/
│   │   └── LoginForm/
│   │       ├── LoginForm.tsx
│   │       └── LoginForm.test.tsx    # Component test
│   └── hooks/
│       └── useAuth.test.ts           # Hook test
├── stores/
│   └── __tests__/
│       └── auth-store.test.ts        # Store test
└── __tests__/                        # Integration tests
    └── auth-flow.test.tsx
e2e/
└── auth.spec.ts                      # E2E tests
```

### Unit Testing with Vitest

```typescript
// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('should increment counter', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  it('should reset counter', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.reset();
    });
    
    expect(result.current.count).toBe(0);
  });
});
```

### Component Testing with RTL

```tsx
// components/LoginForm/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  it('should submit form with valid data', async () => {
    const onSubmit = vi.fn();
    const user = userEvent.setup();
    
    render(<LoginForm onSubmit={onSubmit} />);
    
    // Type in form fields
    await user.type(screen.getByLabelText(/email/i), 'test@example.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    
    // Submit form
    await user.click(screen.getByRole('button', { name: /login/i }));
    
    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123',
      });
    });
  });

  it('should show validation errors', async () => {
    const user = userEvent.setup();
    
    render(<LoginForm onSubmit={vi.fn()} />);
    
    // Submit without filling form
    await user.click(screen.getByRole('button', { name: /login/i }));
    
    expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
  });
});
```

### Testing with Query Wrapper

```tsx
// test/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render as rtlRender } from '@testing-library/react';

/**
 * EN: Custom render with providers
 * VI: Custom render với providers
 */
export function renderWithProviders(ui: React.ReactElement) {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });

  return rtlRender(
    <QueryClientProvider client={queryClient}>
      {ui}
    </QueryClientProvider>
  );
}

// Usage
import { renderWithProviders } from '@/test/utils';

it('should load data', async () => {
  renderWithProviders(<UserProfile userId="1" />);
  expect(await screen.findByText('John Doe')).toBeInTheDocument();
});
```

### Mocking with Vitest

```typescript
// Mocking modules
import { vi } from 'vitest';

// Mock entire module
vi.mock('@/services/api', () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
}));

// Mock specific function
import * as api from '@/services/api';
vi.spyOn(api, 'fetchUser').mockResolvedValue({ id: '1', name: 'Test' });

// Mock with implementation
vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
  }),
  usePathname: () => '/dashboard',
}));
```

### Playwright E2E Testing

```typescript
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('should login successfully', async ({ page }) => {
    await page.goto('/login');
    
    // Fill form
    await page.getByLabel('Email').fill('test@example.com');
    await page.getByLabel('Password').fill('password123');
    
    // Submit
    await page.getByRole('button', { name: 'Login' }).click();
    
    // Verify redirect
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('Welcome back')).toBeVisible();
  });

  test('should show error on invalid credentials', async ({ page }) => {
    await page.goto('/login');
    
    await page.getByLabel('Email').fill('wrong@example.com');
    await page.getByLabel('Password').fill('wrongpassword');
    await page.getByRole('button', { name: 'Login' }).click();
    
    await expect(page.getByText('Invalid credentials')).toBeVisible();
  });
});
```

### Storybook Interaction Testing

```tsx
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
};

export default meta;

export const ClickTest: StoryObj<typeof Button> = {
  args: { children: 'Click me' },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    
    // Test interaction
    await userEvent.click(button);
    
    // Assert
    await expect(button).toHaveFocus();
  },
};
```

## Common Mistakes / Lỗi Thường Gặp

### 1. Testing Implementation Details

```tsx
// ❌ BAD: Testing internal state
expect(component.state.isOpen).toBe(true);

// ✅ GOOD: Testing user-visible behavior
expect(screen.getByRole('dialog')).toBeVisible();
```

### 2. Not Waiting for Async Operations

```tsx
// ❌ BAD: Immediate assertion
render(<AsyncComponent />);
expect(screen.getByText('Data')).toBeInTheDocument(); // Fails!

// ✅ GOOD: Wait for element
render(<AsyncComponent />);
expect(await screen.findByText('Data')).toBeInTheDocument();
```

### 3. Using getBy* for Absent Elements

```tsx
// ❌ BAD: Throws error if not found
expect(screen.getByText('Error')).not.toBeInTheDocument();

// ✅ GOOD: Use queryBy* for absence checks
expect(screen.queryByText('Error')).not.toBeInTheDocument();
```

## Quick Reference / Tham Chiếu Nhanh

| Query | Use Case |
|-------|----------|
| `getBy*` | Element must exist, throws if not |
| `queryBy*` | Element may not exist (for negative assertions) |
| `findBy*` | Async, waits for element |
| `*ByRole` | Preferred, most accessible |
| `*ByLabelText` | Form inputs |
| `*ByText` | Non-interactive text |
| `*ByTestId` | Last resort |

| Tool | Use Case |
|------|----------|
| Vitest | Unit & integration tests |
| RTL | Component testing |
| userEvent | User interaction simulation |
| Playwright | E2E browser tests |
| MSW | API mocking |

## Resources / Tài Nguyên

- [React State Management](../react-state-management/SKILL.md) - Store testing
- [React UI Components](../react-ui-components/SKILL.md) - Component patterns
- [Testing Library Docs](https://testing-library.com/docs/react-testing-library/intro/)
- [Vitest Docs](https://vitest.dev/)
- [Playwright Docs](https://playwright.dev/)
