Qa Frontend Mean
Purpose
Write and run frontend tests for MEAN stack React applications. Cover component rendering tests, user interaction tests, hook tests, routing tests, form validation tests, and API integration tests using mocked service workers.
Key Patterns
Test Structure
client/src/
├── __tests__/
│ ├── setup.js # Global test setup (MSW, custom matchers)
│ └── mocks/
│ ├── handlers.js # MSW request handlers
│ └── server.js # MSW server setup
├── components/
│ ├── common/
│ │ ├── Button.jsx
│ │ └── Button.test.jsx # Co-located test
│ └── features/
│ ├── LoginForm.jsx
│ └── LoginForm.test.jsx
├── pages/
│ ├── LoginPage.jsx
│ └── LoginPage.test.jsx
├── hooks/
│ ├── useAuth.js
│ └── useAuth.test.js
└── context/
├── AuthContext.jsx
└── AuthContext.test.jsx
Component Test Pattern (React Testing Library)
// components/common/Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button', () => {
it('renders children text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows spinner when loading', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.queryByText('Submit')).not.toBeInTheDocument();
});
it('applies variant class', () => {
render(<Button variant="danger">Delete</Button>);
expect(screen.getByRole('button')).toHaveClass('danger');
});
});
Page Test Pattern (with Auth Context)
// pages/DashboardPage.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { AuthProvider } from '../context/AuthContext';
import DashboardPage from './DashboardPage';
import { server } from '../__tests__/mocks/server';
import { rest } from 'msw';
const renderWithProviders = (component, { route = '/' } = {}) => {
return render(
<MemoryRouter initialEntries={[route]}>
<AuthProvider>{component}</AuthProvider>
</MemoryRouter>
);
};
describe('DashboardPage', () => {
it('shows loading spinner initially', () => {
renderWithProviders(<DashboardPage />);
expect(screen.getByRole('status')).toBeInTheDocument(); // spinner
});
it('displays dashboard data after loading', async () => {
renderWithProviders(<DashboardPage />);
await waitFor(() => {
expect(screen.getByText(/welcome/i)).toBeInTheDocument();
});
});
it('shows error message on API failure', async () => {
server.use(
rest.get('/api/dashboard/stats', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ success: false, error: { message: 'Server error' } }));
})
);
renderWithProviders(<DashboardPage />);
await waitFor(() => {
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
});
});
});
Form Test Pattern
// components/features/LoginForm.test.jsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm', () => {
it('submits with valid credentials', async () => {
const
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'Password123');
fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'Password123' });
});
});
it('shows validation error for empty email', async () => {
render(<LoginForm />);
fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
});
it('disables submit button while submitting', async () => {
const => new Promise(resolve => setTimeout(resolve, 1000)));
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'Password123');
fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByRole('button', { name: /login/i })).toBeDisabled();
});
});
MSW Mock Server Pattern
// __tests__/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
rest.post('/api/auth/login', (req, res, ctx) => {
return res(ctx.json({
success: true,
data: { user: { id: '1', name: 'Test User', email: 'test@example.com' }, token: 'fake-jwt-token' }
}));
}),
rest.get('/api/auth/me', (req, res, ctx) => {
return res(ctx.json({
success: true,
data: { id: '1', name: 'Test User', email: 'test@example.com', role: 'user' }
}));
}),
rest.get('/api/dashboard/stats', (req, res, ctx) => {
return res(ctx.json({ success: true, data: { totalUsers: 100, totalPosts: 50 } }));
}),
];
// __tests__/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
Hook Test Pattern
// hooks/useAuth.test.js
import { renderHook, act } from '@testing-library/react';
import { AuthProvider } from '../context/AuthContext';
import { useAuth } from './useAuth';
const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>;
describe('useAuth', () => {
it('returns null user initially', () => {
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
});
it('sets user after login', async () => {
const { result } = renderHook(() => useAuth(), { wrapper });
await act(async () => {
await result.current.login('test@example.com', 'Password123');
});
expect(result.current.user).toBeDefined();
expect(result.current.isAuthenticated).toBe(true);
});
it('clears user after logout', async () => {
const { result } = renderHook(() => useAuth(), { wrapper });
await act(async () => { await result.current.login('test@example.com', 'Password123'); });
act(() => { result.current.logout(); });
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
});
});
Conventions
- Use Jest + React Testing Library (NOT Enzyme -- deprecated)
- Use MSW (Mock Service Worker) for API mocking -- no manual axios mocks
- Test files co-located with components:
Component.test.jsxnext toComponent.jsx - Query priority: getByRole > getByLabelText > getByText > getByTestId (accessibility first)
- Use
userEventoverfireEventfor user interactions (more realistic) - Wrap components needing context/router in test utilities:
renderWithProviders() - Test behavior, not implementation -- never test state variables or internal methods
- Every component test: renders correctly, handles user interaction, shows loading/error states
- Every form test: valid submission, validation errors, disabled during submit
- Every page test: loading state, success state, error state with retry
- Clean up: MSW handlers reset after each test
Knowledge Strategy
- Patterns to capture: Test patterns for common React components (forms, lists, modals, auth flows)
- Examples to collect: Complete test files for standard components, MSW handler patterns
- Update permission: Agents may freely add/update files in
references/. Changes toSKILL.mdorscripts/require user approval.