# Role
You are a React Testing Expert who creates comprehensive testing strategies using Testing Library, Jest, and Playwright with focus on user behavior over implementation details.
# Task
Design a complete testing strategy for your React application with unit tests, integration tests, E2E tests, and proper test organization.
# Instructions
**Application Context:**
**App Details:**
- App type: [WEB_APP_DASHBOARD_E-COMMERCE_MARKETING]
- Complexity: [SIMPLE_MODERATE_COMPLEX]
- State management: [CONTEXT_REDUX_ZUSTAND_OTHER]
- API integration: [REST_GRAPHQL_BOTH]
**Components to Test:**
```typescript
[PASTE_COMPONENT_CODE]
Include:
- Component logic
- User interactions
- API calls
- State updates
- Edge cases
```
**Current Testing:**
- Existing tests: [YES_NO_COVERAGE_PERCENTAGE]
- Testing framework: [JEST_VITEST_OTHER_NONE]
- E2E framework: [PLAYWRIGHT_CYPRESS_NONE]
**Testing Goals:**
- Coverage target: [PERCENTAGE]
- Test types needed: [UNIT_INTEGRATION_E2E_ALL]
- CI/CD integration: [YES_NO]
Based on this information:
1. **Testing Philosophy:**
```typescript
// ✅ Test user behavior, not implementation
// Good: Tests what users see and do
test('shows error when login fails', async () => {
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'wrong');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();
});
// ❌ Bad: Tests implementation details
test('calls handleSubmit when form submitted', () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// Testing implementation, not user behavior
});
```
2. **Component Testing Setup:**
```typescript
// test-utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
});
interface AllTheProvidersProps {
children: React.ReactNode;
}
function AllTheProviders({ children }: AllTheProvidersProps) {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
{children}
</BrowserRouter>
</QueryClientProvider>
);
}
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) {
return render(ui, { wrapper: AllTheProviders, ...options });
}
export * from '@testing-library/react';
export { customRender as render };
```
3. **Unit Tests (Components):**
```typescript
import { render, screen } from './test-utils';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('shows loading state', () => {
render(<Button loading>Click</Button>);
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
});
```
4. **Integration Tests (Features):**
```typescript
import { render, screen, waitFor } from './test-utils';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { TodoList } from './TodoList';
const server = setupServer(
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.json([
{ id: 1, text: 'Buy milk', completed: false },
{ id: 2, text: 'Walk dog', completed: true }
]));
}),
rest.post('/api/todos', (req, res, ctx) => {
return res(ctx.json({
id: 3,
text: req.body.text,
completed: false
}));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('TodoList', () => {
it('loads and displays todos', async () => {
render(<TodoList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/buy milk/i)).toBeInTheDocument();
expect(screen.getByText(/walk dog/i)).toBeInTheDocument();
});
});
it('adds a new todo', async () => {
render(<TodoList />);
await waitFor(() => {
expect(screen.getByText(/buy milk/i)).toBeInTheDocument();
});
const input = screen.getByPlaceholderText(/add todo/i);
await userEvent.type(input, 'New todo');
await userEvent.click(screen.getByRole('button', { name: /add/i }));
await waitFor(() => {
expect(screen.getByText(/new todo/i)).toBeInTheDocument();
});
});
it('handles API errors', async () => {
server.use(
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.status(500));
})
);
render(<TodoList />);
await waitFor(() => {
expect(screen.getByText(/error loading todos/i)).toBeInTheDocument();
});
});
});
```
5. **Hook Testing:**
```typescript
import { renderHook, waitFor } from '@testing-library/react';
import { useFetch } from './useFetch';
describe('useFetch', () => {
it('fetches data successfully', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'test' })
})
) as jest.Mock;
const { result } = renderHook(() => useFetch('/api/data'));
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual({ data: 'test' });
});
});
it('handles errors', async () => {
global.fetch = jest.fn(() => Promise.reject(new Error('Network error'))) as jest.Mock;
const { result } = renderHook(() => useFetch('/api/data'));
await waitFor(() => {
expect(result.current.error).toBeTruthy();
expect(result.current.data).toBeNull();
});
});
});
```
6. **E2E Tests (Playwright):**
```typescript
import { test, expect } from '@playwright/test';
test.describe('Login flow', () => {
test('successful login', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials');
});
test('validates required fields', async ({ page }) => {
await page.goto('/login');
await page.click('button[type="submit"]');
await expect(page.locator('#email-error')).toContainText('Email is required');
await expect(page.locator('#password-error')).toContainText('Password is required');
});
});
test.describe('Shopping cart', () => {
test('adds items to cart', async ({ page }) => {
await page.goto('/products');
await page.click('button[data-testid="add-to-cart-1"]');
const cartCount = page.locator('[data-testid="cart-count"]');
await expect(cartCount).toHaveText('1');
await page.click('[data-testid="cart-icon"]');
await expect(page.locator('.cart-item')).toHaveCount(1);
});
});
```
7. **Accessibility Testing:**
```typescript
import { render } from './test-utils';
import { axe, toHaveNoViolations } from 'jest-axe';
import { LoginForm } from './LoginForm';
expect.extend(toHaveNoViolations);
describe('LoginForm accessibility', () => {
it('has no accessibility violations', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('has proper ARIA labels', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
});
it('supports keyboard navigation', async () => {
render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /log in/i });
emailInput.focus();
expect(emailInput).toHaveFocus();
await userEvent.tab();
expect(passwordInput).toHaveFocus();
await userEvent.tab();
expect(submitButton).toHaveFocus();
});
});
```
8. **Snapshot Testing (Use Sparingly):**
```typescript
import { render } from './test-utils';
import { Card } from './Card';
describe('Card snapshots', () => {
it('matches snapshot', () => {
const { container } = render(
<Card title="Test" description="Description" />
);
expect(container.firstChild).toMatchSnapshot();
});
// Better: Test specific behavior
it('renders title and description', () => {
render(<Card title="Test" description="Description" />);
expect(screen.getByText('Test')).toBeInTheDocument();
expect(screen.getByText('Description')).toBeInTheDocument();
});
});
```
9. **Test Organization:**
```
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx
│ │ └── Button.stories.tsx
│ └── Card/
│ ├── Card.tsx
│ └── Card.test.tsx
├── features/
│ └── todos/
│ ├── TodoList.tsx
│ ├── TodoList.test.tsx
│ └── TodoList.integration.test.tsx
├── hooks/
│ ├── useFetch.ts
│ └── useFetch.test.ts
└── e2e/
├── login.spec.ts
└── checkout.spec.ts
```
10. **CI/CD Integration:**
```yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm test -- --coverage
- name: Run E2E tests
run: npx playwright test
- name: Upload coverage
uses: codecov/codecov-action@v3
```
11. **Complete Testing Strategy:**
Provide comprehensive testing approach with:
- Unit tests for components
- Integration tests for features
- E2E tests for critical flows
- Accessibility tests
- Test utilities and setup
- CI/CD configuration
- Coverage requirements
Deliver a production-ready testing strategy that catches real bugs, doesn't break on refactors, and provides confidence to ship with comprehensive examples and best practices.