feat(frontend): full FR i18n via react-i18next + 2-tab engagement detail (sprint 12) #14

Merged
knacky merged 14 commits from sprint/12-i18n-fr into main 2026-06-22 08:25:51 +00:00
8 changed files with 69 additions and 60 deletions
Showing only changes of commit bdda02b07a - Show all commits

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoadingState } from './LoadingState'; import { LoadingState } from './LoadingState';
import { ErrorState } from './ErrorState'; import { ErrorState } from './ErrorState';
import { extractApiError } from '@/api/client'; import { extractApiError } from '@/api/client';
@@ -41,6 +42,7 @@ export function MitreMatrixModal({
onApply, onApply,
onCancel, onCancel,
}: MitreMatrixModalProps): JSX.Element | null { }: MitreMatrixModalProps): JSX.Element | null {
const { t } = useTranslation();
const { data: matrix, isLoading, isError, error } = useMitreMatrix(isOpen); const { data: matrix, isLoading, isError, error } = useMitreMatrix(isOpen);
const [selectedTechMap, setSelectedTechMap] = useState<Map<string, { id: string; name: string }>>( const [selectedTechMap, setSelectedTechMap] = useState<Map<string, { id: string; name: string }>>(
@@ -177,24 +179,24 @@ export function MitreMatrixModal({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-xl py-md border-b border-hairline flex-shrink-0"> <div className="flex items-center justify-between px-xl py-md border-b border-hairline flex-shrink-0">
<h2 id="matrix-modal-title" className="text-[18px] font-medium text-ink"> <h2 id="matrix-modal-title" className="text-[18px] font-medium text-ink">
MITRE ATT&amp;CK Matrix {t('mitre.matrix.title')}
</h2> </h2>
<input <input
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
placeholder="Filter techniques…" placeholder={t('mitre.matrix.filter')}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
className="text-input w-56 h-9 text-[14px]" className="text-input w-56 h-9 text-[14px]"
aria-label="Filter techniques" aria-label={t('mitre.matrix.filter')}
/> />
</div> </div>
{/* Body — overflow-y-auto, NO overflow-x */} {/* Body — overflow-y-auto, NO overflow-x */}
<div className="flex-1 overflow-y-auto overflow-x-hidden px-md py-md"> <div className="flex-1 overflow-y-auto overflow-x-hidden px-md py-md">
{isLoading && <LoadingState label="Loading MITRE matrix…" />} {isLoading && <LoadingState label={t('mitre.matrix.loading')} />}
{isError && ( {isError && (
<ErrorState message={extractApiError(error, 'Could not load MITRE matrix')} /> <ErrorState message={extractApiError(error, t('mitre.matrix.error'))} />
)} )}
{!isLoading && !isError && matrix && ( {!isLoading && !isError && matrix && (
<div <div
@@ -340,7 +342,7 @@ export function MitreMatrixModal({
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-end gap-md px-xl py-md border-t border-hairline flex-shrink-0"> <div className="flex items-center justify-end gap-md px-xl py-md border-t border-hairline flex-shrink-0">
<button type="button" className="btn-outline-ink" onClick={onCancel}> <button type="button" className="btn-outline-ink" onClick={onCancel}>
Cancel {t('mitre.matrix.close')}
</button> </button>
<button <button
type="button" type="button"
@@ -349,8 +351,8 @@ export function MitreMatrixModal({
disabled={isLoading || isError || (totalSelected === 0 && !hasInitial)} disabled={isLoading || isError || (totalSelected === 0 && !hasInitial)}
> >
{totalSelected === 0 {totalSelected === 0
? 'Clear all' ? t('mitre.matrix.clearAll')
: `Apply ${totalSelected} item${totalSelected !== 1 ? 's' : ''}`} : t(totalSelected === 1 ? 'mitre.matrix.applyItem_one' : 'mitre.matrix.applyItem_other', { count: totalSelected })}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState, type KeyboardEvent } from 'react'; import { useEffect, useRef, useState, type KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { extractApiError } from '@/api/client'; import { extractApiError } from '@/api/client';
import type { MitreTechnique } from '@/api/types'; import type { MitreTechnique } from '@/api/types';
import { useMitreSearch } from '@/hooks/useMitre'; import { useMitreSearch } from '@/hooks/useMitre';
@@ -14,6 +15,7 @@ export function MitreTechniquePicker({
onSelect, onSelect,
disabled = false, disabled = false,
}: MitreTechniquePickerProps): JSX.Element { }: MitreTechniquePickerProps): JSX.Element {
const { t } = useTranslation();
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -95,31 +97,31 @@ export function MitreTechniquePicker({
aria-expanded={open} aria-expanded={open}
aria-controls={listboxId} aria-controls={listboxId}
aria-activedescendant={activeIndex >= 0 ? `mitre-option-${activeIndex}` : undefined} aria-activedescendant={activeIndex >= 0 ? `mitre-option-${activeIndex}` : undefined}
aria-label="Search MITRE technique" aria-label={t('mitre.field.search')}
className="text-input" className="text-input"
value={inputValue} value={inputValue}
onChange={(e) => handleInputChange(e.target.value)} onChange={(e) => handleInputChange(e.target.value)}
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={disabled} disabled={disabled}
placeholder="Search by ID or name (e.g. T1059)" placeholder={t('mitre.field.search')}
autoComplete="off" autoComplete="off"
/> />
{open && ( {open && (
<div className="absolute z-20 w-full mt-xxs bg-paper border border-steel rounded-none overflow-hidden"> <div className="absolute z-20 w-full mt-xxs bg-paper border border-steel rounded-none overflow-hidden">
{isFetching && ( {isFetching && (
<div className="px-md py-sm text-[14px] text-graphite">Searching</div> <div className="px-md py-sm text-[14px] text-graphite">{t('state.loading')}</div>
)} )}
{isError && !isFetching && ( {isError && !isFetching && (
<div className="px-md py-sm text-[14px] text-bloom-deep" role="alert"> <div className="px-md py-sm text-[14px] text-bloom-deep" role="alert">
{extractApiError(error, 'MITRE search unavailable')} {extractApiError(error, t('mitre.matrix.error'))}
</div> </div>
)} )}
{!isFetching && !isError && items.length === 0 && query.trim().length > 0 && ( {!isFetching && !isError && items.length === 0 && query.trim().length > 0 && (
<div className="px-md py-sm text-[14px] text-graphite">No results</div> <div className="px-md py-sm text-[14px] text-graphite">{t('state.empty.default')}</div>
)} )}
{!isFetching && items.length > 0 && ( {!isFetching && items.length > 0 && (

View File

@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { MitreTechnique, MitreTacticRef } from '@/api/types'; import type { MitreTechnique, MitreTacticRef } from '@/api/types';
interface TechniqueTagProps { interface TechniqueTagProps {
@@ -18,6 +19,7 @@ export function MitreTechniqueTag({
onRemove, onRemove,
disabled = false, disabled = false,
}: TechniqueTagProps): JSX.Element { }: TechniqueTagProps): JSX.Element {
const { t } = useTranslation();
return ( return (
<span <span
data-testid="mitre-technique-tag" data-testid="mitre-technique-tag"
@@ -28,7 +30,7 @@ export function MitreTechniqueTag({
{!disabled && ( {!disabled && (
<button <button
type="button" type="button"
aria-label={`Remove ${technique.id}`} aria-label={`${t('mitre.tag.remove')} ${technique.id}`}
onClick={onRemove} onClick={onRemove}
className="text-primary-deep opacity-60 hover:opacity-100 leading-none" className="text-primary-deep opacity-60 hover:opacity-100 leading-none"
> >
@@ -45,6 +47,7 @@ export function MitreTacticTag({
onRemove, onRemove,
disabled = false, disabled = false,
}: TacticTagProps): JSX.Element { }: TacticTagProps): JSX.Element {
const { t } = useTranslation();
return ( return (
<span <span
data-testid="mitre-tactic-tag" data-testid="mitre-tactic-tag"
@@ -55,7 +58,7 @@ export function MitreTacticTag({
{!disabled && ( {!disabled && (
<button <button
type="button" type="button"
aria-label={`Remove ${tactic.id}`} aria-label={`${t('mitre.tag.remove')} ${tactic.id}`}
onClick={onRemove} onClick={onRemove}
className="text-white opacity-60 hover:opacity-100 leading-none" className="text-white opacity-60 hover:opacity-100 leading-none"
> >

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Grid2x2 } from 'lucide-react'; import { Grid2x2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { extractApiError } from '@/api/client'; import { extractApiError } from '@/api/client';
import type { MitreTechnique, MitreTacticRef } from '@/api/types'; import type { MitreTechnique, MitreTacticRef } from '@/api/types';
import { useUpdateSimulation } from '@/hooks/useSimulations'; import { useUpdateSimulation } from '@/hooks/useSimulations';
@@ -24,6 +25,7 @@ export function MitreTechniquesField({
engagementId, engagementId,
disabled = false, disabled = false,
}: MitreTechniquesFieldProps): JSX.Element { }: MitreTechniquesFieldProps): JSX.Element {
const { t } = useTranslation();
const [showMatrix, setShowMatrix] = useState(false); const [showMatrix, setShowMatrix] = useState(false);
const [showPicker, setShowPicker] = useState(false); const [showPicker, setShowPicker] = useState(false);
@@ -33,34 +35,34 @@ export function MitreTechniquesField({
const save = async (techniques: MitreTechnique[], nextTactics: MitreTacticRef[]) => { const save = async (techniques: MitreTechnique[], nextTactics: MitreTacticRef[]) => {
try { try {
await updateMutation.mutateAsync({ await updateMutation.mutateAsync({
technique_ids: techniques.map((t) => t.id), technique_ids: techniques.map((tech) => tech.id),
tactic_ids: nextTactics.map((t) => t.id), tactic_ids: nextTactics.map((tac) => tac.id),
}); });
push('Techniques updated', 'success'); push(t('mitre.field.savedToast'), 'success');
} catch (err) { } catch (err) {
push(extractApiError(err, 'Could not update techniques'), 'error'); push(extractApiError(err, t('mitre.field.errorToast')), 'error');
} }
}; };
const handleRemoveTechnique = (id: string) => { const handleRemoveTechnique = (id: string) => {
void save(value.filter((t) => t.id !== id), tactics); void save(value.filter((tech) => tech.id !== id), tactics);
}; };
const handleRemoveTactic = (id: string) => { const handleRemoveTactic = (id: string) => {
void save(value, tactics.filter((t) => t.id !== id)); void save(value, tactics.filter((tac) => tac.id !== id));
}; };
const handleSelect = (technique: MitreTechnique) => { const handleSelect = (technique: MitreTechnique) => {
if (value.some((t) => t.id === technique.id)) return; if (value.some((tech) => tech.id === technique.id)) return;
void save([...value, technique], tactics); void save([...value, technique], tactics);
setShowPicker(false); setShowPicker(false);
}; };
const handleMatrixApply = ({ techniques, tactics: newTactics }: MatrixSelection) => { const handleMatrixApply = ({ techniques, tactics: newTactics }: MatrixSelection) => {
setShowMatrix(false); setShowMatrix(false);
const merged = techniques.map((s) => { const merged = techniques.map((sel) => {
const existing = value.find((v) => v.id === s.id); const existing = value.find((v) => v.id === sel.id);
return existing ?? s; return existing ?? sel;
}); });
void save(merged, newTactics); void save(merged, newTactics);
}; };
@@ -72,22 +74,22 @@ export function MitreTechniquesField({
<div className="flex flex-col gap-sm"> <div className="flex flex-col gap-sm">
{/* Chips area */} {/* Chips area */}
{isEmpty ? ( {isEmpty ? (
<p className="text-[13px] text-graphite">No techniques selected</p> <p className="text-[13px] text-graphite">{t('mitre.field.empty')}</p>
) : ( ) : (
<div className="flex flex-wrap gap-xs" data-testid="techniques-tag-list"> <div className="flex flex-wrap gap-xs" data-testid="techniques-tag-list">
{tactics.map((t) => ( {tactics.map((tac) => (
<MitreTacticTag <MitreTacticTag
key={t.id} key={tac.id}
tactic={t} tactic={tac}
onRemove={() => handleRemoveTactic(t.id)} onRemove={() => handleRemoveTactic(tac.id)}
disabled={disabled || isPending} disabled={disabled || isPending}
/> />
))} ))}
{value.map((t) => ( {value.map((tech) => (
<MitreTechniqueTag <MitreTechniqueTag
key={t.id} key={tech.id}
technique={t} technique={tech}
onRemove={() => handleRemoveTechnique(t.id)} onRemove={() => handleRemoveTechnique(tech.id)}
disabled={disabled || isPending} disabled={disabled || isPending}
/> />
))} ))}
@@ -107,20 +109,20 @@ export function MitreTechniquesField({
onClick={() => setShowPicker(true)} onClick={() => setShowPicker(true)}
disabled={isPending} disabled={isPending}
> >
Search technique (e.g. T1059) {t('mitre.field.search')}
</button> </button>
)} )}
</div> </div>
<button <button
type="button" type="button"
aria-label="Open MITRE matrix" aria-label={t('mitre.field.openMatrix')}
onClick={() => { setShowPicker(false); setShowMatrix(true); }} onClick={() => { setShowPicker(false); setShowMatrix(true); }}
disabled={isPending} disabled={isPending}
className="flex-shrink-0 flex items-center justify-center w-9 h-9 rounded-none border border-steel text-graphite hover:text-ink hover:border-ink" className="flex-shrink-0 flex items-center justify-center w-9 h-9 rounded-none border border-steel text-graphite hover:text-ink hover:border-ink"
> >
<Grid2x2 size={16} /> <Grid2x2 size={16} />
</button> </button>
{isPending && <span className="text-[12px] text-graphite">Saving</span>} {isPending && <span className="text-[12px] text-graphite">{t('common.saving')}</span>}
</div> </div>
)} )}

View File

@@ -124,7 +124,7 @@ describe('MitreMatrixModal', () => {
); );
await user.click(t1078Btn!); await user.click(t1078Btn!);
await user.click(screen.getByRole('button', { name: /Apply/i })); await user.click(screen.getByRole('button', { name: /Appliquer/i }));
expect(onApply).toHaveBeenCalledWith( expect(onApply).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -148,7 +148,7 @@ describe('MitreMatrixModal', () => {
/>, />,
); );
await user.click(screen.getByRole('button', { name: /Cancel/i })); await user.click(screen.getByRole('button', { name: /Fermer/i }));
expect(onCancel).toHaveBeenCalled(); expect(onCancel).toHaveBeenCalled();
expect(onApply).not.toHaveBeenCalled(); expect(onApply).not.toHaveBeenCalled();
@@ -202,7 +202,7 @@ describe('MitreMatrixModal', () => {
await waitFor(() => screen.getByText('T1078')); await waitFor(() => screen.getByText('T1078'));
const searchInput = screen.getByPlaceholderText(/Filter techniques/i); const searchInput = screen.getByPlaceholderText(/Filtrer/i);
await user.type(searchInput, 'T1059'); await user.type(searchInput, 'T1059');
expect(screen.queryByText('T1078')).toBeNull(); expect(screen.queryByText('T1078')).toBeNull();
@@ -242,7 +242,7 @@ describe('MitreMatrixModal', () => {
); );
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole('button', { name: /Apply 1 item/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Appliquer \(1 élément\)/i })).toBeInTheDocument();
}); });
}); });
@@ -259,11 +259,11 @@ describe('MitreMatrixModal', () => {
await waitFor(() => screen.getByText('T1078')); await waitFor(() => screen.getByText('T1078'));
const applyBtn = screen.getByRole('button', { name: /Clear all/i }); const applyBtn = screen.getByRole('button', { name: /Tout effacer/i });
expect(applyBtn).toBeDisabled(); expect(applyBtn).toBeDisabled();
}); });
it('Apply button shows "Clear all" and is enabled when initial selection is deselected', async () => { it('Apply button shows "Tout effacer" and is enabled when initial selection is deselected', async () => {
const onApply = vi.fn(); const onApply = vi.fn();
const user = userEvent.setup(); const user = userEvent.setup();
@@ -284,7 +284,7 @@ describe('MitreMatrixModal', () => {
); );
await user.click(t1078Btn!); await user.click(t1078Btn!);
const applyBtn = screen.getByRole('button', { name: /Clear all/i }); const applyBtn = screen.getByRole('button', { name: /Tout effacer/i });
expect(applyBtn).not.toBeDisabled(); expect(applyBtn).not.toBeDisabled();
await user.click(applyBtn); await user.click(applyBtn);
expect(onApply).toHaveBeenCalledWith( expect(onApply).toHaveBeenCalledWith(

View File

@@ -30,7 +30,7 @@ describe('MitreTechniquePicker', () => {
vi.useRealTimers(); vi.useRealTimers();
renderWithProviders(<MitreTechniquePicker onSelect={vi.fn()} />); renderWithProviders(<MitreTechniquePicker onSelect={vi.fn()} />);
expect(screen.getByRole('combobox')).toBeInTheDocument(); expect(screen.getByRole('combobox')).toBeInTheDocument();
expect(screen.getByPlaceholderText(/Search by ID or name/i)).toBeInTheDocument(); expect(screen.getByPlaceholderText(/Rechercher une technique/i)).toBeInTheDocument();
}); });
it('is disabled when disabled prop is true', () => { it('is disabled when disabled prop is true', () => {

View File

@@ -23,7 +23,7 @@ describe('MitreTechniqueTag', () => {
renderWithProviders( renderWithProviders(
<MitreTechniqueTag technique={TECHNIQUE} onRemove={vi.fn()} />, <MitreTechniqueTag technique={TECHNIQUE} onRemove={vi.fn()} />,
); );
expect(screen.getByRole('button', { name: /Remove T1059/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Retirer T1059/i })).toBeInTheDocument();
}); });
it('clicking × calls onRemove', async () => { it('clicking × calls onRemove', async () => {
@@ -32,7 +32,7 @@ describe('MitreTechniqueTag', () => {
renderWithProviders( renderWithProviders(
<MitreTechniqueTag technique={TECHNIQUE} onRemove={onRemove} />, <MitreTechniqueTag technique={TECHNIQUE} onRemove={onRemove} />,
); );
await user.click(screen.getByRole('button', { name: /Remove T1059/i })); await user.click(screen.getByRole('button', { name: /Retirer T1059/i }));
expect(onRemove).toHaveBeenCalledOnce(); expect(onRemove).toHaveBeenCalledOnce();
}); });
@@ -40,7 +40,7 @@ describe('MitreTechniqueTag', () => {
renderWithProviders( renderWithProviders(
<MitreTechniqueTag technique={TECHNIQUE} onRemove={vi.fn()} disabled />, <MitreTechniqueTag technique={TECHNIQUE} onRemove={vi.fn()} disabled />,
); );
expect(screen.queryByRole('button', { name: /Remove/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Retirer/i })).toBeNull();
}); });
}); });
@@ -57,7 +57,7 @@ describe('MitreTacticTag', () => {
renderWithProviders( renderWithProviders(
<MitreTacticTag tactic={TACTIC} onRemove={vi.fn()} />, <MitreTacticTag tactic={TACTIC} onRemove={vi.fn()} />,
); );
expect(screen.getByRole('button', { name: /Remove TA0007/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Retirer TA0007/i })).toBeInTheDocument();
}); });
it('clicking × calls onRemove', async () => { it('clicking × calls onRemove', async () => {
@@ -66,7 +66,7 @@ describe('MitreTacticTag', () => {
renderWithProviders( renderWithProviders(
<MitreTacticTag tactic={TACTIC} onRemove={onRemove} />, <MitreTacticTag tactic={TACTIC} onRemove={onRemove} />,
); );
await user.click(screen.getByRole('button', { name: /Remove TA0007/i })); await user.click(screen.getByRole('button', { name: /Retirer TA0007/i }));
expect(onRemove).toHaveBeenCalledOnce(); expect(onRemove).toHaveBeenCalledOnce();
}); });
@@ -74,6 +74,6 @@ describe('MitreTacticTag', () => {
renderWithProviders( renderWithProviders(
<MitreTacticTag tactic={TACTIC} onRemove={vi.fn()} disabled />, <MitreTacticTag tactic={TACTIC} onRemove={vi.fn()} disabled />,
); );
expect(screen.queryByRole('button', { name: /Remove/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Retirer/i })).toBeNull();
}); });
}); });

View File

@@ -48,7 +48,7 @@ describe('MitreTechniquesField', () => {
renderWithProviders( renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />, <MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
); );
expect(screen.getByText(/No techniques selected/i)).toBeInTheDocument(); expect(screen.getByText(/Aucune technique associée/i)).toBeInTheDocument();
}); });
it('renders technique tags for each technique', () => { it('renders technique tags for each technique', () => {
@@ -72,16 +72,16 @@ describe('MitreTechniquesField', () => {
renderWithProviders( renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />, <MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
); );
expect(screen.getByRole('button', { name: /Open MITRE matrix/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Ouvrir la matrice/i })).toBeInTheDocument();
// The search placeholder button // The search placeholder button
expect(screen.getByRole('button', { name: /Search technique/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Rechercher une technique/i })).toBeInTheDocument();
}); });
it('hides input row when disabled', () => { it('hides input row when disabled', () => {
renderWithProviders( renderWithProviders(
<MitreTechniquesField value={[T1059]} tactics={[]} simulationId={7} engagementId={42} disabled />, <MitreTechniquesField value={[T1059]} tactics={[]} simulationId={7} engagementId={42} disabled />,
); );
expect(screen.queryByRole('button', { name: /Open MITRE matrix/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Ouvrir la matrice/i })).toBeNull();
}); });
it('× button on technique tag calls PATCH with technique removed', async () => { it('× button on technique tag calls PATCH with technique removed', async () => {
@@ -94,7 +94,7 @@ describe('MitreTechniquesField', () => {
<MitreTechniquesField value={[T1059, T1078]} tactics={[]} simulationId={7} engagementId={42} />, <MitreTechniquesField value={[T1059, T1078]} tactics={[]} simulationId={7} engagementId={42} />,
); );
const removeBtn = screen.getByRole('button', { name: /Remove T1059/i }); const removeBtn = screen.getByRole('button', { name: /Retirer T1059/i });
await user.click(removeBtn); await user.click(removeBtn);
await waitFor(() => { await waitFor(() => {
@@ -115,7 +115,7 @@ describe('MitreTechniquesField', () => {
<MitreTechniquesField value={[T1059]} tactics={[TA0007]} simulationId={7} engagementId={42} />, <MitreTechniquesField value={[T1059]} tactics={[TA0007]} simulationId={7} engagementId={42} />,
); );
const removeBtn = screen.getByRole('button', { name: /Remove TA0007/i }); const removeBtn = screen.getByRole('button', { name: /Retirer TA0007/i });
await user.click(removeBtn); await user.click(removeBtn);
await waitFor(() => { await waitFor(() => {
@@ -131,7 +131,7 @@ describe('MitreTechniquesField', () => {
renderWithProviders( renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />, <MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
); );
await user.click(screen.getByRole('button', { name: /Search technique/i })); await user.click(screen.getByRole('button', { name: /Rechercher une technique/i }));
expect(screen.getByRole('combobox')).toBeInTheDocument(); expect(screen.getByRole('combobox')).toBeInTheDocument();
}); });
@@ -142,7 +142,7 @@ describe('MitreTechniquesField', () => {
<MitreTechniquesField value={[T1059]} tactics={[]} simulationId={7} engagementId={42} />, <MitreTechniquesField value={[T1059]} tactics={[]} simulationId={7} engagementId={42} />,
); );
await user.click(screen.getByRole('button', { name: /Search technique/i })); await user.click(screen.getByRole('button', { name: /Rechercher une technique/i }));
const combobox = screen.getByRole('combobox'); const combobox = screen.getByRole('combobox');
await user.type(combobox, 'T1059'); await user.type(combobox, 'T1059');
@@ -161,7 +161,7 @@ describe('MitreTechniquesField', () => {
renderWithProviders( renderWithProviders(
<MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />, <MitreTechniquesField value={[]} tactics={[]} simulationId={7} engagementId={42} />,
); );
await user.click(screen.getByRole('button', { name: /Open MITRE matrix/i })); await user.click(screen.getByRole('button', { name: /Ouvrir la matrice/i }));
expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.getByRole('dialog')).toBeInTheDocument();
}); });
}); });