eml to pdf

This commit is contained in:
Connor Yoh 2025-07-31 14:46:14 +01:00
parent 8da72af478
commit 5b8eea686e
12 changed files with 603 additions and 62 deletions

View File

@ -0,0 +1,77 @@
import React from 'react';
import { Stack, Text, NumberInput, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
interface ConvertFromEmailSettingsProps {
parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
disabled?: boolean;
}
const ConvertFromEmailSettings = ({
parameters,
onParameterChange,
disabled = false
}: ConvertFromEmailSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm" data-testid="email-settings">
<Text size="sm" fw={500}>{t("convert.emailOptions", "Email to PDF Options")}:</Text>
<Checkbox
label={t("convert.includeAttachments", "Include email attachments")}
checked={parameters.emailOptions.includeAttachments}
onChange={(event) => onParameterChange('emailOptions', {
...parameters.emailOptions,
includeAttachments: event.currentTarget.checked
})}
disabled={disabled}
data-testid="include-attachments-checkbox"
/>
{parameters.emailOptions.includeAttachments && (
<Stack gap="xs">
<Text size="xs" fw={500}>{t("convert.maxAttachmentSize", "Maximum attachment size (MB)")}:</Text>
<NumberInput
value={parameters.emailOptions.maxAttachmentSizeMB}
onChange={(value) => onParameterChange('emailOptions', {
...parameters.emailOptions,
maxAttachmentSizeMB: Number(value) || 10
})}
min={1}
max={100}
step={1}
disabled={disabled}
data-testid="max-attachment-size-input"
/>
</Stack>
)}
<Checkbox
label={t("convert.includeAllRecipients", "Include CC and BCC recipients in header")}
checked={parameters.emailOptions.includeAllRecipients}
onChange={(event) => onParameterChange('emailOptions', {
...parameters.emailOptions,
includeAllRecipients: event.currentTarget.checked
})}
disabled={disabled}
data-testid="include-all-recipients-checkbox"
/>
<Checkbox
label={t("convert.downloadHtml", "Download HTML intermediate file instead of PDF")}
checked={parameters.emailOptions.downloadHtml}
onChange={(event) => onParameterChange('emailOptions', {
...parameters.emailOptions,
downloadHtml: event.currentTarget.checked
})}
disabled={disabled}
data-testid="download-html-checkbox"
/>
</Stack>
);
};
export default ConvertFromEmailSettings;

View File

@ -0,0 +1,55 @@
import React from 'react';
import { Stack, Text, NumberInput, Slider } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
interface ConvertFromWebSettingsProps {
parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
disabled?: boolean;
}
const ConvertFromWebSettings = ({
parameters,
onParameterChange,
disabled = false
}: ConvertFromWebSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm" data-testid="web-settings">
<Text size="sm" fw={500}>{t("convert.webOptions", "Web to PDF Options")}:</Text>
<Stack gap="xs">
<Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text>
<NumberInput
value={parameters.htmlOptions.zoomLevel}
onChange={(value) => onParameterChange('htmlOptions', {
...parameters.htmlOptions,
zoomLevel: Number(value) || 1.0
})}
min={0.1}
max={3.0}
step={0.1}
precision={1}
disabled={disabled}
data-testid="zoom-level-input"
/>
<Slider
value={parameters.htmlOptions.zoomLevel}
onChange={(value) => onParameterChange('htmlOptions', {
...parameters.htmlOptions,
zoomLevel: value
})}
min={0.1}
max={3.0}
step={0.1}
disabled={disabled}
data-testid="zoom-level-slider"
/>
</Stack>
</Stack>
);
};
export default ConvertFromWebSettings;

View File

