feat(frontend): i18n simulation pages (SimulationFormPage, SimulationList)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-21 23:31:21 +02:00
parent ea870af324
commit 5b93f880a3
3 changed files with 101 additions and 96 deletions

View File

@@ -1,11 +1,13 @@
import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { ChevronDown, Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { extractApiError } from '@/api/client';
import type { SimulationTemplate } from '@/api/types';
import { useAuth } from '@/hooks/useAuth';
import { useEngagementSimulations, useCreateSimulation } from '@/hooks/useSimulations';
import { useToast } from '@/hooks/useToast';
import { formatDateTime } from '@/lib/format';
import { LoadingState } from './LoadingState';
import { ErrorState } from './ErrorState';
import { EmptyState } from './EmptyState';
@@ -16,12 +18,8 @@ interface SimulationListProps {
engagementId: number;
}
function formatDate(value: string | null): string {
if (!value) return '—';
return value.replace('T', ' ').slice(0, 16);
}
function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.Element {
const { t } = useTranslation();
const navigate = useNavigate();
const { push } = useToast();
const [open, setOpen] = useState(false);
@@ -61,10 +59,10 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
try {
const sim = await createMutation.mutateAsync({ name: template.name, template_id: template.id });
setShowPicker(false);
push(`Created "${sim.name}" from template`, 'success');
push(t('simulation.form.toast.created'), 'success');
navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
} catch (err) {
push(extractApiError(err, 'Could not create simulation from template'), 'error');
push(extractApiError(err, t('simulation.form.error.create')), 'error');
}
};
@@ -77,7 +75,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
onClick={handleBlank}
data-testid="new-simulation-btn"
>
<Plus size={14} aria-hidden /> New
<Plus size={14} aria-hidden /> {t('simulation.list.new')}
</button>
<button
type="button"
@@ -133,16 +131,17 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
}
export function SimulationList({ engagementId }: SimulationListProps): JSX.Element {
const { t } = useTranslation();
const { data, isLoading, isError, error, refetch } = useEngagementSimulations(engagementId);
const { canEditEngagements } = useAuth();
const navigate = useNavigate();
if (isLoading) return <LoadingState label="Loading simulations…" />;
if (isLoading) return <LoadingState label={t('common.loading')} />;
if (isError) {
return (
<ErrorState
message={extractApiError(error, 'Could not load simulations')}
message={extractApiError(error, t('simulation.list.error'))}
onRetry={() => refetch()}
/>
);
@@ -151,8 +150,8 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
if (!data || data.length === 0) {
return (
<EmptyState
title="No simulations yet"
description="Create the first simulation to start tracking red team tests."
title={t('simulation.list.empty.title')}
description={t('simulation.list.empty.desc')}
action={
canEditEngagements ? (
<NewSimulationDropdown engagementId={engagementId} />
@@ -165,7 +164,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
return (
<div className="flex flex-col gap-md">
<div className="flex items-center justify-between">
<h2 className="text-[24px] font-medium text-ink">Simulations</h2>
<h2 className="text-[24px] font-medium text-ink">{t('engagement.detail.tabs.simulations')}</h2>
{canEditEngagements ? (
<NewSimulationDropdown engagementId={engagementId} />
) : null}
@@ -175,10 +174,10 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
<table className="table-compact w-full text-left">
<thead className="bg-cloud border-b border-hairline">
<tr>
<th>Name</th>
<th>MITRE</th>
<th>Status</th>
<th>Executed at</th>
<th>{t('simulation.list.col.name')}</th>
<th>{t('simulation.list.col.mitre')}</th>
<th>{t('simulation.list.col.status')}</th>
<th>{t('simulation.list.col.executedAt')}</th>
</tr>
</thead>
<tbody>
@@ -202,8 +201,8 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
<td className="text-charcoal font-mono">
{(() => {
const items = [
...(sim.tactics ?? []).map((t) => t.id),
...sim.techniques.map((t) => t.id),
...(sim.tactics ?? []).map((tactic) => tactic.id),
...sim.techniques.map((tech) => tech.id),
];
if (items.length === 0) return '—';
if (items.length === 1) return items[0];
@@ -214,7 +213,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
<SimulationStatusBadge status={sim.status} />
</td>
<td className="text-charcoal font-mono">
{formatDate(sim.executed_at)}
{sim.executed_at ? formatDateTime(sim.executed_at) : '—'}
</td>
</tr>
))}

View File

@@ -1,6 +1,7 @@
import { useEffect, useState, type FormEvent } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Save, RotateCcw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { extractApiError } from '@/api/client';
import type { SimulationPatchInput } from '@/api/types';
import { useAuth } from '@/hooks/useAuth';
@@ -58,6 +59,7 @@ const EMPTY_SOC: SocFormState = {
};
export function SimulationFormPage(): JSX.Element {
const { t } = useTranslation();
const { eid, sid } = useParams<{ eid: string; sid: string }>();
const engagementId = eid ? Number(eid) : undefined;
const simulationId = sid ? Number(sid) : undefined;
@@ -115,11 +117,11 @@ export function SimulationFormPage(): JSX.Element {
}
}, [isNew, detail.data]);
if (!isNew && detail.isLoading) return <LoadingState label="Loading simulation…" />;
if (!isNew && detail.isLoading) return <LoadingState label={t('simulation.form.loading')} />;
if (!isNew && detail.isError) {
return (
<ErrorState
message={extractApiError(detail.error, 'Could not load simulation')}
message={extractApiError(detail.error, t('simulation.form.error.load'))}
onRetry={() => detail.refetch()}
/>
);
@@ -148,13 +150,13 @@ export function SimulationFormPage(): JSX.Element {
e.preventDefault();
setNameError(null);
setSubmitError(null);
if (!rt.name.trim()) { setNameError('Name is required'); return; }
if (!rt.name.trim()) { setNameError(t('simulation.form.validation.nameRequired')); return; }
try {
const created = await createMutation.mutateAsync({ name: rt.name.trim() });
push('Simulation created', 'success');
push(t('simulation.form.toast.created'), 'success');
navigate(`/engagements/${engagementId}/simulations/${created.id}/edit`);
} catch (err) {
setSubmitError(extractApiError(err, 'Could not create simulation'));
setSubmitError(extractApiError(err, t('simulation.form.error.create')));
}
};
@@ -162,7 +164,7 @@ export function SimulationFormPage(): JSX.Element {
e.preventDefault();
setNameError(null);
setSubmitError(null);
if (!rt.name.trim()) { setNameError('Name is required'); return; }
if (!rt.name.trim()) { setNameError(t('simulation.form.validation.nameRequired')); return; }
const patch: SimulationPatchInput = {
name: rt.name.trim(),
description: rt.description.trim() || null,
@@ -173,9 +175,9 @@ export function SimulationFormPage(): JSX.Element {
};
try {
await updateMutation.mutateAsync(patch);
push('Simulation updated', 'success');
push(t('simulation.form.toast.updated'), 'success');
} catch (err) {
setSubmitError(extractApiError(err, 'Could not update simulation'));
setSubmitError(extractApiError(err, t('simulation.form.error.update')));
}
};
@@ -190,36 +192,36 @@ export function SimulationFormPage(): JSX.Element {
};
try {
await updateMutation.mutateAsync(patch);
push('SOC report updated', 'success');
push(t('simulation.form.toast.socUpdated'), 'success');
} catch (err) {
setSubmitError(extractApiError(err, 'Could not update SOC fields'));
setSubmitError(extractApiError(err, t('simulation.form.error.soc')));
}
};
const onMarkReview = async () => {
try {
await transitionMutation.mutateAsync('review_required');
push('Simulation marked for review', 'success');
push(t('simulation.form.toast.markedReview'), 'success');
} catch (err) {
push(extractApiError(err, 'Transition failed'), 'error');
push(extractApiError(err, t('simulation.form.error.transition')), 'error');
}
};
const onClose = async () => {
try {
await transitionMutation.mutateAsync('done');
push('Simulation closed', 'success');
push(t('simulation.form.toast.closed'), 'success');
} catch (err) {
push(extractApiError(err, 'Transition failed'), 'error');
push(extractApiError(err, t('simulation.form.error.transition')), 'error');
}
};
const onReopen = async () => {
try {
await transitionMutation.mutateAsync('review_required');
push('Simulation reopened', 'success');
push(t('simulation.form.toast.reopened'), 'success');
} catch (err) {
push(extractApiError(err, 'Transition failed'), 'error');
push(extractApiError(err, t('simulation.form.error.transition')), 'error');
}
};
@@ -227,10 +229,10 @@ export function SimulationFormPage(): JSX.Element {
setShowDeleteConfirm(false);
try {
await deleteMutation.mutateAsync(simulationId as number);
push('Simulation deleted', 'success');
push(t('simulation.form.toast.deleted'), 'success');
navigate(`/engagements/${engagementId}`);
} catch (err) {
push(extractApiError(err, 'Could not delete simulation'), 'error');
push(extractApiError(err, t('simulation.form.error.delete')), 'error');
}
};
@@ -240,12 +242,12 @@ export function SimulationFormPage(): JSX.Element {
return (
<div className="flex flex-col gap-xl max-w-2xl">
<header>
<BackLink to={`/engagements/${engagementId}`}>Back to engagement</BackLink>
<h1 className="text-[32px] font-medium leading-none mt-sm">New simulation</h1>
<BackLink to={`/engagements/${engagementId}`}>{t('engagement.detail.backTo')}</BackLink>
<h1 className="text-[32px] font-medium leading-none mt-sm">{t('simulation.form.title.new')}</h1>
</header>
<form onSubmit={onSubmitNew} noValidate className="card-product flex flex-col gap-md">
<FormField label="Name" htmlFor="sim-name" required error={nameError}>
<FormField label={t('simulation.form.field.name')} htmlFor="sim-name" required error={nameError}>
<TextInput
id="sim-name"
name="name"
@@ -260,11 +262,11 @@ export function SimulationFormPage(): JSX.Element {
) : null}
<div className="flex items-center gap-md pt-sm">
<button type="submit" className="btn-primary" disabled={submitting}>
{submitting ? 'Creating' : 'Create simulation'}
<button type="submit" className="btn-primary" disabled={submitting} data-testid="create-sim-btn">
{submitting ? t('simulation.form.btn.creating') : t('simulation.form.btn.create')}
</button>
<Link to={`/engagements/${engagementId}`} className="btn-outline-ink">
Cancel
{t('simulation.form.btn.cancel')}
</Link>
</div>
</form>
@@ -279,14 +281,15 @@ export function SimulationFormPage(): JSX.Element {
<div className="flex flex-col gap-xl">
<header className="flex items-start justify-between gap-md">
<div className="flex flex-col gap-sm">
<BackLink to={`/engagements/${engagementId}`}>Back to engagement</BackLink>
<BackLink to={`/engagements/${engagementId}`}>{t('engagement.detail.backTo')}</BackLink>
<h1 className="text-[32px] font-medium leading-none">{rt.name || simulation?.name}</h1>
{status ? (
<div className="flex items-center gap-md">
<SimulationStatusBadge status={status} />
{simulation?.created_by && (
<span className="text-[14px] text-graphite">
Created by <span className="text-ink">{simulation.created_by.username}</span>
{t('engagement.detail.createdBy')}{' '}
<span className="text-ink">{simulation.created_by.username}</span>
</span>
)}
</div>
@@ -297,7 +300,7 @@ export function SimulationFormPage(): JSX.Element {
{/* Done banner */}
{isDone && (
<AlertBanner variant="success">
This simulation is <strong>done</strong> and read-only. Use Reopen to make changes.
{t('simulation.form.banner.done')}
</AlertBanner>
)}
@@ -305,7 +308,7 @@ export function SimulationFormPage(): JSX.Element {
{socBlocked && (
<div data-testid="soc-blocked-banner">
<AlertBanner variant="warn">
Simulation not yet ready for review the red team must mark it as &quot;Review required&quot; before you can fill in the SOC section.
{t('simulation.form.banner.socNotReady')}
</AlertBanner>
</div>
)}
@@ -321,9 +324,9 @@ export function SimulationFormPage(): JSX.Element {
noValidate
className="card-product flex flex-col gap-md"
>
<h2 className="text-[20px] font-medium text-ink">Red Team</h2>
<h2 className="text-[20px] font-medium text-ink">{t('simulation.form.header.redTeam')}</h2>
<FormField label="Name" htmlFor="sim-name" required error={nameError}>
<FormField label={t('simulation.form.field.name')} htmlFor="sim-name" required error={nameError}>
<TextInput
id="sim-name"
name="name"
@@ -335,7 +338,7 @@ export function SimulationFormPage(): JSX.Element {
</FormField>
<div className="flex flex-col gap-xs">
<span className="text-[14px] font-medium text-ink">MITRE Techniques &amp; Tactics</span>
<span className="text-[14px] font-medium text-ink">{t('simulation.form.field.mitre')}</span>
<MitreTechniquesField
value={simulation?.techniques ?? []}
tactics={simulation?.tactics ?? []}
@@ -345,7 +348,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</div>
<FormField label="Description" htmlFor="sim-description">
<FormField label={t('simulation.form.field.description')} htmlFor="sim-description">
<TextArea
id="sim-description"
name="description"
@@ -355,7 +358,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</FormField>
<FormField label="Commands" htmlFor="sim-commands" hint="One command per line">
<FormField label={t('simulation.form.field.commands')} htmlFor="sim-commands" hint={t('simulation.form.field.commandsHint')}>
<TextArea
id="sim-commands"
name="commands"
@@ -366,7 +369,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</FormField>
<FormField label="Prerequisites" htmlFor="sim-prerequisites">
<FormField label={t('simulation.form.field.prerequisites')} htmlFor="sim-prerequisites">
<TextArea
id="sim-prerequisites"
name="prerequisites"
@@ -376,7 +379,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</FormField>
<FormField label="Executed at" htmlFor="sim-executed-at">
<FormField label={t('simulation.form.field.executedAt')} htmlFor="sim-executed-at">
<TextInput
id="sim-executed-at"
type="datetime-local"
@@ -387,7 +390,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</FormField>
<FormField label="Execution result" htmlFor="sim-exec-result">
<FormField label={t('simulation.form.field.executionResult')} htmlFor="sim-exec-result">
<TextArea
id="sim-exec-result"
name="execution_result"
@@ -406,7 +409,7 @@ export function SimulationFormPage(): JSX.Element {
className="btn-outline"
onClick={() => setShowC2Modal(true)}
>
Execute via C2
{t('simulation.form.btn.executeC2')}
</button>
<button
type="button"
@@ -414,7 +417,7 @@ export function SimulationFormPage(): JSX.Element {
className="btn-outline"
onClick={() => setShowImportModal(true)}
>
Import C2 history
{t('simulation.form.btn.importC2History')}
</button>
</div>
)}
@@ -433,9 +436,9 @@ export function SimulationFormPage(): JSX.Element {
noValidate
className="card-product flex flex-col gap-md"
>
<h2 className="text-[20px] font-medium text-ink">SOC</h2>
<h2 className="text-[20px] font-medium text-ink">{t('simulation.form.header.soc')}</h2>
<FormField label="Log source" htmlFor="sim-log-source">
<FormField label={t('simulation.form.field.logSource')} htmlFor="sim-log-source">
<TextInput
id="sim-log-source"
name="log_source"
@@ -445,7 +448,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</FormField>
<FormField label="Logs" htmlFor="sim-logs">
<FormField label={t('simulation.form.field.logs')} htmlFor="sim-logs">
<TextArea
id="sim-logs"
name="logs"
@@ -455,7 +458,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</FormField>
<FormField label="SOC comment" htmlFor="sim-soc-comment">
<FormField label={t('simulation.form.field.socComment')} htmlFor="sim-soc-comment">
<TextArea
id="sim-soc-comment"
name="soc_comment"
@@ -465,7 +468,7 @@ export function SimulationFormPage(): JSX.Element {
/>
</FormField>
<FormField label="Incident number" htmlFor="sim-incident">
<FormField label={t('simulation.form.field.incidentNumber')} htmlFor="sim-incident">
<TextInput
id="sim-incident"
name="incident_number"
@@ -493,7 +496,7 @@ export function SimulationFormPage(): JSX.Element {
data-testid="reopen-btn"
>
<RotateCcw size={14} aria-hidden />
Reopen
{t('simulation.form.btn.reopen')}
</button>
)}
@@ -501,13 +504,13 @@ export function SimulationFormPage(): JSX.Element {
{!isDone && canEditRT && (
<button type="submit" form="rt-form" className="btn-primary" disabled={submitting}>
<Save size={14} aria-hidden />
{updateMutation.isPending ? 'Saving' : 'Save'}
{updateMutation.isPending ? t('simulation.form.btn.saving') : t('simulation.form.btn.save')}
</button>
)}
{!isDone && canSaveSoc && (
<button type="submit" form="soc-form" className="btn-primary" disabled={submitting}>
<Save size={14} aria-hidden />
{updateMutation.isPending ? 'Saving' : 'Save SOC'}
{updateMutation.isPending ? t('simulation.form.btn.saving') : t('simulation.form.btn.saveSoc')}
</button>
)}
{showMarkReview && (
@@ -516,8 +519,9 @@ export function SimulationFormPage(): JSX.Element {
className="btn-outline"
onClick={onMarkReview}
disabled={transitionMutation.isPending}
data-testid="mark-review-btn"
>
Mark for review
{t('simulation.form.btn.markReview')}
</button>
)}
{showClose && (
@@ -526,8 +530,9 @@ export function SimulationFormPage(): JSX.Element {
className="btn-outline"
onClick={onClose}
disabled={transitionMutation.isPending}
data-testid="close-btn"
>
Close
{t('simulation.form.btn.close')}
</button>
)}
{!isDone && canEditEngagements && simulationId && (
@@ -536,18 +541,19 @@ export function SimulationFormPage(): JSX.Element {
className="btn-text-link text-bloom-deep ml-auto"
onClick={() => setShowDeleteConfirm(true)}
disabled={submitting}
data-testid="delete-btn"
>
Delete
{t('simulation.form.btn.delete')}
</button>
)}
</div>
{showDeleteConfirm && (
<ConfirmDialog
title="Delete simulation"
description="This action is permanent. The simulation will be deleted forever."
confirmLabel="Delete"
cancelLabel="Cancel"
title={t('simulation.form.deleteConfirm.title')}
description={t('simulation.form.deleteConfirm.desc')}
confirmLabel={t('simulation.form.deleteConfirm.confirm')}
cancelLabel={t('simulation.form.deleteConfirm.cancel')}
destructive
onConfirm={onDelete}
onCancel={() => setShowDeleteConfirm(false)}

View File

@@ -88,12 +88,12 @@ describe('SimulationFormPage — redteam mode (edit existing)', () => {
});
await waitFor(() => {
expect(screen.getByLabelText(/^Name/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled();
});
expect(screen.getByLabelText(/Description/i)).not.toBeDisabled();
expect(screen.getByLabelText(/Commands/i)).not.toBeDisabled();
expect(screen.getByLabelText(/Executed at/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Description/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Commandes/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Exécuté le/i)).not.toBeDisabled();
});
it('shows "Mark for review" button when status is pending', async () => {
@@ -102,7 +102,7 @@ describe('SimulationFormPage — redteam mode (edit existing)', () => {
});
await waitFor(() => {
expect(screen.getByRole('button', { name: /Mark for review/i })).toBeInTheDocument();
expect(screen.getByTestId('mark-review-btn')).toBeInTheDocument();
});
});
@@ -111,8 +111,8 @@ describe('SimulationFormPage — redteam mode (edit existing)', () => {
routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] },
});
await waitFor(() => screen.getByRole('button', { name: /Mark for review/i }));
expect(screen.queryByRole('button', { name: /^Close$/i })).toBeNull();
await waitFor(() => screen.getByTestId('mark-review-btn'));
expect(screen.queryByTestId('close-btn')).toBeNull();
});
it('shows "Mark for review" for in_progress status', async () => {
@@ -122,7 +122,7 @@ describe('SimulationFormPage — redteam mode (edit existing)', () => {
});
await waitFor(() => {
expect(screen.getByRole('button', { name: /Mark for review/i })).toBeInTheDocument();
expect(screen.getByTestId('mark-review-btn')).toBeInTheDocument();
});
});
@@ -133,7 +133,7 @@ describe('SimulationFormPage — redteam mode (edit existing)', () => {
});
await waitFor(() => {
expect(screen.getByRole('button', { name: /^Close$/i })).toBeInTheDocument();
expect(screen.getByTestId('close-btn')).toBeInTheDocument();
});
});
@@ -143,7 +143,7 @@ describe('SimulationFormPage — redteam mode (edit existing)', () => {
});
await waitFor(() => {
expect(screen.getByRole('button', { name: /^Delete$/i })).toBeInTheDocument();
expect(screen.getByTestId('delete-btn')).toBeInTheDocument();
});
});
});
@@ -179,10 +179,10 @@ describe('SimulationFormPage — SOC role + pending (blocked)', () => {
});
await waitFor(() => {
expect(screen.getByLabelText(/Log source/i)).toBeDisabled();
expect(screen.getByLabelText(/^Source de log/i)).toBeDisabled();
});
expect(screen.getByLabelText(/Incident number/i)).toBeDisabled();
expect(screen.getByLabelText(/^Numéro d'incident/i)).toBeDisabled();
});
it('Red Team inputs are disabled for SOC', async () => {
@@ -191,10 +191,10 @@ describe('SimulationFormPage — SOC role + pending (blocked)', () => {
});
await waitFor(() => {
expect(screen.getByLabelText(/^Name/i)).toBeDisabled();
expect(screen.getByLabelText(/^Nom/i)).toBeDisabled();
});
expect(screen.getByLabelText(/Description/i)).toBeDisabled();
expect(screen.getByLabelText(/^Description/i)).toBeDisabled();
});
});
@@ -218,10 +218,10 @@ describe('SimulationFormPage — SOC role + review_required (can edit SOC fields
});
await waitFor(() => {
expect(screen.getByLabelText(/Log source/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Source de log/i)).not.toBeDisabled();
});
expect(screen.getByLabelText(/Incident number/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Numéro d'incident/i)).not.toBeDisabled();
});
it('Red Team inputs remain disabled for SOC even when review_required', async () => {
@@ -230,7 +230,7 @@ describe('SimulationFormPage — SOC role + review_required (can edit SOC fields
});
await waitFor(() => {
expect(screen.getByLabelText(/^Name/i)).toBeDisabled();
expect(screen.getByLabelText(/^Nom/i)).toBeDisabled();
});
});
@@ -240,7 +240,7 @@ describe('SimulationFormPage — SOC role + review_required (can edit SOC fields
});
await waitFor(() => {
expect(screen.getByLabelText(/Log source/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Source de log/i)).not.toBeDisabled();
});
expect(screen.queryByTestId('soc-blocked-banner')).toBeNull();
@@ -252,7 +252,7 @@ describe('SimulationFormPage — SOC role + review_required (can edit SOC fields
});
await waitFor(() => {
expect(screen.getByRole('button', { name: /^Close$/i })).toBeInTheDocument();
expect(screen.getByTestId('close-btn')).toBeInTheDocument();
});
});
});
@@ -273,8 +273,8 @@ describe('SimulationFormPage — new simulation', () => {
renderWithProviders(<NewPage />, {
routerProps: { initialEntries: ['/engagements/42/simulations/new'] },
});
expect(screen.getByLabelText(/^Name/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Create simulation/i })).toBeInTheDocument();
expect(screen.getByLabelText(/^Nom/i)).toBeInTheDocument();
expect(screen.getByTestId('create-sim-btn')).toBeInTheDocument();
});
});
@@ -311,7 +311,7 @@ describe('SimulationFormPage — Execute via C2 button visibility', () => {
routerProps: { initialEntries: ['/engagements/42/simulations/7/edit'] },
});
await waitFor(() => {
expect(screen.getByLabelText(/^Name/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled();
});
expect(screen.queryByTestId('c2-execute-btn')).toBeNull();
});
@@ -369,7 +369,7 @@ describe('SimulationFormPage — C2 tasks panel visibility', () => {
});
// Wait for page data to load then confirm no panel
await waitFor(() => {
expect(screen.getByLabelText(/^Name/i)).not.toBeDisabled();
expect(screen.getByLabelText(/^Nom/i)).not.toBeDisabled();
});
expect(screen.queryByTestId('c2-tasks-panel')).toBeNull();
});