Stirling-PDF/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx
Reece Browne 87c63efcec
Feature/v2/filewithid implementation (#4369)
Added Filewithid type
Updated code where file was being used to use filewithid
Updated places we identified files by name or composite keys to use UUID
Updated places we should have been using quickkey
Updated pageeditor issue where we parsed pagenumber from pageid instead
of using pagenumber directly

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-09-05 11:33:03 +01:00

531 lines
18 KiB
TypeScript

/**
* 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, Mock } 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';
import { detectFileExtension } from '../../utils/fileUtils';
import { FIT_OPTIONS } from '../../constants/convertConstants';
import { createTestStirlingFile, createTestFilesWithId } from '../utils/testFileHelpers';
import { StirlingFile } from '../../types/fileContext';
// Mock axios
vi.mock('axios');
const mockedAxios = vi.mocked(axios);
// Mock only essential services that are actually called by the tests
vi.mock('../../services/fileStorage', () => ({
fileStorage: {
init: vi.fn().mockResolvedValue(undefined),
storeFile: vi.fn().mockImplementation((file, thumbnail) => {
return Promise.resolve({
id: `mock-id-${file.name}`,
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
thumbnail: thumbnail
});
}),
getAllFileMetadata: vi.fn().mockResolvedValue([]),
cleanup: vi.fn().mockResolvedValue(undefined)
}
}));
vi.mock('../../services/thumbnailGenerationService', () => ({
thumbnailGenerationService: {
generateThumbnail: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail'),
cleanup: vi.fn(),
destroy: vi.fn()
}
}));
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 as Mock).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 = createTestStirlingFile('document.docx', 'docx content', '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 = createTestStirlingFile('document.xyz', 'unknown content', 'application/octet-stream');
// Test auto-detection
act(() => {
paramsResult.current.analyzeFileTypes([unknownFile]);
});
await waitFor(() => {
expect(paramsResult.current.parameters.fromExtension).toBe('file-xyz');
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 = createTestFilesWithId([
{ name: 'photo1.jpg', content: 'jpg content', type: 'image/jpeg' },
{ name: 'photo2.png', content: 'png content', type: 'image/png' },
{ name: 'photo3.gif', content: 'gif content', 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 as Mock).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 = createTestFilesWithId([
{ name: 'document.pdf', content: 'pdf content', type: 'application/pdf' },
{ name: 'spreadsheet.xlsx', content: 'docx content', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
{ name: 'presentation.pptx', content: 'pptx content', 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'
});
});
test('should detect all web files and use html-to-pdf endpoint', async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
// Create mock web files
const webFiles = createTestFilesWithId([
{ name: 'page1.html', content: '<html>content</html>', type: 'text/html' },
{ name: 'site.zip', content: 'zip content', type: 'application/zip' }
]);
// Test smart detection for web files
act(() => {
paramsResult.current.analyzeFileTypes(webFiles);
});
await waitFor(() => {
expect(paramsResult.current.parameters.fromExtension).toBe('html');
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
expect(paramsResult.current.parameters.isSmartDetection).toBe(true);
expect(paramsResult.current.parameters.smartDetectionType).toBe('web');
});
// Test conversion operation
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
webFiles
);
});
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/html/pdf', expect.any(FormData), {
responseType: 'blob'
});
// Should process files separately for web files
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
});
});
describe('Web and Email Conversion Options Integration', () => {
test('should send correct HTML parameters for web-to-pdf conversion', async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const htmlFile = createTestStirlingFile('page.html', '<html>content</html>', 'text/html');
// Set up HTML conversion parameters
act(() => {
paramsResult.current.analyzeFileTypes([htmlFile]);
paramsResult.current.updateParameter('htmlOptions', {
zoomLevel: 1.5
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[htmlFile]
);
});
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('zoom')).toBe('1.5');
});
test('should send correct email parameters for eml-to-pdf conversion', async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const emlFile = createTestStirlingFile('email.eml', 'email content', 'message/rfc822');
// Set up email conversion parameters
act(() => {
paramsResult.current.updateParameter('fromExtension', 'eml');
paramsResult.current.updateParameter('toExtension', 'pdf');
paramsResult.current.updateParameter('emailOptions', {
includeAttachments: false,
maxAttachmentSizeMB: 20,
downloadHtml: true,
includeAllRecipients: true
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[emlFile]
);
});
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('includeAttachments')).toBe('false');
expect(formData.get('maxAttachmentSizeMB')).toBe('20');
expect(formData.get('downloadHtml')).toBe('true');
expect(formData.get('includeAllRecipients')).toBe('true');
});
test('should send correct PDF/A parameters for pdf-to-pdfa conversion', async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const pdfFile = createTestStirlingFile('document.pdf', 'pdf content', 'application/pdf');
// Set up PDF/A conversion parameters
act(() => {
paramsResult.current.updateParameter('fromExtension', 'pdf');
paramsResult.current.updateParameter('toExtension', 'pdfa');
paramsResult.current.updateParameter('pdfaOptions', {
outputFormat: 'pdfa'
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[pdfFile]
);
});
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('outputFormat')).toBe('pdfa');
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/pdf/pdfa', 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 = createTestFilesWithId([
{ name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' },
{ name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' }
]);
// Set up image conversion parameters
act(() => {
paramsResult.current.analyzeFileTypes(imageFiles);
paramsResult.current.updateParameter('imageOptions', {
colorType: 'grayscale',
dpi: 150,
singleOrMultiple: 'single',
fitOption: FIT_OPTIONS.FIT_PAGE,
autoRotate: false,
combineImages: true
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
imageFiles
);
});
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('fitOption')).toBe(FIT_OPTIONS.FIT_PAGE);
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 = createTestFilesWithId([
{ name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' },
{ name: 'photo2.jpg', content: 'jpg2', 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 as Mock)
.mockResolvedValueOnce({
data: new Blob(['converted1'], { type: 'application/pdf' })
})
.mockRejectedValueOnce(new Error('File 2 failed'));
const mixedFiles = createTestFilesWithId([
{ name: 'doc1.txt', content: 'file1', type: 'text/plain' },
{ name: 'doc2.xyz', content: 'file2', 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 = detectFileExtension(filename);
expect(detected).toBe(expected);
});
});
});
});