mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
Stuff
This commit is contained in:
parent
8dff995a1c
commit
32825acc9b
@ -4,6 +4,8 @@ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
|
||||||
import { isImageFormat } from "../../../utils/convertUtils";
|
import { isImageFormat } from "../../../utils/convertUtils";
|
||||||
|
import { useFileSelectionActions } from "../../../contexts/FileSelectionContext";
|
||||||
|
import { useFileContext } from "../../../contexts/FileContext";
|
||||||
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
||||||
import ConvertToImageSettings from "./ConvertToImageSettings";
|
import ConvertToImageSettings from "./ConvertToImageSettings";
|
||||||
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||||
@ -20,6 +22,7 @@ interface ConvertSettingsProps {
|
|||||||
parameters: ConvertParameters;
|
parameters: ConvertParameters;
|
||||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||||
|
selectedFiles: File[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,11 +30,14 @@ const ConvertSettings = ({
|
|||||||
parameters,
|
parameters,
|
||||||
onParameterChange,
|
onParameterChange,
|
||||||
getAvailableToExtensions,
|
getAvailableToExtensions,
|
||||||
|
selectedFiles,
|
||||||
disabled = false
|
disabled = false
|
||||||
}: ConvertSettingsProps) => {
|
}: ConvertSettingsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const { setSelectedFiles } = useFileSelectionActions();
|
||||||
|
const { setSelectedFiles: setContextSelectedFiles } = useFileContext();
|
||||||
|
|
||||||
// Get all possible conversion endpoints to check their availability
|
// Get all possible conversion endpoints to check their availability
|
||||||
const allEndpoints = useMemo(() => {
|
const allEndpoints = useMemo(() => {
|
||||||
@ -102,6 +108,31 @@ const ConvertSettings = ({
|
|||||||
// Disable smart detection when manually changing source format
|
// 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') {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToExtensionChange = (value: string) => {
|
const handleToExtensionChange = (value: string) => {
|
||||||
@ -133,7 +164,7 @@ const ConvertSettings = ({
|
|||||||
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
||||||
options={enhancedFromOptions}
|
options={enhancedFromOptions}
|
||||||
onChange={handleFromExtensionChange}
|
onChange={handleFromExtensionChange}
|
||||||
disabled={disabled || parameters.isSmartDetection}
|
disabled={disabled}
|
||||||
minWidth="21.875rem"
|
minWidth="21.875rem"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -163,17 +194,6 @@ const ConvertSettings = ({
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
) : parameters.isSmartDetection ? (
|
|
||||||
<GroupedFormatDropdown
|
|
||||||
name="convert-to-dropdown"
|
|
||||||
data-testid="to-format-dropdown"
|
|
||||||
value="pdf"
|
|
||||||
placeholder="PDF"
|
|
||||||
options={[{ value: 'pdf', label: 'PDF', group: 'Document' }]}
|
|
||||||
onChange={() => {}} // No-op since it's disabled
|
|
||||||
disabled={true}
|
|
||||||
minWidth="21.875rem"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<GroupedFormatDropdown
|
<GroupedFormatDropdown
|
||||||
name="convert-to-dropdown"
|
name="convert-to-dropdown"
|
||||||
|
@ -313,58 +313,6 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const executeSingleOperation = useCallback(async (
|
|
||||||
parameters: ConvertParameters,
|
|
||||||
selectedFiles: File[]
|
|
||||||
) => {
|
|
||||||
const { operation, operationId, fileId } = createOperation(parameters, selectedFiles);
|
|
||||||
const formData = buildFormData(parameters, selectedFiles);
|
|
||||||
|
|
||||||
// Get endpoint using utility function
|
|
||||||
const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension);
|
|
||||||
if (!endpoint) {
|
|
||||||
setErrorMessage(t("convert.errorNotSupported", { from: parameters.fromExtension, to: parameters.toExtension }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordOperation(fileId, operation);
|
|
||||||
|
|
||||||
setStatus(t("loading"));
|
|
||||||
setIsLoading(true);
|
|
||||||
setErrorMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
|
||||||
const blob = new Blob([response.data]);
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// Generate filename based on conversion
|
|
||||||
const originalName = selectedFiles.length === 1
|
|
||||||
? selectedFiles[0].name.split('.')[0]
|
|
||||||
: 'combined_images';
|
|
||||||
const filename = `${originalName}_converted.${parameters.toExtension}`;
|
|
||||||
|
|
||||||
setDownloadUrl(url);
|
|
||||||
setDownloadFilename(filename);
|
|
||||||
setStatus(t("downloadComplete"));
|
|
||||||
|
|
||||||
await processResults(blob, filename);
|
|
||||||
markOperationApplied(fileId, operationId);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
let errorMsg = t("convert.errorConversion", "An error occurred while converting the file.");
|
|
||||||
if (error.response?.data && typeof error.response.data === 'string') {
|
|
||||||
errorMsg = error.response.data;
|
|
||||||
} else if (error.message) {
|
|
||||||
errorMsg = error.message;
|
|
||||||
}
|
|
||||||
setErrorMessage(errorMsg);
|
|
||||||
setStatus(t("error._value", "Conversion failed."));
|
|
||||||
markOperationFailed(fileId, operationId, errorMsg);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [t, createOperation, buildFormData, recordOperation, markOperationApplied, markOperationFailed, processResults]);
|
|
||||||
|
|
||||||
const resetResults = useCallback(() => {
|
const resetResults = useCallback(() => {
|
||||||
// Clean up blob URLs to prevent memory leaks
|
// Clean up blob URLs to prevent memory leaks
|
||||||
|
@ -191,7 +191,11 @@ describe('useConvertParameters', () => {
|
|||||||
|
|
||||||
const availableExtensions = result.current.getAvailableToExtensions('invalid');
|
const availableExtensions = result.current.getAvailableToExtensions('invalid');
|
||||||
|
|
||||||
expect(availableExtensions).toEqual([]);
|
expect(availableExtensions).toEqual([{
|
||||||
|
"group": "Document",
|
||||||
|
"label": "PDF",
|
||||||
|
"value": "pdf",
|
||||||
|
}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return empty array for empty source format', () => {
|
test('should return empty array for empty source format', () => {
|
||||||
@ -217,8 +221,8 @@ describe('useConvertParameters', () => {
|
|||||||
test('should handle files without extensions', () => {
|
test('should handle files without extensions', () => {
|
||||||
const { result } = renderHook(() => useConvertParameters());
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
// Files without dots return the entire filename as "extension"
|
// Files without extensions should return empty string
|
||||||
expect(result.current.detectFileExtension('noextension')).toBe('noextension');
|
expect(result.current.detectFileExtension('noextension')).toBe('');
|
||||||
expect(result.current.detectFileExtension('')).toBe('');
|
expect(result.current.detectFileExtension('')).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -118,22 +118,49 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
const getAvailableToExtensions = (fromExtension: string) => {
|
const getAvailableToExtensions = (fromExtension: string) => {
|
||||||
if (!fromExtension) return [];
|
if (!fromExtension) return [];
|
||||||
|
|
||||||
const supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
|
let supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
|
||||||
|
|
||||||
|
// If no explicit conversion exists, but file-to-pdf might be available,
|
||||||
|
// fall back to 'any' conversion (which converts unknown files to PDF via file-to-pdf)
|
||||||
|
if (supportedExtensions.length === 0 && fromExtension !== 'any') {
|
||||||
|
supportedExtensions = CONVERSION_MATRIX['any'] || [];
|
||||||
|
}
|
||||||
|
|
||||||
return TO_FORMAT_OPTIONS.filter(option =>
|
return TO_FORMAT_OPTIONS.filter(option =>
|
||||||
supportedExtensions.includes(option.value)
|
supportedExtensions.includes(option.value)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const detectFileExtension = (filename: string): string => {
|
const detectFileExtension = (filename: string): string => {
|
||||||
const extension = filename.split('.').pop()?.toLowerCase();
|
if (!filename || typeof filename !== 'string') return '';
|
||||||
return extension || '';
|
|
||||||
|
const parts = filename.split('.');
|
||||||
|
// If there's no extension (no dots or only one part), return empty string
|
||||||
|
if (parts.length <= 1) return '';
|
||||||
|
|
||||||
|
// Get the last part (extension) in lowercase
|
||||||
|
let extension = parts[parts.length - 1].toLowerCase();
|
||||||
|
|
||||||
|
// Normalize common extension variants
|
||||||
|
if (extension === 'jpeg') extension = 'jpg';
|
||||||
|
|
||||||
|
return extension;
|
||||||
};
|
};
|
||||||
|
|
||||||
const analyzeFileTypes = (files: Array<{name: string}>) => {
|
const analyzeFileTypes = (files: Array<{name: string}>) => {
|
||||||
if (files.length <= 1) {
|
if (files.length <= 1) {
|
||||||
// Single file or no files - use regular detection with auto-target selection
|
// Single file or no files - use regular detection with auto-target selection
|
||||||
const fromExt = files.length === 1 ? detectFileExtension(files[0].name) : '';
|
const detectedExt = files.length === 1 ? detectFileExtension(files[0].name) : '';
|
||||||
const availableTargets = fromExt ? CONVERSION_MATRIX[fromExt] || [] : [];
|
let fromExt = detectedExt;
|
||||||
|
let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : [];
|
||||||
|
|
||||||
|
// If no explicit conversion exists for this file type, fall back to 'any'
|
||||||
|
// which will attempt file-to-pdf conversion if available
|
||||||
|
if (availableTargets.length === 0) {
|
||||||
|
fromExt = 'any';
|
||||||
|
availableTargets = CONVERSION_MATRIX['any'] || [];
|
||||||
|
}
|
||||||
|
|
||||||
const autoTarget = availableTargets.length === 1 ? availableTargets[0] : '';
|
const autoTarget = availableTargets.length === 1 ? availableTargets[0] : '';
|
||||||
|
|
||||||
setParameters(prev => ({
|
setParameters(prev => ({
|
||||||
@ -152,8 +179,16 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
|
|
||||||
if (uniqueExtensions.length === 1) {
|
if (uniqueExtensions.length === 1) {
|
||||||
// All files are the same type - use regular detection with auto-target selection
|
// All files are the same type - use regular detection with auto-target selection
|
||||||
const fromExt = uniqueExtensions[0];
|
const detectedExt = uniqueExtensions[0];
|
||||||
const availableTargets = CONVERSION_MATRIX[fromExt] || [];
|
let fromExt = detectedExt;
|
||||||
|
let availableTargets = CONVERSION_MATRIX[detectedExt] || [];
|
||||||
|
|
||||||
|
// If no explicit conversion exists for this file type, fall back to 'any'
|
||||||
|
if (availableTargets.length === 0) {
|
||||||
|
fromExt = 'any';
|
||||||
|
availableTargets = CONVERSION_MATRIX['any'] || [];
|
||||||
|
}
|
||||||
|
|
||||||
const autoTarget = availableTargets.length === 1 ? availableTargets[0] : '';
|
const autoTarget = availableTargets.length === 1 ? availableTargets[0] : '';
|
||||||
|
|
||||||
setParameters(prev => ({
|
setParameters(prev => ({
|
||||||
|
@ -0,0 +1,310 @@
|
|||||||
|
/**
|
||||||
|
* Tests for auto-detection and smart conversion features in useConvertParameters
|
||||||
|
* This covers the analyzeFileTypes function and related smart detection logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { useConvertParameters } from './useConvertParameters';
|
||||||
|
|
||||||
|
describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||||
|
|
||||||
|
describe('Single File Detection', () => {
|
||||||
|
|
||||||
|
test('should detect single file extension and set auto-target', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const pdfFile = [{ name: 'document.pdf' }];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(pdfFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||||
|
expect(result.current.parameters.toExtension).toBe(''); // No auto-selection for multiple targets
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(false);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle unknown file types with file-to-pdf fallback', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const unknownFile = [{ name: 'document.xyz' }];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(unknownFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('any');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('pdf'); // Fallback to file-to-pdf
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle files without extensions', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const noExtFile = [{ name: 'document' }];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(noExtFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('any');
|
||||||
|
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', () => {
|
||||||
|
|
||||||
|
test('should detect multiple PDF files and set auto-target', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const pdfFiles = [
|
||||||
|
{ name: 'doc1.pdf' },
|
||||||
|
{ name: 'doc2.pdf' },
|
||||||
|
{ name: 'doc3.pdf' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(pdfFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||||
|
expect(result.current.parameters.toExtension).toBe(''); // Auto-selected
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(false);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple unknown file types with fallback', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const unknownFiles = [
|
||||||
|
{ name: 'file1.xyz' },
|
||||||
|
{ name: 'file2.xyz' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(unknownFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('any');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Smart Detection - All Images', () => {
|
||||||
|
|
||||||
|
test('should detect all image files and enable smart detection', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const imageFiles = [
|
||||||
|
{ name: 'photo1.jpg' },
|
||||||
|
{ name: 'photo2.png' },
|
||||||
|
{ name: 'photo3.gif' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(imageFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('image');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('images');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle mixed case image extensions', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const imageFiles = [
|
||||||
|
{ name: 'photo1.JPG' },
|
||||||
|
{ name: 'photo2.PNG' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(imageFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('images');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Smart Detection - Mixed File Types', () => {
|
||||||
|
|
||||||
|
test('should detect mixed file types and enable smart detection', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const mixedFiles = [
|
||||||
|
{ name: 'document.pdf' },
|
||||||
|
{ name: 'spreadsheet.xlsx' },
|
||||||
|
{ name: 'presentation.pptx' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(mixedFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('any');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('mixed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect mixed images and documents as mixed type', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const mixedFiles = [
|
||||||
|
{ name: 'photo.jpg' },
|
||||||
|
{ name: 'document.pdf' },
|
||||||
|
{ name: 'text.txt' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(mixedFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('mixed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle mixed with unknown file types', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const mixedFiles = [
|
||||||
|
{ name: 'document.pdf' },
|
||||||
|
{ name: 'unknown.xyz' },
|
||||||
|
{ name: 'noextension' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(mixedFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('mixed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Smart Detection Endpoint Resolution', () => {
|
||||||
|
|
||||||
|
test('should return correct endpoint for image smart detection', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const imageFiles = [
|
||||||
|
{ name: 'photo1.jpg' },
|
||||||
|
{ name: 'photo2.png' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(imageFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.getEndpointName()).toBe('img-to-pdf');
|
||||||
|
expect(result.current.getEndpoint()).toBe('/api/v1/convert/img/pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct endpoint for mixed smart detection', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const mixedFiles = [
|
||||||
|
{ name: 'document.pdf' },
|
||||||
|
{ name: 'spreadsheet.xlsx' }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(mixedFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.getEndpointName()).toBe('file-to-pdf');
|
||||||
|
expect(result.current.getEndpoint()).toBe('/api/v1/convert/file/pdf');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Auto-Target Selection Logic', () => {
|
||||||
|
|
||||||
|
test('should select single available target automatically', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
// Markdown has only one conversion target (PDF)
|
||||||
|
const mdFile = [{ name: 'readme.md' }];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(mdFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('md');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('pdf'); // Only available target
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not auto-select when multiple targets available', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
// PDF has multiple conversion targets, so no auto-selection
|
||||||
|
const pdfFile = [{ name: 'document.pdf' }];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(pdfFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||||
|
// Should NOT auto-select when multiple targets available
|
||||||
|
expect(result.current.parameters.toExtension).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
|
||||||
|
test('should handle empty file names', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const emptyFiles = [{ name: '' }];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(emptyFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('any');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle malformed file objects', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const malformedFiles = [
|
||||||
|
{ name: 'valid.pdf' },
|
||||||
|
// @ts-ignore - Testing runtime resilience
|
||||||
|
{ name: null },
|
||||||
|
// @ts-ignore
|
||||||
|
{ name: undefined }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.analyzeFileTypes(malformedFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still process the valid file and handle gracefully
|
||||||
|
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(result.current.parameters.smartDetectionType).toBe('mixed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -36,6 +36,8 @@ global.Worker = vi.fn().mockImplementation(() => ({
|
|||||||
terminate: vi.fn(),
|
terminate: vi.fn(),
|
||||||
addEventListener: vi.fn(),
|
addEventListener: vi.fn(),
|
||||||
removeEventListener: vi.fn(),
|
removeEventListener: vi.fn(),
|
||||||
|
onmessage: null,
|
||||||
|
onerror: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock ResizeObserver for Mantine components
|
// Mock ResizeObserver for Mantine components
|
||||||
@ -66,3 +68,6 @@ Object.defineProperty(window, 'matchMedia', {
|
|||||||
dispatchEvent: vi.fn(),
|
dispatchEvent: vi.fn(),
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set global test timeout to prevent hangs
|
||||||
|
vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 })
|
@ -0,0 +1,357 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests for Convert Tool Smart Detection with real file scenarios
|
||||||
|
* Tests the complete flow from file upload through auto-detection to API calls
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||||
|
import { useConvertOperation } from '../../hooks/tools/convert/useConvertOperation';
|
||||||
|
import { useConvertParameters } from '../../hooks/tools/convert/useConvertParameters';
|
||||||
|
import { FileContextProvider } from '../../contexts/FileContext';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import i18n from '../../i18n/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Mock axios
|
||||||
|
vi.mock('axios');
|
||||||
|
const mockedAxios = vi.mocked(axios);
|
||||||
|
|
||||||
|
// Mock utility modules
|
||||||
|
vi.mock('../../utils/thumbnailUtils', () => ({
|
||||||
|
generateThumbnailForFile: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail')
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<FileContextProvider>
|
||||||
|
{children}
|
||||||
|
</FileContextProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Convert Tool - Smart Detection Integration Tests', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Mock successful API response
|
||||||
|
mockedAxios.post.mockResolvedValue({
|
||||||
|
data: new Blob(['fake converted content'], { type: 'application/pdf' })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up any blob URLs created during tests
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Single File Auto-Detection Flow', () => {
|
||||||
|
test('should auto-detect PDF from DOCX and convert to PDF', async () => {
|
||||||
|
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: operationResult } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create mock DOCX file
|
||||||
|
const docxFile = new File(['docx content'], 'document.docx', { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
|
||||||
|
|
||||||
|
// Test auto-detection
|
||||||
|
act(() => {
|
||||||
|
paramsResult.current.analyzeFileTypes([docxFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(paramsResult.current.parameters.fromExtension).toBe('docx');
|
||||||
|
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
|
||||||
|
expect(paramsResult.current.parameters.isSmartDetection).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test conversion operation
|
||||||
|
await act(async () => {
|
||||||
|
await operationResult.current.executeOperation(
|
||||||
|
paramsResult.current.parameters,
|
||||||
|
[docxFile]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle unknown file type with file-to-pdf fallback', async () => {
|
||||||
|
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: operationResult } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create mock unknown file
|
||||||
|
const unknownFile = new File(['unknown content'], 'document.xyz', { type: 'application/octet-stream' });
|
||||||
|
|
||||||
|
// Test auto-detection
|
||||||
|
act(() => {
|
||||||
|
paramsResult.current.analyzeFileTypes([unknownFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(paramsResult.current.parameters.fromExtension).toBe('any');
|
||||||
|
expect(paramsResult.current.parameters.toExtension).toBe('pdf'); // Fallback
|
||||||
|
expect(paramsResult.current.parameters.isSmartDetection).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test conversion operation
|
||||||
|
await act(async () => {
|
||||||
|
await operationResult.current.executeOperation(
|
||||||
|
paramsResult.current.parameters,
|
||||||
|
[unknownFile]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multi-File Smart Detection Flow', () => {
|
||||||
|
|
||||||
|
test('should detect all images and use img-to-pdf endpoint', async () => {
|
||||||
|
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: operationResult } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create mock image files
|
||||||
|
const imageFiles = [
|
||||||
|
new File(['jpg content'], 'photo1.jpg', { type: 'image/jpeg' }),
|
||||||
|
new File(['png content'], 'photo2.png', { type: 'image/png' }),
|
||||||
|
new File(['gif content'], 'photo3.gif', { type: 'image/gif' })
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test smart detection for all images
|
||||||
|
act(() => {
|
||||||
|
paramsResult.current.analyzeFileTypes(imageFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(paramsResult.current.parameters.fromExtension).toBe('image');
|
||||||
|
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
|
||||||
|
expect(paramsResult.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(paramsResult.current.parameters.smartDetectionType).toBe('images');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test conversion operation
|
||||||
|
await act(async () => {
|
||||||
|
await operationResult.current.executeOperation(
|
||||||
|
paramsResult.current.parameters,
|
||||||
|
imageFiles
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/img/pdf', expect.any(FormData), {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should send all files in single request
|
||||||
|
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
|
||||||
|
const files = formData.getAll('fileInput');
|
||||||
|
expect(files).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect mixed file types and use file-to-pdf endpoint', async () => {
|
||||||
|
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: operationResult } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create mixed file types
|
||||||
|
const mixedFiles = [
|
||||||
|
new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }),
|
||||||
|
new File(['docx content'], 'spreadsheet.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }),
|
||||||
|
new File(['pptx content'], 'presentation.pptx', { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' })
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test smart detection for mixed types
|
||||||
|
act(() => {
|
||||||
|
paramsResult.current.analyzeFileTypes(mixedFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(paramsResult.current.parameters.fromExtension).toBe('any');
|
||||||
|
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
|
||||||
|
expect(paramsResult.current.parameters.isSmartDetection).toBe(true);
|
||||||
|
expect(paramsResult.current.parameters.smartDetectionType).toBe('mixed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test conversion operation
|
||||||
|
await act(async () => {
|
||||||
|
await operationResult.current.executeOperation(
|
||||||
|
paramsResult.current.parameters,
|
||||||
|
mixedFiles
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Image Conversion Options Integration', () => {
|
||||||
|
|
||||||
|
test('should send correct parameters for image-to-pdf conversion', async () => {
|
||||||
|
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: operationResult } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageFiles = [
|
||||||
|
new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }),
|
||||||
|
new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' })
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set up image conversion parameters
|
||||||
|
act(() => {
|
||||||
|
paramsResult.current.analyzeFileTypes(imageFiles);
|
||||||
|
paramsResult.current.updateParameter('imageOptions', {
|
||||||
|
colorType: 'grayscale',
|
||||||
|
dpi: 150,
|
||||||
|
singleOrMultiple: 'single',
|
||||||
|
fitOption: 'fitToPage',
|
||||||
|
autoRotate: false,
|
||||||
|
combineImages: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await operationResult.current.executeOperation(
|
||||||
|
paramsResult.current.parameters,
|
||||||
|
imageFiles
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
|
||||||
|
expect(formData.get('fitOption')).toBe('fitToPage');
|
||||||
|
expect(formData.get('colorType')).toBe('grayscale');
|
||||||
|
expect(formData.get('autoRotate')).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should process images separately when combineImages is false', async () => {
|
||||||
|
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: operationResult } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageFiles = [
|
||||||
|
new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }),
|
||||||
|
new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' })
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set up for separate processing
|
||||||
|
act(() => {
|
||||||
|
paramsResult.current.analyzeFileTypes(imageFiles);
|
||||||
|
paramsResult.current.updateParameter('imageOptions', {
|
||||||
|
...paramsResult.current.parameters.imageOptions,
|
||||||
|
combineImages: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await operationResult.current.executeOperation(
|
||||||
|
paramsResult.current.parameters,
|
||||||
|
imageFiles
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should make separate API calls for each file
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Scenarios in Smart Detection', () => {
|
||||||
|
|
||||||
|
|
||||||
|
test('should handle partial failures in multi-file processing', async () => {
|
||||||
|
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: operationResult } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock one success, one failure
|
||||||
|
mockedAxios.post
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: new Blob(['converted1'], { type: 'application/pdf' })
|
||||||
|
})
|
||||||
|
.mockRejectedValueOnce(new Error('File 2 failed'));
|
||||||
|
|
||||||
|
const mixedFiles = [
|
||||||
|
new File(['file1'], 'doc1.txt', { type: 'text/plain' }),
|
||||||
|
new File(['file2'], 'doc2.xyz', { type: 'application/octet-stream' })
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set up for separate processing (mixed smart detection)
|
||||||
|
act(() => {
|
||||||
|
paramsResult.current.analyzeFileTypes(mixedFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await operationResult.current.executeOperation(
|
||||||
|
paramsResult.current.parameters,
|
||||||
|
mixedFiles
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should have processed at least one file successfully
|
||||||
|
expect(operationResult.current.files.length).toBeGreaterThan(0);
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Real File Extension Detection', () => {
|
||||||
|
|
||||||
|
test('should correctly detect various file extensions', async () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ filename: 'document.PDF', expected: 'pdf' },
|
||||||
|
{ filename: 'image.JPEG', expected: 'jpg' }, // JPEG should normalize to jpg
|
||||||
|
{ filename: 'photo.jpeg', expected: 'jpg' }, // jpeg should normalize to jpg
|
||||||
|
{ filename: 'archive.tar.gz', expected: 'gz' },
|
||||||
|
{ filename: 'file.', expected: '' },
|
||||||
|
{ filename: '.hidden', expected: 'hidden' },
|
||||||
|
{ filename: 'noextension', expected: '' }
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ filename, expected }) => {
|
||||||
|
const detected = result.current.detectFileExtension(filename);
|
||||||
|
expect(detected).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -11,6 +11,8 @@ import { useConvertParameters } from '../hooks/tools/convert/useConvertParameter
|
|||||||
// Mock the hooks
|
// Mock the hooks
|
||||||
vi.mock('../hooks/tools/convert/useConvertParameters');
|
vi.mock('../hooks/tools/convert/useConvertParameters');
|
||||||
vi.mock('../hooks/useEndpointConfig');
|
vi.mock('../hooks/useEndpointConfig');
|
||||||
|
vi.mock('../contexts/FileSelectionContext');
|
||||||
|
vi.mock('../contexts/FileContext');
|
||||||
|
|
||||||
const mockUseConvertParameters = vi.mocked(useConvertParameters);
|
const mockUseConvertParameters = vi.mocked(useConvertParameters);
|
||||||
|
|
||||||
@ -41,12 +43,37 @@ vi.mock('../hooks/useEndpointConfig', () => ({
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock FileSelectionContext hooks
|
||||||
|
vi.mock('../contexts/FileSelectionContext', () => ({
|
||||||
|
useFileSelectionActions: () => ({
|
||||||
|
setSelectedFiles: vi.fn(),
|
||||||
|
clearSelection: vi.fn(),
|
||||||
|
setMaxFiles: vi.fn(),
|
||||||
|
setIsToolMode: vi.fn()
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock FileContext
|
||||||
|
vi.mock('../contexts/FileContext', () => ({
|
||||||
|
FileContextProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
useFileContext: () => ({
|
||||||
|
activeFiles: [],
|
||||||
|
setSelectedFiles: vi.fn(),
|
||||||
|
addFiles: vi.fn(),
|
||||||
|
removeFiles: vi.fn(),
|
||||||
|
clearFiles: vi.fn(),
|
||||||
|
updateFileMetadata: vi.fn(),
|
||||||
|
mode: 'viewer',
|
||||||
|
setMode: vi.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<FileContextProvider>
|
|
||||||
{children}
|
{children}
|
||||||
</FileContextProvider>
|
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
@ -117,6 +144,7 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
}}
|
}}
|
||||||
onParameterChange={mockOnParameterChange}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
/>
|
/>
|
||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
@ -158,6 +186,7 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
}}
|
}}
|
||||||
onParameterChange={mockOnParameterChange}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
/>
|
/>
|
||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
@ -205,6 +234,7 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
}}
|
}}
|
||||||
onParameterChange={mockOnParameterChange}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
/>
|
/>
|
||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
@ -249,6 +279,7 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
}}
|
}}
|
||||||
onParameterChange={mockOnParameterChange}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
/>
|
/>
|
||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
@ -284,6 +315,7 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
}}
|
}}
|
||||||
onParameterChange={mockOnParameterChange}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
/>
|
/>
|
||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
@ -323,6 +355,7 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
}}
|
}}
|
||||||
onParameterChange={mockOnParameterChange}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
/>
|
/>
|
||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
@ -361,6 +394,7 @@ describe('Convert Tool Navigation Tests', () => {
|
|||||||
}}
|
}}
|
||||||
onParameterChange={mockOnParameterChange}
|
onParameterChange={mockOnParameterChange}
|
||||||
getAvailableToExtensions={mockGetAvailableToExtensions}
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
selectedFiles={[]}
|
||||||
/>
|
/>
|
||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
|
@ -144,6 +144,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
parameters={convertParams.parameters}
|
parameters={convertParams.parameters}
|
||||||
onParameterChange={convertParams.updateParameter}
|
onParameterChange={convertParams.updateParameter}
|
||||||
getAvailableToExtensions={convertParams.getAvailableToExtensions}
|
getAvailableToExtensions={convertParams.getAvailableToExtensions}
|
||||||
|
selectedFiles={selectedFiles}
|
||||||
disabled={endpointLoading}
|
disabled={endpointLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ describe('convertUtils', () => {
|
|||||||
|
|
||||||
test('should return empty string for unsupported conversions', () => {
|
test('should return empty string for unsupported conversions', () => {
|
||||||
expect(getEndpointName('pdf', 'exe')).toBe('');
|
expect(getEndpointName('pdf', 'exe')).toBe('');
|
||||||
expect(getEndpointName('wav', 'pdf')).toBe('');
|
expect(getEndpointName('wav', 'pdf')).toBe('file-to-pdf'); // Try using file to pdf as fallback
|
||||||
expect(getEndpointName('png', 'docx')).toBe(''); // Images can't convert to Word docs
|
expect(getEndpointName('png', 'docx')).toBe(''); // Images can't convert to Word docs
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ describe('convertUtils', () => {
|
|||||||
|
|
||||||
test('should return empty string for unsupported conversions', () => {
|
test('should return empty string for unsupported conversions', () => {
|
||||||
expect(getEndpointUrl('pdf', 'exe')).toBe('');
|
expect(getEndpointUrl('pdf', 'exe')).toBe('');
|
||||||
expect(getEndpointUrl('wav', 'pdf')).toBe('');
|
expect(getEndpointUrl('wav', 'pdf')).toBe('/api/v1/convert/file/pdf'); // Try using file to pdf as fallback
|
||||||
expect(getEndpointUrl('invalid', 'invalid')).toBe('');
|
expect(getEndpointUrl('invalid', 'invalid')).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ describe('convertUtils', () => {
|
|||||||
|
|
||||||
test('should return false for unsupported conversions', () => {
|
test('should return false for unsupported conversions', () => {
|
||||||
expect(isConversionSupported('pdf', 'exe')).toBe(false);
|
expect(isConversionSupported('pdf', 'exe')).toBe(false);
|
||||||
expect(isConversionSupported('wav', 'pdf')).toBe(false);
|
expect(isConversionSupported('wav', 'pdf')).toBe(true); // Fallback to file to pdf
|
||||||
expect(isConversionSupported('png', 'docx')).toBe(false);
|
expect(isConversionSupported('png', 'docx')).toBe(false);
|
||||||
expect(isConversionSupported('nonexistent', 'alsononexistent')).toBe(false);
|
expect(isConversionSupported('nonexistent', 'alsononexistent')).toBe(false);
|
||||||
});
|
});
|
||||||
@ -330,7 +330,7 @@ describe('convertUtils', () => {
|
|||||||
const longExtension = 'a'.repeat(100);
|
const longExtension = 'a'.repeat(100);
|
||||||
expect(isImageFormat(longExtension)).toBe(false);
|
expect(isImageFormat(longExtension)).toBe(false);
|
||||||
expect(getEndpointName('pdf', longExtension)).toBe('');
|
expect(getEndpointName('pdf', longExtension)).toBe('');
|
||||||
expect(getEndpointName(longExtension, 'pdf')).toBe('');
|
expect(getEndpointName(longExtension, 'pdf')).toBe('file-to-pdf'); // Fallback to file to pdf
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -10,7 +10,14 @@ import {
|
|||||||
export const getEndpointName = (fromExtension: string, toExtension: string): string => {
|
export const getEndpointName = (fromExtension: string, toExtension: string): string => {
|
||||||
if (!fromExtension || !toExtension) return '';
|
if (!fromExtension || !toExtension) return '';
|
||||||
|
|
||||||
const endpointKey = EXTENSION_TO_ENDPOINT[fromExtension]?.[toExtension];
|
let endpointKey = EXTENSION_TO_ENDPOINT[fromExtension]?.[toExtension];
|
||||||
|
|
||||||
|
// If no explicit mapping exists and we're converting to PDF,
|
||||||
|
// fall back to 'any' which uses file-to-pdf endpoint
|
||||||
|
if (!endpointKey && toExtension === 'pdf' && fromExtension !== 'any') {
|
||||||
|
endpointKey = EXTENSION_TO_ENDPOINT['any']?.[toExtension];
|
||||||
|
}
|
||||||
|
|
||||||
return endpointKey || '';
|
return endpointKey || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
/* Language and Environment */
|
/* Language and Environment */
|
||||||
"target": "es2024", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
"jsx": "react-jsx", /* Specify what JSX code is generated. */
|
"jsx": "react-jsx", /* Specify what JSX code is generated. */
|
||||||
// "libReplacement": true, /* Enable lib replacement. */
|
// "libReplacement": true, /* Enable lib replacement. */
|
||||||
|
9
frontend/vitest.minimal.config.ts
Normal file
9
frontend/vitest.minimal.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
testTimeout: 5000,
|
||||||
|
include: ['src/utils/convertUtils.test.ts']
|
||||||
|
},
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user