V2: Convert Tool (#3828)

🔄 Dynamic Processing Strategies

- Adaptive routing: Same tool uses different backend endpoints based on
file analysis
- Combined vs separate processing: Intelligently chooses between merge
operations and individual file processing
- Cross-format workflows: Enable complex conversions like "mixed files →
PDF" that other tools can't handle

  ⚙️ Format-Specific Intelligence

  Each conversion type gets tailored options:
  - HTML/ZIP → PDF: Zoom controls (0.1-3.0 increments) with live preview
  - Email → PDF: Attachment handling, size limits, recipient control
  - PDF → PDF/A: Digital signature detection with warnings
  - Images → PDF: Smart combining vs individual file options

 File Architecture

  Core Implementation:
  ├── Convert.tsx                     # Main stepped workflow UI
├── ConvertSettings.tsx # Centralized settings with smart detection
├── GroupedFormatDropdown.tsx # Enhanced format selector with grouping
├── useConvertParameters.ts # Smart detection & parameter management
  ├── useConvertOperation.ts         # Multi-strategy processing logic
  └── Settings Components:
      ├── ConvertFromWebSettings.tsx      # HTML zoom controls
      ├── ConvertFromEmailSettings.tsx    # Email attachment options
├── ConvertToPdfaSettings.tsx # PDF/A with signature detection
      ├── ConvertFromImageSettings.tsx    # Image PDF options
      └── ConvertToImageSettings.tsx      # PDF to image options

 Utility Layer

  Utils & Services:
├── convertUtils.ts # Format detection & endpoint routing
  ├── fileResponseUtils.ts          # Generic API response handling
└── setupTests.ts # Enhanced test environment with crypto mocks

  Testing & Quality

  Comprehensive Test Coverage

  Test Suite:
├── useConvertParameters.test.ts # Parameter logic & smart detection
  ├── useConvertParametersAutoDetection.test.ts  # File type analysis
├── ConvertIntegration.test.tsx # End-to-end conversion workflows
  ├── ConvertSmartDetectionIntegration.test.tsx  # Mixed file scenarios
  ├── ConvertE2E.spec.ts                     # Playwright browser tests
├── convertUtils.test.ts # Utility function validation
  └── fileResponseUtils.test.ts              # API response handling

  Advanced Test Features

  - Crypto API mocking: Proper test environment for file hashing
  - File.arrayBuffer() polyfills: Complete browser API simulation
  - Multi-file scenario testing: Complex batch processing validation
- CI/CD integration: Vitest runs in GitHub Actions with proper artifacts

---------

Co-authored-by: Connor Yoh <[email protected]>
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
ConnorYoh
2025-08-01 16:08:04 +01:00
committed by GitHub
co-authored by Connor Yoh Anthony Stirling
parent 8881f19b03
commit 9c9acbfb5b
68 changed files with 9173 additions and 87 deletions
@@ -0,0 +1,429 @@
/**
* End-to-End Tests for Convert Tool
*
* These tests dynamically discover available conversion endpoints and test them.
* Tests are automatically skipped if the backend endpoint is not available.
*
* Run with: npm run test:e2e or npx playwright test
*/
import { test, expect, Page } from '@playwright/test';
import {
conversionDiscovery,
type ConversionEndpoint
} from '../helpers/conversionEndpointDiscovery';
import * as path from 'path';
import * as fs from 'fs';
// Test configuration
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080';
/**
* Resolves test fixture paths dynamically based on current working directory.
* Works from both top-level project directory and frontend subdirectory.
*/
function resolveTestFixturePath(filename: string): string {
const cwd = process.cwd();
// Try frontend/src/tests/test-fixtures/ first (from top-level)
const topLevelPath = path.join(cwd, 'frontend', 'src', 'tests', 'test-fixtures', filename);
if (fs.existsSync(topLevelPath)) {
return topLevelPath;
}
// Try src/tests/test-fixtures/ (from frontend directory)
const frontendPath = path.join(cwd, 'src', 'tests', 'test-fixtures', filename);
if (fs.existsSync(frontendPath)) {
return frontendPath;
}
// Try relative path from current test file location
const relativePath = path.join(__dirname, '..', 'test-fixtures', filename);
if (fs.existsSync(relativePath)) {
return relativePath;
}
// Fallback to the original path format (should work from top-level)
return path.join('.', 'frontend', 'src', 'tests', 'test-fixtures', filename);
}
// Test file paths (dynamically resolved based on current working directory)
const TEST_FILES = {
pdf: resolveTestFixturePath('sample.pdf'),
docx: resolveTestFixturePath('sample.docx'),
doc: resolveTestFixturePath('sample.doc'),
pptx: resolveTestFixturePath('sample.pptx'),
ppt: resolveTestFixturePath('sample.ppt'),
xlsx: resolveTestFixturePath('sample.xlsx'),
xls: resolveTestFixturePath('sample.xls'),
png: resolveTestFixturePath('sample.png'),
jpg: resolveTestFixturePath('sample.jpg'),
jpeg: resolveTestFixturePath('sample.jpeg'),
gif: resolveTestFixturePath('sample.gif'),
bmp: resolveTestFixturePath('sample.bmp'),
tiff: resolveTestFixturePath('sample.tiff'),
webp: resolveTestFixturePath('sample.webp'),
md: resolveTestFixturePath('sample.md'),
eml: resolveTestFixturePath('sample.eml'),
html: resolveTestFixturePath('sample.html'),
txt: resolveTestFixturePath('sample.txt'),
xml: resolveTestFixturePath('sample.xml'),
csv: resolveTestFixturePath('sample.csv')
};
// File format to test file mapping
const getTestFileForFormat = (format: string): string => {
const formatMap: Record<string, string> = {
'pdf': TEST_FILES.pdf,
'docx': TEST_FILES.docx,
'doc': TEST_FILES.doc,
'pptx': TEST_FILES.pptx,
'ppt': TEST_FILES.ppt,
'xlsx': TEST_FILES.xlsx,
'xls': TEST_FILES.xls,
'office': TEST_FILES.docx, // Default office file
'image': TEST_FILES.png, // Default image file
'png': TEST_FILES.png,
'jpg': TEST_FILES.jpg,
'jpeg': TEST_FILES.jpeg,
'gif': TEST_FILES.gif,
'bmp': TEST_FILES.bmp,
'tiff': TEST_FILES.tiff,
'webp': TEST_FILES.webp,
'md': TEST_FILES.md,
'eml': TEST_FILES.eml,
'html': TEST_FILES.html,
'txt': TEST_FILES.txt,
'xml': TEST_FILES.xml,
'csv': TEST_FILES.csv
};
return formatMap[format] || TEST_FILES.pdf; // Fallback to PDF
};
// Expected file extensions for target formats
const getExpectedExtension = (toFormat: string): string => {
const extensionMap: Record<string, string> = {
'pdf': '.pdf',
'docx': '.docx',
'pptx': '.pptx',
'txt': '.txt',
'html': '.zip', // HTML is zipped
'xml': '.xml',
'csv': '.csv',
'md': '.md',
'image': '.png', // Default for image conversion
'png': '.png',
'jpg': '.jpg',
'jpeg': '.jpeg',
'gif': '.gif',
'bmp': '.bmp',
'tiff': '.tiff',
'webp': '.webp',
'pdfa': '.pdf'
};
return extensionMap[toFormat] || '.pdf';
};
/**
* Generic test function for any conversion
*/
async function testConversion(page: Page, conversion: ConversionEndpoint) {
const expectedExtension = getExpectedExtension(conversion.toFormat);
console.log(`Testing ${conversion.endpoint}: ${conversion.fromFormat}${conversion.toFormat}`);
// File should already be uploaded, click the Convert tool button
await page.click('[data-testid="tool-convert"]');
// Wait for the FileEditor to load in convert mode with file thumbnails
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
// Click the file thumbnail checkbox to select it in the FileEditor
await page.click('[data-testid="file-thumbnail-checkbox"]');
// Wait for the conversion settings to appear after file selection
await page.waitForSelector('[data-testid="convert-from-dropdown"]', { timeout: 5000 });
// Select FROM format
await page.click('[data-testid="convert-from-dropdown"]');
const fromFormatOption = page.locator(`[data-testid="format-option-${conversion.fromFormat}"]`);
await fromFormatOption.scrollIntoViewIfNeeded();
await fromFormatOption.click();
// Select TO format
await page.click('[data-testid="convert-to-dropdown"]');
const toFormatOption = page.locator(`[data-testid="format-option-${conversion.toFormat}"]`);
await toFormatOption.scrollIntoViewIfNeeded();
await toFormatOption.click();
// Handle format-specific options
if (conversion.toFormat === 'image' || ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp'].includes(conversion.toFormat)) {
// Set image conversion options if they appear
const imageOptionsVisible = await page.locator('[data-testid="image-options-section"]').isVisible().catch(() => false);
if (imageOptionsVisible) {
// Click the color type dropdown and select "Color"
await page.click('[data-testid="color-type-select"]');
await page.getByRole('option', { name: 'Color' }).click();
// Set DPI value
await page.fill('[data-testid="dpi-input"]', '150');
// Click the output type dropdown and select "Multiple"
await page.click('[data-testid="output-type-select"]');
await page.getByRole('option', { name: 'single' }).click();
}
}
if (conversion.fromFormat === 'image' && conversion.toFormat === 'pdf') {
// Set PDF creation options if they appear
const pdfOptionsVisible = await page.locator('[data-testid="pdf-options-section"]').isVisible().catch(() => false);
if (pdfOptionsVisible) {
// Click the color type dropdown and select "Color"
await page.click('[data-testid="color-type-select"]');
await page.locator('[data-value="color"]').click();
}
}
if (conversion.fromFormat === 'pdf' && conversion.toFormat === 'csv') {
// Set CSV extraction options if they appear
const csvOptionsVisible = await page.locator('[data-testid="csv-options-section"]').isVisible().catch(() => false);
if (csvOptionsVisible) {
// Set specific page numbers for testing (test pages 1-2)
await page.fill('[data-testid="page-numbers-input"]', '1-2');
}
}
// Start conversion
await page.click('[data-testid="convert-button"]');
// Wait for conversion to complete (with generous timeout)
await page.waitForSelector('[data-testid="download-button"]', { timeout: 60000 });
// Verify download is available
const downloadButton = page.locator('[data-testid="download-button"]');
await expect(downloadButton).toBeVisible();
// Start download and verify file
const downloadPromise = page.waitForEvent('download');
await downloadButton.click();
const download = await downloadPromise;
// Verify file extension
expect(download.suggestedFilename()).toMatch(new RegExp(`\\${expectedExtension}$`));
// Save and verify file is not empty
const path = await download.path();
if (path) {
const fs = require('fs');
const stats = fs.statSync(path);
expect(stats.size).toBeGreaterThan(0);
// Format-specific validations
if (conversion.toFormat === 'pdf' || conversion.toFormat === 'pdfa') {
// Verify PDF header
const buffer = fs.readFileSync(path);
const header = buffer.toString('utf8', 0, 4);
expect(header).toBe('%PDF');
}
if (conversion.toFormat === 'txt') {
// Verify text content exists
const content = fs.readFileSync(path, 'utf8');
expect(content.length).toBeGreaterThan(0);
}
if (conversion.toFormat === 'csv') {
// Verify CSV content contains separators
const content = fs.readFileSync(path, 'utf8');
expect(content).toContain(',');
}
}
}
// Discover conversions at module level before tests are defined
let allConversions: ConversionEndpoint[] = [];
let availableConversions: ConversionEndpoint[] = [];
let unavailableConversions: ConversionEndpoint[] = [];
// Pre-populate conversions synchronously for test generation
(async () => {
try {
availableConversions = await conversionDiscovery.getAvailableConversions();
unavailableConversions = await conversionDiscovery.getUnavailableConversions();
allConversions = [...availableConversions, ...unavailableConversions];
} catch (error) {
console.error('Failed to discover conversions during module load:', error);
}
})();
test.describe('Convert Tool E2E Tests', () => {
test.beforeAll(async () => {
// Re-discover to ensure fresh data at test time
console.log('Re-discovering available conversion endpoints...');
availableConversions = await conversionDiscovery.getAvailableConversions();
unavailableConversions = await conversionDiscovery.getUnavailableConversions();
console.log(`Found ${availableConversions.length} available conversions:`);
availableConversions.forEach(conv => {
console.log(`${conv.endpoint}: ${conv.fromFormat}${conv.toFormat}`);
});
if (unavailableConversions.length > 0) {
console.log(`Found ${unavailableConversions.length} unavailable conversions:`);
unavailableConversions.forEach(conv => {
console.log(`${conv.endpoint}: ${conv.fromFormat}${conv.toFormat}`);
});
}
});
test.beforeEach(async ({ page }) => {
// Navigate to the homepage
await page.goto(`${BASE_URL}`);
// Wait for the page to load
await page.waitForLoadState('networkidle');
// Wait for the file upload area to appear (shown when no active files)
await page.waitForSelector('[data-testid="file-dropzone"]', { timeout: 10000 });
});
test.describe('Dynamic Conversion Tests', () => {
// Generate a test for each potentially available conversion
// We'll discover all possible conversions and then skip unavailable ones at runtime
test('PDF to PNG conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/img', fromFormat: 'pdf', toFormat: 'png' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('PDF to DOCX conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/word', fromFormat: 'pdf', toFormat: 'docx' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('DOCX to PDF conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/file/pdf', fromFormat: 'docx', toFormat: 'pdf' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('Image to PDF conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/img/pdf', fromFormat: 'png', toFormat: 'pdf' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('PDF to TXT conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/text', fromFormat: 'pdf', toFormat: 'txt' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('PDF to HTML conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/html', fromFormat: 'pdf', toFormat: 'html' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('PDF to XML conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/xml', fromFormat: 'pdf', toFormat: 'xml' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('PDF to CSV conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/csv', fromFormat: 'pdf', toFormat: 'csv' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
test('PDF to PDFA conversion', async ({ page }) => {
const conversion = { endpoint: '/api/v1/convert/pdf/pdfa', fromFormat: 'pdf', toFormat: 'pdfa' };
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
const testFile = getTestFileForFormat(conversion.fromFormat);
await page.setInputFiles('input[type="file"]', testFile);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
await testConversion(page, conversion);
});
});
test.describe('Static Tests', () => {
// Test that disabled conversions don't appear in dropdowns when they shouldn't
test('should not show conversion button when no valid conversions available', async ({ page }) => {
// This test ensures the convert button is disabled when no valid conversion is possible
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
// Click the Convert tool button
await page.click('[data-testid="tool-convert"]');
// Wait for convert mode and select file
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
await page.click('[data-testid="file-thumbnail-checkbox"]');
// Don't select any formats - convert button should not exist
const convertButton = page.locator('[data-testid="convert-button"]');
await expect(convertButton).toHaveCount(0);
});
});
});
@@ -0,0 +1,581 @@
/**
* Integration tests for Convert Tool - Tests actual conversion functionality
*
* These tests verify the integration between frontend components and backend:
* 1. useConvertOperation hook makes correct API calls
* 2. File upload/download flow functions properly
* 3. Error handling works for various failure scenarios
* 4. Parameter passing works between frontend and backend
* 5. FileContext integration works correctly
*/
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 { ConvertParameters } 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')
}));
vi.mock('../../utils/api', () => ({
makeApiUrl: vi.fn((path: string) => `/api/v1${path}`)
}));
// Create realistic test files
const createTestFile = (name: string, content: string, type: string): File => {
return new File([content], name, { type });
};
const createPDFFile = (): File => {
const pdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\ntrailer\n<<\n/Size 2\n/Root 1 0 R\n>>\nstartxref\n0\n%%EOF';
return createTestFile('test.pdf', pdfContent, 'application/pdf');
};
// Test wrapper component
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<I18nextProvider i18n={i18n}>
<FileContextProvider>
{children}
</FileContextProvider>
</I18nextProvider>
);
describe('Convert Tool Integration Tests', () => {
beforeEach(() => {
vi.clearAllMocks();
// Setup default axios mock
mockedAxios.post = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('useConvertOperation Integration', () => {
test('should make correct API call for PDF to PNG conversion', async () => {
const mockBlob = new Blob(['fake-image-data'], { type: 'image/png' });
mockedAxios.post.mockResolvedValueOnce({
data: mockBlob,
status: 200,
statusText: 'OK'
});
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
// Verify axios was called with correct parameters
expect(mockedAxios.post).toHaveBeenCalledWith(
'/api/v1/convert/pdf/img',
expect.any(FormData),
{ responseType: 'blob' }
);
// Verify FormData contains correct parameters
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
expect(formDataCall.get('imageFormat')).toBe('png');
expect(formDataCall.get('colorType')).toBe('color');
expect(formDataCall.get('dpi')).toBe('300');
expect(formDataCall.get('singleOrMultiple')).toBe('multiple');
// Verify hook state updates
expect(result.current.downloadUrl).toBeTruthy();
expect(result.current.downloadFilename).toBe('test_converted.png');
expect(result.current.isLoading).toBe(false);
expect(result.current.errorMessage).toBe(null);
});
test('should handle API error responses correctly', async () => {
const errorMessage = 'Invalid file format';
mockedAxios.post.mockRejectedValueOnce({
response: {
status: 400,
data: errorMessage
},
message: errorMessage
});
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createTestFile('invalid.txt', 'not a pdf', 'text/plain');
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
// Verify error handling
expect(result.current.errorMessage).toBe(errorMessage);
expect(result.current.isLoading).toBe(false);
expect(result.current.downloadUrl).toBe(null);
});
test('should handle network errors gracefully', async () => {
mockedAxios.post.mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
expect(result.current.errorMessage).toBe('Network error');
expect(result.current.isLoading).toBe(false);
});
});
describe('API and Hook Integration', () => {
test('should correctly map image conversion parameters to API call', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/jpeg' });
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'jpg',
pageNumbers: 'all',
imageOptions: {
colorType: 'grayscale',
dpi: 150,
singleOrMultiple: 'single',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
// Verify integration: hook parameters → FormData → axios call → hook state
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
expect(formDataCall.get('imageFormat')).toBe('jpg');
expect(formDataCall.get('colorType')).toBe('grayscale');
expect(formDataCall.get('dpi')).toBe('150');
expect(formDataCall.get('singleOrMultiple')).toBe('single');
// Verify complete workflow: API response → hook state → FileContext integration
expect(result.current.downloadUrl).toBeTruthy();
expect(result.current.files).toHaveLength(1);
expect(result.current.files[0].name).toBe('test_converted.jpg');
expect(result.current.isLoading).toBe(false);
});
test('should make correct API call for PDF to CSV conversion with simplified workflow', async () => {
const mockBlob = new Blob(['fake-csv-data'], { type: 'text/csv' });
mockedAxios.post.mockResolvedValueOnce({
data: mockBlob,
status: 200,
statusText: 'OK'
});
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'csv',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
// Verify correct endpoint is called
expect(mockedAxios.post).toHaveBeenCalledWith(
'/api/v1/convert/pdf/csv',
expect.any(FormData),
{ responseType: 'blob' }
);
// Verify FormData contains correct parameters for simplified CSV conversion
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
expect(formDataCall.get('pageNumbers')).toBe('all'); // Always "all" for simplified workflow
expect(formDataCall.get('fileInput')).toBe(testFile);
// Verify hook state updates correctly
expect(result.current.downloadUrl).toBeTruthy();
expect(result.current.downloadFilename).toBe('test_converted.csv');
expect(result.current.isLoading).toBe(false);
expect(result.current.errorMessage).toBe(null);
});
test('should handle complete unsupported conversion workflow', async () => {
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'unsupported',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
// Verify integration: utils validation prevents API call, hook shows error
expect(mockedAxios.post).not.toHaveBeenCalled();
expect(result.current.errorMessage).toContain('errorNotSupported');
expect(result.current.isLoading).toBe(false);
expect(result.current.downloadUrl).toBe(null);
});
});
describe('File Upload Integration', () => {
test('should handle multiple file uploads correctly', async () => {
const mockBlob = new Blob(['zip-content'], { type: 'application/zip' });
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const files = [
createPDFFile(),
createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf')
]
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, files);
});
// Verify both files were uploaded
const calls = mockedAxios.post.mock.calls;
for (let i = 0; i < calls.length; i++) {
const formData = calls[i][1] as FormData;
const fileInputs = formData.getAll('fileInput');
expect(fileInputs).toHaveLength(1);
expect(fileInputs[0]).toBeInstanceOf(File);
expect(fileInputs[0].name).toBe(files[i].name);
}
});
test('should handle no files selected', async () => {
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, []);
});
expect(mockedAxios.post).not.toHaveBeenCalled();
expect(result.current.status).toContain('noFileSelected');
});
});
describe('Error Boundary Integration', () => {
test('should handle corrupted file gracefully', async () => {
mockedAxios.post.mockRejectedValueOnce({
response: {
status: 422,
data: 'Processing failed'
}
});
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const corruptedFile = createTestFile('corrupted.pdf', 'not-a-pdf', 'application/pdf');
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [corruptedFile]);
});
expect(result.current.errorMessage).toBe('Processing failed');
expect(result.current.isLoading).toBe(false);
});
test('should handle backend service unavailable', async () => {
mockedAxios.post.mockRejectedValueOnce({
response: {
status: 503,
data: 'Service unavailable'
}
});
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
expect(result.current.errorMessage).toBe('Service unavailable');
expect(result.current.isLoading).toBe(false);
});
});
describe('FileContext Integration', () => {
test('should record operation in FileContext', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
// Verify operation was successful and files were processed
expect(result.current.files).toHaveLength(1);
expect(result.current.files[0].name).toBe('test_converted.png');
expect(result.current.downloadUrl).toBeTruthy();
});
test('should clean up blob URLs on reset', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const testFile = createPDFFile();
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true
},
isSmartDetection: false,
smartDetectionType: 'none'
};
await act(async () => {
await result.current.executeOperation(parameters, [testFile]);
});
expect(result.current.downloadUrl).toBeTruthy();
act(() => {
result.current.resetResults();
});
expect(result.current.downloadUrl).toBe(null);
expect(result.current.files).toHaveLength(0);
expect(result.current.errorMessage).toBe(null);
});
});
});
/**
* Additional Integration Tests That Require Real Backend
*
* These tests would require a running backend server and are better suited
* for E2E testing with tools like Playwright or Cypress:
*
* 1. **Real File Conversion Tests**
* - Upload actual PDF files and verify conversion quality
* - Test image format outputs are valid and viewable
* - Test CSV/TXT outputs contain expected content
* - Test file size limits and memory constraints
*
* 2. **Performance Integration Tests**
* - Test conversion time for various file sizes
* - Test memory usage during large file conversions
* - Test concurrent conversion requests
* - Test timeout handling for long-running conversions
*
* 3. **Authentication Integration**
* - Test conversions with and without authentication
* - Test rate limiting and user quotas
* - Test permission-based endpoint access
*
* 4. **File Preview Integration**
* - Test that converted files integrate correctly with viewer
* - Test thumbnail generation for converted files
* - Test file download functionality
* - Test FileContext persistence across tool switches
*
* 5. **Endpoint Availability Tests**
* - Test real endpoint availability checking
* - Test graceful degradation when endpoints are disabled
* - Test dynamic endpoint configuration updates
*/
@@ -0,0 +1,505 @@
/**
* 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';
import { detectFileExtension } from '../../utils/fileUtils';
// 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('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 = [
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'
});
});
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 = [
new File(['<html>content</html>'], 'page1.html', { type: 'text/html' }),
new File(['zip content'], 'site.zip', { 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 = new File(['<html>content</html>'], 'page.html', { type: '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.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 = new File(['email content'], 'email.eml', { type: '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.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 = new File(['pdf content'], 'document.pdf', { type: '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.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 = [
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 = detectFileExtension(filename);
expect(detected).toBe(expected);
});
});
});
});
+264
View File
@@ -0,0 +1,264 @@
# Convert Tool Test Suite
This directory contains comprehensive tests for the Convert Tool functionality.
## Test Files Overview
### 1. ConvertTool.test.tsx
**Purpose**: Unit/Component testing for the Convert Tool UI components
- Tests dropdown behavior and navigation
- Tests format availability based on endpoint status
- Tests UI state management and form validation
- Mocks backend dependencies for isolated testing
**Key Test Areas**:
- FROM dropdown enables/disables formats based on endpoint availability
- TO dropdown shows correct conversions for selected source format
- Format-specific options appear/disappear correctly
- Parameter validation and state management
### 2. ConvertIntegration.test.ts
**Purpose**: Integration testing for Convert Tool business logic
- Tests parameter validation and conversion matrix logic
- Tests endpoint resolution and availability checking
- Tests file extension detection
- Provides framework for testing actual conversions (requires backend)
**Key Test Areas**:
- Endpoint availability checking matches real backend status
- Conversion parameters are correctly validated
- File extension detection works properly
- Conversion matrix returns correct available formats
### 3. ConvertE2E.spec.ts
**Purpose**: End-to-End testing using Playwright with Dynamic Endpoint Discovery
- **Automatically discovers available conversion endpoints** from the backend
- Tests complete user workflows from file upload to download
- Tests actual file conversions with real backend
- **Skips tests for unavailable endpoints** automatically
- Tests error handling and edge cases
- Tests UI/UX flow and user interactions
**Key Test Areas**:
- **Dynamic endpoint discovery** using `/api/v1/config/endpoints-enabled` API
- Complete conversion workflows for **all available endpoints**
- **Unavailable endpoint testing** - verifies disabled conversions are properly blocked
- File upload, conversion, and download process
- Error handling for corrupted files and network issues
- Performance testing with large files
- UI responsiveness and progress indicators
**Supported Conversions** (tested if available):
- PDF ↔ Images (PNG, JPG, GIF, BMP, TIFF, WebP)
- PDF ↔ Office (DOCX, PPTX)
- PDF ↔ Text (TXT, HTML, XML, CSV, Markdown)
- Office → PDF (DOCX, PPTX, XLSX, etc.)
- Email (EML) → PDF
- HTML → PDF, URL → PDF
- Markdown → PDF
## Running the Tests
**Important**: All commands should be run from the `frontend/` directory:
```bash
cd frontend
```
### Setup (First Time Only)
```bash
# Install dependencies (includes test frameworks)
npm install
# Install Playwright browsers for E2E tests
npx playwright install
```
### Unit Tests (ConvertTool.test.tsx)
```bash
# Run all unit tests
npm test
# Run specific test file
npm test ConvertTool.test.tsx
# Run with coverage
npm run test:coverage
# Run in watch mode (re-runs on file changes)
npm run test:watch
# Run specific test pattern
npm test -- --grep "dropdown"
```
### Integration Tests (ConvertIntegration.test.ts)
```bash
# Run integration tests
npm test ConvertIntegration.test.ts
# Run with verbose output
npm test ConvertIntegration.test.ts -- --reporter=verbose
```
### E2E Tests (ConvertE2E.spec.ts)
```bash
# Prerequisites: Backend must be running on localhost:8080
# Start backend first, then:
# Run all E2E tests (automatically discovers available endpoints)
npm run test:e2e
# Run specific E2E test file
npx playwright test ConvertE2E.spec.ts
# Run with UI mode for debugging
npx playwright test --ui
# Run specific test by endpoint name (dynamic)
npx playwright test -g "pdf-to-img:"
# Run only available conversion tests
npx playwright test -g "Dynamic Conversion Tests"
# Run only unavailable conversion tests
npx playwright test -g "Unavailable Conversions"
# Run in headed mode (see browser)
npx playwright test --headed
# Generate HTML report
npx playwright test ConvertE2E.spec.ts --reporter=html
```
**Test Discovery Process:**
1. Tests automatically query `/api/v1/config/endpoints-enabled` to discover available conversions
2. Tests are generated dynamically for each available endpoint
3. Tests for unavailable endpoints verify they're properly disabled in the UI
4. Console output shows which endpoints were discovered
## Test Requirements
### For Unit Tests
- No special requirements
- All dependencies are mocked
- Can run in any environment
### For Integration Tests
- May require backend API for full functionality
- Uses mock data for endpoint availability
- Tests business logic in isolation
### For E2E Tests
- **Requires running backend server** (localhost:8080)
- **Requires test fixture files** (see ../test-fixtures/README.md)
- Requires frontend dev server (localhost:5173)
- Tests real conversion functionality
## Test Data
The tests use realistic endpoint availability data based on your current server configuration:
**Available Endpoints** (should pass):
- `file-to-pdf`: true (DOCX, XLSX, PPTX → PDF)
- `img-to-pdf`: true (PNG, JPG, etc. → PDF)
- `markdown-to-pdf`: true (MD → PDF)
- `pdf-to-csv`: true (PDF → CSV)
- `pdf-to-img`: true (PDF → PNG, JPG, etc.)
- `pdf-to-text`: true (PDF → TXT)
**Disabled Endpoints** (should be blocked):
- `eml-to-pdf`: false
- `html-to-pdf`: false
- `pdf-to-html`: false
- `pdf-to-markdown`: false
- `pdf-to-pdfa`: false
- `pdf-to-presentation`: false
- `pdf-to-word`: false
- `pdf-to-xml`: false
## Test Scenarios
### Success Scenarios (Available Endpoints)
1. **PDF → Image**: PDF to PNG/JPG with various DPI and color settings
2. **PDF → Data**: PDF to CSV (table extraction), PDF to TXT (text extraction)
3. **Office → PDF**: DOCX/XLSX/PPTX to PDF conversion
4. **Image → PDF**: PNG/JPG to PDF with image options
5. **Markdown → PDF**: MD to PDF with formatting preservation
### Blocked Scenarios (Disabled Endpoints)
1. **EML conversions**: Should be disabled in FROM dropdown
2. **PDF → Office**: PDF to Word/PowerPoint should be disabled
3. **PDF → Web**: PDF to HTML/XML should be disabled
4. **PDF → PDF/A**: Should be disabled
### Error Scenarios
1. **Corrupted files**: Should show helpful error messages
2. **Network failures**: Should handle backend unavailability
3. **Large files**: Should handle memory constraints gracefully
4. **Invalid parameters**: Should validate before submission
## Adding New Tests
When adding new conversion formats:
1. **Update ConvertTool.test.tsx**:
- Add the new format to test data
- Test dropdown behavior for the new format
- Test format-specific options if any
2. **Update ConvertIntegration.test.ts**:
- Add endpoint availability test cases
- Add conversion matrix test cases
- Add parameter validation tests
3. **Update ConvertE2E.spec.ts**:
- Add end-to-end workflow tests
- Add test fixture files
- Test actual conversion functionality
4. **Update test fixtures**:
- Add sample files for the new format
- Update ../test-fixtures/README.md
## Debugging Failed Tests
### Unit Test Failures
- Check mock data matches real endpoint status
- Verify component props and state management
- Check for React hook dependency issues
### Integration Test Failures
- Verify conversion matrix includes new formats
- Check endpoint name mappings
- Ensure parameter validation logic is correct
### E2E Test Failures
- Ensure backend server is running
- Check test fixture files exist and are valid
- Verify element selectors match current UI
- Check for timing issues (increase timeouts if needed)
## Test Maintenance
### Regular Updates Needed
1. **Endpoint Status**: Update mock data when backend endpoints change
2. **UI Selectors**: Update test selectors when UI changes
3. **Test Fixtures**: Replace old test files with new ones periodically
4. **Performance Benchmarks**: Update expected performance metrics
### CI/CD Integration
- Unit tests: Run on every commit
- Integration tests: Run on pull requests
- E2E tests: Run on staging deployment
- Performance tests: Run weekly or on major releases
## Performance Expectations
These tests focus on frontend functionality, not backend performance:
- **File upload/UI**: < 1 second for small test files
- **Dropdown interactions**: < 200ms
- **Form validation**: < 100ms
- **Conversion UI flow**: < 5 seconds for small test files
Tests will fail if UI interactions are slow, indicating frontend performance issues.