@ -1,5 +1,5 @@
import React, { useMemo } from "react";
import { Stack, Text, Group, Divider, UnstyledButton, useMantineTheme, useMantineColorScheme, NumberInput, Slider } from "@mantine/core";
import { Stack, Text, Group, Divider, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import { useTranslation } from "react-i18next";
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
@ -10,6 +10,8 @@ import { detectFileExtension } from "../../../utils/fileUtils";
import GroupedFormatDropdown from "./GroupedFormatDropdown";
import ConvertToImageSettings from "./ConvertToImageSettings";
import ConvertFromImageSettings from "./ConvertFromImageSettings";
import ConvertFromWebSettings from "./ConvertFromWebSettings";
import ConvertFromEmailSettings from "./ConvertFromEmailSettings";
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
import {
FROM_FORMAT_OPTIONS,
@ -106,6 +108,12 @@ const ConvertSettings = ({
autoRotate: true,
combineImages: true,
});
onParameterChange('emailOptions', {
includeAttachments: true,
maxAttachmentSizeMB: 10,
downloadHtml: false,
includeAllRecipients: false,
});
// Disable smart detection when manually changing source format
onParameterChange('isSmartDetection', false);
onParameterChange('smartDetectionType', 'none');
@ -147,6 +155,12 @@ const ConvertSettings = ({
autoRotate: true,
combineImages: true,
});
onParameterChange('emailOptions', {
includeAttachments: true,
maxAttachmentSizeMB: 10,
downloadHtml: false,
includeAllRecipients: false,
});
};
@ -235,50 +249,28 @@ const ConvertSettings = ({
</>
) : null}
{/* HTML to PDF specific options */}
{/* Web to PDF options */}
{((isWebFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') ||
(parameters.isSmartDetection && parameters.smartDetectionType === 'web')) && (
(parameters.isSmartDetection && parameters.smartDetectionType === 'web')) ? (
<>
<Divider />
<Stack gap="sm" data-testid="html-options-section">
<Text size="sm" fw={500} data-testid="html-options-title">{t("convert.htmlOptions", "HTML Options")}:</Text>
<Stack gap="xs">
<Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text>
<NumberInput
value={parameters.htmlOptions.zoomLevel}
onChange={(value) => onParameterChange('htmlOptions', { ...parameters.htmlOptions, zoomLevel: Number(value) || 1.0 })}
min={0.1}
max={3.0}
step={0.1}
precision={1}
<ConvertFromWebSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
data-testid="zoom-level-input"
/>
<Slider
value={parameters.htmlOptions.zoomLevel}
onChange={(value) => onParameterChange('htmlOptions', { ...parameters.htmlOptions, zoomLevel: value })}
min={0.1}
max={3.0}
step={0.1}
disabled={disabled}
data-testid="zoom-level-slider"
/>
</Stack>
</Stack>
</>
)}
) : null}
{/* EML specific options */}
{/* Email to PDF options */}
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
<>
<Divider />
<Stack gap="sm" data-testid="eml-options-section">
<Text size="sm" fw={500} data-testid="eml-options-title">{t("convert.emlOptions", "Email Options")}:</Text>
<Text size="xs" c="dimmed" data-testid="eml-options-note">
{t("convert.emlNote", "Email attachments and embedded images will be included in the PDF conversion.")}
</Text>
</Stack>
<ConvertFromEmailSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</>
)}

View File

