Part of The Coercion Saga — making AI write quality code.
The UI Gate
Backend tested. API returns correct data. But the button that calls it? The form that submits? That's frontend territory.
Vitest Over Jest
Jest is slow. Vitest runs natively in Vite. Same config, instant startup.
// vitest.config.ts
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
globals: true,
},
});
MSW: Fake the Network
Tests that hit real APIs are flaky. MSW intercepts at the network layer. Your code doesn't know it's being lied to.
// src/test/mocks/handlers.ts
export const handlers = [
http.get('/api/users/me', () => {
return HttpResponse.json({ id: 1, email: 'test@example.com' });
}),
];
// src/test/setup.ts
export const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Testing Library
Don't test implementation. Test behavior.
test('displays user email after load', async () => {
render(<UserProfile />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('test@example.com')).toBeInTheDocument();
});
});
Use queries in order: getByRole > getByLabelText > getByText > getByTestId. If you need getByTestId, your UI might not be accessible.
What to Test
Interactions:
test('submits form and shows success', async () => {
const user = userEvent.setup();
render(<ContactForm />);
await user.type(screen.getByLabelText('Email'), 'user@example.com');
await user.click(screen.getByRole('button', { name: /send/i }));
expect(screen.getByRole('button')).toBeDisabled(); // Loading state
await waitFor(() => {
expect(screen.getByText('Message sent')).toBeInTheDocument();
});
});
The disabled check catches double-submit bugs.
Error states:
test('shows error on failure', async () => {
server.use(
http.post('/api/contact', () =>
HttpResponse.json({ detail: 'Server error' }, { status: 500 })
)
);
// ... submit form, see error message
});
Override the default handler. Test that the UI shows the error.
The Gate
test:frontend:
stage: test
image: node:lts-slim
before_script:
- cd frontend && npm ci
script:
- npm test -- --run --coverage
allow_failure: false
--run makes Vitest exit after tests. Without it, CI hangs forever.
Copy, paste, adapt. It works.
The Point
Backend tests prove the API is correct. Frontend tests prove the UI is correct. Neither replaces the other.
A button that calls a working API but doesn't show the result? Backend tests pass. Users see nothing. Frontend tests catch it.
That's the deal.
Next up: The Python–TypeScript Contract — Backend works. Frontend works. Now make sure they work together.
Top comments (0)