feat(frontend): sprint 2 — simulations UI + MITRE picker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
243
frontend/tests/MitreTechniquePicker.test.tsx
Normal file
243
frontend/tests/MitreTechniquePicker.test.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { screen, waitFor, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { MitreTechniquePicker } from '@/components/MitreTechniquePicker';
|
||||
import { renderWithProviders } from './utils';
|
||||
import type { MitreTechnique } from '@/api/types';
|
||||
|
||||
const TECHNIQUES: MitreTechnique[] = [
|
||||
{ id: 'T1059', name: 'Command and Scripting Interpreter', tactics: ['execution'] },
|
||||
{ id: 'T1059.001', name: 'PowerShell', tactics: ['execution'] },
|
||||
{ id: 'T1021', name: 'Remote Services', tactics: ['lateral-movement'] },
|
||||
];
|
||||
|
||||
describe('MitreTechniquePicker', () => {
|
||||
let mock: MockAdapter;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(apiClient);
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders input with placeholder', () => {
|
||||
vi.useRealTimers();
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(/Search by ID or name/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows preselected value when techniqueId and name provided', () => {
|
||||
vi.useRealTimers();
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId="T1059"
|
||||
techniqueName="Command and Scripting Interpreter"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByRole('combobox') as HTMLInputElement;
|
||||
expect(input.value).toContain('T1059');
|
||||
expect(input.value).toContain('Command and Scripting Interpreter');
|
||||
});
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
vi.useRealTimers();
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={vi.fn()}
|
||||
disabled
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('combobox')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('debounces search: no request fires before 200ms', async () => {
|
||||
mock.onGet('/mitre/techniques').reply(200, TECHNIQUES);
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
await user.click(input);
|
||||
await user.type(input, 'T');
|
||||
|
||||
// Before debounce fires
|
||||
expect(mock.history.get.length).toBe(0);
|
||||
|
||||
// Advance past debounce
|
||||
act(() => { vi.advanceTimersByTime(300); });
|
||||
|
||||
await waitFor(() => expect(mock.history.get.length).toBeGreaterThan(0));
|
||||
});
|
||||
|
||||
it('displays results in dropdown after debounce', async () => {
|
||||
mock.onGet('/mitre/techniques').reply(200, TECHNIQUES);
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
await user.click(input);
|
||||
await user.type(input, 'T1059');
|
||||
act(() => { vi.advanceTimersByTime(300); });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options.length).toBeGreaterThan(0);
|
||||
expect(options[0].textContent).toContain('T1059');
|
||||
});
|
||||
|
||||
it('selecting a result calls onChange with id and name', async () => {
|
||||
mock.onGet('/mitre/techniques').reply(200, TECHNIQUES);
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
await user.click(input);
|
||||
await user.type(input, 'T1059');
|
||||
act(() => { vi.advanceTimersByTime(300); });
|
||||
|
||||
await waitFor(() => screen.getByRole('listbox'));
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
await user.click(options[0]);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('T1059', 'Command and Scripting Interpreter');
|
||||
});
|
||||
|
||||
it('populates input display string after selection', async () => {
|
||||
mock.onGet('/mitre/techniques').reply(200, TECHNIQUES);
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox') as HTMLInputElement;
|
||||
await user.click(input);
|
||||
await user.type(input, 'T1059');
|
||||
act(() => { vi.advanceTimersByTime(300); });
|
||||
|
||||
await waitFor(() => screen.getByRole('listbox'));
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
await user.click(options[0]);
|
||||
|
||||
expect(input.value).toContain('T1059');
|
||||
expect(input.value).toContain('Command and Scripting Interpreter');
|
||||
});
|
||||
|
||||
it('keyboard ArrowDown + Enter selects item', async () => {
|
||||
mock.onGet('/mitre/techniques').reply(200, TECHNIQUES);
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
await user.click(input);
|
||||
await user.type(input, 'T105');
|
||||
act(() => { vi.advanceTimersByTime(300); });
|
||||
|
||||
await waitFor(() => screen.getByRole('listbox'));
|
||||
|
||||
await user.keyboard('{ArrowDown}');
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Escape closes the dropdown', async () => {
|
||||
mock.onGet('/mitre/techniques').reply(200, TECHNIQUES);
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
await user.click(input);
|
||||
await user.type(input, 'T1059');
|
||||
act(() => { vi.advanceTimersByTime(300); });
|
||||
|
||||
await waitFor(() => screen.getByRole('listbox'));
|
||||
|
||||
await user.keyboard('{Escape}');
|
||||
|
||||
expect(screen.queryByRole('listbox')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows inline error when API returns 503', async () => {
|
||||
mock.onGet('/mitre/techniques').reply(503, { error: 'mitre bundle not loaded' });
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||
|
||||
renderWithProviders(
|
||||
<MitreTechniquePicker
|
||||
techniqueId={null}
|
||||
techniqueName={null}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
await user.click(input);
|
||||
await user.type(input, 'T1059');
|
||||
act(() => { vi.advanceTimersByTime(300); });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user