mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 02:54:06 +02:00
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:
co-authored by
Connor Yoh
Anthony Stirling
parent
8881f19b03
commit
9c9acbfb5b
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Unit tests for convertUtils
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import {
|
||||
getEndpointName,
|
||||
getEndpointUrl,
|
||||
isConversionSupported,
|
||||
isImageFormat
|
||||
} from './convertUtils';
|
||||
|
||||
describe('convertUtils', () => {
|
||||
|
||||
describe('getEndpointName', () => {
|
||||
|
||||
test('should return correct endpoint names for all supported conversions', () => {
|
||||
// PDF to Image formats
|
||||
expect(getEndpointName('pdf', 'png')).toBe('pdf-to-img');
|
||||
expect(getEndpointName('pdf', 'jpg')).toBe('pdf-to-img');
|
||||
expect(getEndpointName('pdf', 'gif')).toBe('pdf-to-img');
|
||||
expect(getEndpointName('pdf', 'tiff')).toBe('pdf-to-img');
|
||||
expect(getEndpointName('pdf', 'bmp')).toBe('pdf-to-img');
|
||||
expect(getEndpointName('pdf', 'webp')).toBe('pdf-to-img');
|
||||
|
||||
// PDF to Office formats
|
||||
expect(getEndpointName('pdf', 'docx')).toBe('pdf-to-word');
|
||||
expect(getEndpointName('pdf', 'odt')).toBe('pdf-to-word');
|
||||
expect(getEndpointName('pdf', 'pptx')).toBe('pdf-to-presentation');
|
||||
expect(getEndpointName('pdf', 'odp')).toBe('pdf-to-presentation');
|
||||
|
||||
// PDF to Data formats
|
||||
expect(getEndpointName('pdf', 'csv')).toBe('pdf-to-csv');
|
||||
expect(getEndpointName('pdf', 'txt')).toBe('pdf-to-text');
|
||||
expect(getEndpointName('pdf', 'rtf')).toBe('pdf-to-text');
|
||||
expect(getEndpointName('pdf', 'md')).toBe('pdf-to-markdown');
|
||||
|
||||
// PDF to Web formats
|
||||
expect(getEndpointName('pdf', 'html')).toBe('pdf-to-html');
|
||||
expect(getEndpointName('pdf', 'xml')).toBe('pdf-to-xml');
|
||||
|
||||
// PDF to PDF/A
|
||||
expect(getEndpointName('pdf', 'pdfa')).toBe('pdf-to-pdfa');
|
||||
|
||||
// Office Documents to PDF
|
||||
expect(getEndpointName('docx', 'pdf')).toBe('file-to-pdf');
|
||||
expect(getEndpointName('doc', 'pdf')).toBe('file-to-pdf');
|
||||
expect(getEndpointName('odt', 'pdf')).toBe('file-to-pdf');
|
||||
|
||||
// Spreadsheets to PDF
|
||||
expect(getEndpointName('xlsx', 'pdf')).toBe('file-to-pdf');
|
||||
expect(getEndpointName('xls', 'pdf')).toBe('file-to-pdf');
|
||||
expect(getEndpointName('ods', 'pdf')).toBe('file-to-pdf');
|
||||
|
||||
// Presentations to PDF
|
||||
expect(getEndpointName('pptx', 'pdf')).toBe('file-to-pdf');
|
||||
expect(getEndpointName('ppt', 'pdf')).toBe('file-to-pdf');
|
||||
expect(getEndpointName('odp', 'pdf')).toBe('file-to-pdf');
|
||||
|
||||
// Images to PDF
|
||||
expect(getEndpointName('jpg', 'pdf')).toBe('img-to-pdf');
|
||||
expect(getEndpointName('jpeg', 'pdf')).toBe('img-to-pdf');
|
||||
expect(getEndpointName('png', 'pdf')).toBe('img-to-pdf');
|
||||
expect(getEndpointName('gif', 'pdf')).toBe('img-to-pdf');
|
||||
expect(getEndpointName('bmp', 'pdf')).toBe('img-to-pdf');
|
||||
expect(getEndpointName('tiff', 'pdf')).toBe('img-to-pdf');
|
||||
expect(getEndpointName('webp', 'pdf')).toBe('img-to-pdf');
|
||||
|
||||
// Web formats to PDF
|
||||
expect(getEndpointName('html', 'pdf')).toBe('html-to-pdf');
|
||||
|
||||
// Markdown to PDF
|
||||
expect(getEndpointName('md', 'pdf')).toBe('markdown-to-pdf');
|
||||
|
||||
// Text formats to PDF
|
||||
expect(getEndpointName('txt', 'pdf')).toBe('file-to-pdf');
|
||||
expect(getEndpointName('rtf', 'pdf')).toBe('file-to-pdf');
|
||||
|
||||
// Email to PDF
|
||||
expect(getEndpointName('eml', 'pdf')).toBe('eml-to-pdf');
|
||||
});
|
||||
|
||||
test('should return empty string for unsupported conversions', () => {
|
||||
expect(getEndpointName('pdf', 'exe')).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
|
||||
});
|
||||
|
||||
test('should handle empty or invalid inputs', () => {
|
||||
expect(getEndpointName('', '')).toBe('');
|
||||
expect(getEndpointName('pdf', '')).toBe('');
|
||||
expect(getEndpointName('', 'pdf')).toBe('');
|
||||
expect(getEndpointName('nonexistent', 'alsononexistent')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEndpointUrl', () => {
|
||||
|
||||
test('should return correct endpoint URLs for all supported conversions', () => {
|
||||
// PDF to Image formats
|
||||
expect(getEndpointUrl('pdf', 'png')).toBe('/api/v1/convert/pdf/img');
|
||||
expect(getEndpointUrl('pdf', 'jpg')).toBe('/api/v1/convert/pdf/img');
|
||||
expect(getEndpointUrl('pdf', 'gif')).toBe('/api/v1/convert/pdf/img');
|
||||
expect(getEndpointUrl('pdf', 'tiff')).toBe('/api/v1/convert/pdf/img');
|
||||
expect(getEndpointUrl('pdf', 'bmp')).toBe('/api/v1/convert/pdf/img');
|
||||
expect(getEndpointUrl('pdf', 'webp')).toBe('/api/v1/convert/pdf/img');
|
||||
|
||||
// PDF to Office formats
|
||||
expect(getEndpointUrl('pdf', 'docx')).toBe('/api/v1/convert/pdf/word');
|
||||
expect(getEndpointUrl('pdf', 'odt')).toBe('/api/v1/convert/pdf/word');
|
||||
expect(getEndpointUrl('pdf', 'pptx')).toBe('/api/v1/convert/pdf/presentation');
|
||||
expect(getEndpointUrl('pdf', 'odp')).toBe('/api/v1/convert/pdf/presentation');
|
||||
|
||||
// PDF to Data formats
|
||||
expect(getEndpointUrl('pdf', 'csv')).toBe('/api/v1/convert/pdf/csv');
|
||||
expect(getEndpointUrl('pdf', 'txt')).toBe('/api/v1/convert/pdf/text');
|
||||
expect(getEndpointUrl('pdf', 'rtf')).toBe('/api/v1/convert/pdf/text');
|
||||
expect(getEndpointUrl('pdf', 'md')).toBe('/api/v1/convert/pdf/markdown');
|
||||
|
||||
// PDF to Web formats
|
||||
expect(getEndpointUrl('pdf', 'html')).toBe('/api/v1/convert/pdf/html');
|
||||
expect(getEndpointUrl('pdf', 'xml')).toBe('/api/v1/convert/pdf/xml');
|
||||
|
||||
// PDF to PDF/A
|
||||
expect(getEndpointUrl('pdf', 'pdfa')).toBe('/api/v1/convert/pdf/pdfa');
|
||||
|
||||
// Office Documents to PDF
|
||||
expect(getEndpointUrl('docx', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
expect(getEndpointUrl('doc', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
expect(getEndpointUrl('odt', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
|
||||
// Spreadsheets to PDF
|
||||
expect(getEndpointUrl('xlsx', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
expect(getEndpointUrl('xls', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
expect(getEndpointUrl('ods', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
|
||||
// Presentations to PDF
|
||||
expect(getEndpointUrl('pptx', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
expect(getEndpointUrl('ppt', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
expect(getEndpointUrl('odp', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
|
||||
// Images to PDF
|
||||
expect(getEndpointUrl('jpg', 'pdf')).toBe('/api/v1/convert/img/pdf');
|
||||
expect(getEndpointUrl('jpeg', 'pdf')).toBe('/api/v1/convert/img/pdf');
|
||||
expect(getEndpointUrl('png', 'pdf')).toBe('/api/v1/convert/img/pdf');
|
||||
expect(getEndpointUrl('gif', 'pdf')).toBe('/api/v1/convert/img/pdf');
|
||||
expect(getEndpointUrl('bmp', 'pdf')).toBe('/api/v1/convert/img/pdf');
|
||||
expect(getEndpointUrl('tiff', 'pdf')).toBe('/api/v1/convert/img/pdf');
|
||||
expect(getEndpointUrl('webp', 'pdf')).toBe('/api/v1/convert/img/pdf');
|
||||
|
||||
// Web formats to PDF
|
||||
expect(getEndpointUrl('html', 'pdf')).toBe('/api/v1/convert/html/pdf');
|
||||
|
||||
// Markdown to PDF
|
||||
expect(getEndpointUrl('md', 'pdf')).toBe('/api/v1/convert/markdown/pdf');
|
||||
|
||||
// Text formats to PDF
|
||||
expect(getEndpointUrl('txt', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
expect(getEndpointUrl('rtf', 'pdf')).toBe('/api/v1/convert/file/pdf');
|
||||
|
||||
// Email to PDF
|
||||
expect(getEndpointUrl('eml', 'pdf')).toBe('/api/v1/convert/eml/pdf');
|
||||
});
|
||||
|
||||
test('should return empty string for unsupported conversions', () => {
|
||||
expect(getEndpointUrl('pdf', 'exe')).toBe('');
|
||||
expect(getEndpointUrl('wav', 'pdf')).toBe('/api/v1/convert/file/pdf'); // Try using file to pdf as fallback
|
||||
expect(getEndpointUrl('invalid', 'invalid')).toBe('');
|
||||
});
|
||||
|
||||
test('should handle empty inputs', () => {
|
||||
expect(getEndpointUrl('', '')).toBe('');
|
||||
expect(getEndpointUrl('pdf', '')).toBe('');
|
||||
expect(getEndpointUrl('', 'pdf')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConversionSupported', () => {
|
||||
|
||||
test('should return true for all supported conversions', () => {
|
||||
// PDF to Image formats
|
||||
expect(isConversionSupported('pdf', 'png')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'jpg')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'gif')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'tiff')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'bmp')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'webp')).toBe(true);
|
||||
|
||||
// PDF to Office formats
|
||||
expect(isConversionSupported('pdf', 'docx')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'odt')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'pptx')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'odp')).toBe(true);
|
||||
|
||||
// PDF to Data formats
|
||||
expect(isConversionSupported('pdf', 'csv')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'txt')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'rtf')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'md')).toBe(true);
|
||||
|
||||
// PDF to Web formats
|
||||
expect(isConversionSupported('pdf', 'html')).toBe(true);
|
||||
expect(isConversionSupported('pdf', 'xml')).toBe(true);
|
||||
|
||||
// PDF to PDF/A
|
||||
expect(isConversionSupported('pdf', 'pdfa')).toBe(true);
|
||||
|
||||
// Office Documents to PDF
|
||||
expect(isConversionSupported('docx', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('doc', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('odt', 'pdf')).toBe(true);
|
||||
|
||||
// Spreadsheets to PDF
|
||||
expect(isConversionSupported('xlsx', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('xls', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('ods', 'pdf')).toBe(true);
|
||||
|
||||
// Presentations to PDF
|
||||
expect(isConversionSupported('pptx', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('ppt', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('odp', 'pdf')).toBe(true);
|
||||
|
||||
// Images to PDF
|
||||
expect(isConversionSupported('jpg', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('jpeg', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('png', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('gif', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('bmp', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('tiff', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('webp', 'pdf')).toBe(true);
|
||||
|
||||
// Web formats to PDF
|
||||
expect(isConversionSupported('html', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('htm', 'pdf')).toBe(true);
|
||||
|
||||
// Markdown to PDF
|
||||
expect(isConversionSupported('md', 'pdf')).toBe(true);
|
||||
|
||||
// Text formats to PDF
|
||||
expect(isConversionSupported('txt', 'pdf')).toBe(true);
|
||||
expect(isConversionSupported('rtf', 'pdf')).toBe(true);
|
||||
|
||||
// Email to PDF
|
||||
expect(isConversionSupported('eml', 'pdf')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for unsupported conversions', () => {
|
||||
expect(isConversionSupported('pdf', 'exe')).toBe(false);
|
||||
expect(isConversionSupported('wav', 'pdf')).toBe(true); // Fallback to file to pdf
|
||||
expect(isConversionSupported('png', 'docx')).toBe(false);
|
||||
expect(isConversionSupported('nonexistent', 'alsononexistent')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle empty inputs', () => {
|
||||
expect(isConversionSupported('', '')).toBe(false);
|
||||
expect(isConversionSupported('pdf', '')).toBe(false);
|
||||
expect(isConversionSupported('', 'pdf')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isImageFormat', () => {
|
||||
|
||||
test('should return true for image formats', () => {
|
||||
expect(isImageFormat('png')).toBe(true);
|
||||
expect(isImageFormat('jpg')).toBe(true);
|
||||
expect(isImageFormat('jpeg')).toBe(true);
|
||||
expect(isImageFormat('gif')).toBe(true);
|
||||
expect(isImageFormat('tiff')).toBe(true);
|
||||
expect(isImageFormat('bmp')).toBe(true);
|
||||
expect(isImageFormat('webp')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for non-image formats', () => {
|
||||
expect(isImageFormat('pdf')).toBe(false);
|
||||
expect(isImageFormat('docx')).toBe(false);
|
||||
expect(isImageFormat('txt')).toBe(false);
|
||||
expect(isImageFormat('csv')).toBe(false);
|
||||
expect(isImageFormat('html')).toBe(false);
|
||||
expect(isImageFormat('xml')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle case insensitivity', () => {
|
||||
expect(isImageFormat('PNG')).toBe(true);
|
||||
expect(isImageFormat('JPG')).toBe(true);
|
||||
expect(isImageFormat('JPEG')).toBe(true);
|
||||
expect(isImageFormat('Png')).toBe(true);
|
||||
expect(isImageFormat('JpG')).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle empty and invalid inputs', () => {
|
||||
expect(isImageFormat('')).toBe(false);
|
||||
expect(isImageFormat('invalid')).toBe(false);
|
||||
expect(isImageFormat('123')).toBe(false);
|
||||
expect(isImageFormat('.')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle mixed case and edge cases', () => {
|
||||
expect(isImageFormat('webP')).toBe(true);
|
||||
expect(isImageFormat('WEBP')).toBe(true);
|
||||
expect(isImageFormat('tIFf')).toBe(true);
|
||||
expect(isImageFormat('bMp')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
|
||||
test('should handle null and undefined inputs gracefully', () => {
|
||||
// Note: TypeScript prevents these, but test runtime behavior for robustness
|
||||
// The current implementation handles these gracefully by returning falsy values
|
||||
expect(getEndpointName(null as any, null as any)).toBe('');
|
||||
expect(getEndpointUrl(undefined as any, undefined as any)).toBe('');
|
||||
expect(isConversionSupported(null as any, null as any)).toBe(false);
|
||||
|
||||
// isImageFormat will throw because it calls toLowerCase() on null/undefined
|
||||
expect(() => isImageFormat(null as any)).toThrow();
|
||||
expect(() => isImageFormat(undefined as any)).toThrow();
|
||||
});
|
||||
|
||||
test('should handle special characters in file extensions', () => {
|
||||
expect(isImageFormat('png@')).toBe(false);
|
||||
expect(isImageFormat('jpg#')).toBe(false);
|
||||
expect(isImageFormat('png.')).toBe(false);
|
||||
expect(getEndpointName('pdf@', 'png')).toBe('');
|
||||
expect(getEndpointName('pdf', 'png#')).toBe('');
|
||||
});
|
||||
|
||||
test('should handle very long extension names', () => {
|
||||
const longExtension = 'a'.repeat(100);
|
||||
expect(isImageFormat(longExtension)).toBe(false);
|
||||
expect(getEndpointName('pdf', longExtension)).toBe('');
|
||||
expect(getEndpointName(longExtension, 'pdf')).toBe('file-to-pdf'); // Fallback to file to pdf
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
CONVERSION_ENDPOINTS,
|
||||
ENDPOINT_NAMES,
|
||||
EXTENSION_TO_ENDPOINT
|
||||
} from '../constants/convertConstants';
|
||||
|
||||
/**
|
||||
* Resolves the endpoint name for a given conversion
|
||||
*/
|
||||
export const getEndpointName = (fromExtension: string, toExtension: string): string => {
|
||||
if (!fromExtension || !toExtension) return '';
|
||||
|
||||
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 || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves the full endpoint URL for a given conversion
|
||||
*/
|
||||
export const getEndpointUrl = (fromExtension: string, toExtension: string): string => {
|
||||
const endpointName = getEndpointName(fromExtension, toExtension);
|
||||
if (!endpointName) return '';
|
||||
|
||||
// Find the endpoint URL from CONVERSION_ENDPOINTS using the endpoint name
|
||||
for (const [key, endpoint] of Object.entries(CONVERSION_ENDPOINTS)) {
|
||||
if (ENDPOINT_NAMES[key as keyof typeof ENDPOINT_NAMES] === endpointName) {
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a conversion is supported
|
||||
*/
|
||||
export const isConversionSupported = (fromExtension: string, toExtension: string): boolean => {
|
||||
return getEndpointName(fromExtension, toExtension) !== '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given extension is an image format
|
||||
*/
|
||||
export const isImageFormat = (extension: string): boolean => {
|
||||
return ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bmp', 'webp', 'svg'].includes(extension.toLowerCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given extension is a web format
|
||||
*/
|
||||
export const isWebFormat = (extension: string): boolean => {
|
||||
return ['html', 'zip'].includes(extension.toLowerCase());
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Unit tests for file response utility functions
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { getFilenameFromHeaders, createFileFromApiResponse } from './fileResponseUtils';
|
||||
|
||||
describe('fileResponseUtils', () => {
|
||||
|
||||
describe('getFilenameFromHeaders', () => {
|
||||
|
||||
test('should extract filename from content-disposition header', () => {
|
||||
const contentDisposition = 'attachment; filename="document.pdf"';
|
||||
const filename = getFilenameFromHeaders(contentDisposition);
|
||||
|
||||
expect(filename).toBe('document.pdf');
|
||||
});
|
||||
|
||||
test('should extract filename without quotes', () => {
|
||||
const contentDisposition = 'attachment; filename=document.pdf';
|
||||
const filename = getFilenameFromHeaders(contentDisposition);
|
||||
|
||||
expect(filename).toBe('document.pdf');
|
||||
});
|
||||
|
||||
test('should handle single quotes', () => {
|
||||
const contentDisposition = "attachment; filename='document.pdf'";
|
||||
const filename = getFilenameFromHeaders(contentDisposition);
|
||||
|
||||
expect(filename).toBe('document.pdf');
|
||||
});
|
||||
|
||||
test('should return null for malformed header', () => {
|
||||
const contentDisposition = 'attachment; invalid=format';
|
||||
const filename = getFilenameFromHeaders(contentDisposition);
|
||||
|
||||
expect(filename).toBe(null);
|
||||
});
|
||||
|
||||
test('should return null for empty header', () => {
|
||||
const filename = getFilenameFromHeaders('');
|
||||
|
||||
expect(filename).toBe(null);
|
||||
});
|
||||
|
||||
test('should return null for undefined header', () => {
|
||||
const filename = getFilenameFromHeaders();
|
||||
|
||||
expect(filename).toBe(null);
|
||||
});
|
||||
|
||||
test('should handle complex filenames with spaces and special chars', () => {
|
||||
const contentDisposition = 'attachment; filename="My Document (1).pdf"';
|
||||
const filename = getFilenameFromHeaders(contentDisposition);
|
||||
|
||||
expect(filename).toBe('My Document (1).pdf');
|
||||
});
|
||||
|
||||
test('should handle filename with extension when downloadHtml is enabled', () => {
|
||||
const contentDisposition = 'attachment; filename="email_content.html"';
|
||||
const filename = getFilenameFromHeaders(contentDisposition);
|
||||
|
||||
expect(filename).toBe('email_content.html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFileFromApiResponse', () => {
|
||||
|
||||
test('should create file using header filename when available', () => {
|
||||
const responseData = new Uint8Array([1, 2, 3, 4]);
|
||||
const headers = {
|
||||
'content-type': 'application/pdf',
|
||||
'content-disposition': 'attachment; filename="server_filename.pdf"'
|
||||
};
|
||||
const fallbackFilename = 'fallback.pdf';
|
||||
|
||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
|
||||
expect(file.name).toBe('server_filename.pdf');
|
||||
expect(file.type).toBe('application/pdf');
|
||||
expect(file.size).toBe(4);
|
||||
});
|
||||
|
||||
test('should use fallback filename when no header filename', () => {
|
||||
const responseData = new Uint8Array([1, 2, 3, 4]);
|
||||
const headers = {
|
||||
'content-type': 'application/pdf'
|
||||
};
|
||||
const fallbackFilename = 'converted_file.pdf';
|
||||
|
||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
|
||||
expect(file.name).toBe('converted_file.pdf');
|
||||
expect(file.type).toBe('application/pdf');
|
||||
});
|
||||
|
||||
test('should handle HTML response when downloadHtml is enabled', () => {
|
||||
const responseData = '<html><body>Test</body></html>';
|
||||
const headers = {
|
||||
'content-type': 'text/html',
|
||||
'content-disposition': 'attachment; filename="email_content.html"'
|
||||
};
|
||||
const fallbackFilename = 'fallback.pdf';
|
||||
|
||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
|
||||
expect(file.name).toBe('email_content.html');
|
||||
expect(file.type).toBe('text/html');
|
||||
});
|
||||
|
||||
test('should handle ZIP response', () => {
|
||||
const responseData = new Uint8Array([80, 75, 3, 4]); // ZIP file signature
|
||||
const headers = {
|
||||
'content-type': 'application/zip',
|
||||
'content-disposition': 'attachment; filename="converted_files.zip"'
|
||||
};
|
||||
const fallbackFilename = 'fallback.pdf';
|
||||
|
||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
|
||||
expect(file.name).toBe('converted_files.zip');
|
||||
expect(file.type).toBe('application/zip');
|
||||
});
|
||||
|
||||
test('should use default content-type when none provided', () => {
|
||||
const responseData = new Uint8Array([1, 2, 3, 4]);
|
||||
const headers = {};
|
||||
const fallbackFilename = 'test.bin';
|
||||
|
||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
|
||||
expect(file.name).toBe('test.bin');
|
||||
expect(file.type).toBe('application/octet-stream');
|
||||
});
|
||||
|
||||
test('should handle null/undefined headers gracefully', () => {
|
||||
const responseData = new Uint8Array([1, 2, 3, 4]);
|
||||
const headers = null;
|
||||
const fallbackFilename = 'test.bin';
|
||||
|
||||
const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
|
||||
expect(file.name).toBe('test.bin');
|
||||
expect(file.type).toBe('application/octet-stream');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Generic utility functions for handling file responses from API endpoints
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts filename from Content-Disposition header
|
||||
* @param contentDisposition - Content-Disposition header value
|
||||
* @returns Filename if found, null otherwise
|
||||
*/
|
||||
export const getFilenameFromHeaders = (contentDisposition: string = ''): string | null => {
|
||||
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (match && match[1]) {
|
||||
return match[1].replace(/['"]/g, '');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a File object from API response using the filename from headers
|
||||
* @param responseData - The response data (blob/arraybuffer/string)
|
||||
* @param headers - Response headers object
|
||||
* @param fallbackFilename - Filename to use if none provided in headers
|
||||
* @returns File object
|
||||
*/
|
||||
export const createFileFromApiResponse = (
|
||||
responseData: any,
|
||||
headers: any,
|
||||
fallbackFilename: string
|
||||
): File => {
|
||||
const contentType = headers?.['content-type'] || 'application/octet-stream';
|
||||
const contentDisposition = headers?.['content-disposition'] || '';
|
||||
|
||||
const filename = getFilenameFromHeaders(contentDisposition) || fallbackFilename;
|
||||
const blob = new Blob([responseData], { type: contentType });
|
||||
|
||||
return new File([blob], filename, { type: contentType });
|
||||
};
|
||||
@@ -125,4 +125,51 @@ export function cleanupFileUrls(files: FileWithUrl[]): void {
|
||||
export function shouldUseDirectIndexedDBAccess(file: FileWithUrl): boolean {
|
||||
const FILE_SIZE_LIMIT = 100 * 1024 * 1024; // 100MB
|
||||
return file.size > FILE_SIZE_LIMIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects and normalizes file extension from filename
|
||||
* @param filename - The filename to extract extension from
|
||||
* @returns Normalized file extension in lowercase, empty string if no extension
|
||||
*/
|
||||
export function detectFileExtension(filename: string): string {
|
||||
if (!filename || typeof filename !== 'string') return '';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filename without extension
|
||||
* @param filename - The filename to process
|
||||
* @returns Filename without extension
|
||||
*/
|
||||
export function getFilenameWithoutExtension(filename: string): string {
|
||||
if (!filename || typeof filename !== 'string') return '';
|
||||
|
||||
const parts = filename.split('.');
|
||||
if (parts.length <= 1) return filename;
|
||||
|
||||
// Return all parts except the last one (extension)
|
||||
return parts.slice(0, -1).join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new filename with a different extension
|
||||
* @param filename - Original filename
|
||||
* @param newExtension - New extension (without dot)
|
||||
* @returns New filename with the specified extension
|
||||
*/
|
||||
export function changeFileExtension(filename: string, newExtension: string): string {
|
||||
const nameWithoutExt = getFilenameWithoutExtension(filename);
|
||||
return `${nameWithoutExt}.${newExtension}`;
|
||||
}
|
||||
@@ -24,6 +24,11 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
||||
console.log('Skipping thumbnail generation for large file:', file.name);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('application/pdf')) {
|
||||
console.warn('File is not a PDF, skipping thumbnail generation:', file.name);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Generating thumbnail for', file.name);
|
||||
|
||||
Reference in New Issue
Block a user