Compare commits

..

No commits in common. "7679ad3ec64d3cb01404215b5edaeee8d3f3db20" and "7627708fb05900a17094336ca2a7ca443b640182" have entirely different histories.

28 changed files with 158 additions and 1275 deletions

View File

@ -1605,7 +1605,7 @@
"wordsToRedact": { "wordsToRedact": {
"title": "Words to Redact", "title": "Words to Redact",
"placeholder": "Enter a word", "placeholder": "Enter a word",
"add": "Add", "addAnother": "Add Another",
"examples": "Examples: Confidential, Top-Secret" "examples": "Examples: Confidential, Top-Secret"
}, },
"useRegexLabel": "Use Regex", "useRegexLabel": "Use Regex",
@ -1620,7 +1620,7 @@
}, },
"automatic": { "automatic": {
"title": "Automatic Redaction", "title": "Automatic Redaction",
"text": "Automatically finds and redacts specified text throughout the document. Perfect for removing consistent sensitive information like names, addresses, or confidential markers." "text": "Automatically finds and redacts specified text throughout the document. Perfect for removing consistent sensitive information like names, SSNs, or confidential markers."
}, },
"manual": { "manual": {
"title": "Manual Redaction", "title": "Manual Redaction",
@ -1640,7 +1640,7 @@
"bullet3": "Click × to remove words", "bullet3": "Click × to remove words",
"examples": { "examples": {
"title": "Common Examples", "title": "Common Examples",
"text": "Typical words to redact include: bank details, email addresses, or specific names." "text": "Typical words to redact include: 'Confidential', 'SSN:', phone numbers, email addresses, or specific names."
} }
}, },
"advanced": { "advanced": {
@ -1648,8 +1648,8 @@
"title": "Advanced Redaction Settings" "title": "Advanced Redaction Settings"
}, },
"color": { "color": {
"title": "Box Colour & Padding", "title": "Box Color & Padding",
"text": "Customise the appearance of redaction boxes. Black is standard, but you can choose any colour. Padding adds extra space around the found text." "text": "Customize the appearance of redaction boxes. Black is standard, but you can choose any color. Padding adds extra space around the found text."
}, },
"regex": { "regex": {
"title": "Use Regex", "title": "Use Regex",
@ -1669,7 +1669,7 @@
}, },
"manual": { "manual": {
"header": "Manual Redaction", "header": "Manual Redaction",
"textBasedRedaction": "Text-based Redaction", "textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction", "pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)", "convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
"pageRedactionNumbers": { "pageRedactionNumbers": {
@ -1677,7 +1677,7 @@
"placeholder": "(e.g. 1,2,8 or 4,7,12-16 or 2n-1)" "placeholder": "(e.g. 1,2,8 or 4,7,12-16 or 2n-1)"
}, },
"redactionColor": { "redactionColor": {
"title": "Redaction Colour" "title": "Redaction Color"
}, },
"export": "Export", "export": "Export",
"upload": "Upload", "upload": "Upload",
@ -1690,7 +1690,7 @@
"toggleSidebar": "Toggle Sidebar", "toggleSidebar": "Toggle Sidebar",
"showThumbnails": "Show Thumbnails", "showThumbnails": "Show Thumbnails",
"showDocumentOutline": "Show Document Outline (double-click to expand/collapse all items)", "showDocumentOutline": "Show Document Outline (double-click to expand/collapse all items)",
"showAttachments": "Show Attachments", "showAttatchments": "Show Attachments",
"showLayers": "Show Layers (double-click to reset all layers to the default state)", "showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker", "colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item", "findCurrentOutlineItem": "Find current outline item",
@ -1906,11 +1906,6 @@
"title": "Compress", "title": "Compress",
"desc": "Compress PDFs to reduce their file size.", "desc": "Compress PDFs to reduce their file size.",
"header": "Compress PDF", "header": "Compress PDF",
"method": {
"title": "Compression Method",
"quality": "Quality",
"filesize": "File Size"
},
"credit": "This service uses qpdf for PDF Compress/Optimisation.", "credit": "This service uses qpdf for PDF Compress/Optimisation.",
"grayscale": { "grayscale": {
"label": "Apply Grayscale for Compression" "label": "Apply Grayscale for Compression"

View File

@ -385,13 +385,6 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT" "moduleLicenseUrl": "https://opensource.org/licenses/MIT"
}, },
{
"moduleName": "@posthog/core",
"moduleUrl": "https://github.com/PostHog/posthog-js",
"moduleVersion": "1.0.2",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{ {
"moduleName": "@tailwindcss/node", "moduleName": "@tailwindcss/node",
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss", "moduleUrl": "https://github.com/tailwindlabs/tailwindcss",
@ -749,13 +742,6 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT" "moduleLicenseUrl": "https://opensource.org/licenses/MIT"
}, },
{
"moduleName": "core-js",
"moduleUrl": "https://github.com/zloirock/core-js",
"moduleVersion": "3.45.1",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{ {
"moduleName": "core-util-is", "moduleName": "core-util-is",
"moduleUrl": "https://github.com/isaacs/core-util-is", "moduleUrl": "https://github.com/isaacs/core-util-is",
@ -938,13 +924,6 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT" "moduleLicenseUrl": "https://opensource.org/licenses/MIT"
}, },
{
"moduleName": "fflate",
"moduleUrl": "https://github.com/101arrowz/fflate",
"moduleVersion": "0.4.8",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{ {
"moduleName": "file-selector", "moduleName": "file-selector",
"moduleUrl": "https://github.com/react-dropzone/file-selector", "moduleUrl": "https://github.com/react-dropzone/file-selector",
@ -1554,20 +1533,6 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT" "moduleLicenseUrl": "https://opensource.org/licenses/MIT"
}, },
{
"moduleName": "posthog-js",
"moduleUrl": "https://github.com/PostHog/posthog-js",
"moduleVersion": "1.261.0",
"moduleLicense": "MIT*",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "preact",
"moduleUrl": "https://github.com/preactjs/preact",
"moduleVersion": "10.27.1",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{ {
"moduleName": "pretty-format", "moduleName": "pretty-format",
"moduleUrl": "https://github.com/facebook/jest", "moduleUrl": "https://github.com/facebook/jest",
@ -1963,7 +1928,7 @@
{ {
"moduleName": "typescript", "moduleName": "typescript",
"moduleUrl": "https://github.com/microsoft/TypeScript", "moduleUrl": "https://github.com/microsoft/TypeScript",
"moduleVersion": "5.9.2", "moduleVersion": "5.8.3",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
@ -2030,13 +1995,6 @@
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{
"moduleName": "web-vitals",
"moduleUrl": "https://github.com/GoogleChrome/web-vitals",
"moduleVersion": "4.2.4",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{ {
"moduleName": "webidl-conversions", "moduleName": "webidl-conversions",
"moduleUrl": "https://github.com/jsdom/webidl-conversions", "moduleUrl": "https://github.com/jsdom/webidl-conversions",

View File

@ -1,185 +0,0 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import ButtonSelector from './ButtonSelector';
// Wrapper component to provide Mantine context
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider>{children}</MantineProvider>
);
describe('ButtonSelector', () => {
const mockOnChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
test('should render all options as buttons', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
expect(screen.getByText('Option 1')).toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
});
test('should highlight selected button with filled variant', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
const selectedButton = screen.getByRole('button', { name: 'Option 1' });
const unselectedButton = screen.getByRole('button', { name: 'Option 2' });
// Check data-variant attribute for filled/outline
expect(selectedButton).toHaveAttribute('data-variant', 'filled');
expect(unselectedButton).toHaveAttribute('data-variant', 'outline');
});
test('should call onChange when button is clicked', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
expect(mockOnChange).toHaveBeenCalledWith('option2');
});
test('should handle undefined value (no selection)', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value={undefined}
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
// Both buttons should be outlined when no value is selected
const button1 = screen.getByRole('button', { name: 'Option 1' });
const button2 = screen.getByRole('button', { name: 'Option 2' });
expect(button1).toHaveAttribute('data-variant', 'outline');
expect(button2).toHaveAttribute('data-variant', 'outline');
});
test.each([
{
description: 'disable buttons when disabled prop is true',
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
],
globalDisabled: true,
expectedStates: [true, true],
},
{
description: 'disable individual options when option.disabled is true',
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2', disabled: true },
],
globalDisabled: false,
expectedStates: [false, true],
},
])('should $description', ({ options, globalDisabled, expectedStates }) => {
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
disabled={globalDisabled}
/>
</TestWrapper>
);
options.forEach((option, index) => {
const button = screen.getByRole('button', { name: option.label });
expect(button).toHaveProperty('disabled', expectedStates[index]);
});
});
test('should not call onChange when disabled button is clicked', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2', disabled: true },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
expect(mockOnChange).not.toHaveBeenCalled();
});
test('should not apply fullWidth styling when fullWidth is false', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
fullWidth={false}
/>
</TestWrapper>
);
const button = screen.getByRole('button', { name: 'Option 1' });
expect(button).not.toHaveStyle({ flex: '1' });
});
});

View File

@ -1,49 +0,0 @@
import { Button } from "@mantine/core";
export interface ButtonOption<T> {
value: T;
label: string;
disabled?: boolean;
}
interface ButtonSelectorProps<T> {
value: T | undefined;
onChange: (value: T) => void;
options: ButtonOption<T>[];
disabled?: boolean;
fullWidth?: boolean;
}
const ButtonSelector = <T extends string>({
value,
onChange,
options,
disabled = false,
fullWidth = true,
}: ButtonSelectorProps<T>) => {
return (
<div style={{ display: 'flex', gap: '4px' }}>
{options.map((option) => (
<Button
key={option.value}
variant={value === option.value ? 'filled' : 'outline'}
color={value === option.value ? 'blue' : 'var(--text-muted)'}
onClick={() => onChange(option.value)}
disabled={disabled || option.disabled}
style={{
flex: fullWidth ? 1 : undefined,
height: 'auto',
minHeight: '40px',
fontSize: '11px'
}}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{option.label}
</div>
</Button>
))}
</div>
);
};
export default ButtonSelector;

View File

@ -4,7 +4,7 @@ import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPa
interface AddPasswordSettingsProps { interface AddPasswordSettingsProps {
parameters: AddPasswordParameters; parameters: AddPasswordParameters;
onParameterChange: <K extends keyof AddPasswordParameters>(key: K, value: AddPasswordParameters[K]) => void; onParameterChange: (key: keyof AddPasswordParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -1,6 +1,5 @@
import { Stack } from "@mantine/core"; import { Button, Stack } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ButtonSelector from "../../shared/ButtonSelector";
interface WatermarkTypeSettingsProps { interface WatermarkTypeSettingsProps {
watermarkType?: 'text' | 'image'; watermarkType?: 'text' | 'image';
@ -11,25 +10,32 @@ interface WatermarkTypeSettingsProps {
const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => { const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const options = [
{
value: 'text' as const,
label: t('watermark.watermarkType.text', 'Text')
},
{
value: 'image' as const,
label: t('watermark.watermarkType.image', 'Image')
}
];
return ( return (
<Stack gap="sm"> <Stack gap="sm">
<ButtonSelector <div style={{ display: 'flex', gap: '4px' }}>
value={watermarkType} <Button
onChange={onWatermarkTypeChange} variant={watermarkType === 'text' ? 'filled' : 'outline'}
options={options} color={watermarkType === 'text' ? 'blue' : 'var(--text-muted)'}
disabled={disabled} onClick={() => onWatermarkTypeChange('text')}
/> disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('watermark.watermarkType.text', 'Text')}
</div>
</Button>
<Button
variant={watermarkType === 'image' ? 'filled' : 'outline'}
color={watermarkType === 'image' ? 'blue' : 'var(--text-muted)'}
onClick={() => onWatermarkTypeChange('image')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('watermark.watermarkType.image', 'Image')}
</div>
</Button>
</div>
</Stack> </Stack>
); );
}; };

View File

@ -4,7 +4,7 @@ import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissi
interface ChangePermissionsSettingsProps { interface ChangePermissionsSettingsProps {
parameters: ChangePermissionsParameters; parameters: ChangePermissionsParameters;
onParameterChange: <K extends keyof ChangePermissionsParameters>(key: K, value: ChangePermissionsParameters[K]) => void; onParameterChange: (key: keyof ChangePermissionsParameters, value: boolean) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -1,12 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Stack, Text, NumberInput, Select, Divider } from "@mantine/core"; import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters"; import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters";
import ButtonSelector from "../../shared/ButtonSelector";
interface CompressSettingsProps { interface CompressSettingsProps {
parameters: CompressParameters; parameters: CompressParameters;
onParameterChange: <K extends keyof CompressParameters>(key: K, value: CompressParameters[K]) => void; onParameterChange: (key: keyof CompressParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }
@ -20,16 +19,31 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C
<Divider ml='-md'></Divider> <Divider ml='-md'></Divider>
{/* Compression Method */} {/* Compression Method */}
<Stack gap="sm"> <Stack gap="sm">
<Text size="sm" fw={500}>{t('compress.method.title', 'Compression Method')}</Text> <Text size="sm" fw={500}>Compression Method</Text>
<ButtonSelector <div style={{ display: 'flex', gap: '4px' }}>
value={parameters.compressionMethod} <Button
onChange={(value) => onParameterChange('compressionMethod', value)} variant={parameters.compressionMethod === 'quality' ? 'filled' : 'outline'}
options={[ color={parameters.compressionMethod === 'quality' ? 'blue' : 'var(--text-muted)'}
{ value: 'quality', label: t('compress.method.quality', 'Quality') }, onClick={() => onParameterChange('compressionMethod', 'quality')}
{ value: 'filesize', label: t('compress.method.filesize', 'File Size') }, disabled={disabled}
]} style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
disabled={disabled} >
/> <div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
Quality
</div>
</Button>
<Button
variant={parameters.compressionMethod === 'filesize' ? 'filled' : 'outline'}
color={parameters.compressionMethod === 'filesize' ? 'blue' : 'var(--text-muted)'}
onClick={() => onParameterChange('compressionMethod', 'filesize')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
File Size
</div>
</Button>
</div>
</Stack> </Stack>
{/* Quality Adjustment */} {/* Quality Adjustment */}

View File

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

View File

@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
interface ConvertFromImageSettingsProps { interface ConvertFromImageSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void; onParameterChange: (key: keyof ConvertParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -5,28 +5,28 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame
interface ConvertFromWebSettingsProps { interface ConvertFromWebSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void; onParameterChange: (key: keyof ConvertParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }
const ConvertFromWebSettings = ({ const ConvertFromWebSettings = ({
parameters, parameters,
onParameterChange, onParameterChange,
disabled = false disabled = false
}: ConvertFromWebSettingsProps) => { }: ConvertFromWebSettingsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Stack gap="sm" data-testid="web-settings"> <Stack gap="sm" data-testid="web-settings">
<Text size="sm" fw={500}>{t("convert.webOptions", "Web to PDF Options")}:</Text> <Text size="sm" fw={500}>{t("convert.webOptions", "Web to PDF Options")}:</Text>
<Stack gap="xs"> <Stack gap="xs">
<Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text> <Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text>
<NumberInput <NumberInput
value={parameters.htmlOptions.zoomLevel} value={parameters.htmlOptions.zoomLevel}
onChange={(value) => onParameterChange('htmlOptions', { onChange={(value) => onParameterChange('htmlOptions', {
...parameters.htmlOptions, ...parameters.htmlOptions,
zoomLevel: Number(value) || 1.0 zoomLevel: Number(value) || 1.0
})} })}
min={0.1} min={0.1}
max={3.0} max={3.0}
@ -36,9 +36,9 @@ const ConvertFromWebSettings = ({
/> />
<Slider <Slider
value={parameters.htmlOptions.zoomLevel} value={parameters.htmlOptions.zoomLevel}
onChange={(value) => onParameterChange('htmlOptions', { onChange={(value) => onParameterChange('htmlOptions', {
...parameters.htmlOptions, ...parameters.htmlOptions,
zoomLevel: value zoomLevel: value
})} })}
min={0.1} min={0.1}
max={3.0} max={3.0}
@ -51,4 +51,4 @@ const ConvertFromWebSettings = ({
); );
}; };
export default ConvertFromWebSettings; export default ConvertFromWebSettings;

View File

@ -26,7 +26,7 @@ import { StirlingFile } from "../../../types/fileContext";
interface ConvertSettingsProps { interface ConvertSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void; onParameterChange: (key: keyof ConvertParameters, value: any) => void;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
selectedFiles: StirlingFile[]; selectedFiles: StirlingFile[];
disabled?: boolean; disabled?: boolean;

View File

@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
interface ConvertToImageSettingsProps { interface ConvertToImageSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void; onParameterChange: (key: keyof ConvertParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -7,16 +7,16 @@ import { StirlingFile } from '../../../types/fileContext';
interface ConvertToPdfaSettingsProps { interface ConvertToPdfaSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void; onParameterChange: (key: keyof ConvertParameters, value: any) => void;
selectedFiles: StirlingFile[]; selectedFiles: StirlingFile[];
disabled?: boolean; disabled?: boolean;
} }
const ConvertToPdfaSettings = ({ const ConvertToPdfaSettings = ({
parameters, parameters,
onParameterChange, onParameterChange,
selectedFiles, selectedFiles,
disabled = false disabled = false
}: ConvertToPdfaSettingsProps) => { }: ConvertToPdfaSettingsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { hasDigitalSignatures, isChecking } = usePdfSignatureDetection(selectedFiles); const { hasDigitalSignatures, isChecking } = usePdfSignatureDetection(selectedFiles);
@ -29,7 +29,7 @@ const ConvertToPdfaSettings = ({
return ( return (
<Stack gap="sm" data-testid="pdfa-settings"> <Stack gap="sm" data-testid="pdfa-settings">
<Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text> <Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text>
{hasDigitalSignatures && ( {hasDigitalSignatures && (
<Alert color="yellow"> <Alert color="yellow">
<Text size="sm"> <Text size="sm">
@ -37,14 +37,14 @@ const ConvertToPdfaSettings = ({
</Text> </Text>
</Alert> </Alert>
)} )}
<Stack gap="xs"> <Stack gap="xs">
<Text size="xs" fw={500}>{t("convert.outputFormat", "Output Format")}:</Text> <Text size="xs" fw={500}>{t("convert.outputFormat", "Output Format")}:</Text>
<Select <Select
value={parameters.pdfaOptions.outputFormat} value={parameters.pdfaOptions.outputFormat}
onChange={(value) => onParameterChange('pdfaOptions', { onChange={(value) => onParameterChange('pdfaOptions', {
...parameters.pdfaOptions, ...parameters.pdfaOptions,
outputFormat: value || 'pdfa-1' outputFormat: value || 'pdfa-1'
})} })}
data={pdfaFormatOptions} data={pdfaFormatOptions}
disabled={disabled || isChecking} disabled={disabled || isChecking}
@ -58,4 +58,4 @@ const ConvertToPdfaSettings = ({
); );
}; };
export default ConvertToPdfaSettings; export default ConvertToPdfaSettings;

View File

@ -16,7 +16,7 @@ interface AdvancedOption {
interface AdvancedOCRSettingsProps { interface AdvancedOCRSettingsProps {
advancedOptions: string[]; advancedOptions: string[];
ocrRenderType?: string; ocrRenderType?: string;
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void; onParameterChange: (key: keyof OCRParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }
@ -40,7 +40,7 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
// Handle individual checkbox changes // Handle individual checkbox changes
const handleCheckboxChange = (optionValue: string, checked: boolean) => { const handleCheckboxChange = (optionValue: string, checked: boolean) => {
const option = advancedOptionsData.find(opt => opt.value === optionValue); const option = advancedOptionsData.find(opt => opt.value === optionValue);
if (option?.isSpecial) { if (option?.isSpecial) {
// Handle special options (like compatibility mode) differently // Handle special options (like compatibility mode) differently
if (optionValue === 'compatibilityMode') { if (optionValue === 'compatibilityMode') {
@ -69,7 +69,7 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
<Text size="sm" fw={500} mb="md"> <Text size="sm" fw={500} mb="md">
{t('ocr.settings.advancedOptions.label', 'Processing Options')} {t('ocr.settings.advancedOptions.label', 'Processing Options')}
</Text> </Text>
<Stack gap="sm"> <Stack gap="sm">
{advancedOptionsData.map((option) => ( {advancedOptionsData.map((option) => (
<Checkbox <Checkbox
@ -87,4 +87,4 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
); );
}; };
export default AdvancedOCRSettings; export default AdvancedOCRSettings;

View File

@ -6,7 +6,7 @@ import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
interface OCRSettingsProps { interface OCRSettingsProps {
parameters: OCRParameters; parameters: OCRParameters;
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void; onParameterChange: (key: keyof OCRParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -1,211 +0,0 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import RedactAdvancedSettings from './RedactAdvancedSettings';
import { defaultParameters } from '../../../hooks/tools/redact/useRedactParameters';
// Mock useTranslation
const mockT = vi.fn((_key: string, fallback: string) => fallback);
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: mockT })
}));
// Wrapper component to provide Mantine context
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider>{children}</MantineProvider>
);
describe('RedactAdvancedSettings', () => {
const mockOnParameterChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
test('should render all advanced settings controls', () => {
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
expect(screen.getByText('Box Colour')).toBeInTheDocument();
expect(screen.getByText('Custom Extra Padding')).toBeInTheDocument();
expect(screen.getByText('Use Regex')).toBeInTheDocument();
expect(screen.getByText('Whole Word Search')).toBeInTheDocument();
expect(screen.getByText('Convert PDF to PDF-Image (Used to remove text behind the box)')).toBeInTheDocument();
});
test('should display current parameter values', () => {
const customParameters = {
...defaultParameters,
redactColor: '#FF0000',
customPadding: 0.5,
useRegex: true,
wholeWordSearch: true,
convertPDFToImage: false,
};
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={customParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Check color input value
const colorInput = screen.getByDisplayValue('#FF0000');
expect(colorInput).toBeInTheDocument();
// Check number input value
const paddingInput = screen.getByDisplayValue('0.5');
expect(paddingInput).toBeInTheDocument();
// Check checkbox states
const useRegexCheckbox = screen.getByLabelText('Use Regex');
const wholeWordCheckbox = screen.getByLabelText('Whole Word Search');
const convertCheckbox = screen.getByLabelText('Convert PDF to PDF-Image (Used to remove text behind the box)');
expect(useRegexCheckbox).toBeChecked();
expect(wholeWordCheckbox).toBeChecked();
expect(convertCheckbox).not.toBeChecked();
});
test('should call onParameterChange when color is changed', () => {
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
const colorInput = screen.getByDisplayValue('#000000');
fireEvent.change(colorInput, { target: { value: '#FF0000' } });
expect(mockOnParameterChange).toHaveBeenCalledWith('redactColor', '#FF0000');
});
test('should call onParameterChange when padding is changed', () => {
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
const paddingInput = screen.getByDisplayValue('0.1');
fireEvent.change(paddingInput, { target: { value: '0.5' } });
expect(mockOnParameterChange).toHaveBeenCalledWith('customPadding', 0.5);
});
test('should handle invalid padding values', () => {
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
const paddingInput = screen.getByDisplayValue('0.1');
// Simulate NumberInput onChange with invalid value (empty string)
const numberInput = paddingInput.closest('.mantine-NumberInput-root');
if (numberInput) {
// Find the input and trigger change with empty value
fireEvent.change(paddingInput, { target: { value: '' } });
// The component should default to 0.1 for invalid values
expect(mockOnParameterChange).toHaveBeenCalledWith('customPadding', 0.1);
}
});
test.each([
{
paramName: 'useRegex' as const,
label: 'Use Regex',
initialValue: false,
expectedValue: true,
},
{
paramName: 'wholeWordSearch' as const,
label: 'Whole Word Search',
initialValue: false,
expectedValue: true,
},
{
paramName: 'convertPDFToImage' as const,
label: 'Convert PDF to PDF-Image (Used to remove text behind the box)',
initialValue: true,
expectedValue: false,
},
])('should call onParameterChange when $paramName checkbox is toggled', ({ paramName, label, initialValue, expectedValue }) => {
const customParameters = {
...defaultParameters,
[paramName]: initialValue,
};
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={customParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
const checkbox = screen.getByLabelText(label);
fireEvent.click(checkbox);
expect(mockOnParameterChange).toHaveBeenCalledWith(paramName, expectedValue);
});
test.each([
{ controlType: 'color input', getValue: () => screen.getByDisplayValue('#000000') },
{ controlType: 'padding input', getValue: () => screen.getByDisplayValue('0.1') },
{ controlType: 'useRegex checkbox', getValue: () => screen.getByLabelText('Use Regex') },
{ controlType: 'wholeWordSearch checkbox', getValue: () => screen.getByLabelText('Whole Word Search') },
{ controlType: 'convertPDFToImage checkbox', getValue: () => screen.getByLabelText('Convert PDF to PDF-Image (Used to remove text behind the box)') },
])('should disable $controlType when disabled prop is true', ({ getValue }) => {
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
disabled={true}
/>
</TestWrapper>
);
const control = getValue();
expect(control).toBeDisabled();
});
test('should have correct padding input constraints', () => {
render(
<TestWrapper>
<RedactAdvancedSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// NumberInput in Mantine might not expose these attributes directly on the input element
// Instead, check that the NumberInput component is rendered with correct placeholder
const paddingInput = screen.getByPlaceholderText('0.1');
expect(paddingInput).toBeInTheDocument();
expect(paddingInput).toHaveDisplayValue('0.1');
});
});

View File

@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Stack, Text } from '@mantine/core'; import { Button, Stack, Text } from '@mantine/core';
import { RedactMode } from '../../../hooks/tools/redact/useRedactParameters'; import { RedactMode } from '../../../hooks/tools/redact/useRedactParameters';
import ButtonSelector from '../../shared/ButtonSelector';
interface RedactModeSelectorProps { interface RedactModeSelectorProps {
mode: RedactMode; mode: RedactMode;
@ -12,30 +11,36 @@ interface RedactModeSelectorProps {
export default function RedactModeSelector({ mode, onModeChange, disabled }: RedactModeSelectorProps) { export default function RedactModeSelector({ mode, onModeChange, disabled }: RedactModeSelectorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const options = [
{
value: 'automatic' as const,
label: t('redact.modeSelector.automatic', 'Automatic')
},
{
value: 'manual' as const,
label: t('redact.modeSelector.manual', 'Manual'),
disabled: true // Keep manual mode disabled until implemented
}
];
return ( return (
<Stack gap="sm"> <Stack gap="sm">
<Text size="sm" fw={600}> <Text size="sm" fw={600}>
{t('redact.modeSelector.mode', 'Mode')} {t('redact.modeSelector.mode', 'Mode')}
</Text> </Text>
<ButtonSelector <div style={{ display: 'flex', gap: '4px' }}>
value={mode} <Button
onChange={onModeChange} variant={mode === 'automatic' ? 'filled' : 'outline'}
options={options} color={mode === 'automatic' ? 'blue' : 'var(--text-muted)'}
disabled={disabled} onClick={() => onModeChange('automatic')}
/> disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('redact.modeSelector.automatic', 'Automatic')}
</div>
</Button>
<Button
variant={mode === 'manual' ? 'filled' : 'outline'}
color={mode === 'manual' ? 'blue' : 'var(--text-muted)'}
onClick={() => onModeChange('manual')}
disabled={disabled || true} // Keep manual disabled until implemented
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('redact.modeSelector.manual', 'Manual')}
</div>
</Button>
</div>
</Stack> </Stack>
); );
} }

View File

@ -1,183 +0,0 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import RedactSingleStepSettings from './RedactSingleStepSettings';
import { defaultParameters } from '../../../hooks/tools/redact/useRedactParameters';
// Mock useTranslation
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: vi.fn((_key: string, fallback: string) => fallback) })
}));
// Wrapper component to provide Mantine context
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider>{children}</MantineProvider>
);
describe('RedactSingleStepSettings', () => {
const mockOnParameterChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
test('should render mode selector', () => {
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
expect(screen.getByText('Mode')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Automatic' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Manual' })).toBeInTheDocument();
});
test('should render automatic mode settings when mode is automatic', () => {
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Default mode is automatic, so these should be visible
expect(screen.getByText('Words to Redact')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Enter a word')).toBeInTheDocument();
expect(screen.getByText('Box Colour')).toBeInTheDocument();
expect(screen.getByText('Use Regex')).toBeInTheDocument();
});
test('should render manual mode settings when mode is manual', () => {
const manualParameters = {
...defaultParameters,
mode: 'manual' as const,
};
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={manualParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Manual mode should show placeholder text
expect(screen.getByText('Manual redaction interface will be available here when implemented.')).toBeInTheDocument();
// Automatic mode settings should not be visible
expect(screen.queryByText('Words to Redact')).not.toBeInTheDocument();
});
test('should pass through parameter changes from automatic settings', () => {
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Test adding a word
const input = screen.getByPlaceholderText('Enter a word');
const addButton = screen.getByRole('button', { name: '+ Add' });
fireEvent.change(input, { target: { value: 'TestWord' } });
fireEvent.click(addButton);
expect(mockOnParameterChange).toHaveBeenCalledWith('wordsToRedact', ['TestWord']);
});
test('should pass through parameter changes from advanced settings', () => {
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Test changing color
const colorInput = screen.getByDisplayValue('#000000');
fireEvent.change(colorInput, { target: { value: '#FF0000' } });
expect(mockOnParameterChange).toHaveBeenCalledWith('redactColor', '#FF0000');
});
test('should disable all controls when disabled prop is true', () => {
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
disabled={true}
/>
</TestWrapper>
);
// Mode selector buttons should be disabled
expect(screen.getByRole('button', { name: 'Automatic' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Manual' })).toBeDisabled();
// Automatic settings controls should be disabled
expect(screen.getByPlaceholderText('Enter a word')).toBeDisabled();
expect(screen.getByRole('button', { name: '+ Add' })).toBeDisabled();
expect(screen.getByDisplayValue('#000000')).toBeDisabled();
});
test('should show current parameter values in automatic mode', () => {
const customParameters = {
...defaultParameters,
wordsToRedact: ['Word1', 'Word2'],
redactColor: '#FF0000',
useRegex: true,
customPadding: 0.5,
};
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={customParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Check that word tags are displayed
expect(screen.getByText('Word1')).toBeInTheDocument();
expect(screen.getByText('Word2')).toBeInTheDocument();
// Check that color is displayed
expect(screen.getByDisplayValue('#FF0000')).toBeInTheDocument();
// Check that regex checkbox is checked
const useRegexCheckbox = screen.getByLabelText('Use Regex');
expect(useRegexCheckbox).toBeChecked();
// Check that padding value is displayed
expect(screen.getByDisplayValue('0.5')).toBeInTheDocument();
});
test('should maintain consistent spacing and layout', () => {
render(
<TestWrapper>
<RedactSingleStepSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Check that the Stack container exists
const container = screen.getByText('Mode').closest('.mantine-Stack-root');
expect(container).toBeInTheDocument();
});
});

View File

@ -1,191 +0,0 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import WordsToRedactInput from './WordsToRedactInput';
// Mock useTranslation
const mockT = vi.fn((_key: string, fallback: string) => fallback);
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: mockT })
}));
// Wrapper component to provide Mantine context
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider>{children}</MantineProvider>
);
describe('WordsToRedactInput', () => {
const mockOnWordsChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
test('should render with title and input field', () => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={[]}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
expect(screen.getByText('Words to Redact')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Enter a word')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '+ Add' })).toBeInTheDocument();
});
test.each([
{ trigger: 'Add button click', action: (_input: HTMLElement, addButton: HTMLElement) => fireEvent.click(addButton) },
{ trigger: 'Enter key press', action: (input: HTMLElement) => fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }) },
])('should add word when $trigger', ({ action }) => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={[]}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
const input = screen.getByPlaceholderText('Enter a word');
const addButton = screen.getByRole('button', { name: '+ Add' });
fireEvent.change(input, { target: { value: 'TestWord' } });
action(input, addButton);
expect(mockOnWordsChange).toHaveBeenCalledWith(['TestWord']);
});
test('should not add empty word', () => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={[]}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
const addButton = screen.getByRole('button', { name: '+ Add' });
fireEvent.click(addButton);
expect(mockOnWordsChange).not.toHaveBeenCalled();
});
test('should not add duplicate word', () => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={['Existing']}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
const input = screen.getByPlaceholderText('Enter a word');
const addButton = screen.getByRole('button', { name: '+ Add' });
fireEvent.change(input, { target: { value: 'Existing' } });
fireEvent.click(addButton);
expect(mockOnWordsChange).not.toHaveBeenCalled();
});
test('should trim whitespace when adding word', () => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={[]}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
const input = screen.getByPlaceholderText('Enter a word');
const addButton = screen.getByRole('button', { name: '+ Add' });
fireEvent.change(input, { target: { value: ' TestWord ' } });
fireEvent.click(addButton);
expect(mockOnWordsChange).toHaveBeenCalledWith(['TestWord']);
});
test('should remove word when x button is clicked', () => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={['Word1', 'Word2']}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
const removeButtons = screen.getAllByText('×');
fireEvent.click(removeButtons[0]);
expect(mockOnWordsChange).toHaveBeenCalledWith(['Word2']);
});
test('should clear input after adding word', () => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={[]}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
const input = screen.getByPlaceholderText('Enter a word') as HTMLInputElement;
const addButton = screen.getByRole('button', { name: '+ Add' });
fireEvent.change(input, { target: { value: 'TestWord' } });
fireEvent.click(addButton);
expect(input.value).toBe('');
});
test.each([
{ description: 'disable Add button when input is empty', inputValue: '', expectedDisabled: true },
{ description: 'enable Add button when input has text', inputValue: 'TestWord', expectedDisabled: false },
])('should $description', ({ inputValue, expectedDisabled }) => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={[]}
onWordsChange={mockOnWordsChange}
/>
</TestWrapper>
);
const input = screen.getByPlaceholderText('Enter a word');
const addButton = screen.getByRole('button', { name: '+ Add' });
fireEvent.change(input, { target: { value: inputValue } });
expect(addButton).toHaveProperty('disabled', expectedDisabled);
});
test('should disable all controls when disabled prop is true', () => {
render(
<TestWrapper>
<WordsToRedactInput
wordsToRedact={['Word1']}
onWordsChange={mockOnWordsChange}
disabled={true}
/>
</TestWrapper>
);
const input = screen.getByPlaceholderText('Enter a word');
const addButton = screen.getByRole('button', { name: '+ Add' });
const removeButton = screen.getByText('×');
expect(input).toBeDisabled();
expect(addButton).toBeDisabled();
expect(removeButton.closest('button')).toBeDisabled();
});
});

View File

@ -38,10 +38,11 @@ export default function WordsToRedactInput({ wordsToRedact, onWordsChange, disab
{/* Current words */} {/* Current words */}
{wordsToRedact.map((word, index) => ( {wordsToRedact.map((word, index) => (
<Group key={index} justify="space-between" p="sm" style={{ <Group key={index} justify="space-between" style={{
borderRadius: 'var(--mantine-radius-sm)', padding: '8px 12px',
border: `1px solid var(--mantine-color-gray-3)`, backgroundColor: '#f8f9fa',
backgroundColor: 'var(--mantine-color-gray-0)' borderRadius: '4px',
border: '1px solid #e9ecef'
}}> }}>
<Text size="sm">{word}</Text> <Text size="sm">{word}</Text>
<ActionIcon <ActionIcon
@ -57,7 +58,7 @@ export default function WordsToRedactInput({ wordsToRedact, onWordsChange, disab
))} ))}
{/* Add new word input */} {/* Add new word input */}
<Group gap="sm" align="end"> <Stack gap="sm">
<TextInput <TextInput
placeholder={t('redact.auto.wordsToRedact.placeholder', 'Enter a word')} placeholder={t('redact.auto.wordsToRedact.placeholder', 'Enter a word')}
value={currentWord} value={currentWord}
@ -65,17 +66,17 @@ export default function WordsToRedactInput({ wordsToRedact, onWordsChange, disab
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
disabled={disabled} disabled={disabled}
size="sm" size="sm"
style={{ flex: 1 }}
/> />
<Button <Button
size="sm" size="sm"
variant="light" variant="light"
onClick={addWord} onClick={addWord}
disabled={disabled || !currentWord.trim()} disabled={disabled || !currentWord.trim()}
style={{ alignSelf: 'flex-start' }}
> >
+ {t('redact.auto.wordsToRedact.add', 'Add')} + {t('redact.auto.wordsToRedact.addAnother', 'Add Another')}
</Button> </Button>
</Group> </Stack>
{/* Examples */} {/* Examples */}
{wordsToRedact.length === 0 && ( {wordsToRedact.length === 0 && (

View File

@ -4,7 +4,7 @@ import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/us
interface RemovePasswordSettingsProps { interface RemovePasswordSettingsProps {
parameters: RemovePasswordParameters; parameters: RemovePasswordParameters;
onParameterChange: <K extends keyof RemovePasswordParameters>(key: K, value: RemovePasswordParameters[K]) => void; onParameterChange: (key: keyof RemovePasswordParameters, value: string) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -4,7 +4,7 @@ import { SanitizeParameters, defaultParameters } from "../../../hooks/tools/sani
interface SanitizeSettingsProps { interface SanitizeSettingsProps {
parameters: SanitizeParameters; parameters: SanitizeParameters;
onParameterChange: <K extends keyof SanitizeParameters>(key: K, value: SanitizeParameters[K]) => void; onParameterChange: (key: keyof SanitizeParameters, value: boolean) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -1,11 +1,11 @@
import { Stack, TextInput, Select, Checkbox } from '@mantine/core'; import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { isSplitMode, isSplitType, SPLIT_MODES, SPLIT_TYPES } from '../../../constants/splitConstants'; import { isSplitMode, SPLIT_MODES, SPLIT_TYPES } from '../../../constants/splitConstants';
import { SplitParameters } from '../../../hooks/tools/split/useSplitParameters'; import { SplitParameters } from '../../../hooks/tools/split/useSplitParameters';
export interface SplitSettingsProps { export interface SplitSettingsProps {
parameters: SplitParameters; parameters: SplitParameters;
onParameterChange: <K extends keyof SplitParameters>(key: K, value: SplitParameters[K]) => void; onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void;
disabled?: boolean; disabled?: boolean;
} }
@ -62,7 +62,7 @@ const SplitSettings = ({
<Select <Select
label={t("split-by-size-or-count.type.label", "Split Type")} label={t("split-by-size-or-count.type.label", "Split Type")}
value={parameters.splitType} value={parameters.splitType}
onChange={(v) => isSplitType(v) && onParameterChange('splitType', v)} onChange={(v) => v && onParameterChange('splitType', v)}
disabled={disabled} disabled={disabled}
data={[ data={[
{ value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") }, { value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") },

View File

@ -40,7 +40,7 @@ export const useRedactWordsTips = (): TooltipContent => {
}, },
{ {
title: t("redact.tooltip.words.examples.title", "Common Examples"), title: t("redact.tooltip.words.examples.title", "Common Examples"),
description: t("redact.tooltip.words.examples.text", "Typical words to redact include: bank details, email addresses, or specific names.") description: t("redact.tooltip.words.examples.text", "Typical words to redact include: 'Confidential', 'SSN:', phone numbers, email addresses, or specific names.")
} }
] ]
}; };
@ -55,8 +55,8 @@ export const useRedactAdvancedTips = (): TooltipContent => {
}, },
tips: [ tips: [
{ {
title: t("redact.tooltip.advanced.color.title", "Box Colour & Padding"), title: t("redact.tooltip.advanced.color.title", "Box Color & Padding"),
description: t("redact.tooltip.advanced.color.text", "Customise the appearance of redaction boxes. Black is standard, but you can choose any colour. Padding adds extra space around the found text."), description: t("redact.tooltip.advanced.color.text", "Customize the appearance of redaction boxes. Black is standard, but you can choose any color. Padding adds extra space around the found text."),
}, },
{ {
title: t("redact.tooltip.advanced.regex.title", "Use Regex"), title: t("redact.tooltip.advanced.regex.title", "Use Regex"),

View File

@ -1,142 +0,0 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { buildRedactFormData, redactOperationConfig, useRedactOperation } from './useRedactOperation';
import { defaultParameters, RedactParameters } from './useRedactParameters';
// Mock the useToolOperation hook
vi.mock('../shared/useToolOperation', async () => {
const actual = await vi.importActual('../shared/useToolOperation'); // Need to keep ToolType etc.
return {
...actual,
useToolOperation: vi.fn()
};
});
// Mock the translation hook
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: vi.fn((_key: string, fallback: string) => fallback) })
}));
// Mock the error handler utility
vi.mock('../../../utils/toolErrorHandler', () => ({
createStandardErrorHandler: vi.fn(() => vi.fn())
}));
describe('buildRedactFormData', () => {
const mockFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
test('should build form data for automatic mode', () => {
const parameters: RedactParameters = {
...defaultParameters,
mode: 'automatic',
wordsToRedact: ['Confidential', 'Secret'],
useRegex: true,
wholeWordSearch: true,
redactColor: '#FF0000',
customPadding: 0.5,
convertPDFToImage: false,
};
const formData = buildRedactFormData(parameters, mockFile);
expect(formData.get('fileInput')).toBe(mockFile);
expect(formData.get('listOfText')).toBe('Confidential\nSecret');
expect(formData.get('useRegex')).toBe('true');
expect(formData.get('wholeWordSearch')).toBe('true');
expect(formData.get('redactColor')).toBe('FF0000'); // Hash should be removed
expect(formData.get('customPadding')).toBe('0.5');
expect(formData.get('convertPDFToImage')).toBe('false');
});
test('should handle empty words array', () => {
const parameters: RedactParameters = {
...defaultParameters,
mode: 'automatic',
wordsToRedact: [],
};
const formData = buildRedactFormData(parameters, mockFile);
expect(formData.get('listOfText')).toBe('');
});
test('should join multiple words with newlines', () => {
const parameters: RedactParameters = {
...defaultParameters,
mode: 'automatic',
wordsToRedact: ['Word1', 'Word2', 'Word3'],
};
const formData = buildRedactFormData(parameters, mockFile);
expect(formData.get('listOfText')).toBe('Word1\nWord2\nWord3');
});
test.each([
{ description: 'remove hash from redact color', redactColor: '#123456', expected: '123456' },
{ description: 'handle redact color without hash', redactColor: 'ABCDEF', expected: 'ABCDEF' },
])('should $description', ({ redactColor, expected }) => {
const parameters: RedactParameters = {
...defaultParameters,
mode: 'automatic',
redactColor,
};
const formData = buildRedactFormData(parameters, mockFile);
expect(formData.get('redactColor')).toBe(expected);
});
test('should convert boolean parameters to strings', () => {
const parameters: RedactParameters = {
...defaultParameters,
mode: 'automatic',
useRegex: false,
wholeWordSearch: true,
convertPDFToImage: false,
};
const formData = buildRedactFormData(parameters, mockFile);
expect(formData.get('useRegex')).toBe('false');
expect(formData.get('wholeWordSearch')).toBe('true');
expect(formData.get('convertPDFToImage')).toBe('false');
});
test('should throw error for manual mode (not implemented)', () => {
const parameters: RedactParameters = {
...defaultParameters,
mode: 'manual',
};
expect(() => buildRedactFormData(parameters, mockFile)).toThrow('Manual redaction not yet implemented');
});
});
describe('useRedactOperation', () => {
beforeEach(() => {
vi.clearAllMocks();
});
test('should call useToolOperation with correct configuration', async () => {
const { useToolOperation } = await import('../shared/useToolOperation');
const mockUseToolOperation = vi.mocked(useToolOperation);
renderHook(() => useRedactOperation());
expect(mockUseToolOperation).toHaveBeenCalledWith({
...redactOperationConfig,
getErrorMessage: expect.any(Function),
});
});
test('should provide error handler to useToolOperation', async () => {
const { useToolOperation } = await import('../shared/useToolOperation');
const mockUseToolOperation = vi.mocked(useToolOperation);
renderHook(() => useRedactOperation());
const callArgs = mockUseToolOperation.mock.calls[0][0];
expect(typeof callArgs.getErrorMessage).toBe('function');
});
});

View File

@ -1,134 +0,0 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRedactParameters, defaultParameters } from './useRedactParameters';
describe('useRedactParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useRedactParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test.each([
{ paramName: 'mode' as const, value: 'manual' as const },
{ paramName: 'wordsToRedact' as const, value: ['word1', 'word2'] },
{ paramName: 'useRegex' as const, value: true },
{ paramName: 'wholeWordSearch' as const, value: true },
{ paramName: 'redactColor' as const, value: '#FF0000' },
{ paramName: 'customPadding' as const, value: 0.5 },
{ paramName: 'convertPDFToImage' as const, value: false }
])('should update parameter $paramName', ({ paramName, value }) => {
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter(paramName, value);
});
expect(result.current.parameters[paramName]).toStrictEqual(value);
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useRedactParameters());
// Modify some parameters
act(() => {
result.current.updateParameter('mode', 'manual');
result.current.updateParameter('wordsToRedact', ['test']);
result.current.updateParameter('useRegex', true);
});
// Reset parameters
act(() => {
result.current.resetParameters();
});
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
describe('validation', () => {
test.each([
{ description: 'validate when wordsToRedact has non-empty words in automatic mode', wordsToRedact: ['word1', 'word2'], expected: true },
{ description: 'not validate when wordsToRedact is empty in automatic mode', wordsToRedact: [], expected: false },
{ description: 'not validate when wordsToRedact contains only empty strings in automatic mode', wordsToRedact: ['', ' ', ''], expected: false },
{ description: 'validate when wordsToRedact contains at least one non-empty word in automatic mode', wordsToRedact: ['', 'valid', ' '], expected: true },
])('should $description', ({ wordsToRedact, expected }) => {
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'automatic');
result.current.updateParameter('wordsToRedact', wordsToRedact);
});
expect(result.current.validateParameters()).toBe(expected);
});
test('should not validate in manual mode (not implemented)', () => {
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'manual');
});
expect(result.current.validateParameters()).toBe(false);
});
});
describe('endpoint handling', () => {
test('should return correct endpoint for automatic mode', () => {
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'automatic');
});
expect(result.current.getEndpointName()).toBe('/api/v1/security/auto-redact');
});
test('should throw error for manual mode (not implemented)', () => {
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'manual');
});
expect(() => result.current.getEndpointName()).toThrow('Manual redaction not yet implemented');
});
});
test('should maintain parameter state across updates', () => {
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('redactColor', '#FF0000');
result.current.updateParameter('customPadding', 0.5);
result.current.updateParameter('wordsToRedact', ['word1']);
});
// All parameters should be updated
expect(result.current.parameters.redactColor).toBe('#FF0000');
expect(result.current.parameters.customPadding).toBe(0.5);
expect(result.current.parameters.wordsToRedact).toEqual(['word1']);
// Other parameters should remain at defaults
expect(result.current.parameters.mode).toBe('automatic');
expect(result.current.parameters.useRegex).toBe(false);
expect(result.current.parameters.wholeWordSearch).toBe(false);
expect(result.current.parameters.convertPDFToImage).toBe(true);
});
test('should handle array parameter updates correctly', () => {
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('wordsToRedact', ['initial']);
});
expect(result.current.parameters.wordsToRedact).toEqual(['initial']);
act(() => {
result.current.updateParameter('wordsToRedact', ['updated', 'multiple']);
});
expect(result.current.parameters.wordsToRedact).toEqual(['updated', 'multiple']);
});
});

View File

@ -59,7 +59,6 @@ i18n
.init({ .init({
fallbackLng: 'en-GB', fallbackLng: 'en-GB',
supportedLngs: Object.keys(supportedLanguages), supportedLngs: Object.keys(supportedLanguages),
load: 'currentOnly',
nonExplicitSupportedLngs: false, nonExplicitSupportedLngs: false,
debug: process.env.NODE_ENV === 'development', debug: process.env.NODE_ENV === 'development',