fix tests, removed excess logging and warnings

This commit is contained in:
Connor Yoh 2025-07-31 18:08:51 +01:00
parent f01e3081a7
commit 78e5594cc3
6 changed files with 55 additions and 83 deletions

View File

@ -43,7 +43,6 @@ const ConvertSettings = ({
const { setSelectedFiles } = useFileSelectionActions(); const { setSelectedFiles } = useFileSelectionActions();
const { setSelectedFiles: setContextSelectedFiles } = useFileContext(); const { setSelectedFiles: setContextSelectedFiles } = useFileContext();
// Get all possible conversion endpoints to check their availability
const allEndpoints = useMemo(() => { const allEndpoints = useMemo(() => {
const endpoints = new Set<string>(); const endpoints = new Set<string>();
Object.values(EXTENSION_TO_ENDPOINT).forEach(toEndpoints => { Object.values(EXTENSION_TO_ENDPOINT).forEach(toEndpoints => {
@ -56,7 +55,6 @@ const ConvertSettings = ({
const { endpointStatus } = useMultipleEndpointsEnabled(allEndpoints); const { endpointStatus } = useMultipleEndpointsEnabled(allEndpoints);
// Function to check if a conversion is available based on endpoint
const isConversionAvailable = (fromExt: string, toExt: string): boolean => { const isConversionAvailable = (fromExt: string, toExt: string): boolean => {
const endpointKey = EXTENSION_TO_ENDPOINT[fromExt]?.[toExt]; const endpointKey = EXTENSION_TO_ENDPOINT[fromExt]?.[toExt];
if (!endpointKey) return false; if (!endpointKey) return false;
@ -100,7 +98,6 @@ const ConvertSettings = ({
const autoTarget = availableToOptions.length === 1 ? availableToOptions[0].value : ''; const autoTarget = availableToOptions.length === 1 ? availableToOptions[0].value : '';
onParameterChange('toExtension', autoTarget); onParameterChange('toExtension', autoTarget);
// Reset format-specific options
onParameterChange('imageOptions', { onParameterChange('imageOptions', {
colorType: COLOR_TYPES.COLOR, colorType: COLOR_TYPES.COLOR,
dpi: 300, dpi: 300,
@ -118,30 +115,23 @@ const ConvertSettings = ({
onParameterChange('pdfaOptions', { onParameterChange('pdfaOptions', {
outputFormat: 'pdfa-1', outputFormat: 'pdfa-1',
}); });
// Disable smart detection when manually changing source format
onParameterChange('isSmartDetection', false); onParameterChange('isSmartDetection', false);
onParameterChange('smartDetectionType', 'none'); onParameterChange('smartDetectionType', 'none');
// Deselect files that don't match the new source format
if (selectedFiles.length > 0 && value !== 'any') { if (selectedFiles.length > 0 && value !== 'any') {
const matchingFiles = selectedFiles.filter(file => { const matchingFiles = selectedFiles.filter(file => {
const extension = file.name.split('.').pop()?.toLowerCase() || ''; const extension = file.name.split('.').pop()?.toLowerCase() || '';
// For 'image' source format, check if it's an image
if (value === 'image') { if (value === 'image') {
return isImageFormat(extension); return isImageFormat(extension);
} }
// For specific extensions, match exactly
return extension === value; return extension === value;
}); });
// Only update selection if files were filtered out
if (matchingFiles.length !== selectedFiles.length) { if (matchingFiles.length !== selectedFiles.length) {
// Update both selection contexts
setSelectedFiles(matchingFiles); setSelectedFiles(matchingFiles);
// Update File Context selection with file IDs
const matchingFileIds = matchingFiles.map(file => (file as any).id || file.name); const matchingFileIds = matchingFiles.map(file => (file as any).id || file.name);
setContextSelectedFiles(matchingFileIds); setContextSelectedFiles(matchingFileIds);
} }
@ -150,7 +140,6 @@ const ConvertSettings = ({
const handleToExtensionChange = (value: string) => { const handleToExtensionChange = (value: string) => {
onParameterChange('toExtension', value); onParameterChange('toExtension', value);
// Reset format-specific options when target extension changes
onParameterChange('imageOptions', { onParameterChange('imageOptions', {
colorType: COLOR_TYPES.COLOR, colorType: COLOR_TYPES.COLOR,
dpi: 300, dpi: 300,

View File

@ -32,7 +32,6 @@ const GroupedFormatDropdown = ({
const theme = useMantineTheme(); const theme = useMantineTheme();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
// Group options by category
const groupedOptions = useMemo(() => { const groupedOptions = useMemo(() => {
const groups: Record<string, FormatOption[]> = {}; const groups: Record<string, FormatOption[]> = {};
@ -46,7 +45,6 @@ const GroupedFormatDropdown = ({
return groups; return groups;
}, [options]); }, [options]);
// Get selected option label for display in format "Group (EXTENSION)"
const selectedLabel = useMemo(() => { const selectedLabel = useMemo(() => {
if (!value) return placeholder; if (!value) return placeholder;
const selected = options.find(opt => opt.value === value); const selected = options.find(opt => opt.value === value);

View File

@ -31,11 +31,6 @@ export interface ConvertOperationHook {
clearError: () => void; clearError: () => void;
} }
// Utility functions for better maintainability
/**
* Determines if multiple files should be processed separately
*/
const shouldProcessFilesSeparately = ( const shouldProcessFilesSeparately = (
selectedFiles: File[], selectedFiles: File[],
parameters: ConvertParameters parameters: ConvertParameters
@ -58,9 +53,6 @@ const shouldProcessFilesSeparately = (
); );
}; };
/**
* Creates a file from API response with fallback naming
*/
const createFileFromResponse = ( const createFileFromResponse = (
responseData: any, responseData: any,
headers: any, headers: any,
@ -73,9 +65,6 @@ const createFileFromResponse = (
return createFileFromApiResponse(responseData, headers, fallbackFilename); return createFileFromApiResponse(responseData, headers, fallbackFilename);
}; };
/**
* Generates thumbnails for multiple files
*/
const generateThumbnailsForFiles = async (files: File[]): Promise<string[]> => { const generateThumbnailsForFiles = async (files: File[]): Promise<string[]> => {
const thumbnails: string[] = []; const thumbnails: string[] = [];
@ -84,7 +73,6 @@ const generateThumbnailsForFiles = async (files: File[]): Promise<string[]> => {
const thumbnail = await generateThumbnailForFile(file); const thumbnail = await generateThumbnailForFile(file);
thumbnails.push(thumbnail); thumbnails.push(thumbnail);
} catch (error) { } catch (error) {
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
thumbnails.push(''); thumbnails.push('');
} }
} }
@ -92,16 +80,11 @@ const generateThumbnailsForFiles = async (files: File[]): Promise<string[]> => {
return thumbnails; return thumbnails;
}; };
/**
* Creates download URL and filename for single or multiple files
*/
const createDownloadInfo = async (files: File[]): Promise<{ url: string; filename: string }> => { const createDownloadInfo = async (files: File[]): Promise<{ url: string; filename: string }> => {
if (files.length === 1) { if (files.length === 1) {
// Single file - direct download
const url = window.URL.createObjectURL(files[0]); const url = window.URL.createObjectURL(files[0]);
return { url, filename: files[0].name }; return { url, filename: files[0].name };
} else { } else {
// Multiple files - create ZIP for convenient download
const JSZip = (await import('jszip')).default; const JSZip = (await import('jszip')).default;
const zip = new JSZip(); const zip = new JSZip();
@ -125,7 +108,6 @@ export const useConvertOperation = (): ConvertOperationHook => {
addFiles addFiles
} = useFileContext(); } = useFileContext();
// Internal state management
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [thumbnails, setThumbnails] = useState<string[]>([]); const [thumbnails, setThumbnails] = useState<string[]>([]);
const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false); const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false);
@ -147,7 +129,6 @@ export const useConvertOperation = (): ConvertOperationHook => {
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions } = parameters; const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions } = parameters;
// Add conversion-specific parameters
if (isImageFormat(toExtension)) { if (isImageFormat(toExtension)) {
formData.append("imageFormat", toExtension); formData.append("imageFormat", toExtension);
formData.append("colorType", imageOptions.colorType); formData.append("colorType", imageOptions.colorType);
@ -164,19 +145,15 @@ export const useConvertOperation = (): ConvertOperationHook => {
formData.append("colorType", imageOptions.colorType); formData.append("colorType", imageOptions.colorType);
formData.append("autoRotate", imageOptions.autoRotate.toString()); formData.append("autoRotate", imageOptions.autoRotate.toString());
} else if ((fromExtension === 'html' || fromExtension === 'zip') && toExtension === 'pdf') { } 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()); formData.append("zoom", htmlOptions.zoomLevel.toString());
} else if (fromExtension === 'eml' && toExtension === 'pdf') { } else if (fromExtension === 'eml' && toExtension === 'pdf') {
// Email to PDF conversion with email-specific options
formData.append("includeAttachments", emailOptions.includeAttachments.toString()); formData.append("includeAttachments", emailOptions.includeAttachments.toString());
formData.append("maxAttachmentSizeMB", emailOptions.maxAttachmentSizeMB.toString()); formData.append("maxAttachmentSizeMB", emailOptions.maxAttachmentSizeMB.toString());
formData.append("downloadHtml", emailOptions.downloadHtml.toString()); formData.append("downloadHtml", emailOptions.downloadHtml.toString());
formData.append("includeAllRecipients", emailOptions.includeAllRecipients.toString()); formData.append("includeAllRecipients", emailOptions.includeAllRecipients.toString());
} else if (fromExtension === 'pdf' && toExtension === 'pdfa') { } else if (fromExtension === 'pdf' && toExtension === 'pdfa') {
// PDF to PDF/A conversion with output format
formData.append("outputFormat", pdfaOptions.outputFormat); formData.append("outputFormat", pdfaOptions.outputFormat);
} else if (fromExtension === 'pdf' && toExtension === 'csv') { } else if (fromExtension === 'pdf' && toExtension === 'csv') {
// CSV extraction - always process all pages for simplified workflow
formData.append("pageNumbers", "all"); formData.append("pageNumbers", "all");
} }
@ -250,12 +227,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
return; return;
} }
// Use utility function to determine processing strategy
if (shouldProcessFilesSeparately(selectedFiles, parameters)) { if (shouldProcessFilesSeparately(selectedFiles, parameters)) {
// Process each file separately with appropriate endpoint
await executeMultipleSeparateFiles(parameters, selectedFiles); await executeMultipleSeparateFiles(parameters, selectedFiles);
} else { } else {
// Process all files together (default behavior)
await executeSingleCombinedOperation(parameters, selectedFiles); await executeSingleCombinedOperation(parameters, selectedFiles);
} }
}, [t]); }, [t]);
@ -276,14 +250,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
const file = selectedFiles[i]; const file = selectedFiles[i];
setStatus(t("convert.processingFile", `Processing file ${i + 1} of ${selectedFiles.length}...`)); 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); const fileExtension = detectFileExtension(file.name);
// Determine the best endpoint for this specific file type
let endpoint = getEndpointUrl(fileExtension, parameters.toExtension); let endpoint = getEndpointUrl(fileExtension, parameters.toExtension);
let fileSpecificParams = { ...parameters, fromExtension: fileExtension }; let fileSpecificParams = { ...parameters, fromExtension: fileExtension };
// Fallback to file-to-pdf if specific endpoint doesn't exist
if (!endpoint && parameters.toExtension === 'pdf') { if (!endpoint && parameters.toExtension === 'pdf') {
endpoint = '/api/v1/convert/file/pdf'; endpoint = '/api/v1/convert/file/pdf';
console.log(`Using file-to-pdf fallback for ${fileExtension} file: ${file.name}`); console.log(`Using file-to-pdf fallback for ${fileExtension} file: ${file.name}`);
@ -291,10 +260,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
if (!endpoint) { if (!endpoint) {
console.error(`No endpoint available for ${fileExtension} to ${parameters.toExtension}`); 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 { operation, operationId, fileId } = createOperation(fileSpecificParams, [file]);
const formData = buildFormData(fileSpecificParams, [file]); const formData = buildFormData(fileSpecificParams, [file]);
@ -316,32 +284,24 @@ export const useConvertOperation = (): ConvertOperationHook => {
} catch (error: any) { } catch (error: any) {
console.error(`Error converting file ${file.name}:`, error); console.error(`Error converting file ${file.name}:`, error);
markOperationFailed(fileId, operationId); markOperationFailed(fileId, operationId);
// Continue with other files even if one fails
} }
} }
if (results.length > 0) { 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); const generatedThumbnails = await generateThumbnailsForFiles(results);
// Set results for multiple files
setFiles(results); setFiles(results);
setThumbnails(generatedThumbnails); setThumbnails(generatedThumbnails);
// Add all converted files to FileContext
await addFiles(results); await addFiles(results);
// Use utility function to create download info
try { try {
const { url, filename } = await createDownloadInfo(results); const { url, filename } = await createDownloadInfo(results);
setDownloadUrl(url); setDownloadUrl(url);
setDownloadFilename(filename); setDownloadFilename(filename);
} catch (error) { } catch (error) {
console.error('Failed to create download info:', error); console.error('Failed to create download info:', error);
// Fallback to first file only
const url = window.URL.createObjectURL(results[0]); const url = window.URL.createObjectURL(results[0]);
setDownloadUrl(url); setDownloadUrl(url);
setDownloadFilename(results[0].name); setDownloadFilename(results[0].name);

View File

@ -4,7 +4,7 @@
*/ */
import { describe, test, expect } from 'vitest'; 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'; import { useConvertParameters } from './useConvertParameters';
describe('useConvertParameters - Auto Detection & Smart Conversion', () => { 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 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', () => { describe('Multiple Identical Files', () => {

View File

@ -30,6 +30,56 @@ vi.mock('i18next-http-backend', () => ({
global.URL.createObjectURL = vi.fn(() => 'mocked-url') global.URL.createObjectURL = vi.fn(() => 'mocked-url')
global.URL.revokeObjectURL = vi.fn() 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) // Mock Worker for tests (Web Workers not available in test environment)
global.Worker = vi.fn().mockImplementation(() => ({ global.Worker = vi.fn().mockImplementation(() => ({
postMessage: vi.fn(), postMessage: vi.fn(),

View File

@ -27,12 +27,10 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const convertParams = useConvertParameters(); const convertParams = useConvertParameters();
const convertOperation = useConvertOperation(); const convertOperation = useConvertOperation();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled( const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
convertParams.getEndpointName() convertParams.getEndpointName()
); );
// Auto-scroll to bottom when content grows
const scrollToBottom = () => { const scrollToBottom = () => {
if (scrollContainerRef.current) { if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({ scrollContainerRef.current.scrollTo({
@ -42,13 +40,11 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
} }
}; };
// Calculate state variables first
const hasFiles = selectedFiles.length > 0; const hasFiles = selectedFiles.length > 0;
const hasResults = convertOperation.downloadUrl !== null; const hasResults = convertOperation.downloadUrl !== null;
const filesCollapsed = hasFiles; const filesCollapsed = hasFiles;
const settingsCollapsed = hasResults; const settingsCollapsed = hasResults;
// Auto-detect extension when files change - now with smart detection
useEffect(() => { useEffect(() => {
if (selectedFiles.length > 0) { if (selectedFiles.length > 0) {
convertParams.analyzeFileTypes(selectedFiles); convertParams.analyzeFileTypes(selectedFiles);
@ -62,17 +58,15 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
onPreviewFile?.(null); onPreviewFile?.(null);
}, [convertParams.parameters, selectedFiles]); }, [convertParams.parameters, selectedFiles]);
// Auto-scroll when settings step becomes visible (files selected)
useEffect(() => { useEffect(() => {
if (hasFiles) { if (hasFiles) {
setTimeout(scrollToBottom, 100); // Small delay to ensure DOM update setTimeout(scrollToBottom, 100);
} }
}, [hasFiles]); }, [hasFiles]);
// Auto-scroll when results appear
useEffect(() => { useEffect(() => {
if (hasResults) { if (hasResults) {
setTimeout(scrollToBottom, 100); // Small delay to ensure DOM update setTimeout(scrollToBottom, 100);
} }
}, [hasResults]); }, [hasResults]);
@ -116,7 +110,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
<div className="h-full max-h-screen overflow-y-auto" ref={scrollContainerRef}> <div className="h-full max-h-screen overflow-y-auto" ref={scrollContainerRef}>
<ToolStepContainer> <ToolStepContainer>
<Stack gap="sm" p="sm"> <Stack gap="sm" p="sm">
{/* Files Step */}
<ToolStep <ToolStep
title={t("convert.files", "Files")} title={t("convert.files", "Files")}
isVisible={true} isVisible={true}
@ -130,7 +123,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
/> />
</ToolStep> </ToolStep>
{/* Settings Step */}
<ToolStep <ToolStep
title={t("convert.settings", "Settings")} title={t("convert.settings", "Settings")}
isVisible={true} isVisible={true}
@ -161,7 +153,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
</Stack> </Stack>
</ToolStep> </ToolStep>
{/* Results Step */}
<ToolStep <ToolStep
title={t("convert.results", "Results")} title={t("convert.results", "Results")}
isVisible={hasResults} isVisible={hasResults}