134 lines
5.0 KiB
TypeScript
134 lines
5.0 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 { ExportEngagementButton } from '@/components/ExportEngagementButton';
|
||
|
|
import { ToastViewport } from '@/components/Toast';
|
||
|
|
import { renderWithProviders } from './utils';
|
||
|
|
|
||
|
|
function ExportButtonWithToast({ engagementId }: { engagementId: number }) {
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<ExportEngagementButton engagementId={engagementId} />
|
||
|
|
<ToastViewport />
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
vi.mock('@/api/exports', () => ({
|
||
|
|
downloadEngagementExport: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
import { downloadEngagementExport } from '@/api/exports';
|
||
|
|
const mockDownload = downloadEngagementExport as ReturnType<typeof vi.fn>;
|
||
|
|
|
||
|
|
vi.mock('@/hooks/useAuth', () => ({
|
||
|
|
useAuth: () => ({
|
||
|
|
user: { id: 1, username: 'alice', role: 'admin', created_at: '2026-01-01' },
|
||
|
|
status: 'authenticated',
|
||
|
|
login: vi.fn(),
|
||
|
|
logout: vi.fn(),
|
||
|
|
isAdmin: true,
|
||
|
|
isRedteam: false,
|
||
|
|
isSoc: false,
|
||
|
|
canEditEngagements: true,
|
||
|
|
}),
|
||
|
|
}));
|
||
|
|
|
||
|
|
describe('ExportEngagementButton', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders Export button with chevron', () => {
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={1} />);
|
||
|
|
expect(screen.getByTestId('export-btn')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('export-dropdown-toggle')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('Export')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clicking primary opens dropdown with three formats', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={1} />);
|
||
|
|
await user.click(screen.getByTestId('export-btn'));
|
||
|
|
expect(screen.getByText('Markdown')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('CSV')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('PDF')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clicking outside closes dropdown', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={1} />);
|
||
|
|
await user.click(screen.getByTestId('export-btn'));
|
||
|
|
expect(screen.getByText('Markdown')).toBeInTheDocument();
|
||
|
|
await user.click(document.body);
|
||
|
|
expect(screen.queryByText('Markdown')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('Escape closes dropdown', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={1} />);
|
||
|
|
await user.click(screen.getByTestId('export-dropdown-toggle'));
|
||
|
|
expect(screen.getByText('Markdown')).toBeInTheDocument();
|
||
|
|
await user.keyboard('{Escape}');
|
||
|
|
expect(screen.queryByText('Markdown')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clicking Markdown triggers download with format=md', async () => {
|
||
|
|
mockDownload.mockResolvedValue(undefined);
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={42} />);
|
||
|
|
await user.click(screen.getByTestId('export-btn'));
|
||
|
|
await user.click(screen.getByTestId('export-format-md'));
|
||
|
|
expect(mockDownload).toHaveBeenCalledWith(42, 'md');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clicking CSV triggers download with format=csv', async () => {
|
||
|
|
mockDownload.mockResolvedValue(undefined);
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={42} />);
|
||
|
|
await user.click(screen.getByTestId('export-btn'));
|
||
|
|
await user.click(screen.getByTestId('export-format-csv'));
|
||
|
|
expect(mockDownload).toHaveBeenCalledWith(42, 'csv');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clicking PDF triggers download with format=pdf', async () => {
|
||
|
|
mockDownload.mockResolvedValue(undefined);
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={42} />);
|
||
|
|
await user.click(screen.getByTestId('export-btn'));
|
||
|
|
await user.click(screen.getByTestId('export-format-pdf'));
|
||
|
|
expect(mockDownload).toHaveBeenCalledWith(42, 'pdf');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('loading state disables items during in-flight', async () => {
|
||
|
|
let resolve!: () => void;
|
||
|
|
mockDownload.mockReturnValue(new Promise<void>((r) => { resolve = r; }));
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportEngagementButton engagementId={1} />);
|
||
|
|
await user.click(screen.getByTestId('export-btn'));
|
||
|
|
await user.click(screen.getByTestId('export-format-md'));
|
||
|
|
// Items should be disabled while in-flight
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('export-format-csv')).toBeDisabled();
|
||
|
|
expect(screen.getByTestId('export-format-pdf')).toBeDisabled();
|
||
|
|
});
|
||
|
|
resolve();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('error response shows toast', async () => {
|
||
|
|
mockDownload.mockRejectedValue(new Error('Export failed: 403 Forbidden'));
|
||
|
|
const user = userEvent.setup();
|
||
|
|
renderWithProviders(<ExportButtonWithToast engagementId={1} />);
|
||
|
|
await user.click(screen.getByTestId('export-btn'));
|
||
|
|
await user.click(screen.getByTestId('export-format-md'));
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('toast')).toHaveTextContent('Export failed: 403 Forbidden');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|