diff --git a/frontend/src/components/tools/merge/MergeFileSorter.test.tsx b/frontend/src/components/tools/merge/MergeFileSorter.test.tsx new file mode 100644 index 000000000..302777261 --- /dev/null +++ b/frontend/src/components/tools/merge/MergeFileSorter.test.tsx @@ -0,0 +1,182 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import MergeFileSorter from './MergeFileSorter'; + +// Mock useTranslation with predictable return values +const mockT = vi.fn((key: string) => `mock-${key}`); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: mockT }) +})); + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('MergeFileSorter', () => { + const mockOnSortFiles = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render sort options dropdown, direction toggle, and sort button', () => { + render( + + + + ); + + // Should have a select dropdown (Mantine Select uses textbox role) + expect(screen.getByRole('textbox')).toBeInTheDocument(); + + // Should have direction toggle button + const buttons = screen.getAllByRole('button'); + expect(buttons).toHaveLength(2); // ActionIcon + Sort Button + + // Should have sort button with text + expect(screen.getByText('mock-merge.sortBy.sort')).toBeInTheDocument(); + }); + + test('should render description text', () => { + render( + + + + ); + + expect(screen.getByText('mock-merge.sortBy.description')).toBeInTheDocument(); + }); + + test('should have filename selected by default', () => { + render( + + + + ); + + const select = screen.getByRole('textbox'); + expect(select).toHaveValue('mock-merge.sortBy.filename'); + }); + + test('should show ascending direction by default', () => { + render( + + + + ); + + // Should show ascending arrow icon + const directionButton = screen.getAllByRole('button')[0]; + expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.ascending'); + }); + + test('should toggle direction when direction button is clicked', () => { + render( + + + + ); + + const directionButton = screen.getAllByRole('button')[0]; + + // Initially ascending + expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.ascending'); + + // Click to toggle to descending + fireEvent.click(directionButton); + expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.descending'); + + // Click again to toggle back to ascending + fireEvent.click(directionButton); + expect(directionButton).toHaveAttribute('title', 'mock-merge.sortBy.ascending'); + }); + + test('should call onSortFiles with correct parameters when sort button is clicked', () => { + render( + + + + ); + + const sortButton = screen.getByText('mock-merge.sortBy.sort'); + fireEvent.click(sortButton); + + // Should be called with default values (filename, ascending) + expect(mockOnSortFiles).toHaveBeenCalledWith('filename', true); + }); + + test('should call onSortFiles with dateModified when dropdown is changed', () => { + render( + + + + ); + + // Open the dropdown by clicking on the current selected value + const currentSelection = screen.getByText('mock-merge.sortBy.filename'); + fireEvent.mouseDown(currentSelection); + + // Click on the dateModified option + const dateModifiedOption = screen.getByText('mock-merge.sortBy.dateModified'); + fireEvent.click(dateModifiedOption); + + const sortButton = screen.getByText('mock-merge.sortBy.sort'); + fireEvent.click(sortButton); + + expect(mockOnSortFiles).toHaveBeenCalledWith('dateModified', true); + }); + + test('should call onSortFiles with descending direction when toggled', () => { + render( + + + + ); + + const directionButton = screen.getAllByRole('button')[0]; + const sortButton = screen.getByText('mock-merge.sortBy.sort'); + + // Toggle to descending + fireEvent.click(directionButton); + + // Click sort + fireEvent.click(sortButton); + + expect(mockOnSortFiles).toHaveBeenCalledWith('filename', false); + }); + + test('should handle complex user interaction sequence', () => { + render( + + + + ); + + const directionButton = screen.getAllByRole('button')[0]; + const sortButton = screen.getByText('mock-merge.sortBy.sort'); + + // 1. Change to dateModified + const currentSelection = screen.getByText('mock-merge.sortBy.filename'); + fireEvent.mouseDown(currentSelection); + const dateModifiedOption = screen.getByText('mock-merge.sortBy.dateModified'); + fireEvent.click(dateModifiedOption); + + // 2. Toggle to descending + fireEvent.click(directionButton); + + // 3. Click sort + fireEvent.click(sortButton); + + expect(mockOnSortFiles).toHaveBeenCalledWith('dateModified', false); + + // 4. Toggle back to ascending + fireEvent.click(directionButton); + + // 5. Sort again + fireEvent.click(sortButton); + + expect(mockOnSortFiles).toHaveBeenCalledWith('dateModified', true); + }); +}); diff --git a/frontend/src/components/tools/merge/MergeSettings.test.tsx b/frontend/src/components/tools/merge/MergeSettings.test.tsx new file mode 100644 index 000000000..7989fa6d0 --- /dev/null +++ b/frontend/src/components/tools/merge/MergeSettings.test.tsx @@ -0,0 +1,100 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import MergeSettings from './MergeSettings'; +import { MergeParameters } from '../../../hooks/tools/merge/useMergeParameters'; + +// Mock useTranslation with predictable return values +const mockT = vi.fn((key: string) => `mock-${key}`); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: mockT }) +})); + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('MergeSettings', () => { + const defaultParameters: MergeParameters = { + removeDigitalSignature: false, + generateTableOfContents: false, + }; + + const mockOnParameterChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render both merge option checkboxes', () => { + render( + + + + ); + + // Should render one checkbox for each parameter + const expectedCheckboxCount = Object.keys(defaultParameters).length; + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes).toHaveLength(expectedCheckboxCount); + }); + + test('should show correct initial checkbox states based on parameters', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + + // Both checkboxes should be unchecked initially + checkboxes.forEach(checkbox => { + expect(checkbox).not.toBeChecked(); + }); + }); + + test('should call onParameterChange with correct parameters when checkboxes are clicked', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + + // Click the first checkbox (removeDigitalSignature - should toggle from false to true) + fireEvent.click(checkboxes[0]); + expect(mockOnParameterChange).toHaveBeenCalledWith('removeDigitalSignature', true); + + // Click the second checkbox (generateTableOfContents - should toggle from false to true) + fireEvent.click(checkboxes[1]); + expect(mockOnParameterChange).toHaveBeenCalledWith('generateTableOfContents', true); + }); + + test('should call translation function with correct keys', () => { + render( + + + + ); + + // Verify that translation keys are being called + expect(mockT).toHaveBeenCalledWith('merge.removeDigitalSignature', 'Remove digital signature in the merged file?'); + expect(mockT).toHaveBeenCalledWith('merge.generateTableOfContents', 'Generate table of contents in the merged file?'); + }); + +}); diff --git a/frontend/src/hooks/tools/merge/useMergeOperation.test.ts b/frontend/src/hooks/tools/merge/useMergeOperation.test.ts new file mode 100644 index 000000000..9f23b8c6c --- /dev/null +++ b/frontend/src/hooks/tools/merge/useMergeOperation.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { useMergeOperation } from './useMergeOperation'; +import type { MergeParameters } from './useMergeParameters'; + +// Mock the useToolOperation hook +vi.mock('../shared/useToolOperation', () => ({ + useToolOperation: vi.fn() +})); + +// Mock the translation hook +const mockT = vi.fn((key: string) => `translated-${key}`); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: mockT }) +})); + +// Mock the error handler +vi.mock('../../../utils/toolErrorHandler', () => ({ + createStandardErrorHandler: vi.fn(() => 'error-handler-function') +})); + +// Import the mocked function +import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation'; + +describe('useMergeOperation', () => { + const mockUseToolOperation = vi.mocked(useToolOperation); + + const getToolConfig = (): ToolOperationConfig => mockUseToolOperation.mock.calls[0][0]; + + const mockToolOperationReturn: ToolOperationHook = { + files: [], + thumbnails: [], + downloadUrl: null, + downloadFilename: '', + isLoading: false, + errorMessage: null, + status: '', + isGeneratingThumbnails: false, + progress: null, + executeOperation: vi.fn(), + resetResults: vi.fn(), + clearError: vi.fn(), + cancelOperation: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseToolOperation.mockReturnValue(mockToolOperationReturn); + }); + + test('should build FormData correctly', () => { + renderHook(() => useMergeOperation()); + + const config = getToolConfig(); + const mockFiles = [ + new File(['content1'], 'file1.pdf', { type: 'application/pdf' }), + new File(['content2'], 'file2.pdf', { type: 'application/pdf' }) + ]; + const parameters: MergeParameters = { + removeDigitalSignature: true, + generateTableOfContents: false + }; + + const formData = config.buildFormData(parameters, mockFiles as any /* FIX ME */); + + // Verify files are appended + expect(formData.getAll('fileInput')).toHaveLength(2); + expect(formData.getAll('fileInput')[0]).toBe(mockFiles[0]); + expect(formData.getAll('fileInput')[1]).toBe(mockFiles[1]); + + // Verify parameters are appended correctly + expect(formData.get('sortType')).toBe('orderProvided'); + expect(formData.get('removeCertSign')).toBe('true'); + expect(formData.get('generateToc')).toBe('false'); + }); + + test('should handle response correctly', () => { + renderHook(() => useMergeOperation()); + + const config = getToolConfig(); + const mockBlob = new Blob(['merged content'], { type: 'application/pdf' }); + const mockFiles = [ + new File(['content1'], 'file1.pdf', { type: 'application/pdf' }), + new File(['content2'], 'file2.pdf', { type: 'application/pdf' }) + ]; + + const result = config.responseHandler!(mockBlob, mockFiles) as File[]; + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('merged_file1.pdf'); + expect(result[0].type).toBe('application/pdf'); + expect(result[0].size).toBe(mockBlob.size); + }); + + test('should return the hook result from useToolOperation', () => { + const { result } = renderHook(() => useMergeOperation()); + + expect(result.current).toBe(mockToolOperationReturn); + }); + + test('should use correct translation keys for error handling', () => { + renderHook(() => useMergeOperation()); + + expect(mockT).toHaveBeenCalledWith('merge.error.failed', 'An error occurred while merging the PDFs.'); + }); + + test('should build FormData with different parameter combinations', () => { + renderHook(() => useMergeOperation()); + + const config = getToolConfig(); + const mockFiles = [new File(['test'], 'test.pdf', { type: 'application/pdf' })]; + + // Test case 1: All options disabled + const params1: MergeParameters = { + removeDigitalSignature: false, + generateTableOfContents: false + }; + const formData1 = config.buildFormData(params1, mockFiles as any /* FIX ME */); + expect(formData1.get('removeCertSign')).toBe('false'); + expect(formData1.get('generateToc')).toBe('false'); + + // Test case 2: All options enabled + const params2: MergeParameters = { + removeDigitalSignature: true, + generateTableOfContents: true + }; + const formData2 = config.buildFormData(params2, mockFiles as any /* FIX ME */); + expect(formData2.get('removeCertSign')).toBe('true'); + expect(formData2.get('generateToc')).toBe('true'); + }); +}); diff --git a/frontend/src/hooks/tools/merge/useMergeParameters.test.ts b/frontend/src/hooks/tools/merge/useMergeParameters.test.ts new file mode 100644 index 000000000..c2ef59178 --- /dev/null +++ b/frontend/src/hooks/tools/merge/useMergeParameters.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useMergeParameters, defaultMergeParameters } from './useMergeParameters'; + +describe('useMergeParameters', () => { + test('should initialize with default parameters', () => { + const { result } = renderHook(() => useMergeParameters()); + + expect(result.current.parameters).toStrictEqual(defaultMergeParameters); + }); + + test.each([ + { paramName: 'removeDigitalSignature' as const, value: true }, + { paramName: 'removeDigitalSignature' as const, value: false }, + { paramName: 'generateTableOfContents' as const, value: true }, + { paramName: 'generateTableOfContents' as const, value: false } + ])('should update parameter $paramName to $value', ({ paramName, value }) => { + const { result } = renderHook(() => useMergeParameters()); + + act(() => { + result.current.updateParameter(paramName, value); + }); + + expect(result.current.parameters[paramName]).toBe(value); + }); + + test('should reset parameters to defaults', () => { + const { result } = renderHook(() => useMergeParameters()); + + // First, change some parameters + act(() => { + result.current.updateParameter('removeDigitalSignature', true); + result.current.updateParameter('generateTableOfContents', true); + }); + + expect(result.current.parameters.removeDigitalSignature).toBe(true); + expect(result.current.parameters.generateTableOfContents).toBe(true); + + // Then reset + act(() => { + result.current.resetParameters(); + }); + + expect(result.current.parameters).toStrictEqual(defaultMergeParameters); + }); + + test('should validate parameters correctly - always returns true', () => { + const { result } = renderHook(() => useMergeParameters()); + + // Default state should be valid + expect(result.current.validateParameters()).toBe(true); + + // Change parameters and validate again + act(() => { + result.current.updateParameter('removeDigitalSignature', true); + result.current.updateParameter('generateTableOfContents', true); + }); + + expect(result.current.validateParameters()).toBe(true); + + // Reset and validate again + act(() => { + result.current.resetParameters(); + }); + + expect(result.current.validateParameters()).toBe(true); + }); +});