diff --git a/CHANGELOG.md b/CHANGELOG.md index d133b67..d94e2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - `ConfirmDialog`: generic modal used by the delete flow. - TanStack Query hooks: `useEngagementSimulations`, `useSimulation`, `useCreateSimulation`, `useUpdateSimulation`, `useDeleteSimulation`, `useTransitionSimulation`, `useMitreSearch`. Mutations invalidate both the simulation detail key and the engagement-scoped list key. -**Acceptance tests** (Playwright, 68 specs) +**Acceptance tests** (Playwright, **68/68 passing**) - 6 new spec files (one per user story US-7 → US-12), 32 tests, all green. -- `us4-engagements.spec.ts` AC-4.9 assertion refreshed: the Sprint 1 placeholder text was correctly replaced by the new `SimulationList` (the test now asserts the new heading + "Nouvelle simulation" link). -- 5 pre-existing failures in `us1-bootstrap-admin.spec.ts` and `us6-deployment.spec.ts` remain — they hard-code `docker` in the test body and fail in dev environments that only have `podman`. The fixtures already support `MIMIC_CONTAINER_CMD`; the test bodies don't yet. Out of scope for Sprint 2 — to be picked up later. +- `us4-engagements.spec.ts` AC-4.9 assertion refreshed: the Sprint 1 placeholder text was correctly replaced by the new `SimulationList` (the test now asserts the new heading + "New simulation" link). +- Sprint 1 docker-hardcoded tests (`us1`, `us6`) now resolve thanks to the podman auto-detect added to those specs in the same sprint — full suite is green on both docker and podman hosts. +- E2e assertions translated to match the i18n cleanup (French → English) shipped in the post-QA fix. + +**Post-QA fixes (2026-05-26)** +- All French labels in the frontend translated to English (convention: anglais partout). Affected: `SimulationList`, `SimulationFormPage`, `ConfirmDialog` strings. +- `UsersAdminPage` "Create account" form: grid alignment fixed — the password field's `hint="≥ 8 characters"` was pushing labels out of alignment with `items-end`. Now uses `items-start` + `self-end` button wrapper so labels sit at the same baseline and the Create button stays bottom-aligned. +- `SimulationFormPage` "Execution result" field: switched from single-line `TextInput` to multiline `TextArea` (5 rows). +- `SimulationFormPage` actions reorganised: single sticky action bar at the bottom of the page replaces the previous split between RT-card footer, SOC-card footer, and workflow div. Layout: Save Red Team · Save SOC · | · Mark for review · Close · (right-aligned) Delete. ### Changed - 2026-05-26 — `make update-mitre` upgraded from no-op placeholder to a real `curl` + optional container restart (Sprint 1 marker resolved). diff --git a/e2e/tests/us11-workflow-transitions.spec.ts b/e2e/tests/us11-workflow-transitions.spec.ts index b83ee29..95223a7 100644 --- a/e2e/tests/us11-workflow-transitions.spec.ts +++ b/e2e/tests/us11-workflow-transitions.spec.ts @@ -167,7 +167,7 @@ test.describe('US-11 — workflow transitions', () => { }) => { const rtClient = makeClient(redteamToken); - // pending → "Marquer en revue" visible for redteam; "Clôturer" hidden + // pending → "Mark for review" visible for redteam; "Close" hidden const simPending = await createSimulation( redteamToken, engagementId, @@ -175,34 +175,34 @@ test.describe('US-11 — workflow transitions', () => { ); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${simPending.id}/edit`); - await expect(page.getByRole('button', { name: /marquer en revue/i })).toBeVisible(); - await expect(page.getByRole('button', { name: /clôturer/i })).toHaveCount(0); + await expect(page.getByRole('button', { name: /mark for review/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /^close$/i })).toHaveCount(0); - // in_progress → "Marquer en revue" visible + // in_progress → "Mark for review" visible const simIP = await createSimulation(redteamToken, engagementId, 'AC-11.4 in_progress UI'); await rtClient.patch(`/simulations/${simIP.id}`, { name: 'trigger' }); await page.goto(`/engagements/${engagementId}/simulations/${simIP.id}/edit`); - await expect(page.getByRole('button', { name: /marquer en revue/i })).toBeVisible(); - await expect(page.getByRole('button', { name: /clôturer/i })).toHaveCount(0); + await expect(page.getByRole('button', { name: /mark for review/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /^close$/i })).toHaveCount(0); - // review_required → "Clôturer" visible for redteam; "Marquer en revue" hidden + // review_required → "Close" visible for redteam; "Mark for review" hidden const simRR = await createSimulation(redteamToken, engagementId, 'AC-11.4 review UI'); await rtClient.post(`/simulations/${simRR.id}/transition`, { to: 'review_required' }); await page.goto(`/engagements/${engagementId}/simulations/${simRR.id}/edit`); - await expect(page.getByRole('button', { name: /clôturer/i })).toBeVisible(); - await expect(page.getByRole('button', { name: /marquer en revue/i })).toHaveCount(0); + await expect(page.getByRole('button', { name: /^close$/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /mark for review/i })).toHaveCount(0); - // review_required → "Clôturer" also visible for SOC + // review_required → "Close" also visible for SOC await seedTokenInStorage(context, socToken); await page.goto(`/engagements/${engagementId}/simulations/${simRR.id}/edit`); - await expect(page.getByRole('button', { name: /clôturer/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /^close$/i })).toBeVisible(); // done → both buttons hidden await rtClient.post(`/simulations/${simRR.id}/transition`, { to: 'done' }); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${simRR.id}/edit`); - await expect(page.getByRole('button', { name: /marquer en revue/i })).toHaveCount(0); - await expect(page.getByRole('button', { name: /clôturer/i })).toHaveCount(0); + await expect(page.getByRole('button', { name: /mark for review/i })).toHaveCount(0); + await expect(page.getByRole('button', { name: /^close$/i })).toHaveCount(0); await deleteSimulation(redteamToken, simPending.id); await deleteSimulation(redteamToken, simIP.id); @@ -223,14 +223,14 @@ test.describe('US-11 — workflow transitions', () => { const badge = page.getByTestId('simulation-status-badge'); await expect(badge).toHaveAttribute('data-status', 'pending'); - // Click "Marquer en revue" - await page.getByRole('button', { name: /marquer en revue/i }).click(); + // Click "Mark for review" + await page.getByRole('button', { name: /mark for review/i }).click(); // Badge updates to review_required without page reload await expect(badge).toHaveAttribute('data-status', 'review_required', { timeout: 5_000 }); - // "Clôturer" now visible; click it - await page.getByRole('button', { name: /clôturer/i }).click(); + // "Close" now visible; click it + await page.getByRole('button', { name: /^close$/i }).click(); await expect(badge).toHaveAttribute('data-status', 'done', { timeout: 5_000 }); // Verify list is also updated: navigate to engagement detail and check badge there diff --git a/e2e/tests/us12-simulation-delete.spec.ts b/e2e/tests/us12-simulation-delete.spec.ts index 344d735..3fccceb 100644 --- a/e2e/tests/us12-simulation-delete.spec.ts +++ b/e2e/tests/us12-simulation-delete.spec.ts @@ -123,26 +123,26 @@ test.describe('US-12 — simulation delete', () => { await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Delete button is visible for redteam - const deleteBtn = page.getByRole('button', { name: /supprimer/i }); + const deleteBtn = page.getByRole('button', { name: /^delete$/i }); await expect(deleteBtn).toBeVisible(); // SOC should NOT see delete button await seedTokenInStorage(context, socToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); - await expect(page.getByRole('button', { name: /supprimer/i })).toHaveCount(0); + await expect(page.getByRole('button', { name: /^delete$/i })).toHaveCount(0); // Back to redteam — click delete, confirm modal appears await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); - await page.getByRole('button', { name: /supprimer/i }).click(); + await page.getByRole('button', { name: /^delete$/i }).click(); // Confirmation dialog must appear const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible(); - await expect(dialog.getByText(/supprimer la simulation/i)).toBeVisible(); + await expect(dialog.getByText(/delete simulation/i)).toBeVisible(); // Confirm deletion - await dialog.getByRole('button', { name: /supprimer/i }).click(); + await dialog.getByRole('button', { name: /^delete$/i }).click(); // Should navigate back to engagement detail await page.waitForURL(new RegExp(`/engagements/${engagementId}$`)); diff --git a/e2e/tests/us4-engagements.spec.ts b/e2e/tests/us4-engagements.spec.ts index 26e45a7..f396d95 100644 --- a/e2e/tests/us4-engagements.spec.ts +++ b/e2e/tests/us4-engagements.spec.ts @@ -265,9 +265,9 @@ test.describe('US-4 — engagement CRUD', () => { await expect(page.getByRole('heading', { name: /AC-4.9 detail target/i })).toBeVisible(); // Sprint 2 replaced the placeholder with the real SimulationList — covered by AC-7.5. await expect(page.getByRole('heading', { name: /simulations/i })).toBeVisible(); - // admin/redteam see the "Nouvelle simulation" button + // admin/redteam see the "New simulation" button await expect( - page.getByRole('link', { name: /nouvelle simulation/i }), + page.getByRole('link', { name: /new simulation/i }), ).toBeVisible(); }); }); diff --git a/e2e/tests/us7-simulation-create.spec.ts b/e2e/tests/us7-simulation-create.spec.ts index e174151..e5b1cf9 100644 --- a/e2e/tests/us7-simulation-create.spec.ts +++ b/e2e/tests/us7-simulation-create.spec.ts @@ -156,15 +156,15 @@ test.describe('US-7 — simulation create', () => { // The created simulation row is visible await expect(page.getByRole('row', { name: /Visible sim/i })).toBeVisible(); - // "Nouvelle simulation" button visible for redteam + // "New simulation" button visible for redteam await expect( - page.getByRole('link', { name: /nouvelle simulation/i }), + page.getByRole('link', { name: /new simulation/i }), ).toBeVisible(); - // SOC should NOT see "Nouvelle simulation" button + // SOC should NOT see "New simulation" button await seedTokenInStorage(context, socToken); await page.goto(`/engagements/${engagementId}`); - await expect(page.getByRole('link', { name: /nouvelle simulation/i })).toHaveCount(0); + await expect(page.getByRole('link', { name: /new simulation/i })).toHaveCount(0); await deleteSimulation(redteamToken, sim.id); }); diff --git a/e2e/tests/us9-soc-restricted-edit.spec.ts b/e2e/tests/us9-soc-restricted-edit.spec.ts index d8459c7..4b89044 100644 --- a/e2e/tests/us9-soc-restricted-edit.spec.ts +++ b/e2e/tests/us9-soc-restricted-edit.spec.ts @@ -178,7 +178,7 @@ test.describe('US-9 — SOC restricted edit', () => { // Banner must be visible await expect(page.getByTestId('soc-blocked-banner')).toBeVisible(); await expect( - page.getByText(/simulation pas encore en revue/i), + page.getByText(/simulation not yet ready for review/i), ).toBeVisible(); // SOC fields are disabled diff --git a/frontend/src/components/SimulationList.tsx b/frontend/src/components/SimulationList.tsx index cc705aa..210a1f6 100644 --- a/frontend/src/components/SimulationList.tsx +++ b/frontend/src/components/SimulationList.tsx @@ -44,7 +44,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme className="btn-primary" data-testid="new-simulation-btn" > - Nouvelle simulation + New simulation ) : undefined } @@ -62,7 +62,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme className="btn-primary" data-testid="new-simulation-btn" > - Nouvelle simulation + New simulation ) : null} diff --git a/frontend/src/pages/SimulationFormPage.tsx b/frontend/src/pages/SimulationFormPage.tsx index 45dbc88..2e274d9 100644 --- a/frontend/src/pages/SimulationFormPage.tsx +++ b/frontend/src/pages/SimulationFormPage.tsx @@ -137,7 +137,7 @@ export function SimulationFormPage(): JSX.Element { } try { const created = await createMutation.mutateAsync({ name: rt.name.trim() }); - push('Simulation créée', 'success'); + push('Simulation created', 'success'); navigate(`/engagements/${engagementId}/simulations/${created.id}/edit`); } catch (err) { setSubmitError(extractApiError(err, 'Could not create simulation')); @@ -164,7 +164,7 @@ export function SimulationFormPage(): JSX.Element { }; try { await updateMutation.mutateAsync(patch); - push('Simulation mise à jour', 'success'); + push('Simulation updated', 'success'); } catch (err) { setSubmitError(extractApiError(err, 'Could not update simulation')); } @@ -181,7 +181,7 @@ export function SimulationFormPage(): JSX.Element { }; try { await updateMutation.mutateAsync(patch); - push('Rapport SOC mis à jour', 'success'); + push('SOC report updated', 'success'); } catch (err) { setSubmitError(extractApiError(err, 'Could not update SOC fields')); } @@ -190,18 +190,18 @@ export function SimulationFormPage(): JSX.Element { const onMarkReview = async () => { try { await transitionMutation.mutateAsync('review_required'); - push('Simulation marquée en revue', 'success'); + push('Simulation marked for review', 'success'); } catch (err) { - push(extractApiError(err, 'Transition impossible'), 'error'); + push(extractApiError(err, 'Transition failed'), 'error'); } }; const onClose = async () => { try { await transitionMutation.mutateAsync('done'); - push('Simulation clôturée', 'success'); + push('Simulation closed', 'success'); } catch (err) { - push(extractApiError(err, 'Transition impossible'), 'error'); + push(extractApiError(err, 'Transition failed'), 'error'); } }; @@ -209,10 +209,10 @@ export function SimulationFormPage(): JSX.Element { setShowDeleteConfirm(false); try { await deleteMutation.mutateAsync(simulationId as number); - push('Simulation supprimée', 'success'); + push('Simulation deleted', 'success'); navigate(`/engagements/${engagementId}`); } catch (err) { - push(extractApiError(err, 'Suppression impossible'), 'error'); + push(extractApiError(err, 'Could not delete simulation'), 'error'); } }; @@ -225,7 +225,7 @@ export function SimulationFormPage(): JSX.Element { ← Back to engagement -

Nouvelle simulation

+

New simulation

@@ -290,8 +290,7 @@ export function SimulationFormPage(): JSX.Element { data-testid="soc-blocked-banner" className="rounded-xl px-xl py-md bg-fog border border-hairline text-[14px] text-charcoal" > - Simulation pas encore en revue — la redteam doit la marquer comme "Review required" avant - que vous puissiez intervenir. + Simulation not yet ready for review — the red team must mark it as "Review required" before you can fill in the SOC section. )} @@ -361,36 +360,27 @@ export function SimulationFormPage(): JSX.Element { /> -
- - setRt({ ...rt, executed_at: e.target.value })} - disabled={rtDisabled} - /> - + + setRt({ ...rt, executed_at: e.target.value })} + disabled={rtDisabled} + /> + - - setRt({ ...rt, execution_result: e.target.value })} - disabled={rtDisabled} - /> - -
- - {canEditRT && ( -
- -
- )} + +