test(frontend): Vitest coverage on sprint 1 wiring
Adds a small test harness (src/test/testUtils.tsx): - renderWithProviders mounts a fresh QueryClient (no retries, no cache) + MemoryRouter so screens using useNavigate / <Link> don't crash. - installFetchMock(responses[]) replaces globalThis.fetch with a typed sequence of canned responses and records call URLs + init. Specs (10 cases, all green): LoginPage.test.tsx - happy path: submit posts to /api/v1/auth/login with credentials:'include', correct JSON body shape (username/password). - 401 surfaces "Identifiants invalides" and does NOT leak the backend detail string. - empty submit is intercepted by HTML5 `required` — no fetch fires. EngagementsPage.test.tsx - loading row renders while /engagements is in flight. - empty state renders on 200 []. - error state + Retry button render on 500. - populated table renders the snake_case fields correctly (name, client_name, c2_type uppercased). EngagementCreateDialog.test.tsx - client-side validation: empty name blocks submission, no fetch fires. - 422 Pydantic error on the `name` field maps to the inline message next to the input. - 201 success triggers onClose() and POSTs to /api/v1/engagements.
This commit is contained in:
61
frontend/src/screens/engagements/EngagementsPage.test.tsx
Normal file
61
frontend/src/screens/engagements/EngagementsPage.test.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { EngagementsPage } from './EngagementsPage';
|
||||
import { installFetchMock, renderWithProviders } from '@/test/testUtils';
|
||||
|
||||
describe('EngagementsPage', () => {
|
||||
let fetchMock: ReturnType<typeof installFetchMock>;
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock?.restore();
|
||||
});
|
||||
|
||||
it('shows the loading row while the engagements query is pending', () => {
|
||||
fetchMock = installFetchMock([]); // never resolve in this test
|
||||
// Replace with a long-lived promise so the query sits in pending state.
|
||||
const pending: typeof fetch = () => new Promise(() => null);
|
||||
globalThis.fetch = pending;
|
||||
renderWithProviders(<EngagementsPage />);
|
||||
expect(screen.getByText(/fetching engagements/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the empty state when the list is empty', async () => {
|
||||
fetchMock = installFetchMock([{ status: 200, body: [] }]);
|
||||
renderWithProviders(<EngagementsPage />);
|
||||
await screen.findByText(/no engagements yet/i);
|
||||
});
|
||||
|
||||
it('renders the error state on 500', async () => {
|
||||
fetchMock = installFetchMock([{ status: 500, body: { detail: 'internal_error' } }]);
|
||||
renderWithProviders(<EngagementsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/fetch failed/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders rows when the backend returns engagements', async () => {
|
||||
fetchMock = installFetchMock([
|
||||
{
|
||||
status: 200,
|
||||
body: [
|
||||
{
|
||||
id: 'eng_1',
|
||||
name: 'OPERATION ALPHA',
|
||||
client_name: 'Acme',
|
||||
description: null,
|
||||
status: 'active',
|
||||
c2_type: 'mythic',
|
||||
start_date: '2026-05-20',
|
||||
end_date: '2026-05-30',
|
||||
created_at: '2026-05-20T10:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
renderWithProviders(<EngagementsPage />);
|
||||
await screen.findByText('OPERATION ALPHA');
|
||||
expect(screen.getByText('Acme')).toBeInTheDocument();
|
||||
expect(screen.getByText('MYTHIC')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user