feat(frontend): i18n MITRE components (matrix modal, picker, field, tag)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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&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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user