@ -6,6 +6,7 @@ import { FileOperation } from '../../../types/fileContext';
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
import { ConvertParameters } from './useConvertParameters';
import { detectFileExtension } from '../../../utils/fileUtils';
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
@ -56,7 +57,7 @@ const shouldProcessFilesSeparately = (
};
/**
* Creates a file from API response with appropriate naming
* Creates a file from API response with fallback naming
*/
const createFileFromResponse = (
responseData: any,
@ -64,20 +65,10 @@ const createFileFromResponse = (
originalFileName: string,
targetExtension: string
): File => {
const contentType = headers?.['content-type'] || 'application/octet-stream';
const blob = new Blob([responseData], { type: contentType });
const originalName = originalFileName.split('.')[0];
let filename: string;
const fallbackFilename = `${originalName}_converted.${targetExtension}`;
// Check if response is a ZIP
if (contentType.includes('zip') || headers?.['content-disposition']?.includes('.zip')) {
filename = `${originalName}_converted.zip`;
} else {
filename = `${originalName}_converted.${targetExtension}`;
}
return new File([blob], filename, { type: contentType });
return createFileFromApiResponse(responseData, headers, fallbackFilename);
};
/**
@ -152,7 +143,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
formData.append("fileInput", file);
});
const { fromExtension, toExtension, imageOptions, htmlOptions } = parameters;
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions } = parameters;
// Add conversion-specific parameters
if (isImageFormat(toExtension)) {
@ -173,6 +164,12 @@ export const useConvertOperation = (): ConvertOperationHook => {
} else if ((fromExtension === 'html' || fromExtension === 'zip') && toExtension === 'pdf') {
// HTML to PDF conversion with zoom level (includes ZIP files with HTML)
formData.append("zoom", htmlOptions.zoomLevel.toString());
} else if (fromExtension === 'eml' && toExtension === 'pdf') {
// Email to PDF conversion with email-specific options
formData.append("includeAttachments", emailOptions.includeAttachments.toString());
formData.append("maxAttachmentSizeMB", emailOptions.maxAttachmentSizeMB.toString());
formData.append("downloadHtml", emailOptions.downloadHtml.toString());
formData.append("includeAllRecipients", emailOptions.includeAllRecipients.toString());
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
// CSV extraction - always process all pages for simplified workflow
formData.append("pageNumbers", "all");
@ -201,6 +198,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
toExtension: parameters.toExtension,
imageOptions: parameters.imageOptions,
htmlOptions: parameters.htmlOptions,
emailOptions: parameters.emailOptions,
},
fileSize: selectedFiles[0].size
}

View File

@ -18,6 +18,11 @@ describe('useConvertParameters', () => {
expect(result.current.parameters.imageOptions.colorType).toBe('color');
expect(result.current.parameters.imageOptions.dpi).toBe(300);
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('multiple');
expect(result.current.parameters.htmlOptions.zoomLevel).toBe(1.0);
expect(result.current.parameters.emailOptions.includeAttachments).toBe(true);
expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(10);
expect(result.current.parameters.emailOptions.downloadHtml).toBe(false);
expect(result.current.parameters.emailOptions.includeAllRecipients).toBe(false);
});
test('should update individual parameters', () => {
@ -47,6 +52,36 @@ describe('useConvertParameters', () => {
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('single');
});
test('should update nested HTML options', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('htmlOptions', {
zoomLevel: 1.5
});
});
expect(result.current.parameters.htmlOptions.zoomLevel).toBe(1.5);
});
test('should update nested email options', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('emailOptions', {
includeAttachments: false,
maxAttachmentSizeMB: 20,
downloadHtml: true,
includeAllRecipients: true
});
});
expect(result.current.parameters.emailOptions.includeAttachments).toBe(false);
expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(20);
expect(result.current.parameters.emailOptions.downloadHtml).toBe(true);
expect(result.current.parameters.emailOptions.includeAllRecipients).toBe(true);
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useConvertParameters());

View File

@ -26,6 +26,12 @@ export interface ConvertParameters {
htmlOptions: {
zoomLevel: number;
};
emailOptions: {
includeAttachments: boolean;
maxAttachmentSizeMB: number;
downloadHtml: boolean;
includeAllRecipients: boolean;
};
isSmartDetection: boolean;
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
}
@ -55,6 +61,12 @@ const initialParameters: ConvertParameters = {
htmlOptions: {
zoomLevel: 1.0,
},
emailOptions: {
includeAttachments: true,
maxAttachmentSizeMB: 10,
downloadHtml: false,
includeAllRecipients: false,
},
isSmartDetection: false,
smartDetectionType: 'none',
};

View File

@ -149,6 +149,61 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
});
describe('Smart Detection - All Web Files', () => {
test('should detect all web files and enable web smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const webFiles = [
{ name: 'page1.html' },
{ name: 'archive.zip' }
];
act(() => {
result.current.analyzeFileTypes(webFiles);
});
expect(result.current.parameters.fromExtension).toBe('html');
expect(result.current.parameters.toExtension).toBe('pdf');
expect(result.current.parameters.isSmartDetection).toBe(true);
expect(result.current.parameters.smartDetectionType).toBe('web');
});
test('should handle mixed case web extensions', () => {
const { result } = renderHook(() => useConvertParameters());
const webFiles = [
{ name: 'page1.HTML' },
{ name: 'archive.ZIP' }
];
act(() => {
result.current.analyzeFileTypes(webFiles);
});
expect(result.current.parameters.isSmartDetection).toBe(true);
expect(result.current.parameters.smartDetectionType).toBe('web');
});
test('should detect multiple web files and enable web smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const zipFiles = [
{ name: 'site1.zip' },
{ name: 'site2.html' }
];
act(() => {
result.current.analyzeFileTypes(zipFiles);
});
expect(result.current.parameters.fromExtension).toBe('html');
expect(result.current.parameters.toExtension).toBe('pdf');
expect(result.current.parameters.isSmartDetection).toBe(true);
expect(result.current.parameters.smartDetectionType).toBe('web');
});
});
describe('Smart Detection - Mixed File Types', () => {
test('should detect mixed file types and enable smart detection', () => {
@ -223,6 +278,22 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
expect(result.current.getEndpoint()).toBe('/api/v1/convert/img/pdf');
});
test('should return correct endpoint for web smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const webFiles = [
{ name: 'page1.html' },
{ name: 'archive.zip' }
];
act(() => {
result.current.analyzeFileTypes(webFiles);
});
expect(result.current.getEndpointName()).toBe('html-to-pdf');
expect(result.current.getEndpoint()).toBe('/api/v1/convert/html/pdf');
});
test('should return correct endpoint for mixed smart detection', () => {
const { result } = renderHook(() => useConvertParameters());

View File

@ -328,9 +328,10 @@ describe('Convert Tool Integration Tests', () => {
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const file1 = createPDFFile();
const file2 = createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf');
const files = [
createPDFFile(),
createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf')
]
const parameters: ConvertParameters = {
fromExtension: 'pdf',
toExtension: 'png',
@ -347,15 +348,20 @@ describe('Convert Tool Integration Tests', () => {
};
await act(async () => {
await result.current.executeOperation(parameters, [file1, file2]);
await result.current.executeOperation(parameters, files);
});
// Verify both files were uploaded
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
const fileInputs = formDataCall.getAll('fileInput');
expect(fileInputs).toHaveLength(2);
expect(fileInputs[0]).toBe(file1);
expect(fileInputs[1]).toBe(file2);
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 () => {

View File

@ -209,6 +209,119 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
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');
});
});
describe('Image Conversion Options Integration', () => {

View File

@ -68,7 +68,6 @@ describe('convertUtils', () => {
// Web formats to PDF
expect(getEndpointName('html', 'pdf')).toBe('html-to-pdf');
expect(getEndpointName('htm', 'pdf')).toBe('html-to-pdf');
// Markdown to PDF
expect(getEndpointName('md', 'pdf')).toBe('markdown-to-pdf');
@ -151,7 +150,6 @@ describe('convertUtils', () => {
// Web formats to PDF
expect(getEndpointUrl('html', 'pdf')).toBe('/api/v1/convert/html/pdf');
expect(getEndpointUrl('htm', 'pdf')).toBe('/api/v1/convert/html/pdf');
// Markdown to PDF
expect(getEndpointUrl('md', 'pdf')).toBe('/api/v1/convert/markdown/pdf');

View File

@ -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');
});
});
});

View File

@ -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 });
};