mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
eml to pdf
This commit is contained in:
parent
8da72af478
commit
5b8eea686e
@ -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;
|
@ -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;
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo } from "react";
|
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 KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
|
||||||
@ -10,6 +10,8 @@ import { detectFileExtension } from "../../../utils/fileUtils";
|
|||||||
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
||||||
import ConvertToImageSettings from "./ConvertToImageSettings";
|
import ConvertToImageSettings from "./ConvertToImageSettings";
|
||||||
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||||
|
import ConvertFromWebSettings from "./ConvertFromWebSettings";
|
||||||
|
import ConvertFromEmailSettings from "./ConvertFromEmailSettings";
|
||||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
import {
|
import {
|
||||||
FROM_FORMAT_OPTIONS,
|
FROM_FORMAT_OPTIONS,
|
||||||
@ -106,6 +108,12 @@ const ConvertSettings = ({
|
|||||||
autoRotate: true,
|
autoRotate: true,
|
||||||
combineImages: true,
|
combineImages: true,
|
||||||
});
|
});
|
||||||
|
onParameterChange('emailOptions', {
|
||||||
|
includeAttachments: true,
|
||||||
|
maxAttachmentSizeMB: 10,
|
||||||
|
downloadHtml: false,
|
||||||
|
includeAllRecipients: false,
|
||||||
|
});
|
||||||
// Disable smart detection when manually changing source format
|
// Disable smart detection when manually changing source format
|
||||||
onParameterChange('isSmartDetection', false);
|
onParameterChange('isSmartDetection', false);
|
||||||
onParameterChange('smartDetectionType', 'none');
|
onParameterChange('smartDetectionType', 'none');
|
||||||
@ -147,6 +155,12 @@ const ConvertSettings = ({
|
|||||||
autoRotate: true,
|
autoRotate: true,
|
||||||
combineImages: true,
|
combineImages: true,
|
||||||
});
|
});
|
||||||
|
onParameterChange('emailOptions', {
|
||||||
|
includeAttachments: true,
|
||||||
|
maxAttachmentSizeMB: 10,
|
||||||
|
downloadHtml: false,
|
||||||
|
includeAllRecipients: false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -235,50 +249,28 @@ const ConvertSettings = ({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* HTML to PDF specific options */}
|
{/* Web to PDF options */}
|
||||||
{((isWebFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') ||
|
{((isWebFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') ||
|
||||||
(parameters.isSmartDetection && parameters.smartDetectionType === 'web')) && (
|
(parameters.isSmartDetection && parameters.smartDetectionType === 'web')) ? (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack gap="sm" data-testid="html-options-section">
|
<ConvertFromWebSettings
|
||||||
<Text size="sm" fw={500} data-testid="html-options-title">{t("convert.htmlOptions", "HTML Options")}:</Text>
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
<Stack gap="xs">
|
disabled={disabled}
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{/* EML specific options */}
|
{/* Email to PDF options */}
|
||||||
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack gap="sm" data-testid="eml-options-section">
|
<ConvertFromEmailSettings
|
||||||
<Text size="sm" fw={500} data-testid="eml-options-title">{t("convert.emlOptions", "Email Options")}:</Text>
|
parameters={parameters}
|
||||||
<Text size="xs" c="dimmed" data-testid="eml-options-note">
|
onParameterChange={onParameterChange}
|
||||||
{t("convert.emlNote", "Email attachments and embedded images will be included in the PDF conversion.")}
|
disabled={disabled}
|
||||||
</Text>
|
/>
|
||||||
</Stack>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { FileOperation } from '../../../types/fileContext';
|
|||||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||||
import { ConvertParameters } from './useConvertParameters';
|
import { ConvertParameters } from './useConvertParameters';
|
||||||
import { detectFileExtension } from '../../../utils/fileUtils';
|
import { detectFileExtension } from '../../../utils/fileUtils';
|
||||||
|
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
||||||
|
|
||||||
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
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 = (
|
const createFileFromResponse = (
|
||||||
responseData: any,
|
responseData: any,
|
||||||
@ -64,20 +65,10 @@ const createFileFromResponse = (
|
|||||||
originalFileName: string,
|
originalFileName: string,
|
||||||
targetExtension: string
|
targetExtension: string
|
||||||
): File => {
|
): File => {
|
||||||
const contentType = headers?.['content-type'] || 'application/octet-stream';
|
|
||||||
const blob = new Blob([responseData], { type: contentType });
|
|
||||||
|
|
||||||
const originalName = originalFileName.split('.')[0];
|
const originalName = originalFileName.split('.')[0];
|
||||||
let filename: string;
|
const fallbackFilename = `${originalName}_converted.${targetExtension}`;
|
||||||
|
|
||||||
// Check if response is a ZIP
|
return createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||||
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 });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +143,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fromExtension, toExtension, imageOptions, htmlOptions } = parameters;
|
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions } = parameters;
|
||||||
|
|
||||||
// Add conversion-specific parameters
|
// Add conversion-specific parameters
|
||||||
if (isImageFormat(toExtension)) {
|
if (isImageFormat(toExtension)) {
|
||||||
@ -173,6 +164,12 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
} else if ((fromExtension === 'html' || fromExtension === 'zip') && toExtension === 'pdf') {
|
} else if ((fromExtension === 'html' || fromExtension === 'zip') && toExtension === 'pdf') {
|
||||||
// HTML to PDF conversion with zoom level (includes ZIP files with HTML)
|
// HTML to PDF conversion with zoom level (includes ZIP files with HTML)
|
||||||
formData.append("zoom", htmlOptions.zoomLevel.toString());
|
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') {
|
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
||||||
// CSV extraction - always process all pages for simplified workflow
|
// CSV extraction - always process all pages for simplified workflow
|
||||||
formData.append("pageNumbers", "all");
|
formData.append("pageNumbers", "all");
|
||||||
@ -201,6 +198,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
toExtension: parameters.toExtension,
|
toExtension: parameters.toExtension,
|
||||||
imageOptions: parameters.imageOptions,
|
imageOptions: parameters.imageOptions,
|
||||||
htmlOptions: parameters.htmlOptions,
|
htmlOptions: parameters.htmlOptions,
|
||||||
|
emailOptions: parameters.emailOptions,
|
||||||
},
|
},
|
||||||
fileSize: selectedFiles[0].size
|
fileSize: selectedFiles[0].size
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,11 @@ describe('useConvertParameters', () => {
|
|||||||
expect(result.current.parameters.imageOptions.colorType).toBe('color');
|
expect(result.current.parameters.imageOptions.colorType).toBe('color');
|
||||||
expect(result.current.parameters.imageOptions.dpi).toBe(300);
|
expect(result.current.parameters.imageOptions.dpi).toBe(300);
|
||||||
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('multiple');
|
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', () => {
|
test('should update individual parameters', () => {
|
||||||
@ -47,6 +52,36 @@ describe('useConvertParameters', () => {
|
|||||||
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('single');
|
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', () => {
|
test('should reset parameters to defaults', () => {
|
||||||
const { result } = renderHook(() => useConvertParameters());
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
@ -26,6 +26,12 @@ export interface ConvertParameters {
|
|||||||
htmlOptions: {
|
htmlOptions: {
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
};
|
};
|
||||||
|
emailOptions: {
|
||||||
|
includeAttachments: boolean;
|
||||||
|
maxAttachmentSizeMB: number;
|
||||||
|
downloadHtml: boolean;
|
||||||
|
includeAllRecipients: boolean;
|
||||||
|
};
|
||||||
isSmartDetection: boolean;
|
isSmartDetection: boolean;
|
||||||
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
|
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
|
||||||
}
|
}
|
||||||
@ -55,6 +61,12 @@ const initialParameters: ConvertParameters = {
|
|||||||
htmlOptions: {
|
htmlOptions: {
|
||||||
zoomLevel: 1.0,
|
zoomLevel: 1.0,
|
||||||
},
|
},
|
||||||
|
emailOptions: {
|
||||||
|
includeAttachments: true,
|
||||||
|
maxAttachmentSizeMB: 10,
|
||||||
|
downloadHtml: false,
|
||||||
|
includeAllRecipients: false,
|
||||||
|
},
|
||||||
isSmartDetection: false,
|
isSmartDetection: false,
|
||||||
smartDetectionType: 'none',
|
smartDetectionType: 'none',
|
||||||
};
|
};
|
||||||
|
@ -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', () => {
|
describe('Smart Detection - Mixed File Types', () => {
|
||||||
|
|
||||||
test('should detect mixed file types and enable smart detection', () => {
|
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');
|
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', () => {
|
test('should return correct endpoint for mixed smart detection', () => {
|
||||||
const { result } = renderHook(() => useConvertParameters());
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
@ -328,9 +328,10 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
const { result } = renderHook(() => useConvertOperation(), {
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
wrapper: TestWrapper
|
wrapper: TestWrapper
|
||||||
});
|
});
|
||||||
|
const files = [
|
||||||
const file1 = createPDFFile();
|
createPDFFile(),
|
||||||
const file2 = createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf');
|
createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf')
|
||||||
|
]
|
||||||
const parameters: ConvertParameters = {
|
const parameters: ConvertParameters = {
|
||||||
fromExtension: 'pdf',
|
fromExtension: 'pdf',
|
||||||
toExtension: 'png',
|
toExtension: 'png',
|
||||||
@ -347,15 +348,20 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.executeOperation(parameters, [file1, file2]);
|
await result.current.executeOperation(parameters, files);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify both files were uploaded
|
// Verify both files were uploaded
|
||||||
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
|
const calls = mockedAxios.post.mock.calls;
|
||||||
const fileInputs = formDataCall.getAll('fileInput');
|
|
||||||
expect(fileInputs).toHaveLength(2);
|
for (let i = 0; i < calls.length; i++) {
|
||||||
expect(fileInputs[0]).toBe(file1);
|
const formData = calls[i][1] as FormData;
|
||||||
expect(fileInputs[1]).toBe(file2);
|
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 () => {
|
test('should handle no files selected', async () => {
|
||||||
|
@ -209,6 +209,119 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
responseType: 'blob'
|
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', () => {
|
describe('Image Conversion Options Integration', () => {
|
||||||
|
@ -68,7 +68,6 @@ describe('convertUtils', () => {
|
|||||||
|
|
||||||
// Web formats to PDF
|
// Web formats to PDF
|
||||||
expect(getEndpointName('html', 'pdf')).toBe('html-to-pdf');
|
expect(getEndpointName('html', 'pdf')).toBe('html-to-pdf');
|
||||||
expect(getEndpointName('htm', 'pdf')).toBe('html-to-pdf');
|
|
||||||
|
|
||||||
// Markdown to PDF
|
// Markdown to PDF
|
||||||
expect(getEndpointName('md', 'pdf')).toBe('markdown-to-pdf');
|
expect(getEndpointName('md', 'pdf')).toBe('markdown-to-pdf');
|
||||||
@ -151,7 +150,6 @@ describe('convertUtils', () => {
|
|||||||
|
|
||||||
// Web formats to PDF
|
// Web formats to PDF
|
||||||
expect(getEndpointUrl('html', 'pdf')).toBe('/api/v1/convert/html/pdf');
|
expect(getEndpointUrl('html', 'pdf')).toBe('/api/v1/convert/html/pdf');
|
||||||
expect(getEndpointUrl('htm', 'pdf')).toBe('/api/v1/convert/html/pdf');
|
|
||||||
|
|
||||||
// Markdown to PDF
|
// Markdown to PDF
|
||||||
expect(getEndpointUrl('md', 'pdf')).toBe('/api/v1/convert/markdown/pdf');
|
expect(getEndpointUrl('md', 'pdf')).toBe('/api/v1/convert/markdown/pdf');
|
||||||
|
147
frontend/src/utils/fileResponseUtils.test.ts
Normal file
147
frontend/src/utils/fileResponseUtils.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
37
frontend/src/utils/fileResponseUtils.ts
Normal file
37
frontend/src/utils/fileResponseUtils.ts
Normal 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 });
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user