fix(frontend): sprint 2 review fixes — MitrePicker input retention + SPA navigation
- MitreTechniquePicker: use hasHydratedFromProps ref so onChange(null,null) on
keystrokes does not propagate back and wipe inputValue mid-stroke
- SimulationList: replace window.location.href with useNavigate(); drop
redundant stopPropagation on inner Link
- SimulationFormPage: hoist canSaveSoc flag; replace duplicated ternary
expressions at onSubmit and button visibility guard
- SimulationFormPage: drop dead .replace(' ', 'T') on executed_at (isoformat
always emits 'T')
- Tests: add regression for MitrePicker input retention and SimulationList
SPA navigation (63 tests total)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,26 @@ describe('MitreTechniquePicker', () => {
|
|||||||
expect(screen.queryByRole('listbox')).toBeNull();
|
expect(screen.queryByRole('listbox')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('typing while techniqueId is null does not reset inputValue between keystrokes', async () => {
|
||||||
|
mock.onGet('/mitre/techniques').reply(200, []);
|
||||||
|
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, 'T10');
|
||||||
|
|
||||||
|
// Input must retain the full typed value — no mid-stroke reset
|
||||||
|
expect(input.value).toBe('T10');
|
||||||
|
});
|
||||||
|
|
||||||
it('shows inline error when API returns 503', async () => {
|
it('shows inline error when API returns 503', async () => {
|
||||||
mock.onGet('/mitre/techniques').reply(503, { error: 'mitre bundle not loaded' });
|
mock.onGet('/mitre/techniques').reply(503, { error: 'mitre bundle not loaded' });
|
||||||
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
const user = userEvent.setup({ advanceTimers: (ms) => vi.advanceTimersByTime(ms) });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor, fireEvent } from '@testing-library/react';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import { apiClient } from '@/api/client';
|
import { apiClient } from '@/api/client';
|
||||||
import { SimulationList } from '@/components/SimulationList';
|
import { SimulationList } from '@/components/SimulationList';
|
||||||
@@ -104,6 +104,25 @@ describe('SimulationList — admin/redteam', () => {
|
|||||||
});
|
});
|
||||||
expect(screen.getByTestId('new-simulation-btn')).toBeInTheDocument();
|
expect(screen.getByTestId('new-simulation-btn')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('clicking a row uses SPA navigation and does not trigger window.location change', async () => {
|
||||||
|
mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS);
|
||||||
|
const originalHref = window.location.href;
|
||||||
|
|
||||||
|
renderWithProviders(<SimulationList engagementId={42} />, {
|
||||||
|
routerProps: { initialEntries: ['/engagements/42'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Lateral movement test')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = screen.getByText('Lateral movement test').closest('tr') as HTMLElement;
|
||||||
|
fireEvent.click(row);
|
||||||
|
|
||||||
|
// window.location.href must be unchanged (no full-page reload)
|
||||||
|
expect(window.location.href).toBe(originalHref);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SimulationList — SOC role (no edit button)', () => {
|
describe('SimulationList — SOC role (no edit button)', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user