feat(frontend): sprint 5 — templates CRUD pages + nav + picker modal + dropdown
- types.ts: SimulationTemplate, SimulationTemplateCreateInput, SimulationTemplatePatchInput, extend SimulationCreateInput with template_id - api/templates.ts: listTemplates, getTemplate, createTemplate, updateTemplate, deleteTemplate - hooks/useTemplates.ts: useTemplates, useTemplate, useCreateTemplate, useUpdateTemplate, useDeleteTemplate (TanStack Query, invalidates ["templates"]) - TemplatesListPage: /admin/templates — table (name, MITRE count, created by, updated), New/Edit/Delete actions, loading/error/empty states - TemplateFormPage: /admin/templates/new + /admin/templates/:id/edit — controlled form with inline MITRE field (picker + matrix modal), ConfirmDialog for delete - TemplatePickerModal: reusable modal listing templates with empty state (AC-27.6) - SimulationList: replace "New simulation" link with split-button dropdown (Blank → /simulations/new | From template… → TemplatePickerModal + POST template_id) - Layout: "Templates" nav link (admin | redteam, before "Users") - App.tsx: /admin/templates routes gated roles=["admin","redteam"] - 26 new Vitest tests (118 total, 92 original preserved) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { SimulationList } from '@/components/SimulationList';
|
||||
@@ -105,6 +106,42 @@ describe('SimulationList — admin/redteam', () => {
|
||||
expect(screen.getByTestId('new-simulation-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows dropdown toggle button when simulations exist', async () => {
|
||||
mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS);
|
||||
renderWithProviders(<SimulationList engagementId={42} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Lateral movement test')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('new-simulation-dropdown-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens dropdown and shows "From template…" option', async () => {
|
||||
mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS);
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SimulationList engagementId={42} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Lateral movement test')).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByTestId('new-simulation-dropdown-toggle'));
|
||||
expect(screen.getByTestId('from-template-btn')).toBeInTheDocument();
|
||||
expect(screen.getByText('Blank')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens TemplatePickerModal when "From template…" is clicked', async () => {
|
||||
mock.onGet('/engagements/42/simulations').reply(200, SIMULATIONS);
|
||||
mock.onGet('/simulation-templates').reply(200, []);
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SimulationList engagementId={42} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Lateral movement test')).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByTestId('new-simulation-dropdown-toggle'));
|
||||
await user.click(screen.getByTestId('from-template-btn'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).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;
|
||||
|
||||
Reference in New Issue
Block a user