mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 10:35:22 +00:00
Added testing, csv to pdf
This commit is contained in:
parent
73edf3f08c
commit
4d725947db
@ -6,7 +6,10 @@
|
||||
"Bash(./gradlew:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(npm test)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(ls:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -22,3 +22,6 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.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",
|
||||
"build": "vite build",
|
||||
"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": {
|
||||
"extends": [
|
||||
@ -57,15 +62,19 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"@vitest/coverage-v8": "^1.0.0",
|
||||
"jsdom": "^23.0.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"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)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={onSelect}
|
||||
data-testid="file-card"
|
||||
>
|
||||
<Stack gap={6} align="center">
|
||||
<Box
|
||||
|
@ -81,6 +81,7 @@ const FileThumbnail = ({
|
||||
}
|
||||
}}
|
||||
data-file-id={file.id}
|
||||
data-testid="file-thumbnail"
|
||||
className={`
|
||||
${styles.pageContainer}
|
||||
!rounded-lg
|
||||
@ -119,6 +120,7 @@ const FileThumbnail = ({
|
||||
{selectionMode && (
|
||||
<div
|
||||
className={styles.checkboxContainer}
|
||||
data-testid="file-thumbnail-checkbox"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
|
@ -155,6 +155,7 @@ const FileUploadSelector = ({
|
||||
disabled={disabled || loading}
|
||||
style={{ width: '100%', height: "5rem" }}
|
||||
activateOnClick={true}
|
||||
data-testid="file-dropzone"
|
||||
>
|
||||
<Center>
|
||||
<Stack align="center" gap="sm">
|
||||
@ -192,6 +193,7 @@ const FileUploadSelector = ({
|
||||
accept={accept.join(',')}
|
||||
onChange={handleFileInputChange}
|
||||
style={{ display: 'none' }}
|
||||
data-testid="file-input"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
@ -43,6 +43,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, toolRegistry }: ToolPickerProps
|
||||
filteredTools.map(([id, { icon, name }]) => (
|
||||
<Button
|
||||
key={id}
|
||||
data-testid={`tool-${id}`}
|
||||
variant={selectedToolKey === id ? "filled" : "subtle"}
|
||||
onClick={() => onSelect(id)}
|
||||
size="md"
|
||||
|
@ -18,9 +18,10 @@ const ConvertFromImageSettings = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Stack gap="sm" data-testid="pdf-options-section">
|
||||
<Text size="sm" fw={500}>{t("convert.pdfOptions", "PDF Options")}:</Text>
|
||||
<Select
|
||||
data-testid="color-type-select"
|
||||
label={t("convert.colorType", "Color Type")}
|
||||
value={parameters.imageOptions.colorType}
|
||||
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 ConvertToImageSettings from "./ConvertToImageSettings";
|
||||
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||
import ConvertFromPdfToCsvSettings from "./ConvertFromPdfToCsvSettings";
|
||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||
import {
|
||||
FROM_FORMAT_OPTIONS,
|
||||
@ -101,6 +102,7 @@ const ConvertSettings = ({
|
||||
dpi: 300,
|
||||
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||
});
|
||||
onParameterChange('pageNumbers', 'all');
|
||||
};
|
||||
|
||||
|
||||
@ -112,6 +114,8 @@ const ConvertSettings = ({
|
||||
{t("convert.convertFrom", "Convert from")}:
|
||||
</Text>
|
||||
<GroupedFormatDropdown
|
||||
name="convert-from-dropdown"
|
||||
data-testid="from-format-dropdown"
|
||||
value={parameters.fromExtension}
|
||||
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
|
||||
options={enhancedFromOptions}
|
||||
@ -148,8 +152,10 @@ const ConvertSettings = ({
|
||||
</UnstyledButton>
|
||||
) : (
|
||||
<GroupedFormatDropdown
|
||||
name="convert-to-dropdown"
|
||||
data-testid="to-format-dropdown"
|
||||
value={parameters.toExtension}
|
||||
placeholder={t("convert.targetFormatPlaceholder", "Target format")}
|
||||
placeholder={t("convert.targetFormatPlaceholder", "Target format")}
|
||||
options={enhancedToOptions}
|
||||
onChange={handleToExtensionChange}
|
||||
disabled={disabled}
|
||||
@ -187,14 +193,26 @@ const ConvertSettings = ({
|
||||
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
||||
<>
|
||||
<Divider />
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>{t("convert.emlOptions", "Email Options")}:</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Stack gap="sm" data-testid="eml-options-section">
|
||||
<Text size="sm" fw={500} data-testid="eml-options-title">{t("convert.emlOptions", "Email Options")}:</Text>
|
||||
<Text size="xs" c="dimmed" data-testid="eml-options-note">
|
||||
{t("convert.emlNote", "Email attachments and embedded images will be included in the PDF conversion.")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* CSV specific options */}
|
||||
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'csv' && (
|
||||
<>
|
||||
<Divider />
|
||||
<ConvertFromPdfToCsvSettings
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -18,10 +18,11 @@ const ConvertToImageSettings = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>{t("convert.imageOptions", "Image Options")}:</Text>
|
||||
<Stack gap="sm" data-testid="image-options-section">
|
||||
<Text size="sm" fw={500} data-testid="image-options-title">{t("convert.imageOptions", "Image Options")}:</Text>
|
||||
<Group grow>
|
||||
<Select
|
||||
data-testid="color-type-select"
|
||||
label={t("convert.colorType", "Color Type")}
|
||||
value={parameters.imageOptions.colorType}
|
||||
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||
@ -36,6 +37,7 @@ const ConvertToImageSettings = ({
|
||||
disabled={disabled}
|
||||
/>
|
||||
<NumberInput
|
||||
data-testid="dpi-input"
|
||||
label={t("convert.dpi", "DPI")}
|
||||
value={parameters.imageOptions.dpi}
|
||||
onChange={(val) => typeof val === 'number' && onParameterChange('imageOptions', {
|
||||
@ -49,6 +51,7 @@ const ConvertToImageSettings = ({
|
||||
/>
|
||||
</Group>
|
||||
<Select
|
||||
data-testid="output-type-select"
|
||||
label={t("convert.output", "Output")}
|
||||
value={parameters.imageOptions.singleOrMultiple}
|
||||
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||
|
@ -16,6 +16,7 @@ interface GroupedFormatDropdownProps {
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
minWidth?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const GroupedFormatDropdown = ({
|
||||
@ -24,7 +25,8 @@ const GroupedFormatDropdown = ({
|
||||
options,
|
||||
onChange,
|
||||
disabled = false,
|
||||
minWidth = "18.75rem"
|
||||
minWidth = "18.75rem",
|
||||
name
|
||||
}: GroupedFormatDropdownProps) => {
|
||||
const [dropdownOpened, setDropdownOpened] = useState(false);
|
||||
const theme = useMantineTheme();
|
||||
@ -69,6 +71,8 @@ const GroupedFormatDropdown = ({
|
||||
>
|
||||
<Popover.Target>
|
||||
<UnstyledButton
|
||||
name={name}
|
||||
data-testid={name}
|
||||
onClick={() => setDropdownOpened(!dropdownOpened)}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
@ -122,6 +126,7 @@ const GroupedFormatDropdown = ({
|
||||
{groupOptions.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
data-testid={`format-option-${option.value}`}
|
||||
variant={value === option.value ? "filled" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handleOptionSelect(option.value)}
|
||||
|
@ -13,6 +13,7 @@ export interface OperationButtonProps {
|
||||
fullWidth?: boolean;
|
||||
mt?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
const OperationButton = ({
|
||||
@ -25,7 +26,8 @@ const OperationButton = ({
|
||||
color = 'blue',
|
||||
fullWidth = true,
|
||||
mt = 'md',
|
||||
type = 'button'
|
||||
type = 'button',
|
||||
'data-testid': dataTestId
|
||||
}: OperationButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -39,6 +41,7 @@ const OperationButton = ({
|
||||
disabled={disabled}
|
||||
variant={variant}
|
||||
color={color}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{isLoading
|
||||
? (loadingText || t("loading", "Loading..."))
|
||||
|
@ -37,28 +37,29 @@ const ResultsPreview = ({
|
||||
}
|
||||
|
||||
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 && (
|
||||
<Text fw={500} size="md" mb="sm">
|
||||
<Text fw={500} size="md" mb="sm" data-testid="results-preview-title">
|
||||
{title} ({files.length} files)
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{isGeneratingThumbnails ? (
|
||||
<Center p="lg">
|
||||
<Center p="lg" data-testid="results-preview-loading">
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader size="sm" />
|
||||
<Text size="sm" c="dimmed">{loadingMessage}</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<Grid>
|
||||
<Grid data-testid="results-preview-grid">
|
||||
{files.map((result, index) => (
|
||||
<Grid.Col span={{ base: 6, sm: 4, md: 3 }} key={index}>
|
||||
<Paper
|
||||
p="xs"
|
||||
withBorder
|
||||
onClick={() => onFileClick?.(result.file)}
|
||||
data-testid={`results-preview-thumbnail-${index}`}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
height: '10rem',
|
||||
|
@ -81,6 +81,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
||||
formData.append("fitOption", "fillPage");
|
||||
formData.append("colorType", imageOptions.colorType);
|
||||
formData.append("autoRotate", "true");
|
||||
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
||||
// CSV extraction requires page numbers parameter
|
||||
formData.append("pageNumbers", parameters.pageNumbers || "all");
|
||||
}
|
||||
|
||||
return formData;
|
||||
@ -104,6 +107,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
||||
parameters: {
|
||||
fromExtension: parameters.fromExtension,
|
||||
toExtension: parameters.toExtension,
|
||||
pageNumbers: parameters.pageNumbers,
|
||||
imageOptions: parameters.imageOptions,
|
||||
},
|
||||
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 {
|
||||
fromExtension: string;
|
||||
toExtension: string;
|
||||
pageNumbers: string;
|
||||
imageOptions: {
|
||||
colorType: ColorType;
|
||||
dpi: number;
|
||||
@ -33,6 +34,7 @@ export interface ConvertParametersHook {
|
||||
const initialParameters: ConvertParameters = {
|
||||
fromExtension: '',
|
||||
toExtension: '',
|
||||
pageNumbers: 'all',
|
||||
imageOptions: {
|
||||
colorType: COLOR_TYPES.COLOR,
|
||||
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}
|
||||
loadingText={t("convert.converting", "Converting...")}
|
||||
submitText={t("convert.convertFiles", "Convert Files")}
|
||||
data-testid="convert-button"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@ -131,6 +132,7 @@ const Convert = ({ selectedFiles = [], onPreviewFile }: ConvertProps) => {
|
||||
<ToolStep
|
||||
title="Results"
|
||||
isVisible={hasResults}
|
||||
data-testid="conversion-results"
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{convertOperation.status && (
|
||||
@ -151,6 +153,7 @@ const Convert = ({ selectedFiles = [], onPreviewFile }: ConvertProps) => {
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
data-testid="download-button"
|
||||
>
|
||||
{t("convert.downloadConverted", "Download Converted File")}
|
||||
</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