import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { CSPBuilder } from '../CSPBuilder'; describe('CSPBuilder', () => { const mockOnChange = vi.fn(); const mockOnValidate = vi.fn(); const defaultProps = { value: '', onChange: mockOnChange, onValidate: mockOnValidate, }; beforeEach(() => { vi.clearAllMocks(); }); it('should render with empty directives', () => { render(); expect(screen.getByText('Content Security Policy Builder')).toBeInTheDocument(); expect(screen.getByText('No CSP directives configured. Add directives above to build your policy.')).toBeInTheDocument(); }); it('should add a directive', async () => { render(); const valueInput = screen.getByPlaceholderText(/Enter value/); const addButton = screen.getByRole('button', { name: '' }); // Plus button fireEvent.change(valueInput, { target: { value: "'self'" } }); fireEvent.click(addButton); await waitFor(() => { expect(mockOnChange).toHaveBeenCalled(); }); const callArg = mockOnChange.mock.calls[0][0]; const parsed = JSON.parse(callArg); expect(parsed).toEqual([ { directive: 'default-src', values: ["'self'"] }, ]); }); it('should remove a directive', async () => { const initialValue = JSON.stringify([ { directive: 'default-src', values: ["'self'"] }, ]); render(); await waitFor(() => { const directiveElements = screen.getAllByText('default-src'); expect(directiveElements.length).toBeGreaterThan(0); }); // Find the X button in the directive row (not in the select) const allButtons = screen.getAllByRole('button'); const removeButton = allButtons.find(btn => { const svg = btn.querySelector('svg'); return svg && btn.closest('.bg-gray-50, .dark\\:bg-gray-800'); }); if (removeButton) { fireEvent.click(removeButton); } await waitFor(() => { expect(mockOnChange).toHaveBeenCalled(); }); }); it('should apply preset', async () => { render(); const presetButton = screen.getByRole('button', { name: 'Strict Default' }); fireEvent.click(presetButton); await waitFor(() => { expect(mockOnChange).toHaveBeenCalled(); }); const callArg = mockOnChange.mock.calls[0][0]; const parsed = JSON.parse(callArg); expect(parsed.length).toBeGreaterThan(0); expect(parsed[0].directive).toBe('default-src'); }); it('should toggle preview display', () => { render(); const previewButton = screen.getByRole('button', { name: /Show Preview/ }); expect(screen.queryByText('Generated CSP Header:')).not.toBeInTheDocument(); fireEvent.click(previewButton); expect(screen.getByRole('button', { name: /Hide Preview/ })).toBeInTheDocument(); }); it('should validate CSP and show warnings', async () => { render(); // Add an unsafe directive to trigger validation const directiveSelect = screen.getAllByRole('combobox')[0]; const valueInput = screen.getByPlaceholderText(/Enter value/); const addButton = screen.getAllByRole('button').find(btn => btn.querySelector('.lucide-plus')); fireEvent.change(directiveSelect, { target: { value: 'script-src' } }); fireEvent.change(valueInput, { target: { value: "'unsafe-inline'" } }); if (addButton) { fireEvent.click(addButton); } await waitFor(() => { expect(mockOnValidate).toHaveBeenCalled(); const validateCall = mockOnValidate.mock.calls.find(call => call[1]?.length > 0); expect(validateCall).toBeDefined(); }); const validateCall = mockOnValidate.mock.calls.find(call => call[1]?.length > 0); expect(validateCall?.[0]).toBe(false); expect(validateCall?.[1]).toContain('Using unsafe-inline or unsafe-eval weakens CSP protection'); }); it('should not add duplicate values to same directive', async () => { const initialValue = JSON.stringify([ { directive: 'default-src', values: ["'self'"] }, ]); render(); const valueInput = screen.getByPlaceholderText(/Enter value/); const allButtons = screen.getAllByRole('button'); const addButton = allButtons.find(btn => btn.querySelector('.lucide-plus')); // Try to add the same value again fireEvent.change(valueInput, { target: { value: "'self'" } }); if (addButton) { fireEvent.click(addButton); } await waitFor(() => { // Should not call onChange since it's a duplicate const calls = mockOnChange.mock.calls.filter(call => { const parsed = JSON.parse(call[0]); return parsed[0].values.filter((v: string) => v === "'self'").length > 1; }); expect(calls.length).toBe(0); }); }); it('should parse initial value correctly', () => { const initialValue = JSON.stringify([ { directive: 'default-src', values: ["'self'", 'https:'] }, { directive: 'script-src', values: ["'self'"] }, ]); render(); // Use getAllByText since these appear in both the select and the directive list const defaultSrcElements = screen.getAllByText('default-src'); expect(defaultSrcElements.length).toBeGreaterThan(0); const scriptSrcElements = screen.getAllByText('script-src'); expect(scriptSrcElements.length).toBeGreaterThan(0); const selfElements = screen.getAllByText("'self'"); expect(selfElements.length).toBeGreaterThan(0); }); it('should change directive selector', () => { render(); // Get the first combobox (the directive selector) const allSelects = screen.getAllByRole('combobox'); const directiveSelect = allSelects[0]; fireEvent.change(directiveSelect, { target: { value: 'script-src' } }); expect(directiveSelect).toHaveValue('script-src'); }); it('should handle Enter key to add directive', async () => { render(); const valueInput = screen.getByPlaceholderText(/Enter value/); fireEvent.change(valueInput, { target: { value: "'self'" } }); fireEvent.keyDown(valueInput, { key: 'Enter', code: 'Enter' }); await waitFor(() => { expect(mockOnChange).toHaveBeenCalled(); }); }); it('should not add empty values', () => { render(); const addButton = screen.getByRole('button', { name: '' }); fireEvent.click(addButton); expect(mockOnChange).not.toHaveBeenCalled(); }); it('should remove individual values from directive', async () => { const initialValue = JSON.stringify([ { directive: 'default-src', values: ["'self'", 'https:', 'data:'] }, ]); render(); const selfBadge = screen.getByText("'self'"); fireEvent.click(selfBadge); await waitFor(() => { expect(mockOnChange).toHaveBeenCalled(); }); const callArg = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0]; const parsed = JSON.parse(callArg); expect(parsed[0].values).not.toContain("'self'"); expect(parsed[0].values).toContain('https:'); }); it('should show success alert when valid', async () => { const validValue = JSON.stringify([ { directive: 'default-src', values: ["'self'"] }, ]); render(); await waitFor(() => { expect(screen.getByText('CSP configuration looks good!')).toBeInTheDocument(); }); }); });