mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
Added testing, csv to pdf
This commit is contained in:
parent
73edf3f08c
commit
4d725947db
@ -6,7 +6,10 @@
|
|||||||
"Bash(./gradlew:*)",
|
"Bash(./gradlew:*)",
|
||||||
"Bash(grep:*)",
|
"Bash(grep:*)",
|
||||||
"Bash(cat:*)",
|
"Bash(cat:*)",
|
||||||
"Bash(find:*)"
|
"Bash(find:*)",
|
||||||
|
"Bash(npm test)",
|
||||||
|
"Bash(npm test:*)",
|
||||||
|
"Bash(ls:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -22,3 +22,6 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
playwright-report
|
||||||
|
test-results
|
2616
frontend/package-lock.json
generated
2616
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,7 +36,12 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate-licenses": "node scripts/generate-licenses.js"
|
"generate-licenses": "node scripts/generate-licenses.js",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"test:coverage": "vitest --coverage",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:install": "playwright install"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -57,15 +62,19 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.40.0",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@vitejs/plugin-react": "^4.5.0",
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
|
"@vitest/coverage-v8": "^1.0.0",
|
||||||
|
"jsdom": "^23.0.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
75
frontend/playwright.config.ts
Normal file
75
frontend/playwright.config.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './src/tests',
|
||||||
|
testMatch: '**/*.spec.ts',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: 'http://localhost:5173',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
viewport: { width: 1920, height: 1080 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:5173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
29
frontend/public/locales/en/translation.json
Normal file
29
frontend/public/locales/en/translation.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"convert": {
|
||||||
|
"selectSourceFormat": "Select source file format",
|
||||||
|
"selectTargetFormat": "Select target file format",
|
||||||
|
"selectFirst": "Select a source format first",
|
||||||
|
"imageOptions": "Image Options:",
|
||||||
|
"emailOptions": "Email Options:",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"dpi": "DPI",
|
||||||
|
"singleOrMultiple": "Output",
|
||||||
|
"emailNote": "Email attachments and embedded images will be included"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"color": "Color",
|
||||||
|
"grayscale": "Grayscale",
|
||||||
|
"blackWhite": "Black & White",
|
||||||
|
"single": "Single Image",
|
||||||
|
"multiple": "Multiple Images"
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"document": "Document",
|
||||||
|
"spreadsheet": "Spreadsheet",
|
||||||
|
"presentation": "Presentation",
|
||||||
|
"image": "Image",
|
||||||
|
"web": "Web",
|
||||||
|
"text": "Text",
|
||||||
|
"email": "Email"
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
@ -44,6 +44,7 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
|
data-testid="file-card"
|
||||||
>
|
>
|
||||||
<Stack gap={6} align="center">
|
<Stack gap={6} align="center">
|
||||||
<Box
|
<Box
|
||||||
|
@ -81,6 +81,7 @@ const FileThumbnail = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data-file-id={file.id}
|
data-file-id={file.id}
|
||||||
|
data-testid="file-thumbnail"
|
||||||
className={`
|
className={`
|
||||||
${styles.pageContainer}
|
${styles.pageContainer}
|
||||||
!rounded-lg
|
!rounded-lg
|
||||||
@ -119,6 +120,7 @@ const FileThumbnail = ({
|
|||||||
{selectionMode && (
|
{selectionMode && (
|
||||||
<div
|
<div
|
||||||
className={styles.checkboxContainer}
|
className={styles.checkboxContainer}
|
||||||
|
data-testid="file-thumbnail-checkbox"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
|
@ -155,6 +155,7 @@ const FileUploadSelector = ({
|
|||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
style={{ width: '100%', height: "5rem" }}
|
style={{ width: '100%', height: "5rem" }}
|
||||||
activateOnClick={true}
|
activateOnClick={true}
|
||||||
|
data-testid="file-dropzone"
|
||||||
>
|
>
|
||||||
<Center>
|
<Center>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
@ -192,6 +193,7 @@ const FileUploadSelector = ({
|
|||||||
accept={accept.join(',')}
|
accept={accept.join(',')}
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
|
data-testid="file-input"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
@ -43,6 +43,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, toolRegistry }: ToolPickerProps
|
|||||||
filteredTools.map(([id, { icon, name }]) => (
|
filteredTools.map(([id, { icon, name }]) => (
|
||||||
<Button
|
<Button
|
||||||
key={id}
|
key={id}
|
||||||
|
data-testid={`tool-${id}`}
|
||||||
variant={selectedToolKey === id ? "filled" : "subtle"}
|
variant={selectedToolKey === id ? "filled" : "subtle"}
|
||||||
onClick={() => onSelect(id)}
|
onClick={() => onSelect(id)}
|
||||||
size="md"
|
size="md"
|
||||||
|
@ -18,9 +18,10 @@ const ConvertFromImageSettings = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm" data-testid="pdf-options-section">
|
||||||
<Text size="sm" fw={500}>{t("convert.pdfOptions", "PDF Options")}:</Text>
|
<Text size="sm" fw={500}>{t("convert.pdfOptions", "PDF Options")}:</Text>
|
||||||
<Select
|
<Select
|
||||||
|
data-testid="color-type-select"
|
||||||
label={t("convert.colorType", "Color Type")}
|
label={t("convert.colorType", "Color Type")}
|
||||||
value={parameters.imageOptions.colorType}
|
value={parameters.imageOptions.colorType}
|
||||||
onChange={(val) => val && onParameterChange('imageOptions', {
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Text, TextInput } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
|
|
||||||
|
interface ConvertFromPdfToCsvSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertFromPdfToCsvSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertFromPdfToCsvSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm" data-testid="csv-options-section">
|
||||||
|
<Text size="sm" fw={500} data-testid="csv-options-title">
|
||||||
|
{t("convert.csvOptions", "CSV Options")}:
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
data-testid="page-numbers-input"
|
||||||
|
label={t("convert.pageNumbers", "Page Numbers")}
|
||||||
|
placeholder={t("convert.pageNumbersPlaceholder", "e.g., 1,3,5-9, 2n+1, or 'all'")}
|
||||||
|
description={t("convert.pageNumbersDescription", "Specify pages to extract CSV data from. Supports ranges (e.g., '1,3,5-9'), functions (e.g., '2n+1', '3n'), or 'all' for all pages.")}
|
||||||
|
value={parameters.pageNumbers}
|
||||||
|
onChange={(event) => onParameterChange('pageNumbers', event.currentTarget.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertFromPdfToCsvSettings;
|
@ -7,6 +7,7 @@ import { isImageFormat } from "../../../utils/convertUtils";
|
|||||||
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 ConvertFromPdfToCsvSettings from "./ConvertFromPdfToCsvSettings";
|
||||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
import {
|
import {
|
||||||
FROM_FORMAT_OPTIONS,
|
FROM_FORMAT_OPTIONS,
|
||||||
@ -101,6 +102,7 @@ const ConvertSettings = ({
|
|||||||
dpi: 300,
|
dpi: 300,
|
||||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
});
|
});
|
||||||
|
onParameterChange('pageNumbers', 'all');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -112,6 +114,8 @@ const ConvertSettings = ({
|
|||||||
{t("convert.convertFrom", "Convert from")}:
|
{t("convert.convertFrom", "Convert from")}:
|
||||||
</Text>
|
</Text>
|
||||||
<GroupedFormatDropdown
|
<GroupedFormatDropdown
|
||||||
|
name="convert-from-dropdown"
|
||||||
|
data-testid="from-format-dropdown"
|
||||||
value={parameters.fromExtension}
|
value={parameters.fromExtension}
|
||||||
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
||||||
options={enhancedFromOptions}
|
options={enhancedFromOptions}
|
||||||
@ -148,8 +152,10 @@ const ConvertSettings = ({
|
|||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
) : (
|
) : (
|
||||||
<GroupedFormatDropdown
|
<GroupedFormatDropdown
|
||||||
|
name="convert-to-dropdown"
|
||||||
|
data-testid="to-format-dropdown"
|
||||||
value={parameters.toExtension}
|
value={parameters.toExtension}
|
||||||
placeholder={t("convert.targetFormatPlaceholder", "Target format")}
|
placeholder={t("convert.targetFormatPlaceholder", "Target format")}
|
||||||
options={enhancedToOptions}
|
options={enhancedToOptions}
|
||||||
onChange={handleToExtensionChange}
|
onChange={handleToExtensionChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -187,14 +193,26 @@ const ConvertSettings = ({
|
|||||||
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack gap="sm">
|
<Stack gap="sm" data-testid="eml-options-section">
|
||||||
<Text size="sm" fw={500}>{t("convert.emlOptions", "Email Options")}:</Text>
|
<Text size="sm" fw={500} data-testid="eml-options-title">{t("convert.emlOptions", "Email Options")}:</Text>
|
||||||
<Text size="xs" c="dimmed">
|
<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.")}
|
{t("convert.emlNote", "Email attachments and embedded images will be included in the PDF conversion.")}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* CSV specific options */}
|
||||||
|
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'csv' && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<ConvertFromPdfToCsvSettings
|
||||||
|
parameters={parameters}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,10 +18,11 @@ const ConvertToImageSettings = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm" data-testid="image-options-section">
|
||||||
<Text size="sm" fw={500}>{t("convert.imageOptions", "Image Options")}:</Text>
|
<Text size="sm" fw={500} data-testid="image-options-title">{t("convert.imageOptions", "Image Options")}:</Text>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<Select
|
<Select
|
||||||
|
data-testid="color-type-select"
|
||||||
label={t("convert.colorType", "Color Type")}
|
label={t("convert.colorType", "Color Type")}
|
||||||
value={parameters.imageOptions.colorType}
|
value={parameters.imageOptions.colorType}
|
||||||
onChange={(val) => val && onParameterChange('imageOptions', {
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
@ -36,6 +37,7 @@ const ConvertToImageSettings = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
data-testid="dpi-input"
|
||||||
label={t("convert.dpi", "DPI")}
|
label={t("convert.dpi", "DPI")}
|
||||||
value={parameters.imageOptions.dpi}
|
value={parameters.imageOptions.dpi}
|
||||||
onChange={(val) => typeof val === 'number' && onParameterChange('imageOptions', {
|
onChange={(val) => typeof val === 'number' && onParameterChange('imageOptions', {
|
||||||
@ -49,6 +51,7 @@ const ConvertToImageSettings = ({
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Select
|
<Select
|
||||||
|
data-testid="output-type-select"
|
||||||
label={t("convert.output", "Output")}
|
label={t("convert.output", "Output")}
|
||||||
value={parameters.imageOptions.singleOrMultiple}
|
value={parameters.imageOptions.singleOrMultiple}
|
||||||
onChange={(val) => val && onParameterChange('imageOptions', {
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
@ -16,6 +16,7 @@ interface GroupedFormatDropdownProps {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
minWidth?: string;
|
minWidth?: string;
|
||||||
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupedFormatDropdown = ({
|
const GroupedFormatDropdown = ({
|
||||||
@ -24,7 +25,8 @@ const GroupedFormatDropdown = ({
|
|||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
minWidth = "18.75rem"
|
minWidth = "18.75rem",
|
||||||
|
name
|
||||||
}: GroupedFormatDropdownProps) => {
|
}: GroupedFormatDropdownProps) => {
|
||||||
const [dropdownOpened, setDropdownOpened] = useState(false);
|
const [dropdownOpened, setDropdownOpened] = useState(false);
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
@ -69,6 +71,8 @@ const GroupedFormatDropdown = ({
|
|||||||
>
|
>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
|
name={name}
|
||||||
|
data-testid={name}
|
||||||
onClick={() => setDropdownOpened(!dropdownOpened)}
|
onClick={() => setDropdownOpened(!dropdownOpened)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{
|
style={{
|
||||||
@ -122,6 +126,7 @@ const GroupedFormatDropdown = ({
|
|||||||
{groupOptions.map((option) => (
|
{groupOptions.map((option) => (
|
||||||
<Button
|
<Button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
data-testid={`format-option-${option.value}`}
|
||||||
variant={value === option.value ? "filled" : "outline"}
|
variant={value === option.value ? "filled" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleOptionSelect(option.value)}
|
onClick={() => handleOptionSelect(option.value)}
|
||||||
|
@ -13,6 +13,7 @@ export interface OperationButtonProps {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
mt?: string;
|
mt?: string;
|
||||||
type?: 'button' | 'submit' | 'reset';
|
type?: 'button' | 'submit' | 'reset';
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OperationButton = ({
|
const OperationButton = ({
|
||||||
@ -25,7 +26,8 @@ const OperationButton = ({
|
|||||||
color = 'blue',
|
color = 'blue',
|
||||||
fullWidth = true,
|
fullWidth = true,
|
||||||
mt = 'md',
|
mt = 'md',
|
||||||
type = 'button'
|
type = 'button',
|
||||||
|
'data-testid': dataTestId
|
||||||
}: OperationButtonProps) => {
|
}: OperationButtonProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -39,6 +41,7 @@ const OperationButton = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
color={color}
|
color={color}
|
||||||
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
{isLoading
|
{isLoading
|
||||||
? (loadingText || t("loading", "Loading..."))
|
? (loadingText || t("loading", "Loading..."))
|
||||||
|
@ -37,28 +37,29 @@ const ResultsPreview = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mt="lg" p="md" style={{ backgroundColor: 'var(--mantine-color-gray-0)', borderRadius: 8 }}>
|
<Box mt="lg" p="md" style={{ backgroundColor: 'var(--mantine-color-gray-0)', borderRadius: 8 }} data-testid="results-preview-container">
|
||||||
{title && (
|
{title && (
|
||||||
<Text fw={500} size="md" mb="sm">
|
<Text fw={500} size="md" mb="sm" data-testid="results-preview-title">
|
||||||
{title} ({files.length} files)
|
{title} ({files.length} files)
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGeneratingThumbnails ? (
|
{isGeneratingThumbnails ? (
|
||||||
<Center p="lg">
|
<Center p="lg" data-testid="results-preview-loading">
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Loader size="sm" />
|
<Loader size="sm" />
|
||||||
<Text size="sm" c="dimmed">{loadingMessage}</Text>
|
<Text size="sm" c="dimmed">{loadingMessage}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
<Grid>
|
<Grid data-testid="results-preview-grid">
|
||||||
{files.map((result, index) => (
|
{files.map((result, index) => (
|
||||||
<Grid.Col span={{ base: 6, sm: 4, md: 3 }} key={index}>
|
<Grid.Col span={{ base: 6, sm: 4, md: 3 }} key={index}>
|
||||||
<Paper
|
<Paper
|
||||||
p="xs"
|
p="xs"
|
||||||
withBorder
|
withBorder
|
||||||
onClick={() => onFileClick?.(result.file)}
|
onClick={() => onFileClick?.(result.file)}
|
||||||
|
data-testid={`results-preview-thumbnail-${index}`}
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
height: '10rem',
|
height: '10rem',
|
||||||
|
@ -81,6 +81,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
formData.append("fitOption", "fillPage");
|
formData.append("fitOption", "fillPage");
|
||||||
formData.append("colorType", imageOptions.colorType);
|
formData.append("colorType", imageOptions.colorType);
|
||||||
formData.append("autoRotate", "true");
|
formData.append("autoRotate", "true");
|
||||||
|
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
||||||
|
// CSV extraction requires page numbers parameter
|
||||||
|
formData.append("pageNumbers", parameters.pageNumbers || "all");
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
@ -104,6 +107,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
parameters: {
|
parameters: {
|
||||||
fromExtension: parameters.fromExtension,
|
fromExtension: parameters.fromExtension,
|
||||||
toExtension: parameters.toExtension,
|
toExtension: parameters.toExtension,
|
||||||
|
pageNumbers: parameters.pageNumbers,
|
||||||
imageOptions: parameters.imageOptions,
|
imageOptions: parameters.imageOptions,
|
||||||
},
|
},
|
||||||
fileSize: selectedFiles[0].size
|
fileSize: selectedFiles[0].size
|
||||||
|
233
frontend/src/hooks/tools/convert/useConvertParameters.test.ts
Normal file
233
frontend/src/hooks/tools/convert/useConvertParameters.test.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for useConvertParameters hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { useConvertParameters } from './useConvertParameters';
|
||||||
|
|
||||||
|
describe('useConvertParameters', () => {
|
||||||
|
|
||||||
|
describe('Parameter Management', () => {
|
||||||
|
|
||||||
|
test('should initialize with default parameters', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('');
|
||||||
|
expect(result.current.parameters.imageOptions.colorType).toBe('color');
|
||||||
|
expect(result.current.parameters.imageOptions.dpi).toBe(300);
|
||||||
|
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('multiple');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update individual parameters', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||||
|
expect(result.current.parameters.toExtension).toBe(''); // Should not affect other params
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update nested image options', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('imageOptions', {
|
||||||
|
colorType: 'grayscale',
|
||||||
|
dpi: 150,
|
||||||
|
singleOrMultiple: 'single'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.imageOptions.colorType).toBe('grayscale');
|
||||||
|
expect(result.current.parameters.imageOptions.dpi).toBe(150);
|
||||||
|
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('single');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reset parameters to defaults', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'pdf');
|
||||||
|
result.current.updateParameter('toExtension', 'png');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.resetParameters();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.fromExtension).toBe('');
|
||||||
|
expect(result.current.parameters.toExtension).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Parameter Validation', () => {
|
||||||
|
|
||||||
|
test('should validate parameters correctly', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
// No parameters - should be invalid
|
||||||
|
expect(result.current.validateParameters()).toBe(false);
|
||||||
|
|
||||||
|
// Only fromExtension - should be invalid
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'pdf');
|
||||||
|
});
|
||||||
|
expect(result.current.validateParameters()).toBe(false);
|
||||||
|
|
||||||
|
// Both extensions with supported conversion - should be valid
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('toExtension', 'png');
|
||||||
|
});
|
||||||
|
expect(result.current.validateParameters()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate unsupported conversions', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'pdf');
|
||||||
|
result.current.updateParameter('toExtension', 'unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.validateParameters()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate DPI ranges for image conversions', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'pdf');
|
||||||
|
result.current.updateParameter('toExtension', 'png');
|
||||||
|
result.current.updateParameter('imageOptions', {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 50, // Below minimum
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.validateParameters()).toBe(false);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('imageOptions', {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300, // Valid range
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.validateParameters()).toBe(true);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('imageOptions', {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 700, // Above maximum
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.validateParameters()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Endpoint Generation', () => {
|
||||||
|
|
||||||
|
test('should generate correct endpoint names', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'pdf');
|
||||||
|
result.current.updateParameter('toExtension', 'png');
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpointName = result.current.getEndpointName();
|
||||||
|
expect(endpointName).toBe('pdf-to-img');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct endpoint URLs', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'pdf');
|
||||||
|
result.current.updateParameter('toExtension', 'png');
|
||||||
|
});
|
||||||
|
|
||||||
|
const endpoint = result.current.getEndpoint();
|
||||||
|
expect(endpoint).toBe('/api/v1/convert/pdf/img');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty strings for invalid conversions', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('fromExtension', 'invalid');
|
||||||
|
result.current.updateParameter('toExtension', 'invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.getEndpointName()).toBe('');
|
||||||
|
expect(result.current.getEndpoint()).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Available Extensions', () => {
|
||||||
|
|
||||||
|
test('should return available extensions for valid source format', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const availableExtensions = result.current.getAvailableToExtensions('pdf');
|
||||||
|
|
||||||
|
expect(availableExtensions.length).toBeGreaterThan(0);
|
||||||
|
expect(availableExtensions.some(ext => ext.value === 'png')).toBe(true);
|
||||||
|
expect(availableExtensions.some(ext => ext.value === 'jpg')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty array for invalid source format', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const availableExtensions = result.current.getAvailableToExtensions('invalid');
|
||||||
|
|
||||||
|
expect(availableExtensions).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty array for empty source format', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
const availableExtensions = result.current.getAvailableToExtensions('');
|
||||||
|
|
||||||
|
expect(availableExtensions).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File Extension Detection', () => {
|
||||||
|
|
||||||
|
test('should detect file extensions correctly', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
expect(result.current.detectFileExtension('document.pdf')).toBe('pdf');
|
||||||
|
expect(result.current.detectFileExtension('image.PNG')).toBe('png'); // Should lowercase
|
||||||
|
expect(result.current.detectFileExtension('file.docx')).toBe('docx');
|
||||||
|
expect(result.current.detectFileExtension('archive.tar.gz')).toBe('gz'); // Last extension
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle files without extensions', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
// Files without dots return the entire filename as "extension"
|
||||||
|
expect(result.current.detectFileExtension('noextension')).toBe('noextension');
|
||||||
|
expect(result.current.detectFileExtension('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle edge cases', () => {
|
||||||
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
expect(result.current.detectFileExtension('file.')).toBe('');
|
||||||
|
expect(result.current.detectFileExtension('.hidden')).toBe('hidden');
|
||||||
|
expect(result.current.detectFileExtension('file.UPPER')).toBe('upper');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -12,6 +12,7 @@ import { getEndpointName as getEndpointNameUtil, getEndpointUrl } from '../../..
|
|||||||
export interface ConvertParameters {
|
export interface ConvertParameters {
|
||||||
fromExtension: string;
|
fromExtension: string;
|
||||||
toExtension: string;
|
toExtension: string;
|
||||||
|
pageNumbers: string;
|
||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: ColorType;
|
colorType: ColorType;
|
||||||
dpi: number;
|
dpi: number;
|
||||||
@ -33,6 +34,7 @@ export interface ConvertParametersHook {
|
|||||||
const initialParameters: ConvertParameters = {
|
const initialParameters: ConvertParameters = {
|
||||||
fromExtension: '',
|
fromExtension: '',
|
||||||
toExtension: '',
|
toExtension: '',
|
||||||
|
pageNumbers: 'all',
|
||||||
imageOptions: {
|
imageOptions: {
|
||||||
colorType: COLOR_TYPES.COLOR,
|
colorType: COLOR_TYPES.COLOR,
|
||||||
dpi: 300,
|
dpi: 300,
|
||||||
|
48
frontend/src/i18n/config.ts
Normal file
48
frontend/src/i18n/config.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import Backend from 'i18next-http-backend';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(Backend)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
lng: 'en',
|
||||||
|
fallbackLng: 'en',
|
||||||
|
debug: false,
|
||||||
|
backend: {
|
||||||
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
|
},
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
// For testing environment, provide fallback resources
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
translation: {
|
||||||
|
'convert.selectSourceFormat': 'Select source file format',
|
||||||
|
'convert.selectTargetFormat': 'Select target file format',
|
||||||
|
'convert.selectFirst': 'Select a source format first',
|
||||||
|
'convert.imageOptions': 'Image Options:',
|
||||||
|
'convert.emailOptions': 'Email Options:',
|
||||||
|
'convert.colorType': 'Color Type',
|
||||||
|
'convert.dpi': 'DPI',
|
||||||
|
'convert.singleOrMultiple': 'Output',
|
||||||
|
'convert.emailNote': 'Email attachments and embedded images will be included',
|
||||||
|
'common.color': 'Color',
|
||||||
|
'common.grayscale': 'Grayscale',
|
||||||
|
'common.blackWhite': 'Black & White',
|
||||||
|
'common.single': 'Single Image',
|
||||||
|
'common.multiple': 'Multiple Images',
|
||||||
|
'groups.document': 'Document',
|
||||||
|
'groups.spreadsheet': 'Spreadsheet',
|
||||||
|
'groups.presentation': 'Presentation',
|
||||||
|
'groups.image': 'Image',
|
||||||
|
'groups.web': 'Web',
|
||||||
|
'groups.text': 'Text',
|
||||||
|
'groups.email': 'Email'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
68
frontend/src/setupTests.ts
Normal file
68
frontend/src/setupTests.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
// Mock i18next for tests
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
i18n: {
|
||||||
|
changeLanguage: vi.fn(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
initReactI18next: {
|
||||||
|
type: '3rdParty',
|
||||||
|
init: vi.fn(),
|
||||||
|
},
|
||||||
|
I18nextProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock i18next-http-backend
|
||||||
|
vi.mock('i18next-http-backend', () => ({
|
||||||
|
default: {
|
||||||
|
type: 'backend',
|
||||||
|
init: vi.fn(),
|
||||||
|
read: vi.fn(),
|
||||||
|
save: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock window.URL.createObjectURL and revokeObjectURL for tests
|
||||||
|
global.URL.createObjectURL = vi.fn(() => 'mocked-url')
|
||||||
|
global.URL.revokeObjectURL = vi.fn()
|
||||||
|
|
||||||
|
// Mock Worker for tests (Web Workers not available in test environment)
|
||||||
|
global.Worker = vi.fn().mockImplementation(() => ({
|
||||||
|
postMessage: vi.fn(),
|
||||||
|
terminate: vi.fn(),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock ResizeObserver for Mantine components
|
||||||
|
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||||
|
observe: vi.fn(),
|
||||||
|
unobserve: vi.fn(),
|
||||||
|
disconnect: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock IntersectionObserver for components that might use it
|
||||||
|
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||||
|
observe: vi.fn(),
|
||||||
|
unobserve: vi.fn(),
|
||||||
|
disconnect: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock matchMedia for responsive components
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: vi.fn().mockImplementation(query => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: vi.fn(), // deprecated
|
||||||
|
removeListener: vi.fn(), // deprecated
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
})),
|
||||||
|
})
|
633
frontend/src/tests/convert/ConvertE2E.spec.ts
Normal file
633
frontend/src/tests/convert/ConvertE2E.spec.ts
Normal file
@ -0,0 +1,633 @@
|
|||||||
|
/**
|
||||||
|
* End-to-End Tests for Convert Tool
|
||||||
|
*
|
||||||
|
* These tests dynamically discover available conversion endpoints and test them.
|
||||||
|
* Tests are automatically skipped if the backend endpoint is not available.
|
||||||
|
*
|
||||||
|
* Run with: npm run test:e2e or npx playwright test
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect, Page } from '@playwright/test';
|
||||||
|
import {
|
||||||
|
ConversionEndpointDiscovery,
|
||||||
|
conversionDiscovery,
|
||||||
|
type ConversionEndpoint
|
||||||
|
} from '../helpers/conversionEndpointDiscovery';
|
||||||
|
|
||||||
|
// Test configuration
|
||||||
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
||||||
|
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080';
|
||||||
|
|
||||||
|
// Test file paths (these would need to exist in your test fixtures)
|
||||||
|
const TEST_FILES = {
|
||||||
|
pdf: './src/tests/test-fixtures/sample.pdf',
|
||||||
|
docx: './src/tests/test-fixtures/sample.docx',
|
||||||
|
doc: './src/tests/test-fixtures/sample.doc',
|
||||||
|
pptx: './src/tests/test-fixtures/sample.pptx',
|
||||||
|
ppt: './src/tests/test-fixtures/sample.ppt',
|
||||||
|
xlsx: './src/tests/test-fixtures/sample.xlsx',
|
||||||
|
xls: './src/tests/test-fixtures/sample.xls',
|
||||||
|
png: './src/tests/test-fixtures/sample.png',
|
||||||
|
jpg: './src/tests/test-fixtures/sample.jpg',
|
||||||
|
jpeg: './src/tests/test-fixtures/sample.jpeg',
|
||||||
|
gif: './src/tests/test-fixtures/sample.gif',
|
||||||
|
bmp: './src/tests/test-fixtures/sample.bmp',
|
||||||
|
tiff: './src/tests/test-fixtures/sample.tiff',
|
||||||
|
webp: './src/tests/test-fixtures/sample.webp',
|
||||||
|
md: './src/tests/test-fixtures/sample.md',
|
||||||
|
eml: './src/tests/test-fixtures/sample.eml',
|
||||||
|
html: './src/tests/test-fixtures/sample.html',
|
||||||
|
txt: './src/tests/test-fixtures/sample.txt',
|
||||||
|
xml: './src/tests/test-fixtures/sample.xml',
|
||||||
|
csv: './src/tests/test-fixtures/sample.csv'
|
||||||
|
};
|
||||||
|
|
||||||
|
// File format to test file mapping
|
||||||
|
const getTestFileForFormat = (format: string): string => {
|
||||||
|
const formatMap: Record<string, string> = {
|
||||||
|
'pdf': TEST_FILES.pdf,
|
||||||
|
'docx': TEST_FILES.docx,
|
||||||
|
'doc': TEST_FILES.doc,
|
||||||
|
'pptx': TEST_FILES.pptx,
|
||||||
|
'ppt': TEST_FILES.ppt,
|
||||||
|
'xlsx': TEST_FILES.xlsx,
|
||||||
|
'xls': TEST_FILES.xls,
|
||||||
|
'office': TEST_FILES.docx, // Default office file
|
||||||
|
'image': TEST_FILES.png, // Default image file
|
||||||
|
'png': TEST_FILES.png,
|
||||||
|
'jpg': TEST_FILES.jpg,
|
||||||
|
'jpeg': TEST_FILES.jpeg,
|
||||||
|
'gif': TEST_FILES.gif,
|
||||||
|
'bmp': TEST_FILES.bmp,
|
||||||
|
'tiff': TEST_FILES.tiff,
|
||||||
|
'webp': TEST_FILES.webp,
|
||||||
|
'md': TEST_FILES.md,
|
||||||
|
'eml': TEST_FILES.eml,
|
||||||
|
'html': TEST_FILES.html,
|
||||||
|
'txt': TEST_FILES.txt,
|
||||||
|
'xml': TEST_FILES.xml,
|
||||||
|
'csv': TEST_FILES.csv
|
||||||
|
};
|
||||||
|
|
||||||
|
return formatMap[format] || TEST_FILES.pdf; // Fallback to PDF
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expected file extensions for target formats
|
||||||
|
const getExpectedExtension = (toFormat: string): string => {
|
||||||
|
const extensionMap: Record<string, string> = {
|
||||||
|
'pdf': '.pdf',
|
||||||
|
'docx': '.docx',
|
||||||
|
'pptx': '.pptx',
|
||||||
|
'txt': '.txt',
|
||||||
|
'html': '.html',
|
||||||
|
'xml': '.xml',
|
||||||
|
'csv': '.csv',
|
||||||
|
'md': '.md',
|
||||||
|
'image': '.png', // Default for image conversion
|
||||||
|
'png': '.png',
|
||||||
|
'jpg': '.jpg',
|
||||||
|
'jpeg': '.jpeg',
|
||||||
|
'gif': '.gif',
|
||||||
|
'bmp': '.bmp',
|
||||||
|
'tiff': '.tiff',
|
||||||
|
'webp': '.webp',
|
||||||
|
'pdfa': '.pdf'
|
||||||
|
};
|
||||||
|
|
||||||
|
return extensionMap[toFormat] || '.pdf';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic test function for any conversion
|
||||||
|
*/
|
||||||
|
async function testConversion(page: Page, conversion: ConversionEndpoint) {
|
||||||
|
const expectedExtension = getExpectedExtension(conversion.toFormat);
|
||||||
|
|
||||||
|
console.log(`Testing ${conversion.endpoint}: ${conversion.fromFormat} → ${conversion.toFormat}`);
|
||||||
|
|
||||||
|
// File should already be uploaded, click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for the FileEditor to load in convert mode with file thumbnails
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
|
||||||
|
// Click the file thumbnail checkbox to select it in the FileEditor
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
// Wait for the conversion settings to appear after file selection
|
||||||
|
await page.waitForSelector('[data-testid="convert-from-dropdown"]', { timeout: 5000 });
|
||||||
|
|
||||||
|
// Select FROM format
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
const fromFormatOption = page.locator(`[data-testid="format-option-${conversion.fromFormat}"]`);
|
||||||
|
await fromFormatOption.scrollIntoViewIfNeeded();
|
||||||
|
await fromFormatOption.click();
|
||||||
|
|
||||||
|
// Select TO format
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
const toFormatOption = page.locator(`[data-testid="format-option-${conversion.toFormat}"]`);
|
||||||
|
await toFormatOption.scrollIntoViewIfNeeded();
|
||||||
|
await toFormatOption.click();
|
||||||
|
|
||||||
|
// Handle format-specific options
|
||||||
|
if (conversion.toFormat === 'image' || ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp'].includes(conversion.toFormat)) {
|
||||||
|
// Set image conversion options if they appear
|
||||||
|
const imageOptionsVisible = await page.locator('[data-testid="image-options-section"]').isVisible().catch(() => false);
|
||||||
|
if (imageOptionsVisible) {
|
||||||
|
// Click the color type dropdown and select "Color"
|
||||||
|
await page.click('[data-testid="color-type-select"]');
|
||||||
|
await page.getByRole('option', { name: 'Color' }).click();
|
||||||
|
|
||||||
|
// Set DPI value
|
||||||
|
await page.fill('[data-testid="dpi-input"]', '150');
|
||||||
|
|
||||||
|
// Click the output type dropdown and select "Multiple"
|
||||||
|
await page.click('[data-testid="output-type-select"]');
|
||||||
|
|
||||||
|
await page.getByRole('option', { name: 'multiple' }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversion.fromFormat === 'image' && conversion.toFormat === 'pdf') {
|
||||||
|
// Set PDF creation options if they appear
|
||||||
|
const pdfOptionsVisible = await page.locator('[data-testid="pdf-options-section"]').isVisible().catch(() => false);
|
||||||
|
if (pdfOptionsVisible) {
|
||||||
|
// Click the color type dropdown and select "Color"
|
||||||
|
await page.click('[data-testid="color-type-select"]');
|
||||||
|
await page.locator('[data-value="color"]').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversion.fromFormat === 'pdf' && conversion.toFormat === 'csv') {
|
||||||
|
// Set CSV extraction options if they appear
|
||||||
|
const csvOptionsVisible = await page.locator('[data-testid="csv-options-section"]').isVisible().catch(() => false);
|
||||||
|
if (csvOptionsVisible) {
|
||||||
|
// Set specific page numbers for testing (test pages 1-2)
|
||||||
|
await page.fill('[data-testid="page-numbers-input"]', '1-2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start conversion
|
||||||
|
await page.click('[data-testid="convert-button"]');
|
||||||
|
|
||||||
|
// Wait for conversion to complete (with generous timeout)
|
||||||
|
await page.waitForSelector('[data-testid="download-button"]', { timeout: 60000 });
|
||||||
|
|
||||||
|
// Verify download is available
|
||||||
|
const downloadButton = page.locator('[data-testid="download-button"]');
|
||||||
|
await expect(downloadButton).toBeVisible();
|
||||||
|
|
||||||
|
// Start download and verify file
|
||||||
|
const downloadPromise = page.waitForEvent('download');
|
||||||
|
await downloadButton.click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
|
||||||
|
// Verify file extension
|
||||||
|
expect(download.suggestedFilename()).toMatch(new RegExp(`\\${expectedExtension}$`));
|
||||||
|
|
||||||
|
// Save and verify file is not empty
|
||||||
|
const path = await download.path();
|
||||||
|
if (path) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const stats = fs.statSync(path);
|
||||||
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Format-specific validations
|
||||||
|
if (conversion.toFormat === 'pdf' || conversion.toFormat === 'pdfa') {
|
||||||
|
// Verify PDF header
|
||||||
|
const buffer = fs.readFileSync(path);
|
||||||
|
const header = buffer.toString('utf8', 0, 4);
|
||||||
|
expect(header).toBe('%PDF');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversion.toFormat === 'txt') {
|
||||||
|
// Verify text content exists
|
||||||
|
const content = fs.readFileSync(path, 'utf8');
|
||||||
|
expect(content.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversion.toFormat === 'csv') {
|
||||||
|
// Verify CSV content contains separators
|
||||||
|
const content = fs.readFileSync(path, 'utf8');
|
||||||
|
expect(content).toContain(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover conversions at module level before tests are defined
|
||||||
|
let allConversions: ConversionEndpoint[] = [];
|
||||||
|
let availableConversions: ConversionEndpoint[] = [];
|
||||||
|
let unavailableConversions: ConversionEndpoint[] = [];
|
||||||
|
|
||||||
|
// Pre-populate conversions synchronously for test generation
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
availableConversions = await conversionDiscovery.getAvailableConversions();
|
||||||
|
unavailableConversions = await conversionDiscovery.getUnavailableConversions();
|
||||||
|
allConversions = [...availableConversions, ...unavailableConversions];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to discover conversions during module load:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
test.describe('Convert Tool E2E Tests', () => {
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
// Re-discover to ensure fresh data at test time
|
||||||
|
console.log('Re-discovering available conversion endpoints...');
|
||||||
|
availableConversions = await conversionDiscovery.getAvailableConversions();
|
||||||
|
unavailableConversions = await conversionDiscovery.getUnavailableConversions();
|
||||||
|
|
||||||
|
console.log(`Found ${availableConversions.length} available conversions:`);
|
||||||
|
availableConversions.forEach(conv => {
|
||||||
|
console.log(` ✓ ${conv.endpoint}: ${conv.fromFormat} → ${conv.toFormat}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unavailableConversions.length > 0) {
|
||||||
|
console.log(`Found ${unavailableConversions.length} unavailable conversions:`);
|
||||||
|
unavailableConversions.forEach(conv => {
|
||||||
|
console.log(` ✗ ${conv.endpoint}: ${conv.fromFormat} → ${conv.toFormat}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Navigate to the homepage
|
||||||
|
await page.goto(`${BASE_URL}`);
|
||||||
|
|
||||||
|
// Wait for the page to load
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Wait for the file upload area to appear (shown when no active files)
|
||||||
|
await page.waitForSelector('[data-testid="file-dropzone"]', { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Dynamic Conversion Tests', () => {
|
||||||
|
|
||||||
|
// Generate a test for each potentially available conversion
|
||||||
|
// We'll discover all possible conversions and then skip unavailable ones at runtime
|
||||||
|
test('PDF to PNG conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/pdf/img', fromFormat: 'pdf', toFormat: 'png' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PDF to DOCX conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/pdf/word', fromFormat: 'pdf', toFormat: 'docx' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DOCX to PDF conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/file/pdf', fromFormat: 'docx', toFormat: 'pdf' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Image to PDF conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/img/pdf', fromFormat: 'image', toFormat: 'pdf' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PDF to TXT conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/pdf/text', fromFormat: 'pdf', toFormat: 'txt' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PDF to HTML conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/pdf/html', fromFormat: 'pdf', toFormat: 'html' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PDF to XML conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/pdf/xml', fromFormat: 'pdf', toFormat: 'xml' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PDF to CSV conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/pdf/csv', fromFormat: 'pdf', toFormat: 'csv' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PDF to PDFA conversion', async ({ page }) => {
|
||||||
|
const conversion = { endpoint: '/api/v1/convert/pdf/pdfa', fromFormat: 'pdf', toFormat: 'pdfa' };
|
||||||
|
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
||||||
|
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
||||||
|
|
||||||
|
const testFile = getTestFileForFormat(conversion.fromFormat);
|
||||||
|
await page.setInputFiles('input[type="file"]', testFile);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await testConversion(page, conversion);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Static Tests', () => {
|
||||||
|
|
||||||
|
// Test that disabled conversions don't appear in dropdowns when they shouldn't
|
||||||
|
test('should not show conversion button when no valid conversions available', async ({ page }) => {
|
||||||
|
// This test ensures the convert button is disabled when no valid conversion is possible
|
||||||
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
// Don't select any formats - convert button should not exist
|
||||||
|
const convertButton = page.locator('[data-testid="convert-button"]');
|
||||||
|
await expect(convertButton).toHaveCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Error Handling', () => {
|
||||||
|
|
||||||
|
test('should handle corrupted file gracefully', async ({ page }) => {
|
||||||
|
// Create a corrupted file
|
||||||
|
const fs = require('fs');
|
||||||
|
const corruptedPath = './src/tests/test-fixtures/corrupted.pdf';
|
||||||
|
fs.writeFileSync(corruptedPath, 'This is not a valid PDF file');
|
||||||
|
|
||||||
|
await page.setInputFiles('input[type="file"]', corruptedPath);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-pdf"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-png"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-button"]');
|
||||||
|
|
||||||
|
// Should show error message
|
||||||
|
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
|
||||||
|
// Error text content will vary based on translation, so just check error message exists
|
||||||
|
await expect(page.locator('[data-testid="error-message"]')).not.toBeEmpty();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
fs.unlinkSync(corruptedPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle backend unavailability', async ({ page }) => {
|
||||||
|
// This test would require mocking the backend or testing during downtime
|
||||||
|
// For now, we'll simulate by checking error handling UI
|
||||||
|
|
||||||
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
// Mock network failure
|
||||||
|
await page.route('**/api/v1/convert/**', route => {
|
||||||
|
route.abort('failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-pdf"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-png"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-button"]');
|
||||||
|
|
||||||
|
// Should show network error
|
||||||
|
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('UI/UX Flow', () => {
|
||||||
|
|
||||||
|
test('should reset TO dropdown when FROM changes', async ({ page }) => {
|
||||||
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
// Select PDF -> PNG
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-pdf"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-png"]');
|
||||||
|
|
||||||
|
// Verify PNG is selected
|
||||||
|
await expect(page.locator('[data-testid="convert-to-dropdown"]')).toContainText('Image (PNG)');
|
||||||
|
|
||||||
|
// Change FROM to DOCX
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-docx"]');
|
||||||
|
|
||||||
|
// TO dropdown should reset
|
||||||
|
await expect(page.locator('[data-testid="convert-to-dropdown"]')).toContainText('Select target file format');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show/hide format-specific options correctly', async ({ page }) => {
|
||||||
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-pdf"]');
|
||||||
|
|
||||||
|
// Select image format - should show image options
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-png"]');
|
||||||
|
|
||||||
|
await expect(page.locator('[data-testid="image-options-section"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="dpi-input"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Change to CSV format - should hide image options and show CSV options
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-csv"]');
|
||||||
|
|
||||||
|
await expect(page.locator('[data-testid="image-options-section"]')).not.toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="dpi-input"]')).not.toBeVisible();
|
||||||
|
|
||||||
|
// Should show CSV options
|
||||||
|
await expect(page.locator('[data-testid="csv-options-section"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="page-numbers-input"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle CSV page number input correctly', async ({ page }) => {
|
||||||
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-pdf"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-csv"]');
|
||||||
|
|
||||||
|
// Should show CSV options with default value
|
||||||
|
await expect(page.locator('[data-testid="page-numbers-input"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveValue('all');
|
||||||
|
|
||||||
|
// Should be able to change page numbers
|
||||||
|
await page.fill('[data-testid="page-numbers-input"]', '1,3,5-7');
|
||||||
|
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveValue('1,3,5-7');
|
||||||
|
|
||||||
|
// Should show help text
|
||||||
|
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveAttribute('placeholder', /e\.g\.,.*all/);
|
||||||
|
|
||||||
|
// Mathematical function should work too
|
||||||
|
await page.fill('[data-testid="page-numbers-input"]', '2n+1');
|
||||||
|
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveValue('2n+1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show progress indicators during conversion', async ({ page }) => {
|
||||||
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-pdf"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-png"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-button"]');
|
||||||
|
|
||||||
|
// Should show loading state (button becomes disabled during loading)
|
||||||
|
await expect(page.locator('[data-testid="convert-button"]')).toBeDisabled();
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
await page.waitForSelector('[data-testid="conversion-results"]', { timeout: 30000 });
|
||||||
|
|
||||||
|
// Loading should be gone (button should be enabled again, though it may be collapsed in results view)
|
||||||
|
// We check for results instead since the button might be in a collapsed state
|
||||||
|
await expect(page.locator('[data-testid="conversion-results"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('File Preview Integration', () => {
|
||||||
|
|
||||||
|
test('should integrate with file preview system', async ({ page }) => {
|
||||||
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click the Convert tool button
|
||||||
|
await page.click('[data-testid="tool-convert"]');
|
||||||
|
|
||||||
|
// Wait for convert mode and select file
|
||||||
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
||||||
|
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-from-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-pdf"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-to-dropdown"]');
|
||||||
|
await page.click('[data-testid="format-option-png"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="convert-button"]');
|
||||||
|
await page.waitForSelector('[data-testid="conversion-results"]', { timeout: 30000 });
|
||||||
|
|
||||||
|
// Should show preview of converted file in the results preview container
|
||||||
|
await expect(page.locator('[data-testid="results-preview-container"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Should show the preview title with file count
|
||||||
|
await expect(page.locator('[data-testid="results-preview-title"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Should be able to click on result thumbnails to preview (first thumbnail)
|
||||||
|
const firstThumbnail = page.locator('[data-testid="results-preview-thumbnail-0"]');
|
||||||
|
if (await firstThumbnail.isVisible()) {
|
||||||
|
await firstThumbnail.click();
|
||||||
|
// After clicking, should switch to viewer mode (this happens through handleThumbnailClick)
|
||||||
|
// We can verify this by checking if the current mode changed
|
||||||
|
await page.waitForTimeout(500); // Small wait for mode change
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
468
frontend/src/tests/convert/ConvertIntegration.test.tsx
Normal file
468
frontend/src/tests/convert/ConvertIntegration.test.tsx
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests for Convert Tool - Tests actual conversion functionality
|
||||||
|
*
|
||||||
|
* These tests verify the integration between frontend components and backend:
|
||||||
|
* 1. useConvertOperation hook makes correct API calls
|
||||||
|
* 2. File upload/download flow functions properly
|
||||||
|
* 3. Error handling works for various failure scenarios
|
||||||
|
* 4. Parameter passing works between frontend and backend
|
||||||
|
* 5. FileContext integration works correctly
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||||
|
import { useConvertOperation } from '../../hooks/tools/convert/useConvertOperation';
|
||||||
|
import { ConvertParameters } from '../../hooks/tools/convert/useConvertParameters';
|
||||||
|
import { FileContextProvider } from '../../contexts/FileContext';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import i18n from '../../i18n/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Mock axios
|
||||||
|
vi.mock('axios');
|
||||||
|
const mockedAxios = vi.mocked(axios);
|
||||||
|
|
||||||
|
// Mock utility modules
|
||||||
|
vi.mock('../../utils/thumbnailUtils', () => ({
|
||||||
|
generateThumbnailForFile: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail')
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../utils/api', () => ({
|
||||||
|
makeApiUrl: vi.fn((path: string) => `/api/v1${path}`)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create realistic test files
|
||||||
|
const createTestFile = (name: string, content: string, type: string): File => {
|
||||||
|
return new File([content], name, { type });
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPDFFile = (): File => {
|
||||||
|
const pdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\ntrailer\n<<\n/Size 2\n/Root 1 0 R\n>>\nstartxref\n0\n%%EOF';
|
||||||
|
return createTestFile('test.pdf', pdfContent, 'application/pdf');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test wrapper component
|
||||||
|
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<FileContextProvider>
|
||||||
|
{children}
|
||||||
|
</FileContextProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Convert Tool Integration Tests', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
// Setup default axios mock
|
||||||
|
mockedAxios.post = vi.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useConvertOperation Integration', () => {
|
||||||
|
|
||||||
|
test('should make correct API call for PDF to PNG conversion', async () => {
|
||||||
|
const mockBlob = new Blob(['fake-image-data'], { type: 'image/png' });
|
||||||
|
mockedAxios.post.mockResolvedValueOnce({
|
||||||
|
data: mockBlob,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createPDFFile();
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify axios was called with correct parameters
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/convert/pdf/img',
|
||||||
|
expect.any(FormData),
|
||||||
|
{ responseType: 'blob' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify FormData contains correct parameters
|
||||||
|
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
|
||||||
|
expect(formDataCall.get('imageFormat')).toBe('png');
|
||||||
|
expect(formDataCall.get('colorType')).toBe('color');
|
||||||
|
expect(formDataCall.get('dpi')).toBe('300');
|
||||||
|
expect(formDataCall.get('singleOrMultiple')).toBe('multiple');
|
||||||
|
|
||||||
|
// Verify hook state updates
|
||||||
|
expect(result.current.downloadUrl).toBeTruthy();
|
||||||
|
expect(result.current.downloadFilename).toBe('test_converted.png');
|
||||||
|
expect(result.current.isLoading).toBe(false);
|
||||||
|
expect(result.current.errorMessage).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle API error responses correctly', async () => {
|
||||||
|
const errorMessage = 'Invalid file format';
|
||||||
|
mockedAxios.post.mockRejectedValueOnce({
|
||||||
|
response: {
|
||||||
|
status: 400,
|
||||||
|
data: errorMessage
|
||||||
|
},
|
||||||
|
message: errorMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createTestFile('invalid.txt', 'not a pdf', 'text/plain');
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify error handling
|
||||||
|
expect(result.current.errorMessage).toBe(errorMessage);
|
||||||
|
expect(result.current.isLoading).toBe(false);
|
||||||
|
expect(result.current.downloadUrl).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle network errors gracefully', async () => {
|
||||||
|
mockedAxios.post.mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createPDFFile();
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.errorMessage).toBe('Network error');
|
||||||
|
expect(result.current.isLoading).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('API and Hook Integration', () => {
|
||||||
|
|
||||||
|
test('should correctly map image conversion parameters to API call', async () => {
|
||||||
|
const mockBlob = new Blob(['fake-data'], { type: 'image/jpeg' });
|
||||||
|
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createPDFFile();
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'jpg',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'grayscale',
|
||||||
|
dpi: 150,
|
||||||
|
singleOrMultiple: 'single'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify integration: hook parameters → FormData → axios call → hook state
|
||||||
|
const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData;
|
||||||
|
expect(formDataCall.get('imageFormat')).toBe('jpg');
|
||||||
|
expect(formDataCall.get('colorType')).toBe('grayscale');
|
||||||
|
expect(formDataCall.get('dpi')).toBe('150');
|
||||||
|
expect(formDataCall.get('singleOrMultiple')).toBe('single');
|
||||||
|
|
||||||
|
// Verify complete workflow: API response → hook state → FileContext integration
|
||||||
|
expect(result.current.downloadUrl).toBeTruthy();
|
||||||
|
expect(result.current.files).toHaveLength(1);
|
||||||
|
expect(result.current.files[0].name).toBe('test_converted.jpg');
|
||||||
|
expect(result.current.isLoading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle complete unsupported conversion workflow', async () => {
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createPDFFile();
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'unsupported',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify integration: utils validation prevents API call, hook shows error
|
||||||
|
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||||
|
expect(result.current.errorMessage).toContain('errorNotSupported');
|
||||||
|
expect(result.current.isLoading).toBe(false);
|
||||||
|
expect(result.current.downloadUrl).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File Upload Integration', () => {
|
||||||
|
|
||||||
|
test('should handle multiple file uploads correctly', async () => {
|
||||||
|
const mockBlob = new Blob(['zip-content'], { type: 'application/zip' });
|
||||||
|
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const file1 = createPDFFile();
|
||||||
|
const file2 = createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf');
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [file1, file2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle no files selected', async () => {
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||||
|
expect(result.current.status).toContain('noFileSelected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Boundary Integration', () => {
|
||||||
|
|
||||||
|
test('should handle corrupted file gracefully', async () => {
|
||||||
|
mockedAxios.post.mockRejectedValueOnce({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
data: 'Processing failed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const corruptedFile = createTestFile('corrupted.pdf', 'not-a-pdf', 'application/pdf');
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [corruptedFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.errorMessage).toBe('Processing failed');
|
||||||
|
expect(result.current.isLoading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle backend service unavailable', async () => {
|
||||||
|
mockedAxios.post.mockRejectedValueOnce({
|
||||||
|
response: {
|
||||||
|
status: 503,
|
||||||
|
data: 'Service unavailable'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createPDFFile();
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.errorMessage).toBe('Service unavailable');
|
||||||
|
expect(result.current.isLoading).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FileContext Integration', () => {
|
||||||
|
|
||||||
|
test('should record operation in FileContext', async () => {
|
||||||
|
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
|
||||||
|
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createPDFFile();
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify operation was successful and files were processed
|
||||||
|
expect(result.current.files).toHaveLength(1);
|
||||||
|
expect(result.current.files[0].name).toBe('test_converted.png');
|
||||||
|
expect(result.current.downloadUrl).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clean up blob URLs on reset', async () => {
|
||||||
|
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
|
||||||
|
mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
|
wrapper: TestWrapper
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFile = createPDFFile();
|
||||||
|
const parameters: ConvertParameters = {
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.executeOperation(parameters, [testFile]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.downloadUrl).toBeTruthy();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.resetResults();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.downloadUrl).toBe(null);
|
||||||
|
expect(result.current.files).toHaveLength(0);
|
||||||
|
expect(result.current.errorMessage).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional Integration Tests That Require Real Backend
|
||||||
|
*
|
||||||
|
* These tests would require a running backend server and are better suited
|
||||||
|
* for E2E testing with tools like Playwright or Cypress:
|
||||||
|
*
|
||||||
|
* 1. **Real File Conversion Tests**
|
||||||
|
* - Upload actual PDF files and verify conversion quality
|
||||||
|
* - Test image format outputs are valid and viewable
|
||||||
|
* - Test CSV/TXT outputs contain expected content
|
||||||
|
* - Test file size limits and memory constraints
|
||||||
|
*
|
||||||
|
* 2. **Performance Integration Tests**
|
||||||
|
* - Test conversion time for various file sizes
|
||||||
|
* - Test memory usage during large file conversions
|
||||||
|
* - Test concurrent conversion requests
|
||||||
|
* - Test timeout handling for long-running conversions
|
||||||
|
*
|
||||||
|
* 3. **Authentication Integration**
|
||||||
|
* - Test conversions with and without authentication
|
||||||
|
* - Test rate limiting and user quotas
|
||||||
|
* - Test permission-based endpoint access
|
||||||
|
*
|
||||||
|
* 4. **File Preview Integration**
|
||||||
|
* - Test that converted files integrate correctly with viewer
|
||||||
|
* - Test thumbnail generation for converted files
|
||||||
|
* - Test file download functionality
|
||||||
|
* - Test FileContext persistence across tool switches
|
||||||
|
*
|
||||||
|
* 5. **Endpoint Availability Tests**
|
||||||
|
* - Test real endpoint availability checking
|
||||||
|
* - Test graceful degradation when endpoints are disabled
|
||||||
|
* - Test dynamic endpoint configuration updates
|
||||||
|
*/
|
264
frontend/src/tests/convert/README.md
Normal file
264
frontend/src/tests/convert/README.md
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# Convert Tool Test Suite
|
||||||
|
|
||||||
|
This directory contains comprehensive tests for the Convert Tool functionality.
|
||||||
|
|
||||||
|
## Test Files Overview
|
||||||
|
|
||||||
|
### 1. ConvertTool.test.tsx
|
||||||
|
**Purpose**: Unit/Component testing for the Convert Tool UI components
|
||||||
|
- Tests dropdown behavior and navigation
|
||||||
|
- Tests format availability based on endpoint status
|
||||||
|
- Tests UI state management and form validation
|
||||||
|
- Mocks backend dependencies for isolated testing
|
||||||
|
|
||||||
|
**Key Test Areas**:
|
||||||
|
- FROM dropdown enables/disables formats based on endpoint availability
|
||||||
|
- TO dropdown shows correct conversions for selected source format
|
||||||
|
- Format-specific options appear/disappear correctly
|
||||||
|
- Parameter validation and state management
|
||||||
|
|
||||||
|
### 2. ConvertIntegration.test.ts
|
||||||
|
**Purpose**: Integration testing for Convert Tool business logic
|
||||||
|
- Tests parameter validation and conversion matrix logic
|
||||||
|
- Tests endpoint resolution and availability checking
|
||||||
|
- Tests file extension detection
|
||||||
|
- Provides framework for testing actual conversions (requires backend)
|
||||||
|
|
||||||
|
**Key Test Areas**:
|
||||||
|
- Endpoint availability checking matches real backend status
|
||||||
|
- Conversion parameters are correctly validated
|
||||||
|
- File extension detection works properly
|
||||||
|
- Conversion matrix returns correct available formats
|
||||||
|
|
||||||
|
### 3. ConvertE2E.spec.ts
|
||||||
|
**Purpose**: End-to-End testing using Playwright with Dynamic Endpoint Discovery
|
||||||
|
- **Automatically discovers available conversion endpoints** from the backend
|
||||||
|
- Tests complete user workflows from file upload to download
|
||||||
|
- Tests actual file conversions with real backend
|
||||||
|
- **Skips tests for unavailable endpoints** automatically
|
||||||
|
- Tests error handling and edge cases
|
||||||
|
- Tests UI/UX flow and user interactions
|
||||||
|
|
||||||
|
**Key Test Areas**:
|
||||||
|
- **Dynamic endpoint discovery** using `/api/v1/config/endpoints-enabled` API
|
||||||
|
- Complete conversion workflows for **all available endpoints**
|
||||||
|
- **Unavailable endpoint testing** - verifies disabled conversions are properly blocked
|
||||||
|
- File upload, conversion, and download process
|
||||||
|
- Error handling for corrupted files and network issues
|
||||||
|
- Performance testing with large files
|
||||||
|
- UI responsiveness and progress indicators
|
||||||
|
|
||||||
|
**Supported Conversions** (tested if available):
|
||||||
|
- PDF ↔ Images (PNG, JPG, GIF, BMP, TIFF, WebP)
|
||||||
|
- PDF ↔ Office (DOCX, PPTX)
|
||||||
|
- PDF ↔ Text (TXT, HTML, XML, CSV, Markdown)
|
||||||
|
- Office → PDF (DOCX, PPTX, XLSX, etc.)
|
||||||
|
- Email (EML) → PDF
|
||||||
|
- HTML → PDF, URL → PDF
|
||||||
|
- Markdown → PDF
|
||||||
|
|
||||||
|
## Running the Tests
|
||||||
|
|
||||||
|
**Important**: All commands should be run from the `frontend/` directory:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup (First Time Only)
|
||||||
|
```bash
|
||||||
|
# Install dependencies (includes test frameworks)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Install Playwright browsers for E2E tests
|
||||||
|
npx playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unit Tests (ConvertTool.test.tsx)
|
||||||
|
```bash
|
||||||
|
# Run all unit tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
npm test ConvertTool.test.tsx
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Run in watch mode (re-runs on file changes)
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# Run specific test pattern
|
||||||
|
npm test -- --grep "dropdown"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests (ConvertIntegration.test.ts)
|
||||||
|
```bash
|
||||||
|
# Run integration tests
|
||||||
|
npm test ConvertIntegration.test.ts
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
npm test ConvertIntegration.test.ts -- --reporter=verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E Tests (ConvertE2E.spec.ts)
|
||||||
|
```bash
|
||||||
|
# Prerequisites: Backend must be running on localhost:8080
|
||||||
|
# Start backend first, then:
|
||||||
|
|
||||||
|
# Run all E2E tests (automatically discovers available endpoints)
|
||||||
|
npm run test:e2e
|
||||||
|
|
||||||
|
# Run specific E2E test file
|
||||||
|
npx playwright test ConvertE2E.spec.ts
|
||||||
|
|
||||||
|
# Run with UI mode for debugging
|
||||||
|
npx playwright test --ui
|
||||||
|
|
||||||
|
# Run specific test by endpoint name (dynamic)
|
||||||
|
npx playwright test -g "pdf-to-img:"
|
||||||
|
|
||||||
|
# Run only available conversion tests
|
||||||
|
npx playwright test -g "Dynamic Conversion Tests"
|
||||||
|
|
||||||
|
# Run only unavailable conversion tests
|
||||||
|
npx playwright test -g "Unavailable Conversions"
|
||||||
|
|
||||||
|
# Run in headed mode (see browser)
|
||||||
|
npx playwright test --headed
|
||||||
|
|
||||||
|
# Generate HTML report
|
||||||
|
npx playwright test ConvertE2E.spec.ts --reporter=html
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Discovery Process:**
|
||||||
|
1. Tests automatically query `/api/v1/config/endpoints-enabled` to discover available conversions
|
||||||
|
2. Tests are generated dynamically for each available endpoint
|
||||||
|
3. Tests for unavailable endpoints verify they're properly disabled in the UI
|
||||||
|
4. Console output shows which endpoints were discovered
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
### For Unit Tests
|
||||||
|
- No special requirements
|
||||||
|
- All dependencies are mocked
|
||||||
|
- Can run in any environment
|
||||||
|
|
||||||
|
### For Integration Tests
|
||||||
|
- May require backend API for full functionality
|
||||||
|
- Uses mock data for endpoint availability
|
||||||
|
- Tests business logic in isolation
|
||||||
|
|
||||||
|
### For E2E Tests
|
||||||
|
- **Requires running backend server** (localhost:8080)
|
||||||
|
- **Requires test fixture files** (see ../test-fixtures/README.md)
|
||||||
|
- Requires frontend dev server (localhost:5173)
|
||||||
|
- Tests real conversion functionality
|
||||||
|
|
||||||
|
## Test Data
|
||||||
|
|
||||||
|
The tests use realistic endpoint availability data based on your current server configuration:
|
||||||
|
|
||||||
|
**Available Endpoints** (should pass):
|
||||||
|
- `file-to-pdf`: true (DOCX, XLSX, PPTX → PDF)
|
||||||
|
- `img-to-pdf`: true (PNG, JPG, etc. → PDF)
|
||||||
|
- `markdown-to-pdf`: true (MD → PDF)
|
||||||
|
- `pdf-to-csv`: true (PDF → CSV)
|
||||||
|
- `pdf-to-img`: true (PDF → PNG, JPG, etc.)
|
||||||
|
- `pdf-to-text`: true (PDF → TXT)
|
||||||
|
|
||||||
|
**Disabled Endpoints** (should be blocked):
|
||||||
|
- `eml-to-pdf`: false
|
||||||
|
- `html-to-pdf`: false
|
||||||
|
- `pdf-to-html`: false
|
||||||
|
- `pdf-to-markdown`: false
|
||||||
|
- `pdf-to-pdfa`: false
|
||||||
|
- `pdf-to-presentation`: false
|
||||||
|
- `pdf-to-word`: false
|
||||||
|
- `pdf-to-xml`: false
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### Success Scenarios (Available Endpoints)
|
||||||
|
1. **PDF → Image**: PDF to PNG/JPG with various DPI and color settings
|
||||||
|
2. **PDF → Data**: PDF to CSV (table extraction), PDF to TXT (text extraction)
|
||||||
|
3. **Office → PDF**: DOCX/XLSX/PPTX to PDF conversion
|
||||||
|
4. **Image → PDF**: PNG/JPG to PDF with image options
|
||||||
|
5. **Markdown → PDF**: MD to PDF with formatting preservation
|
||||||
|
|
||||||
|
### Blocked Scenarios (Disabled Endpoints)
|
||||||
|
1. **EML conversions**: Should be disabled in FROM dropdown
|
||||||
|
2. **PDF → Office**: PDF to Word/PowerPoint should be disabled
|
||||||
|
3. **PDF → Web**: PDF to HTML/XML should be disabled
|
||||||
|
4. **PDF → PDF/A**: Should be disabled
|
||||||
|
|
||||||
|
### Error Scenarios
|
||||||
|
1. **Corrupted files**: Should show helpful error messages
|
||||||
|
2. **Network failures**: Should handle backend unavailability
|
||||||
|
3. **Large files**: Should handle memory constraints gracefully
|
||||||
|
4. **Invalid parameters**: Should validate before submission
|
||||||
|
|
||||||
|
## Adding New Tests
|
||||||
|
|
||||||
|
When adding new conversion formats:
|
||||||
|
|
||||||
|
1. **Update ConvertTool.test.tsx**:
|
||||||
|
- Add the new format to test data
|
||||||
|
- Test dropdown behavior for the new format
|
||||||
|
- Test format-specific options if any
|
||||||
|
|
||||||
|
2. **Update ConvertIntegration.test.ts**:
|
||||||
|
- Add endpoint availability test cases
|
||||||
|
- Add conversion matrix test cases
|
||||||
|
- Add parameter validation tests
|
||||||
|
|
||||||
|
3. **Update ConvertE2E.spec.ts**:
|
||||||
|
- Add end-to-end workflow tests
|
||||||
|
- Add test fixture files
|
||||||
|
- Test actual conversion functionality
|
||||||
|
|
||||||
|
4. **Update test fixtures**:
|
||||||
|
- Add sample files for the new format
|
||||||
|
- Update ../test-fixtures/README.md
|
||||||
|
|
||||||
|
## Debugging Failed Tests
|
||||||
|
|
||||||
|
### Unit Test Failures
|
||||||
|
- Check mock data matches real endpoint status
|
||||||
|
- Verify component props and state management
|
||||||
|
- Check for React hook dependency issues
|
||||||
|
|
||||||
|
### Integration Test Failures
|
||||||
|
- Verify conversion matrix includes new formats
|
||||||
|
- Check endpoint name mappings
|
||||||
|
- Ensure parameter validation logic is correct
|
||||||
|
|
||||||
|
### E2E Test Failures
|
||||||
|
- Ensure backend server is running
|
||||||
|
- Check test fixture files exist and are valid
|
||||||
|
- Verify element selectors match current UI
|
||||||
|
- Check for timing issues (increase timeouts if needed)
|
||||||
|
|
||||||
|
## Test Maintenance
|
||||||
|
|
||||||
|
### Regular Updates Needed
|
||||||
|
1. **Endpoint Status**: Update mock data when backend endpoints change
|
||||||
|
2. **UI Selectors**: Update test selectors when UI changes
|
||||||
|
3. **Test Fixtures**: Replace old test files with new ones periodically
|
||||||
|
4. **Performance Benchmarks**: Update expected performance metrics
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
- Unit tests: Run on every commit
|
||||||
|
- Integration tests: Run on pull requests
|
||||||
|
- E2E tests: Run on staging deployment
|
||||||
|
- Performance tests: Run weekly or on major releases
|
||||||
|
|
||||||
|
## Performance Expectations
|
||||||
|
|
||||||
|
These tests focus on frontend functionality, not backend performance:
|
||||||
|
|
||||||
|
- **File upload/UI**: < 1 second for small test files
|
||||||
|
- **Dropdown interactions**: < 200ms
|
||||||
|
- **Form validation**: < 100ms
|
||||||
|
- **Conversion UI flow**: < 5 seconds for small test files
|
||||||
|
|
||||||
|
Tests will fail if UI interactions are slow, indicating frontend performance issues.
|
304
frontend/src/tests/helpers/conversionEndpointDiscovery.ts
Normal file
304
frontend/src/tests/helpers/conversionEndpointDiscovery.ts
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* Conversion Endpoint Discovery for E2E Testing
|
||||||
|
*
|
||||||
|
* Uses the backend's endpoint configuration API to discover available conversions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMultipleEndpointsEnabled } from '../../hooks/useEndpointConfig';
|
||||||
|
|
||||||
|
export interface ConversionEndpoint {
|
||||||
|
endpoint: string;
|
||||||
|
fromFormat: string;
|
||||||
|
toFormat: string;
|
||||||
|
description: string;
|
||||||
|
apiPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete list of conversion endpoints based on EndpointConfiguration.java
|
||||||
|
const ALL_CONVERSION_ENDPOINTS: ConversionEndpoint[] = [
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-img',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'image',
|
||||||
|
description: 'Convert PDF to images (PNG, JPG, GIF, etc.)',
|
||||||
|
apiPath: '/api/v1/convert/pdf/img'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'img-to-pdf',
|
||||||
|
fromFormat: 'image',
|
||||||
|
toFormat: 'pdf',
|
||||||
|
description: 'Convert images to PDF',
|
||||||
|
apiPath: '/api/v1/convert/img/pdf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-pdfa',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'pdfa',
|
||||||
|
description: 'Convert PDF to PDF/A',
|
||||||
|
apiPath: '/api/v1/convert/pdf/pdfa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'file-to-pdf',
|
||||||
|
fromFormat: 'office',
|
||||||
|
toFormat: 'pdf',
|
||||||
|
description: 'Convert office files to PDF',
|
||||||
|
apiPath: '/api/v1/convert/file/pdf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-word',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'docx',
|
||||||
|
description: 'Convert PDF to Word document',
|
||||||
|
apiPath: '/api/v1/convert/pdf/word'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-presentation',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'pptx',
|
||||||
|
description: 'Convert PDF to PowerPoint presentation',
|
||||||
|
apiPath: '/api/v1/convert/pdf/presentation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-text',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'txt',
|
||||||
|
description: 'Convert PDF to plain text',
|
||||||
|
apiPath: '/api/v1/convert/pdf/text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-html',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'html',
|
||||||
|
description: 'Convert PDF to HTML',
|
||||||
|
apiPath: '/api/v1/convert/pdf/html'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-xml',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'xml',
|
||||||
|
description: 'Convert PDF to XML',
|
||||||
|
apiPath: '/api/v1/convert/pdf/xml'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'html-to-pdf',
|
||||||
|
fromFormat: 'html',
|
||||||
|
toFormat: 'pdf',
|
||||||
|
description: 'Convert HTML to PDF',
|
||||||
|
apiPath: '/api/v1/convert/html/pdf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'url-to-pdf',
|
||||||
|
fromFormat: 'url',
|
||||||
|
toFormat: 'pdf',
|
||||||
|
description: 'Convert web page to PDF',
|
||||||
|
apiPath: '/api/v1/convert/url/pdf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'markdown-to-pdf',
|
||||||
|
fromFormat: 'md',
|
||||||
|
toFormat: 'pdf',
|
||||||
|
description: 'Convert Markdown to PDF',
|
||||||
|
apiPath: '/api/v1/convert/markdown/pdf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-csv',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'csv',
|
||||||
|
description: 'Extract CSV data from PDF',
|
||||||
|
apiPath: '/api/v1/convert/pdf/csv'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'pdf-to-markdown',
|
||||||
|
fromFormat: 'pdf',
|
||||||
|
toFormat: 'md',
|
||||||
|
description: 'Convert PDF to Markdown',
|
||||||
|
apiPath: '/api/v1/convert/pdf/markdown'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: 'eml-to-pdf',
|
||||||
|
fromFormat: 'eml',
|
||||||
|
toFormat: 'pdf',
|
||||||
|
description: 'Convert email (EML) to PDF',
|
||||||
|
apiPath: '/api/v1/convert/eml/pdf'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export class ConversionEndpointDiscovery {
|
||||||
|
private baseUrl: string;
|
||||||
|
private cache: Map<string, boolean> | null = null;
|
||||||
|
private cacheExpiry: number = 0;
|
||||||
|
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
constructor(baseUrl: string = process.env.BACKEND_URL || 'http://localhost:8080') {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available conversion endpoints by checking with backend
|
||||||
|
*/
|
||||||
|
async getAvailableConversions(): Promise<ConversionEndpoint[]> {
|
||||||
|
const endpointStatuses = await this.getEndpointStatuses();
|
||||||
|
|
||||||
|
return ALL_CONVERSION_ENDPOINTS.filter(conversion =>
|
||||||
|
endpointStatuses.get(conversion.endpoint) === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all unavailable conversion endpoints
|
||||||
|
*/
|
||||||
|
async getUnavailableConversions(): Promise<ConversionEndpoint[]> {
|
||||||
|
const endpointStatuses = await this.getEndpointStatuses();
|
||||||
|
|
||||||
|
return ALL_CONVERSION_ENDPOINTS.filter(conversion =>
|
||||||
|
endpointStatuses.get(conversion.endpoint) === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific conversion is available
|
||||||
|
*/
|
||||||
|
async isConversionAvailable(endpoint: string): Promise<boolean> {
|
||||||
|
const endpointStatuses = await this.getEndpointStatuses();
|
||||||
|
return endpointStatuses.get(endpoint) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available conversions grouped by source format
|
||||||
|
*/
|
||||||
|
async getConversionsByFormat(): Promise<Record<string, ConversionEndpoint[]>> {
|
||||||
|
const availableConversions = await this.getAvailableConversions();
|
||||||
|
|
||||||
|
const grouped: Record<string, ConversionEndpoint[]> = {};
|
||||||
|
|
||||||
|
availableConversions.forEach(conversion => {
|
||||||
|
if (!grouped[conversion.fromFormat]) {
|
||||||
|
grouped[conversion.fromFormat] = [];
|
||||||
|
}
|
||||||
|
grouped[conversion.fromFormat].push(conversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get supported target formats for a given source format
|
||||||
|
*/
|
||||||
|
async getSupportedTargetFormats(fromFormat: string): Promise<string[]> {
|
||||||
|
const availableConversions = await this.getAvailableConversions();
|
||||||
|
|
||||||
|
return availableConversions
|
||||||
|
.filter(conversion => conversion.fromFormat === fromFormat)
|
||||||
|
.map(conversion => conversion.toFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all supported source formats
|
||||||
|
*/
|
||||||
|
async getSupportedSourceFormats(): Promise<string[]> {
|
||||||
|
const availableConversions = await this.getAvailableConversions();
|
||||||
|
|
||||||
|
const sourceFormats = new Set(
|
||||||
|
availableConversions.map(conversion => conversion.fromFormat)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Array.from(sourceFormats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get endpoint statuses from backend using batch API
|
||||||
|
*/
|
||||||
|
private async getEndpointStatuses(): Promise<Map<string, boolean>> {
|
||||||
|
// Return cached result if still valid
|
||||||
|
if (this.cache && Date.now() < this.cacheExpiry) {
|
||||||
|
return this.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpointNames = ALL_CONVERSION_ENDPOINTS.map(conv => conv.endpoint);
|
||||||
|
const endpointsParam = endpointNames.join(',');
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch endpoint statuses: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap: Record<string, boolean> = await response.json();
|
||||||
|
|
||||||
|
// Convert to Map and cache
|
||||||
|
this.cache = new Map(Object.entries(statusMap));
|
||||||
|
this.cacheExpiry = Date.now() + this.CACHE_DURATION;
|
||||||
|
|
||||||
|
console.log(`Retrieved status for ${Object.keys(statusMap).length} conversion endpoints`);
|
||||||
|
return this.cache;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get endpoint statuses:', error);
|
||||||
|
|
||||||
|
// Fallback: assume all endpoints are disabled
|
||||||
|
const fallbackMap = new Map<string, boolean>();
|
||||||
|
ALL_CONVERSION_ENDPOINTS.forEach(conv => {
|
||||||
|
fallbackMap.set(conv.endpoint, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fallbackMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to create a skipping condition for tests
|
||||||
|
*/
|
||||||
|
static createSkipCondition(endpoint: string, discovery: ConversionEndpointDiscovery) {
|
||||||
|
return async () => {
|
||||||
|
const available = await discovery.isConversionAvailable(endpoint);
|
||||||
|
return !available;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed conversion info by endpoint name
|
||||||
|
*/
|
||||||
|
getConversionInfo(endpoint: string): ConversionEndpoint | undefined {
|
||||||
|
return ALL_CONVERSION_ENDPOINTS.find(conv => conv.endpoint === endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all conversion endpoints (regardless of availability)
|
||||||
|
*/
|
||||||
|
getAllConversions(): ConversionEndpoint[] {
|
||||||
|
return [...ALL_CONVERSION_ENDPOINTS];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance for reuse across tests
|
||||||
|
export const conversionDiscovery = new ConversionEndpointDiscovery();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook version for use in components (wraps the class)
|
||||||
|
*/
|
||||||
|
export function useConversionEndpoints() {
|
||||||
|
const endpointNames = ALL_CONVERSION_ENDPOINTS.map(conv => conv.endpoint);
|
||||||
|
const { endpointStatus, loading, error, refetch } = useMultipleEndpointsEnabled(endpointNames);
|
||||||
|
|
||||||
|
const availableConversions = ALL_CONVERSION_ENDPOINTS.filter(
|
||||||
|
conv => endpointStatus[conv.endpoint] === true
|
||||||
|
);
|
||||||
|
|
||||||
|
const unavailableConversions = ALL_CONVERSION_ENDPOINTS.filter(
|
||||||
|
conv => endpointStatus[conv.endpoint] === false
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableConversions,
|
||||||
|
unavailableConversions,
|
||||||
|
allConversions: ALL_CONVERSION_ENDPOINTS,
|
||||||
|
endpointStatus,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
isConversionAvailable: (endpoint: string) => endpointStatus[endpoint] === true
|
||||||
|
};
|
||||||
|
}
|
132
frontend/src/tests/test-fixtures/README.md
Normal file
132
frontend/src/tests/test-fixtures/README.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Test Fixtures for Convert Tool Testing
|
||||||
|
|
||||||
|
This directory contains sample files for testing the convert tool functionality.
|
||||||
|
|
||||||
|
## Required Test Files
|
||||||
|
|
||||||
|
To run the full test suite, please add the following test files to this directory:
|
||||||
|
|
||||||
|
### 1. sample.pdf
|
||||||
|
- A small PDF document (1-2 pages)
|
||||||
|
- Should contain text and ideally a simple table for CSV conversion testing
|
||||||
|
- Should be under 1MB for fast testing
|
||||||
|
|
||||||
|
### 2. sample.docx
|
||||||
|
- A Microsoft Word document with basic formatting
|
||||||
|
- Should contain headers, paragraphs, and possibly a table
|
||||||
|
- Should be under 500KB
|
||||||
|
|
||||||
|
### 3. sample.png
|
||||||
|
- A small PNG image (e.g., 500x500 pixels)
|
||||||
|
- Should be a real image, not just a test pattern
|
||||||
|
- Should be under 100KB
|
||||||
|
|
||||||
|
### 3b. sample.jpg
|
||||||
|
- A small JPG image (same image as PNG, different format)
|
||||||
|
- Should be under 100KB
|
||||||
|
- Can be created by converting sample.png to JPG
|
||||||
|
|
||||||
|
### 4. sample.md
|
||||||
|
- A Markdown file with various formatting elements:
|
||||||
|
```markdown
|
||||||
|
# Test Document
|
||||||
|
|
||||||
|
This is a **test** markdown file.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Lists
|
||||||
|
- **Bold text**
|
||||||
|
- *Italic text*
|
||||||
|
- [Links](https://example.com)
|
||||||
|
|
||||||
|
### Code Block
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log('Hello, world!');
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column 1 | Column 2 |
|
||||||
|
|----------|----------|
|
||||||
|
| Data 1 | Data 2 |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. sample.eml (Optional)
|
||||||
|
- An email file with headers and body
|
||||||
|
- Can be exported from any email client
|
||||||
|
- Should contain some attachments for testing
|
||||||
|
|
||||||
|
### 6. sample.html (Optional)
|
||||||
|
- A simple HTML file with various elements
|
||||||
|
- Should include text, headings, and basic styling
|
||||||
|
|
||||||
|
|
||||||
|
## File Creation Tips
|
||||||
|
|
||||||
|
### Creating a test PDF:
|
||||||
|
1. Create a document in LibreOffice Writer or Google Docs
|
||||||
|
2. Add some text, headers, and a simple table
|
||||||
|
3. Export/Save as PDF
|
||||||
|
|
||||||
|
### Creating a test DOCX:
|
||||||
|
1. Create a document in Microsoft Word or LibreOffice Writer
|
||||||
|
2. Add formatted content (headers, bold, italic, lists)
|
||||||
|
3. Save as DOCX format
|
||||||
|
|
||||||
|
### Creating a test PNG:
|
||||||
|
1. Use any image editor or screenshot tool
|
||||||
|
2. Create a simple image with text or shapes
|
||||||
|
3. Save as PNG format
|
||||||
|
|
||||||
|
### Creating a test EML:
|
||||||
|
1. In your email client, save an email as .eml format
|
||||||
|
2. Or create manually with proper headers:
|
||||||
|
```
|
||||||
|
From: test@example.com
|
||||||
|
To: recipient@example.com
|
||||||
|
Subject: Test Email
|
||||||
|
Date: Mon, 1 Jan 2024 12:00:00 +0000
|
||||||
|
|
||||||
|
This is a test email for conversion testing.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/src/tests/test-fixtures/
|
||||||
|
├── README.md (this file)
|
||||||
|
├── sample.pdf
|
||||||
|
├── sample.docx
|
||||||
|
├── sample.png
|
||||||
|
├── sample.jpg
|
||||||
|
├── sample.md
|
||||||
|
├── sample.eml (optional)
|
||||||
|
└── sample.html (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Tests
|
||||||
|
|
||||||
|
These files are referenced in the test files:
|
||||||
|
|
||||||
|
- `ConvertE2E.spec.ts` - Uses all files for E2E testing
|
||||||
|
- `ConvertIntegration.test.ts` - Uses files for integration testing
|
||||||
|
- Manual testing scenarios
|
||||||
|
|
||||||
|
## Security Note
|
||||||
|
|
||||||
|
These are test files only and should not contain any sensitive information. They will be committed to the repository and used in automated testing.
|
||||||
|
|
||||||
|
## File Size Guidelines
|
||||||
|
|
||||||
|
- Keep test files small for fast CI/CD pipelines and frontend testing
|
||||||
|
- PDF files: < 1MB (preferably 100-500KB)
|
||||||
|
- Image files: < 100KB
|
||||||
|
- Text files: < 50KB
|
||||||
|
- Focus on frontend functionality, not backend performance
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
When updating the convert tool with new formats:
|
||||||
|
1. Add corresponding test files to this directory
|
||||||
|
2. Update the test files list above
|
||||||
|
3. Update the test cases to include the new formats
|
1
frontend/src/tests/test-fixtures/corrupted.pdf
Normal file
1
frontend/src/tests/test-fixtures/corrupted.pdf
Normal file
@ -0,0 +1 @@
|
|||||||
|
This is not a valid PDF file
|
6
frontend/src/tests/test-fixtures/sample.csv
Normal file
6
frontend/src/tests/test-fixtures/sample.csv
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Name,Age,City,Country
|
||||||
|
John Doe,30,New York,USA
|
||||||
|
Jane Smith,25,London,UK
|
||||||
|
Bob Johnson,35,Toronto,Canada
|
||||||
|
Alice Brown,28,Sydney,Australia
|
||||||
|
Charlie Wilson,42,Berlin,Germany
|
|
10
frontend/src/tests/test-fixtures/sample.doc
Normal file
10
frontend/src/tests/test-fixtures/sample.doc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Test DOC File
|
||||||
|
|
||||||
|
This is a test DOC file for conversion testing.
|
||||||
|
|
||||||
|
Content:
|
||||||
|
- Test bullet point 1
|
||||||
|
- Test bullet point 2
|
||||||
|
- Test bullet point 3
|
||||||
|
|
||||||
|
This file should be sufficient for testing office document conversions.
|
BIN
frontend/src/tests/test-fixtures/sample.docx
Normal file
BIN
frontend/src/tests/test-fixtures/sample.docx
Normal file
Binary file not shown.
105
frontend/src/tests/test-fixtures/sample.eml
Normal file
105
frontend/src/tests/test-fixtures/sample.eml
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
Return-Path: <test@example.com>
|
||||||
|
Delivered-To: recipient@example.com
|
||||||
|
Received: from mail.example.com (mail.example.com [192.168.1.1])
|
||||||
|
by mx.example.com (Postfix) with ESMTP id 1234567890
|
||||||
|
for <recipient@example.com>; Mon, 1 Jan 2024 12:00:00 +0000 (UTC)
|
||||||
|
Message-ID: <test123@example.com>
|
||||||
|
Date: Mon, 1 Jan 2024 12:00:00 +0000
|
||||||
|
From: Test Sender <test@example.com>
|
||||||
|
User-Agent: Mozilla/5.0 (compatible; Test Email Client)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
To: Test Recipient <recipient@example.com>
|
||||||
|
Subject: Test Email for Convert Tool
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="------------boundary123456789"
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
--------------boundary123456789
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Test Email for Convert Tool
|
||||||
|
===========================
|
||||||
|
|
||||||
|
This is a test email for testing the EML to PDF conversion functionality.
|
||||||
|
|
||||||
|
Email Details:
|
||||||
|
- From: test@example.com
|
||||||
|
- To: recipient@example.com
|
||||||
|
- Subject: Test Email for Convert Tool
|
||||||
|
- Date: January 1, 2024
|
||||||
|
|
||||||
|
Content Features:
|
||||||
|
- Plain text content
|
||||||
|
- HTML content (in alternative part)
|
||||||
|
- Headers and metadata
|
||||||
|
- MIME structure
|
||||||
|
|
||||||
|
This email should convert to a PDF that includes:
|
||||||
|
1. Email headers (From, To, Subject, Date)
|
||||||
|
2. Email body content
|
||||||
|
3. Proper formatting
|
||||||
|
|
||||||
|
Important Notes:
|
||||||
|
- This is a test email only
|
||||||
|
- Generated for Stirling PDF testing
|
||||||
|
- Contains no sensitive information
|
||||||
|
- Should preserve email formatting in PDF
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Test Email System
|
||||||
|
|
||||||
|
--------------boundary123456789
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Test Email</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<h1 style="color: #2c3e50;">Test Email for Convert Tool</h1>
|
||||||
|
|
||||||
|
<p>This is a <strong>test email</strong> for testing the EML to PDF conversion functionality.</p>
|
||||||
|
|
||||||
|
<h2 style="color: #34495e;">Email Details:</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>From:</strong> test@example.com</li>
|
||||||
|
<li><strong>To:</strong> recipient@example.com</li>
|
||||||
|
<li><strong>Subject:</strong> Test Email for Convert Tool</li>
|
||||||
|
<li><strong>Date:</strong> January 1, 2024</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 style="color: #34495e;">Content Features:</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Plain text content</li>
|
||||||
|
<li><em>HTML content</em> (this part)</li>
|
||||||
|
<li>Headers and metadata</li>
|
||||||
|
<li>MIME structure</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div style="background-color: #f8f9fa; padding: 15px; border-left: 4px solid #007bff; margin: 20px 0;">
|
||||||
|
<p><strong>This email should convert to a PDF that includes:</strong></p>
|
||||||
|
<ol>
|
||||||
|
<li>Email headers (From, To, Subject, Date)</li>
|
||||||
|
<li>Email body content</li>
|
||||||
|
<li>Proper formatting</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="color: #6c757d;">Important Notes:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>This is a test email only</li>
|
||||||
|
<li>Generated for Stirling PDF testing</li>
|
||||||
|
<li>Contains no sensitive information</li>
|
||||||
|
<li>Should preserve email formatting in PDF</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Best regards,<br>
|
||||||
|
<strong>Test Email System</strong></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--------------boundary123456789--
|
125
frontend/src/tests/test-fixtures/sample.html
Normal file
125
frontend/src/tests/test-fixtures/sample.html
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Test HTML Document</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 40px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #34495e;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.highlight {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Test HTML Document for Convert Tool</h1>
|
||||||
|
|
||||||
|
<p>This is a <strong>test HTML file</strong> for testing the HTML to PDF conversion functionality. It contains various HTML elements to ensure proper conversion.</p>
|
||||||
|
|
||||||
|
<h2>Text Formatting</h2>
|
||||||
|
<p>This paragraph contains <strong>bold text</strong>, <em>italic text</em>, and <code>inline code</code>.</p>
|
||||||
|
|
||||||
|
<div class="highlight">
|
||||||
|
<p><strong>Important:</strong> This is a highlighted section that should be preserved in the PDF output.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Lists</h2>
|
||||||
|
<h3>Unordered List</h3>
|
||||||
|
<ul>
|
||||||
|
<li>First item</li>
|
||||||
|
<li>Second item with <a href="https://example.com">a link</a></li>
|
||||||
|
<li>Third item</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Ordered List</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Primary point</li>
|
||||||
|
<li>Secondary point</li>
|
||||||
|
<li>Tertiary point</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Table</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Column 1</th>
|
||||||
|
<th>Column 2</th>
|
||||||
|
<th>Column 3</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Data A</td>
|
||||||
|
<td>Data B</td>
|
||||||
|
<td>Data C</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Test 1</td>
|
||||||
|
<td>Test 2</td>
|
||||||
|
<td>Test 3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Sample X</td>
|
||||||
|
<td>Sample Y</td>
|
||||||
|
<td>Sample Z</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Code Block</h2>
|
||||||
|
<pre><code>function testFunction() {
|
||||||
|
console.log("This is a test function");
|
||||||
|
return "Hello from HTML to PDF conversion";
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<h2>Final Notes</h2>
|
||||||
|
<p>This HTML document should convert to a well-formatted PDF that preserves:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Text formatting (bold, italic)</li>
|
||||||
|
<li>Headings and hierarchy</li>
|
||||||
|
<li>Tables with proper borders</li>
|
||||||
|
<li>Lists (ordered and unordered)</li>
|
||||||
|
<li>Code formatting</li>
|
||||||
|
<li>Basic CSS styling</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p><small>Generated for Stirling PDF Convert Tool testing purposes.</small></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
frontend/src/tests/test-fixtures/sample.jpg
Normal file
BIN
frontend/src/tests/test-fixtures/sample.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
49
frontend/src/tests/test-fixtures/sample.md
Normal file
49
frontend/src/tests/test-fixtures/sample.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Test Document for Convert Tool
|
||||||
|
|
||||||
|
This is a **test** markdown file for testing the markdown to PDF conversion functionality.
|
||||||
|
|
||||||
|
## Features Being Tested
|
||||||
|
|
||||||
|
- **Bold text**
|
||||||
|
- *Italic text*
|
||||||
|
- [Links](https://example.com)
|
||||||
|
- Lists and formatting
|
||||||
|
|
||||||
|
### Code Block
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log('Hello, world!');
|
||||||
|
function testFunction() {
|
||||||
|
return "This is a test";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Table
|
||||||
|
|
||||||
|
| Column 1 | Column 2 | Column 3 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Data 1 | Data 2 | Data 3 |
|
||||||
|
| Test A | Test B | Test C |
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
### Unordered List
|
||||||
|
- Item 1
|
||||||
|
- Item 2
|
||||||
|
- Nested item
|
||||||
|
- Another nested item
|
||||||
|
- Item 3
|
||||||
|
|
||||||
|
### Ordered List
|
||||||
|
1. First item
|
||||||
|
2. Second item
|
||||||
|
3. Third item
|
||||||
|
|
||||||
|
## Blockquote
|
||||||
|
|
||||||
|
> This is a blockquote for testing purposes.
|
||||||
|
> It should be properly formatted in the PDF output.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This markdown file contains various elements to test the conversion functionality. The PDF output should preserve formatting, tables, code blocks, and other markdown elements.
|
BIN
frontend/src/tests/test-fixtures/sample.pdf
Normal file
BIN
frontend/src/tests/test-fixtures/sample.pdf
Normal file
Binary file not shown.
BIN
frontend/src/tests/test-fixtures/sample.png
Normal file
BIN
frontend/src/tests/test-fixtures/sample.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
12
frontend/src/tests/test-fixtures/sample.pptx
Normal file
12
frontend/src/tests/test-fixtures/sample.pptx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Test PPTX Presentation
|
||||||
|
|
||||||
|
## Slide 1: Title
|
||||||
|
This is a test PowerPoint presentation for conversion testing.
|
||||||
|
|
||||||
|
## Slide 2: Content
|
||||||
|
- Test bullet point 1
|
||||||
|
- Test bullet point 2
|
||||||
|
- Test bullet point 3
|
||||||
|
|
||||||
|
## Slide 3: Conclusion
|
||||||
|
This file should be sufficient for testing presentation conversions.
|
32
frontend/src/tests/test-fixtures/sample.svg
Normal file
32
frontend/src/tests/test-fixtures/sample.svg
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="400" height="300" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="200" y="40" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="bold" fill="#2c3e50">
|
||||||
|
Test Image for Convert Tool
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Shapes for visual content -->
|
||||||
|
<circle cx="100" cy="120" r="30" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
||||||
|
<rect x="180" y="90" width="60" height="60" fill="#e74c3c" stroke="#c0392b" stroke-width="2"/>
|
||||||
|
<polygon points="320,90 350,150 290,150" fill="#f39c12" stroke="#e67e22" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Labels -->
|
||||||
|
<text x="100" y="170" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#7f8c8d">Circle</text>
|
||||||
|
<text x="210" y="170" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#7f8c8d">Square</text>
|
||||||
|
<text x="320" y="170" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#7f8c8d">Triangle</text>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<text x="200" y="210" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#34495e">
|
||||||
|
This image tests conversion functionality
|
||||||
|
</text>
|
||||||
|
<text x="200" y="230" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#95a5a6">
|
||||||
|
PNG/JPG ↔ PDF conversions
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<text x="200" y="270" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#bdc3c7">
|
||||||
|
Generated for Stirling PDF testing - 400x300px
|
||||||
|
</text>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
8
frontend/src/tests/test-fixtures/sample.txt
Normal file
8
frontend/src/tests/test-fixtures/sample.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
This is a test text file for conversion testing.
|
||||||
|
|
||||||
|
It contains multiple lines of text to test various conversion scenarios.
|
||||||
|
Special characters: àáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
|
||||||
|
Numbers: 1234567890
|
||||||
|
Symbols: !@#$%^&*()_+-=[]{}|;':\",./<>?
|
||||||
|
|
||||||
|
This file should be sufficient for testing text-based conversions.
|
6
frontend/src/tests/test-fixtures/sample.xlsx
Normal file
6
frontend/src/tests/test-fixtures/sample.xlsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Name,Age,City,Country,Department,Salary
|
||||||
|
John Doe,30,New York,USA,Engineering,75000
|
||||||
|
Jane Smith,25,London,UK,Marketing,65000
|
||||||
|
Bob Johnson,35,Toronto,Canada,Sales,70000
|
||||||
|
Alice Brown,28,Sydney,Australia,Design,68000
|
||||||
|
Charlie Wilson,42,Berlin,Germany,Operations,72000
|
18
frontend/src/tests/test-fixtures/sample.xml
Normal file
18
frontend/src/tests/test-fixtures/sample.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document>
|
||||||
|
<title>Test Document</title>
|
||||||
|
<content>
|
||||||
|
<section id="1">
|
||||||
|
<heading>Introduction</heading>
|
||||||
|
<paragraph>This is a test XML document for conversion testing.</paragraph>
|
||||||
|
</section>
|
||||||
|
<section id="2">
|
||||||
|
<heading>Data</heading>
|
||||||
|
<data>
|
||||||
|
<item name="test1" value="value1"/>
|
||||||
|
<item name="test2" value="value2"/>
|
||||||
|
<item name="test3" value="value3"/>
|
||||||
|
</data>
|
||||||
|
</section>
|
||||||
|
</content>
|
||||||
|
</document>
|
304
frontend/src/tools/Convert.test.tsx
Normal file
304
frontend/src/tools/Convert.test.tsx
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import i18n from '../i18n/config';
|
||||||
|
import { FileContextProvider } from '../contexts/FileContext';
|
||||||
|
import ConvertSettings from '../components/tools/convert/ConvertSettings';
|
||||||
|
import { useConvertParameters } from '../hooks/tools/convert/useConvertParameters';
|
||||||
|
|
||||||
|
// Mock the hooks
|
||||||
|
vi.mock('../hooks/tools/convert/useConvertParameters');
|
||||||
|
vi.mock('../hooks/useEndpointConfig');
|
||||||
|
|
||||||
|
const mockUseConvertParameters = vi.mocked(useConvertParameters);
|
||||||
|
|
||||||
|
// Mock endpoint availability - based on the real data you provided
|
||||||
|
const mockEndpointStatus = {
|
||||||
|
'file-to-pdf': true,
|
||||||
|
'img-to-pdf': true,
|
||||||
|
'markdown-to-pdf': true,
|
||||||
|
'pdf-to-csv': true,
|
||||||
|
'pdf-to-img': true,
|
||||||
|
'pdf-to-text': true,
|
||||||
|
'eml-to-pdf': false,
|
||||||
|
'html-to-pdf': false,
|
||||||
|
'pdf-to-html': false,
|
||||||
|
'pdf-to-markdown': false,
|
||||||
|
'pdf-to-pdfa': false,
|
||||||
|
'pdf-to-presentation': false,
|
||||||
|
'pdf-to-word': false,
|
||||||
|
'pdf-to-xml': false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock useMultipleEndpointsEnabled
|
||||||
|
vi.mock('../hooks/useEndpointConfig', () => ({
|
||||||
|
useMultipleEndpointsEnabled: () => ({
|
||||||
|
endpointStatus: mockEndpointStatus,
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<MantineProvider>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<FileContextProvider>
|
||||||
|
{children}
|
||||||
|
</FileContextProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Convert Tool Navigation Tests', () => {
|
||||||
|
const mockOnParameterChange = vi.fn();
|
||||||
|
const mockGetAvailableToExtensions = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockUseConvertParameters.mockReturnValue({
|
||||||
|
parameters: {
|
||||||
|
fromExtension: '',
|
||||||
|
toExtension: '',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateParameter: mockOnParameterChange,
|
||||||
|
resetParameters: vi.fn(),
|
||||||
|
validateParameters: vi.fn(() => true),
|
||||||
|
getEndpointName: vi.fn(() => ''),
|
||||||
|
getEndpoint: vi.fn(() => ''),
|
||||||
|
getAvailableToExtensions: mockGetAvailableToExtensions,
|
||||||
|
detectFileExtension: vi.fn()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FROM Dropdown - Endpoint Availability', () => {
|
||||||
|
test('should enable formats with available endpoints', async () => {
|
||||||
|
// Mock available conversions for formats with working endpoints
|
||||||
|
mockGetAvailableToExtensions.mockImplementation((fromExt) => {
|
||||||
|
const mockConversions = {
|
||||||
|
'pdf': [{ value: 'png', label: 'PNG', group: 'Image' }, { value: 'csv', label: 'CSV', group: 'Spreadsheet' }],
|
||||||
|
'docx': [{ value: 'pdf', label: 'PDF', group: 'Document' }],
|
||||||
|
'png': [{ value: 'pdf', label: 'PDF', group: 'Document' }],
|
||||||
|
'md': [{ value: 'pdf', label: 'PDF', group: 'Document' }],
|
||||||
|
'eml': [{ value: 'pdf', label: 'PDF', group: 'Document' }],
|
||||||
|
'html': [{ value: 'pdf', label: 'PDF', group: 'Document' }]
|
||||||
|
};
|
||||||
|
return mockConversions[fromExt] || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={{
|
||||||
|
fromExtension: '',
|
||||||
|
toExtension: '',
|
||||||
|
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||||
|
}}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open FROM dropdown by test id
|
||||||
|
const fromDropdown = screen.getByTestId('convert-from-dropdown');
|
||||||
|
fireEvent.click(fromDropdown);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should enable formats with available endpoints
|
||||||
|
expect(screen.getByTestId('format-option-pdf')).not.toBeDisabled();
|
||||||
|
expect(screen.getByTestId('format-option-docx')).not.toBeDisabled();
|
||||||
|
expect(screen.getByTestId('format-option-png')).not.toBeDisabled();
|
||||||
|
expect(screen.getByTestId('format-option-md')).not.toBeDisabled();
|
||||||
|
|
||||||
|
// Should disable formats without available endpoints
|
||||||
|
const emlButton = screen.getByTestId('format-option-eml');
|
||||||
|
expect(emlButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show correct format groups', async () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={{
|
||||||
|
fromExtension: '',
|
||||||
|
toExtension: '',
|
||||||
|
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||||
|
}}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const fromDropdown = screen.getByTestId('convert-from-dropdown');
|
||||||
|
fireEvent.click(fromDropdown);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Check if format groups are displayed
|
||||||
|
expect(screen.getByText('Document')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Image')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Text')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Email')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TO Dropdown - Available Conversions', () => {
|
||||||
|
test('should show available conversions for PDF', async () => {
|
||||||
|
// Mock PDF conversions
|
||||||
|
mockGetAvailableToExtensions.mockReturnValue([
|
||||||
|
{ value: 'png', label: 'PNG', group: 'Image' },
|
||||||
|
{ value: 'csv', label: 'CSV', group: 'Spreadsheet' },
|
||||||
|
{ value: 'txt', label: 'TXT', group: 'Text' },
|
||||||
|
{ value: 'docx', label: 'DOCX', group: 'Document' },
|
||||||
|
{ value: 'html', label: 'HTML', group: 'Web' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={{
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: '',
|
||||||
|
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||||
|
}}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open TO dropdown
|
||||||
|
const toDropdown = screen.getByTestId('convert-to-dropdown');
|
||||||
|
fireEvent.click(toDropdown);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should enable formats with available endpoints
|
||||||
|
expect(screen.getByTestId('format-option-png')).not.toBeDisabled();
|
||||||
|
expect(screen.getByTestId('format-option-csv')).not.toBeDisabled();
|
||||||
|
expect(screen.getByTestId('format-option-txt')).not.toBeDisabled();
|
||||||
|
|
||||||
|
// Should disable formats without available endpoints
|
||||||
|
expect(screen.getByTestId('format-option-docx')).toBeDisabled(); // pdf-to-word is false
|
||||||
|
expect(screen.getByTestId('format-option-html')).toBeDisabled(); // pdf-to-html is false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show image-specific options when converting to image formats', async () => {
|
||||||
|
mockGetAvailableToExtensions.mockReturnValue([
|
||||||
|
{ value: 'png', label: 'PNG', group: 'Image' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={{
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||||
|
}}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show image conversion settings
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('image-options-section')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('dpi-input')).toHaveValue('300');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show email-specific note for EML conversions', async () => {
|
||||||
|
mockGetAvailableToExtensions.mockReturnValue([
|
||||||
|
{ value: 'pdf', label: 'PDF', group: 'Document' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={{
|
||||||
|
fromExtension: 'eml',
|
||||||
|
toExtension: 'pdf',
|
||||||
|
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||||
|
}}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show EML-specific options
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('eml-options-section')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('eml-options-note')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Conversion Flow Navigation', () => {
|
||||||
|
test('should reset TO extension when FROM extension changes', async () => {
|
||||||
|
mockGetAvailableToExtensions.mockImplementation((fromExt) => {
|
||||||
|
if (fromExt === 'pdf') return [{ value: 'png', label: 'PNG', group: 'Image' }];
|
||||||
|
if (fromExt === 'docx') return [{ value: 'pdf', label: 'PDF', group: 'Document' }];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={{
|
||||||
|
fromExtension: 'pdf',
|
||||||
|
toExtension: 'png',
|
||||||
|
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||||
|
}}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Select a different FROM format
|
||||||
|
const fromDropdown = screen.getByTestId('convert-from-dropdown');
|
||||||
|
fireEvent.click(fromDropdown);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const docxButton = screen.getByTestId('format-option-docx');
|
||||||
|
fireEvent.click(docxButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should reset TO extension
|
||||||
|
expect(mockOnParameterChange).toHaveBeenCalledWith('fromExtension', 'docx');
|
||||||
|
expect(mockOnParameterChange).toHaveBeenCalledWith('toExtension', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show placeholder when no FROM format is selected', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={{
|
||||||
|
fromExtension: '',
|
||||||
|
toExtension: '',
|
||||||
|
imageOptions: { colorType: 'color', dpi: 300, singleOrMultiple: 'multiple' }
|
||||||
|
}}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
getAvailableToExtensions={mockGetAvailableToExtensions}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// TO dropdown should show disabled state
|
||||||
|
expect(screen.getByText('Select a source format first')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -122,6 +122,7 @@ const Convert = ({ selectedFiles = [], onPreviewFile }: ConvertProps) => {
|
|||||||
disabled={!convertParams.validateParameters() || !hasFiles || !endpointEnabled}
|
disabled={!convertParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||||
loadingText={t("convert.converting", "Converting...")}
|
loadingText={t("convert.converting", "Converting...")}
|
||||||
submitText={t("convert.convertFiles", "Convert Files")}
|
submitText={t("convert.convertFiles", "Convert Files")}
|
||||||
|
data-testid="convert-button"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -131,6 +132,7 @@ const Convert = ({ selectedFiles = [], onPreviewFile }: ConvertProps) => {
|
|||||||
<ToolStep
|
<ToolStep
|
||||||
title="Results"
|
title="Results"
|
||||||
isVisible={hasResults}
|
isVisible={hasResults}
|
||||||
|
data-testid="conversion-results"
|
||||||
>
|
>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{convertOperation.status && (
|
{convertOperation.status && (
|
||||||
@ -151,6 +153,7 @@ const Convert = ({ selectedFiles = [], onPreviewFile }: ConvertProps) => {
|
|||||||
color="green"
|
color="green"
|
||||||
fullWidth
|
fullWidth
|
||||||
mb="md"
|
mb="md"
|
||||||
|
data-testid="download-button"
|
||||||
>
|
>
|
||||||
{t("convert.downloadConverted", "Download Converted File")}
|
{t("convert.downloadConverted", "Download Converted File")}
|
||||||
</Button>
|
</Button>
|
||||||
|
336
frontend/src/utils/convertUtils.test.ts
Normal file
336
frontend/src/utils/convertUtils.test.ts
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
expect(getEndpointName('htm', '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('');
|
||||||
|
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');
|
||||||
|
expect(getEndpointUrl('htm', '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('');
|
||||||
|
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(false);
|
||||||
|
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('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
40
frontend/vitest.config.ts
Normal file
40
frontend/vitest.config.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./src/setupTests.ts'],
|
||||||
|
css: false, // Disable CSS processing to speed up tests
|
||||||
|
include: [
|
||||||
|
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'src/**/*.spec.ts', // Exclude Playwright E2E tests
|
||||||
|
'src/tests/test-fixtures/**'
|
||||||
|
],
|
||||||
|
testTimeout: 10000, // 10 second timeout
|
||||||
|
hookTimeout: 10000, // 10 second timeout for setup/teardown
|
||||||
|
coverage: {
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'src/setupTests.ts',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'src/tests/test-fixtures/**',
|
||||||
|
'src/**/*.spec.ts' // Exclude Playwright files from coverage
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
target: 'es2020' // Use older target to avoid warnings
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/src'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user