Files
mimic/frontend/tests/MitreMatrixModal.test.tsx
Knacky f5ea9d16af feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence

92/92 tests passing, typecheck and lint clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00

315 lines
8.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MockAdapter from 'axios-mock-adapter';
import { apiClient } from '@/api/client';
import { MitreMatrixModal } from '@/components/MitreMatrixModal';
import { renderWithProviders } from './utils';
import type { MitreTactic, MitreTechnique } from '@/api/types';
const MATRIX: MitreTactic[] = [
{
tactic_id: 'TA0001',
tactic_name: 'Initial Access',
techniques: [
{
id: 'T1078',
name: 'Valid Accounts',
subtechniques: [
{ id: 'T1078.001', name: 'Default Accounts' },
{ id: 'T1078.002', name: 'Domain Accounts' },
],
},
{
id: 'T1190',
name: 'Exploit Public-Facing Application',
subtechniques: [],
},
],
},
{
tactic_id: 'TA0002',
tactic_name: 'Execution',
techniques: [
{
id: 'T1059',
name: 'Command and Scripting Interpreter',
subtechniques: [{ id: 'T1059.001', name: 'PowerShell' }],
},
],
},
];
const SELECTION: MitreTechnique[] = [
{ id: 'T1078', name: 'Valid Accounts', tactics: ['initial-access'] },
];
const NO_TACTICS: never[] = [];
describe('MitreMatrixModal', () => {
let mock: MockAdapter;
beforeEach(() => {
mock = new MockAdapter(apiClient);
mock.onGet('/mitre/matrix').reply(200, MATRIX);
});
afterEach(() => {
mock.restore();
});
it('returns null when isOpen=false', () => {
const { container } = renderWithProviders(
<MitreMatrixModal
isOpen={false}
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(container.firstChild).toBeNull();
});
it('renders dialog with tactic columns when open', async () => {
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
await waitFor(() => {
expect(screen.getByText('Initial Access')).toBeInTheDocument();
expect(screen.getByText('Execution')).toBeInTheDocument();
});
});
it('renders techniques for each tactic', async () => {
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
await waitFor(() => {
expect(screen.getByText('T1078')).toBeInTheDocument();
expect(screen.getByText('T1059')).toBeInTheDocument();
});
});
it('Apply button calls onApply with selected techniques', async () => {
const onApply = vi.fn();
const user = userEvent.setup();
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={onApply}
onCancel={vi.fn()}
/>,
);
await waitFor(() => screen.getByText('T1078'));
const t1078Btn = screen.getAllByRole('button').find(
(btn) => btn.textContent?.includes('T1078') && !btn.getAttribute('aria-label'),
);
await user.click(t1078Btn!);
await user.click(screen.getByRole('button', { name: /Apply/i }));
expect(onApply).toHaveBeenCalledWith(
expect.objectContaining({
techniques: expect.arrayContaining([expect.objectContaining({ id: 'T1078' })]),
}),
);
});
it('Cancel button calls onCancel without onApply', async () => {
const onApply = vi.fn();
const onCancel = vi.fn();
const user = userEvent.setup();
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={onApply}
onCancel={onCancel}
/>,
);
await user.click(screen.getByRole('button', { name: /Cancel/i }));
expect(onCancel).toHaveBeenCalled();
expect(onApply).not.toHaveBeenCalled();
});
it('Escape key calls onCancel', async () => {
const onCancel = vi.fn();
const user = userEvent.setup();
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={onCancel}
/>,
);
await user.keyboard('{Escape}');
expect(onCancel).toHaveBeenCalled();
});
it('shows initial technique selection as selected (count in header)', async () => {
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={SELECTION}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
await waitFor(() => screen.getByText('T1078'));
expect(screen.getByText(/1 sel\./i)).toBeInTheDocument();
});
it('search filter narrows visible techniques', async () => {
const user = userEvent.setup();
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
await waitFor(() => screen.getByText('T1078'));
const searchInput = screen.getByPlaceholderText(/Filter techniques/i);
await user.type(searchInput, 'T1059');
expect(screen.queryByText('T1078')).toBeNull();
expect(screen.getByText('T1059')).toBeInTheDocument();
});
it('chevron expands subtechniques', async () => {
const user = userEvent.setup();
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
await waitFor(() => screen.getByText('T1078'));
expect(screen.queryByText(/Default Accounts/)).toBeNull();
const expandBtn = screen.getByRole('button', { name: /Expand T1078/i });
await user.click(expandBtn);
expect(screen.getByText(/Default Accounts/)).toBeInTheDocument();
});
it('Apply button shows item count when techniques selected', async () => {
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={SELECTION}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
await waitFor(() => {
expect(screen.getByRole('button', { name: /Apply 1 item/i })).toBeInTheDocument();
});
});
it('Apply button is disabled when nothing selected and no initial selection', async () => {
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={vi.fn()}
/>,
);
await waitFor(() => screen.getByText('T1078'));
const applyBtn = screen.getByRole('button', { name: /Clear all/i });
expect(applyBtn).toBeDisabled();
});
it('Apply button shows "Clear all" and is enabled when initial selection is deselected', async () => {
const onApply = vi.fn();
const user = userEvent.setup();
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={SELECTION}
initialTactics={NO_TACTICS}
onApply={onApply}
onCancel={vi.fn()}
/>,
);
await waitFor(() => screen.getByText('T1078'));
const t1078Btn = screen.getAllByRole('button').find(
(btn) => btn.textContent?.includes('T1078') && !btn.getAttribute('aria-label'),
);
await user.click(t1078Btn!);
const applyBtn = screen.getByRole('button', { name: /Clear all/i });
expect(applyBtn).not.toBeDisabled();
await user.click(applyBtn);
expect(onApply).toHaveBeenCalledWith(
expect.objectContaining({ techniques: [], tactics: [] }),
);
});
it('backdrop click calls onCancel', async () => {
const onCancel = vi.fn();
const user = userEvent.setup();
renderWithProviders(
<MitreMatrixModal
isOpen
initialTechniques={[]}
initialTactics={NO_TACTICS}
onApply={vi.fn()}
onCancel={onCancel}
/>,
);
const backdrop = document.querySelector('.bg-ink\\/60') as HTMLElement;
if (backdrop) await user.click(backdrop);
expect(onCancel).toHaveBeenCalled();
});
});