diff --git a/frontend/src/components/shared/ButtonSelector.test.tsx b/frontend/src/components/shared/ButtonSelector.test.tsx new file mode 100644 index 000000000..18268aa2b --- /dev/null +++ b/frontend/src/components/shared/ButtonSelector.test.tsx @@ -0,0 +1,185 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import ButtonSelector from './ButtonSelector'; + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('ButtonSelector', () => { + const mockOnChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render all options as buttons', () => { + const options = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ]; + + render( + + + + ); + + expect(screen.getByText('Option 1')).toBeInTheDocument(); + expect(screen.getByText('Option 2')).toBeInTheDocument(); + }); + + test('should highlight selected button with filled variant', () => { + const options = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ]; + + render( + + + + ); + + const selectedButton = screen.getByRole('button', { name: 'Option 1' }); + const unselectedButton = screen.getByRole('button', { name: 'Option 2' }); + + // Check data-variant attribute for filled/outline + expect(selectedButton).toHaveAttribute('data-variant', 'filled'); + expect(unselectedButton).toHaveAttribute('data-variant', 'outline'); + }); + + test('should call onChange when button is clicked', () => { + const options = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ]; + + render( + + + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Option 2' })); + + expect(mockOnChange).toHaveBeenCalledWith('option2'); + }); + + test('should handle undefined value (no selection)', () => { + const options = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ]; + + render( + + + + ); + + // Both buttons should be outlined when no value is selected + const button1 = screen.getByRole('button', { name: 'Option 1' }); + const button2 = screen.getByRole('button', { name: 'Option 2' }); + + expect(button1).toHaveAttribute('data-variant', 'outline'); + expect(button2).toHaveAttribute('data-variant', 'outline'); + }); + + test.each([ + { + description: 'disable buttons when disabled prop is true', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + globalDisabled: true, + expectedStates: [true, true], + }, + { + description: 'disable individual options when option.disabled is true', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2', disabled: true }, + ], + globalDisabled: false, + expectedStates: [false, true], + }, + ])('should $description', ({ options, globalDisabled, expectedStates }) => { + render( + + + + ); + + options.forEach((option, index) => { + const button = screen.getByRole('button', { name: option.label }); + expect(button).toHaveProperty('disabled', expectedStates[index]); + }); + }); + + test('should not call onChange when disabled button is clicked', () => { + const options = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2', disabled: true }, + ]; + + render( + + + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Option 2' })); + + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + test('should not apply fullWidth styling when fullWidth is false', () => { + const options = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ]; + + render( + + + + ); + + const button = screen.getByRole('button', { name: 'Option 1' }); + expect(button).not.toHaveStyle({ flex: '1' }); + }); +}); diff --git a/frontend/src/components/tools/redact/RedactAdvancedSettings.test.tsx b/frontend/src/components/tools/redact/RedactAdvancedSettings.test.tsx new file mode 100644 index 000000000..92f359abd --- /dev/null +++ b/frontend/src/components/tools/redact/RedactAdvancedSettings.test.tsx @@ -0,0 +1,211 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import RedactAdvancedSettings from './RedactAdvancedSettings'; +import { defaultParameters } from '../../../hooks/tools/redact/useRedactParameters'; + +// Mock useTranslation +const mockT = vi.fn((_key: string, fallback: string) => fallback); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: mockT }) +})); + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('RedactAdvancedSettings', () => { + const mockOnParameterChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render all advanced settings controls', () => { + render( + + + + ); + + expect(screen.getByText('Box Colour')).toBeInTheDocument(); + expect(screen.getByText('Custom Extra Padding')).toBeInTheDocument(); + expect(screen.getByText('Use Regex')).toBeInTheDocument(); + expect(screen.getByText('Whole Word Search')).toBeInTheDocument(); + expect(screen.getByText('Convert PDF to PDF-Image (Used to remove text behind the box)')).toBeInTheDocument(); + }); + + test('should display current parameter values', () => { + const customParameters = { + ...defaultParameters, + redactColor: '#FF0000', + customPadding: 0.5, + useRegex: true, + wholeWordSearch: true, + convertPDFToImage: false, + }; + + render( + + + + ); + + // Check color input value + const colorInput = screen.getByDisplayValue('#FF0000'); + expect(colorInput).toBeInTheDocument(); + + // Check number input value + const paddingInput = screen.getByDisplayValue('0.5'); + expect(paddingInput).toBeInTheDocument(); + + // Check checkbox states + const useRegexCheckbox = screen.getByLabelText('Use Regex'); + const wholeWordCheckbox = screen.getByLabelText('Whole Word Search'); + const convertCheckbox = screen.getByLabelText('Convert PDF to PDF-Image (Used to remove text behind the box)'); + + expect(useRegexCheckbox).toBeChecked(); + expect(wholeWordCheckbox).toBeChecked(); + expect(convertCheckbox).not.toBeChecked(); + }); + + test('should call onParameterChange when color is changed', () => { + render( + + + + ); + + const colorInput = screen.getByDisplayValue('#000000'); + fireEvent.change(colorInput, { target: { value: '#FF0000' } }); + + expect(mockOnParameterChange).toHaveBeenCalledWith('redactColor', '#FF0000'); + }); + + test('should call onParameterChange when padding is changed', () => { + render( + + + + ); + + const paddingInput = screen.getByDisplayValue('0.1'); + fireEvent.change(paddingInput, { target: { value: '0.5' } }); + + expect(mockOnParameterChange).toHaveBeenCalledWith('customPadding', 0.5); + }); + + test('should handle invalid padding values', () => { + render( + + + + ); + + const paddingInput = screen.getByDisplayValue('0.1'); + + // Simulate NumberInput onChange with invalid value (empty string) + const numberInput = paddingInput.closest('.mantine-NumberInput-root'); + if (numberInput) { + // Find the input and trigger change with empty value + fireEvent.change(paddingInput, { target: { value: '' } }); + + // The component should default to 0.1 for invalid values + expect(mockOnParameterChange).toHaveBeenCalledWith('customPadding', 0.1); + } + }); + + test.each([ + { + paramName: 'useRegex' as const, + label: 'Use Regex', + initialValue: false, + expectedValue: true, + }, + { + paramName: 'wholeWordSearch' as const, + label: 'Whole Word Search', + initialValue: false, + expectedValue: true, + }, + { + paramName: 'convertPDFToImage' as const, + label: 'Convert PDF to PDF-Image (Used to remove text behind the box)', + initialValue: true, + expectedValue: false, + }, + ])('should call onParameterChange when $paramName checkbox is toggled', ({ paramName, label, initialValue, expectedValue }) => { + const customParameters = { + ...defaultParameters, + [paramName]: initialValue, + }; + + render( + + + + ); + + const checkbox = screen.getByLabelText(label); + fireEvent.click(checkbox); + + expect(mockOnParameterChange).toHaveBeenCalledWith(paramName, expectedValue); + }); + + test.each([ + { controlType: 'color input', getValue: () => screen.getByDisplayValue('#000000') }, + { controlType: 'padding input', getValue: () => screen.getByDisplayValue('0.1') }, + { controlType: 'useRegex checkbox', getValue: () => screen.getByLabelText('Use Regex') }, + { controlType: 'wholeWordSearch checkbox', getValue: () => screen.getByLabelText('Whole Word Search') }, + { controlType: 'convertPDFToImage checkbox', getValue: () => screen.getByLabelText('Convert PDF to PDF-Image (Used to remove text behind the box)') }, + ])('should disable $controlType when disabled prop is true', ({ getValue }) => { + render( + + + + ); + + const control = getValue(); + expect(control).toBeDisabled(); + }); + + test('should have correct padding input constraints', () => { + render( + + + + ); + + // NumberInput in Mantine might not expose these attributes directly on the input element + // Instead, check that the NumberInput component is rendered with correct placeholder + const paddingInput = screen.getByPlaceholderText('0.1'); + expect(paddingInput).toBeInTheDocument(); + expect(paddingInput).toHaveDisplayValue('0.1'); + }); +}); diff --git a/frontend/src/components/tools/redact/RedactSingleStepSettings.test.tsx b/frontend/src/components/tools/redact/RedactSingleStepSettings.test.tsx new file mode 100644 index 000000000..2bfe94e06 --- /dev/null +++ b/frontend/src/components/tools/redact/RedactSingleStepSettings.test.tsx @@ -0,0 +1,183 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import RedactSingleStepSettings from './RedactSingleStepSettings'; +import { defaultParameters } from '../../../hooks/tools/redact/useRedactParameters'; + +// Mock useTranslation +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: vi.fn((_key: string, fallback: string) => fallback) }) +})); + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('RedactSingleStepSettings', () => { + const mockOnParameterChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render mode selector', () => { + render( + + + + ); + + expect(screen.getByText('Mode')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Automatic' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Manual' })).toBeInTheDocument(); + }); + + test('should render automatic mode settings when mode is automatic', () => { + render( + + + + ); + + // Default mode is automatic, so these should be visible + expect(screen.getByText('Words to Redact')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Enter a word')).toBeInTheDocument(); + expect(screen.getByText('Box Colour')).toBeInTheDocument(); + expect(screen.getByText('Use Regex')).toBeInTheDocument(); + }); + + test('should render manual mode settings when mode is manual', () => { + const manualParameters = { + ...defaultParameters, + mode: 'manual' as const, + }; + + render( + + + + ); + + // Manual mode should show placeholder text + expect(screen.getByText('Manual redaction interface will be available here when implemented.')).toBeInTheDocument(); + + // Automatic mode settings should not be visible + expect(screen.queryByText('Words to Redact')).not.toBeInTheDocument(); + }); + + test('should pass through parameter changes from automatic settings', () => { + render( + + + + ); + + // Test adding a word + const input = screen.getByPlaceholderText('Enter a word'); + const addButton = screen.getByRole('button', { name: '+ Add' }); + + fireEvent.change(input, { target: { value: 'TestWord' } }); + fireEvent.click(addButton); + + expect(mockOnParameterChange).toHaveBeenCalledWith('wordsToRedact', ['TestWord']); + }); + + test('should pass through parameter changes from advanced settings', () => { + render( + + + + ); + + // Test changing color + const colorInput = screen.getByDisplayValue('#000000'); + fireEvent.change(colorInput, { target: { value: '#FF0000' } }); + + expect(mockOnParameterChange).toHaveBeenCalledWith('redactColor', '#FF0000'); + }); + + test('should disable all controls when disabled prop is true', () => { + render( + + + + ); + + // Mode selector buttons should be disabled + expect(screen.getByRole('button', { name: 'Automatic' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Manual' })).toBeDisabled(); + + // Automatic settings controls should be disabled + expect(screen.getByPlaceholderText('Enter a word')).toBeDisabled(); + expect(screen.getByRole('button', { name: '+ Add' })).toBeDisabled(); + expect(screen.getByDisplayValue('#000000')).toBeDisabled(); + }); + + test('should show current parameter values in automatic mode', () => { + const customParameters = { + ...defaultParameters, + wordsToRedact: ['Word1', 'Word2'], + redactColor: '#FF0000', + useRegex: true, + customPadding: 0.5, + }; + + render( + + + + ); + + // Check that word tags are displayed + expect(screen.getByText('Word1')).toBeInTheDocument(); + expect(screen.getByText('Word2')).toBeInTheDocument(); + + // Check that color is displayed + expect(screen.getByDisplayValue('#FF0000')).toBeInTheDocument(); + + // Check that regex checkbox is checked + const useRegexCheckbox = screen.getByLabelText('Use Regex'); + expect(useRegexCheckbox).toBeChecked(); + + // Check that padding value is displayed + expect(screen.getByDisplayValue('0.5')).toBeInTheDocument(); + }); + + test('should maintain consistent spacing and layout', () => { + render( + + + + ); + + // Check that the Stack container exists + const container = screen.getByText('Mode').closest('.mantine-Stack-root'); + expect(container).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/tools/redact/WordsToRedactInput.test.tsx b/frontend/src/components/tools/redact/WordsToRedactInput.test.tsx new file mode 100644 index 000000000..35bb3dc5d --- /dev/null +++ b/frontend/src/components/tools/redact/WordsToRedactInput.test.tsx @@ -0,0 +1,191 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import WordsToRedactInput from './WordsToRedactInput'; + +// Mock useTranslation +const mockT = vi.fn((_key: string, fallback: string) => fallback); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: mockT }) +})); + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('WordsToRedactInput', () => { + const mockOnWordsChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render with title and input field', () => { + render( + + + + ); + + expect(screen.getByText('Words to Redact')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Enter a word')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '+ Add' })).toBeInTheDocument(); + }); + + test.each([ + { trigger: 'Add button click', action: (_input: HTMLElement, addButton: HTMLElement) => fireEvent.click(addButton) }, + { trigger: 'Enter key press', action: (input: HTMLElement) => fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }) }, + ])('should add word when $trigger', ({ action }) => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Enter a word'); + const addButton = screen.getByRole('button', { name: '+ Add' }); + + fireEvent.change(input, { target: { value: 'TestWord' } }); + action(input, addButton); + + expect(mockOnWordsChange).toHaveBeenCalledWith(['TestWord']); + }); + + test('should not add empty word', () => { + render( + + + + ); + + const addButton = screen.getByRole('button', { name: '+ Add' }); + + fireEvent.click(addButton); + + expect(mockOnWordsChange).not.toHaveBeenCalled(); + }); + + test('should not add duplicate word', () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Enter a word'); + const addButton = screen.getByRole('button', { name: '+ Add' }); + + fireEvent.change(input, { target: { value: 'Existing' } }); + fireEvent.click(addButton); + + expect(mockOnWordsChange).not.toHaveBeenCalled(); + }); + + test('should trim whitespace when adding word', () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Enter a word'); + const addButton = screen.getByRole('button', { name: '+ Add' }); + + fireEvent.change(input, { target: { value: ' TestWord ' } }); + fireEvent.click(addButton); + + expect(mockOnWordsChange).toHaveBeenCalledWith(['TestWord']); + }); + + test('should remove word when x button is clicked', () => { + render( + + + + ); + + const removeButtons = screen.getAllByText('×'); + fireEvent.click(removeButtons[0]); + + expect(mockOnWordsChange).toHaveBeenCalledWith(['Word2']); + }); + + test('should clear input after adding word', () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Enter a word') as HTMLInputElement; + const addButton = screen.getByRole('button', { name: '+ Add' }); + + fireEvent.change(input, { target: { value: 'TestWord' } }); + fireEvent.click(addButton); + + expect(input.value).toBe(''); + }); + + test.each([ + { description: 'disable Add button when input is empty', inputValue: '', expectedDisabled: true }, + { description: 'enable Add button when input has text', inputValue: 'TestWord', expectedDisabled: false }, + ])('should $description', ({ inputValue, expectedDisabled }) => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Enter a word'); + const addButton = screen.getByRole('button', { name: '+ Add' }); + + fireEvent.change(input, { target: { value: inputValue } }); + + expect(addButton).toHaveProperty('disabled', expectedDisabled); + }); + + test('should disable all controls when disabled prop is true', () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Enter a word'); + const addButton = screen.getByRole('button', { name: '+ Add' }); + const removeButton = screen.getByText('×'); + + expect(input).toBeDisabled(); + expect(addButton).toBeDisabled(); + expect(removeButton.closest('button')).toBeDisabled(); + }); +}); diff --git a/frontend/src/hooks/tools/redact/useRedactOperation.test.ts b/frontend/src/hooks/tools/redact/useRedactOperation.test.ts new file mode 100644 index 000000000..5da2bb6ca --- /dev/null +++ b/frontend/src/hooks/tools/redact/useRedactOperation.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { buildRedactFormData, redactOperationConfig, useRedactOperation } from './useRedactOperation'; +import { defaultParameters, RedactParameters } from './useRedactParameters'; +import { ToolOperationHook } from '../shared/useToolOperation'; + +// Mock the useToolOperation hook +vi.mock('../shared/useToolOperation', async () => { + const actual = await vi.importActual('../shared/useToolOperation'); // Need to keep ToolType etc. + return { + ...actual, + useToolOperation: vi.fn() + }; +}); + +// Mock the translation hook +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: vi.fn((_key: string, fallback: string) => fallback) }) +})); + +// Mock the error handler utility +vi.mock('../../../utils/toolErrorHandler', () => ({ + createStandardErrorHandler: vi.fn(() => vi.fn()) +})); + +describe('buildRedactFormData', () => { + const mockFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); + + test('should build form data for automatic mode', () => { + const parameters: RedactParameters = { + ...defaultParameters, + mode: 'automatic', + wordsToRedact: ['Confidential', 'Secret'], + useRegex: true, + wholeWordSearch: true, + redactColor: '#FF0000', + customPadding: 0.5, + convertPDFToImage: false, + }; + + const formData = buildRedactFormData(parameters, mockFile); + + expect(formData.get('fileInput')).toBe(mockFile); + expect(formData.get('listOfText')).toBe('Confidential\nSecret'); + expect(formData.get('useRegex')).toBe('true'); + expect(formData.get('wholeWordSearch')).toBe('true'); + expect(formData.get('redactColor')).toBe('FF0000'); // Hash should be removed + expect(formData.get('customPadding')).toBe('0.5'); + expect(formData.get('convertPDFToImage')).toBe('false'); + }); + + test('should handle empty words array', () => { + const parameters: RedactParameters = { + ...defaultParameters, + mode: 'automatic', + wordsToRedact: [], + }; + + const formData = buildRedactFormData(parameters, mockFile); + + expect(formData.get('listOfText')).toBe(''); + }); + + test('should join multiple words with newlines', () => { + const parameters: RedactParameters = { + ...defaultParameters, + mode: 'automatic', + wordsToRedact: ['Word1', 'Word2', 'Word3'], + }; + + const formData = buildRedactFormData(parameters, mockFile); + + expect(formData.get('listOfText')).toBe('Word1\nWord2\nWord3'); + }); + + test.each([ + { description: 'remove hash from redact color', redactColor: '#123456', expected: '123456' }, + { description: 'handle redact color without hash', redactColor: 'ABCDEF', expected: 'ABCDEF' }, + ])('should $description', ({ redactColor, expected }) => { + const parameters: RedactParameters = { + ...defaultParameters, + mode: 'automatic', + redactColor, + }; + + const formData = buildRedactFormData(parameters, mockFile); + + expect(formData.get('redactColor')).toBe(expected); + }); + + test('should convert boolean parameters to strings', () => { + const parameters: RedactParameters = { + ...defaultParameters, + mode: 'automatic', + useRegex: false, + wholeWordSearch: true, + convertPDFToImage: false, + }; + + const formData = buildRedactFormData(parameters, mockFile); + + expect(formData.get('useRegex')).toBe('false'); + expect(formData.get('wholeWordSearch')).toBe('true'); + expect(formData.get('convertPDFToImage')).toBe('false'); + }); + + test('should throw error for manual mode (not implemented)', () => { + const parameters: RedactParameters = { + ...defaultParameters, + mode: 'manual', + }; + + expect(() => buildRedactFormData(parameters, mockFile)).toThrow('Manual redaction not yet implemented'); + }); +}); + +describe('useRedactOperation', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should call useToolOperation with correct configuration', async () => { + const { useToolOperation } = await import('../shared/useToolOperation'); + const mockUseToolOperation = vi.mocked(useToolOperation); + + renderHook(() => useRedactOperation()); + + expect(mockUseToolOperation).toHaveBeenCalledWith({ + ...redactOperationConfig, + getErrorMessage: expect.any(Function), + }); + }); + + test('should provide error handler to useToolOperation', async () => { + const { useToolOperation } = await import('../shared/useToolOperation'); + const mockUseToolOperation = vi.mocked(useToolOperation); + + renderHook(() => useRedactOperation()); + + const callArgs = mockUseToolOperation.mock.calls[0][0]; + expect(typeof callArgs.getErrorMessage).toBe('function'); + }); +}); diff --git a/frontend/src/hooks/tools/redact/useRedactParameters.test.ts b/frontend/src/hooks/tools/redact/useRedactParameters.test.ts new file mode 100644 index 000000000..b87719ad9 --- /dev/null +++ b/frontend/src/hooks/tools/redact/useRedactParameters.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, test } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useRedactParameters, defaultParameters } from './useRedactParameters'; + +describe('useRedactParameters', () => { + test('should initialize with default parameters', () => { + const { result } = renderHook(() => useRedactParameters()); + + expect(result.current.parameters).toStrictEqual(defaultParameters); + }); + + test.each([ + { paramName: 'mode' as const, value: 'manual' as const }, + { paramName: 'wordsToRedact' as const, value: ['word1', 'word2'] }, + { paramName: 'useRegex' as const, value: true }, + { paramName: 'wholeWordSearch' as const, value: true }, + { paramName: 'redactColor' as const, value: '#FF0000' }, + { paramName: 'customPadding' as const, value: 0.5 }, + { paramName: 'convertPDFToImage' as const, value: false } + ])('should update parameter $paramName', ({ paramName, value }) => { + const { result } = renderHook(() => useRedactParameters()); + + act(() => { + result.current.updateParameter(paramName, value); + }); + + expect(result.current.parameters[paramName]).toStrictEqual(value); + }); + + test('should reset parameters to defaults', () => { + const { result } = renderHook(() => useRedactParameters()); + + // Modify some parameters + act(() => { + result.current.updateParameter('mode', 'manual'); + result.current.updateParameter('wordsToRedact', ['test']); + result.current.updateParameter('useRegex', true); + }); + + // Reset parameters + act(() => { + result.current.resetParameters(); + }); + + expect(result.current.parameters).toStrictEqual(defaultParameters); + }); + + describe('validation', () => { + test.each([ + { description: 'validate when wordsToRedact has non-empty words in automatic mode', wordsToRedact: ['word1', 'word2'], expected: true }, + { description: 'not validate when wordsToRedact is empty in automatic mode', wordsToRedact: [], expected: false }, + { description: 'not validate when wordsToRedact contains only empty strings in automatic mode', wordsToRedact: ['', ' ', ''], expected: false }, + { description: 'validate when wordsToRedact contains at least one non-empty word in automatic mode', wordsToRedact: ['', 'valid', ' '], expected: true }, + ])('should $description', ({ wordsToRedact, expected }) => { + const { result } = renderHook(() => useRedactParameters()); + + act(() => { + result.current.updateParameter('mode', 'automatic'); + result.current.updateParameter('wordsToRedact', wordsToRedact); + }); + + expect(result.current.validateParameters()).toBe(expected); + }); + + test('should not validate in manual mode (not implemented)', () => { + const { result } = renderHook(() => useRedactParameters()); + + act(() => { + result.current.updateParameter('mode', 'manual'); + }); + + expect(result.current.validateParameters()).toBe(false); + }); + }); + + describe('endpoint handling', () => { + test('should return correct endpoint for automatic mode', () => { + const { result } = renderHook(() => useRedactParameters()); + + act(() => { + result.current.updateParameter('mode', 'automatic'); + }); + + expect(result.current.getEndpointName()).toBe('/api/v1/security/auto-redact'); + }); + + test('should throw error for manual mode (not implemented)', () => { + const { result } = renderHook(() => useRedactParameters()); + + act(() => { + result.current.updateParameter('mode', 'manual'); + }); + + expect(() => result.current.getEndpointName()).toThrow('Manual redaction not yet implemented'); + }); + }); + + test('should maintain parameter state across updates', () => { + const { result } = renderHook(() => useRedactParameters()); + + act(() => { + result.current.updateParameter('redactColor', '#FF0000'); + result.current.updateParameter('customPadding', 0.5); + result.current.updateParameter('wordsToRedact', ['word1']); + }); + + // All parameters should be updated + expect(result.current.parameters.redactColor).toBe('#FF0000'); + expect(result.current.parameters.customPadding).toBe(0.5); + expect(result.current.parameters.wordsToRedact).toEqual(['word1']); + + // Other parameters should remain at defaults + expect(result.current.parameters.mode).toBe('automatic'); + expect(result.current.parameters.useRegex).toBe(false); + expect(result.current.parameters.wholeWordSearch).toBe(false); + expect(result.current.parameters.convertPDFToImage).toBe(true); + }); + + test('should handle array parameter updates correctly', () => { + const { result } = renderHook(() => useRedactParameters()); + + act(() => { + result.current.updateParameter('wordsToRedact', ['initial']); + }); + + expect(result.current.parameters.wordsToRedact).toEqual(['initial']); + + act(() => { + result.current.updateParameter('wordsToRedact', ['updated', 'multiple']); + }); + + expect(result.current.parameters.wordsToRedact).toEqual(['updated', 'multiple']); + }); +});