diff --git a/frontend/src/components/tools/convert/ConvertSettings.tsx b/frontend/src/components/tools/convert/ConvertSettings.tsx index 93237113a..6ed20bb93 100644 --- a/frontend/src/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertSettings.tsx @@ -43,7 +43,6 @@ const ConvertSettings = ({ const { setSelectedFiles } = useFileSelectionActions(); const { setSelectedFiles: setContextSelectedFiles } = useFileContext(); - // Get all possible conversion endpoints to check their availability const allEndpoints = useMemo(() => { const endpoints = new Set(); Object.values(EXTENSION_TO_ENDPOINT).forEach(toEndpoints => { @@ -56,7 +55,6 @@ const ConvertSettings = ({ const { endpointStatus } = useMultipleEndpointsEnabled(allEndpoints); - // Function to check if a conversion is available based on endpoint const isConversionAvailable = (fromExt: string, toExt: string): boolean => { const endpointKey = EXTENSION_TO_ENDPOINT[fromExt]?.[toExt]; if (!endpointKey) return false; @@ -100,7 +98,6 @@ const ConvertSettings = ({ const autoTarget = availableToOptions.length === 1 ? availableToOptions[0].value : ''; onParameterChange('toExtension', autoTarget); - // Reset format-specific options onParameterChange('imageOptions', { colorType: COLOR_TYPES.COLOR, dpi: 300, @@ -118,30 +115,23 @@ const ConvertSettings = ({ onParameterChange('pdfaOptions', { outputFormat: 'pdfa-1', }); - // Disable smart detection when manually changing source format onParameterChange('isSmartDetection', false); onParameterChange('smartDetectionType', 'none'); - // Deselect files that don't match the new source format if (selectedFiles.length > 0 && value !== 'any') { const matchingFiles = selectedFiles.filter(file => { const extension = file.name.split('.').pop()?.toLowerCase() || ''; - // For 'image' source format, check if it's an image if (value === 'image') { return isImageFormat(extension); } - // For specific extensions, match exactly return extension === value; }); - // Only update selection if files were filtered out if (matchingFiles.length !== selectedFiles.length) { - // Update both selection contexts setSelectedFiles(matchingFiles); - // Update File Context selection with file IDs const matchingFileIds = matchingFiles.map(file => (file as any).id || file.name); setContextSelectedFiles(matchingFileIds); } @@ -150,7 +140,6 @@ const ConvertSettings = ({ const handleToExtensionChange = (value: string) => { onParameterChange('toExtension', value); - // Reset format-specific options when target extension changes onParameterChange('imageOptions', { colorType: COLOR_TYPES.COLOR, dpi: 300, diff --git a/frontend/src/components/tools/convert/GroupedFormatDropdown.tsx b/frontend/src/components/tools/convert/GroupedFormatDropdown.tsx index 83a955c51..f7e1c45ae 100644 --- a/frontend/src/components/tools/convert/GroupedFormatDropdown.tsx +++ b/frontend/src/components/tools/convert/GroupedFormatDropdown.tsx @@ -32,7 +32,6 @@ const GroupedFormatDropdown = ({ const theme = useMantineTheme(); const { colorScheme } = useMantineColorScheme(); - // Group options by category const groupedOptions = useMemo(() => { const groups: Record = {}; @@ -46,7 +45,6 @@ const GroupedFormatDropdown = ({ return groups; }, [options]); - // Get selected option label for display in format "Group (EXTENSION)" const selectedLabel = useMemo(() => { if (!value) return placeholder; const selected = options.find(opt => opt.value === value); diff --git a/frontend/src/hooks/tools/convert/useConvertOperation.ts b/frontend/src/hooks/tools/convert/useConvertOperation.ts index 1d8d3f08e..3e12ec9e8 100644 --- a/frontend/src/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/hooks/tools/convert/useConvertOperation.ts @@ -31,11 +31,6 @@ export interface ConvertOperationHook { clearError: () => void; } -// Utility functions for better maintainability - -/** - * Determines if multiple files should be processed separately - */ const shouldProcessFilesSeparately = ( selectedFiles: File[], parameters: ConvertParameters @@ -58,9 +53,6 @@ const shouldProcessFilesSeparately = ( ); }; -/** - * Creates a file from API response with fallback naming - */ const createFileFromResponse = ( responseData: any, headers: any, @@ -73,9 +65,6 @@ const createFileFromResponse = ( return createFileFromApiResponse(responseData, headers, fallbackFilename); }; -/** - * Generates thumbnails for multiple files - */ const generateThumbnailsForFiles = async (files: File[]): Promise => { const thumbnails: string[] = []; @@ -84,7 +73,6 @@ const generateThumbnailsForFiles = async (files: File[]): Promise => { const thumbnail = await generateThumbnailForFile(file); thumbnails.push(thumbnail); } catch (error) { - console.warn(`Failed to generate thumbnail for ${file.name}:`, error); thumbnails.push(''); } } @@ -92,16 +80,11 @@ const generateThumbnailsForFiles = async (files: File[]): Promise => { return thumbnails; }; -/** - * Creates download URL and filename for single or multiple files - */ const createDownloadInfo = async (files: File[]): Promise<{ url: string; filename: string }> => { if (files.length === 1) { - // Single file - direct download const url = window.URL.createObjectURL(files[0]); return { url, filename: files[0].name }; } else { - // Multiple files - create ZIP for convenient download const JSZip = (await import('jszip')).default; const zip = new JSZip(); @@ -125,7 +108,6 @@ export const useConvertOperation = (): ConvertOperationHook => { addFiles } = useFileContext(); - // Internal state management const [files, setFiles] = useState([]); const [thumbnails, setThumbnails] = useState([]); const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false); @@ -147,7 +129,6 @@ export const useConvertOperation = (): ConvertOperationHook => { const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions } = parameters; - // Add conversion-specific parameters if (isImageFormat(toExtension)) { formData.append("imageFormat", toExtension); formData.append("colorType", imageOptions.colorType); @@ -164,19 +145,15 @@ export const useConvertOperation = (): ConvertOperationHook => { formData.append("colorType", imageOptions.colorType); formData.append("autoRotate", imageOptions.autoRotate.toString()); } else if ((fromExtension === 'html' || fromExtension === 'zip') && toExtension === 'pdf') { - // HTML to PDF conversion with zoom level (includes ZIP files with HTML) formData.append("zoom", htmlOptions.zoomLevel.toString()); } else if (fromExtension === 'eml' && toExtension === 'pdf') { - // Email to PDF conversion with email-specific options formData.append("includeAttachments", emailOptions.includeAttachments.toString()); formData.append("maxAttachmentSizeMB", emailOptions.maxAttachmentSizeMB.toString()); formData.append("downloadHtml", emailOptions.downloadHtml.toString()); formData.append("includeAllRecipients", emailOptions.includeAllRecipients.toString()); } else if (fromExtension === 'pdf' && toExtension === 'pdfa') { - // PDF to PDF/A conversion with output format formData.append("outputFormat", pdfaOptions.outputFormat); } else if (fromExtension === 'pdf' && toExtension === 'csv') { - // CSV extraction - always process all pages for simplified workflow formData.append("pageNumbers", "all"); } @@ -250,12 +227,9 @@ export const useConvertOperation = (): ConvertOperationHook => { return; } - // Use utility function to determine processing strategy if (shouldProcessFilesSeparately(selectedFiles, parameters)) { - // Process each file separately with appropriate endpoint await executeMultipleSeparateFiles(parameters, selectedFiles); } else { - // Process all files together (default behavior) await executeSingleCombinedOperation(parameters, selectedFiles); } }, [t]); @@ -276,14 +250,9 @@ export const useConvertOperation = (): ConvertOperationHook => { const file = selectedFiles[i]; setStatus(t("convert.processingFile", `Processing file ${i + 1} of ${selectedFiles.length}...`)); - // Detect the specific file type for this file using the shared utility const fileExtension = detectFileExtension(file.name); - - // Determine the best endpoint for this specific file type let endpoint = getEndpointUrl(fileExtension, parameters.toExtension); let fileSpecificParams = { ...parameters, fromExtension: fileExtension }; - - // Fallback to file-to-pdf if specific endpoint doesn't exist if (!endpoint && parameters.toExtension === 'pdf') { endpoint = '/api/v1/convert/file/pdf'; console.log(`Using file-to-pdf fallback for ${fileExtension} file: ${file.name}`); @@ -291,10 +260,9 @@ export const useConvertOperation = (): ConvertOperationHook => { if (!endpoint) { console.error(`No endpoint available for ${fileExtension} to ${parameters.toExtension}`); - continue; // Skip this file + continue; } - // Create individual operation for this file const { operation, operationId, fileId } = createOperation(fileSpecificParams, [file]); const formData = buildFormData(fileSpecificParams, [file]); @@ -316,32 +284,24 @@ export const useConvertOperation = (): ConvertOperationHook => { } catch (error: any) { console.error(`Error converting file ${file.name}:`, error); markOperationFailed(fileId, operationId); - // Continue with other files even if one fails } } if (results.length > 0) { - console.log(`Multi-file conversion completed: ${results.length} files processed from ${selectedFiles.length} input files`); - console.log('Result files:', results.map(f => f.name)); - // Use utility function to generate thumbnails const generatedThumbnails = await generateThumbnailsForFiles(results); - // Set results for multiple files setFiles(results); setThumbnails(generatedThumbnails); - // Add all converted files to FileContext await addFiles(results); - // Use utility function to create download info try { const { url, filename } = await createDownloadInfo(results); setDownloadUrl(url); setDownloadFilename(filename); } catch (error) { console.error('Failed to create download info:', error); - // Fallback to first file only const url = window.URL.createObjectURL(results[0]); setDownloadUrl(url); setDownloadFilename(results[0].name); diff --git a/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts b/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts index 32f49e816..d64f01693 100644 --- a/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts +++ b/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts @@ -4,7 +4,7 @@ */ import { describe, test, expect } from 'vitest'; -import { renderHook, act } from '@testing-library/react'; +import { renderHook, act, waitFor } from '@testing-library/react'; import { useConvertParameters } from './useConvertParameters'; describe('useConvertParameters - Auto Detection & Smart Conversion', () => { @@ -53,23 +53,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { expect(result.current.parameters.toExtension).toBe('pdf'); // Fallback to file-to-pdf }); - test('should reset parameters when no files provided', () => { - const { result } = renderHook(() => useConvertParameters()); - - // First set some parameters - act(() => { - result.current.analyzeFileTypes([{ name: 'test.pdf' }]); - }); - - // Then analyze empty file list - act(() => { - result.current.analyzeFileTypes([]); - }); - - expect(result.current.parameters.fromExtension).toBe(''); - expect(result.current.parameters.toExtension).toBe(''); - expect(result.current.parameters.isSmartDetection).toBe(false); - }); + }); describe('Multiple Identical Files', () => { diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 04c716080..0f5ca8648 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -30,6 +30,56 @@ vi.mock('i18next-http-backend', () => ({ global.URL.createObjectURL = vi.fn(() => 'mocked-url') global.URL.revokeObjectURL = vi.fn() +// Mock File and Blob API methods that aren't available in jsdom +if (!globalThis.File.prototype.arrayBuffer) { + globalThis.File.prototype.arrayBuffer = function() { + // Return a simple ArrayBuffer with some mock data + const buffer = new ArrayBuffer(8); + const view = new Uint8Array(buffer); + view.set([1, 2, 3, 4, 5, 6, 7, 8]); + return Promise.resolve(buffer); + }; +} + +if (!globalThis.Blob.prototype.arrayBuffer) { + globalThis.Blob.prototype.arrayBuffer = function() { + // Return a simple ArrayBuffer with some mock data + const buffer = new ArrayBuffer(8); + const view = new Uint8Array(buffer); + view.set([1, 2, 3, 4, 5, 6, 7, 8]); + return Promise.resolve(buffer); + }; +} + +// Mock crypto.subtle for hashing in tests - force override even if exists +const mockHashBuffer = new ArrayBuffer(32); +const mockHashView = new Uint8Array(mockHashBuffer); +// Fill with predictable mock hash data +for (let i = 0; i < 32; i++) { + mockHashView[i] = i; +} + +// Force override crypto.subtle to avoid Node.js native implementation +Object.defineProperty(globalThis, 'crypto', { + value: { + subtle: { + digest: vi.fn().mockImplementation(async (algorithm: string, data: any) => { + // Always return the mock hash buffer regardless of input + return mockHashBuffer.slice(); + }), + }, + getRandomValues: vi.fn().mockImplementation((array: any) => { + // Mock getRandomValues if needed + for (let i = 0; i < array.length; i++) { + array[i] = Math.floor(Math.random() * 256); + } + return array; + }), + } as Crypto, + writable: true, + configurable: true, +}); + // Mock Worker for tests (Web Workers not available in test environment) global.Worker = vi.fn().mockImplementation(() => ({ postMessage: vi.fn(), diff --git a/frontend/src/tools/Convert.tsx b/frontend/src/tools/Convert.tsx index dafc133b4..832b20a82 100644 --- a/frontend/src/tools/Convert.tsx +++ b/frontend/src/tools/Convert.tsx @@ -27,12 +27,10 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const convertParams = useConvertParameters(); const convertOperation = useConvertOperation(); - // Endpoint validation const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled( convertParams.getEndpointName() ); - // Auto-scroll to bottom when content grows const scrollToBottom = () => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ @@ -42,13 +40,11 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { } }; - // Calculate state variables first const hasFiles = selectedFiles.length > 0; const hasResults = convertOperation.downloadUrl !== null; const filesCollapsed = hasFiles; const settingsCollapsed = hasResults; - // Auto-detect extension when files change - now with smart detection useEffect(() => { if (selectedFiles.length > 0) { convertParams.analyzeFileTypes(selectedFiles); @@ -62,17 +58,15 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { onPreviewFile?.(null); }, [convertParams.parameters, selectedFiles]); - // Auto-scroll when settings step becomes visible (files selected) useEffect(() => { if (hasFiles) { - setTimeout(scrollToBottom, 100); // Small delay to ensure DOM update + setTimeout(scrollToBottom, 100); } }, [hasFiles]); - // Auto-scroll when results appear useEffect(() => { if (hasResults) { - setTimeout(scrollToBottom, 100); // Small delay to ensure DOM update + setTimeout(scrollToBottom, 100); } }, [hasResults]); @@ -116,7 +110,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
- {/* Files Step */} { /> - {/* Settings Step */} { - {/* Results Step */}