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": {
"title": "Words to Redact",
"placeholder": "Enter a word",
"add": "Add",
"addAnother": "Add Another",
"examples": "Examples: Confidential, Top-Secret"
},
"useRegexLabel": "Use Regex",
@ -1620,7 +1620,7 @@
},
"automatic": {
"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": {
"title": "Manual Redaction",
@ -1640,7 +1640,7 @@
"bullet3": "Click × to remove words",
"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": {
@ -1648,8 +1648,8 @@
"title": "Advanced Redaction Settings"
},
"color": {
"title": "Box Colour & 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."
"title": "Box Color & Padding",
"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": {
"title": "Use Regex",
@ -1669,7 +1669,7 @@
},
"manual": {
"header": "Manual Redaction",
"textBasedRedaction": "Text-based Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
"pageRedactionNumbers": {
@ -1677,7 +1677,7 @@
"placeholder": "(e.g. 1,2,8 or 4,7,12-16 or 2n-1)"
},
"redactionColor": {
"title": "Redaction Colour"
"title": "Redaction Color"
},
"export": "Export",
"upload": "Upload",
@ -1690,7 +1690,7 @@
"toggleSidebar": "Toggle Sidebar",
"showThumbnails": "Show Thumbnails",
"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)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
@ -1906,11 +1906,6 @@
"title": "Compress",
"desc": "Compress PDFs to reduce their file size.",
"header": "Compress PDF",
"method": {
"title": "Compression Method",
"quality": "Quality",
"filesize": "File Size"
},
"credit": "This service uses qpdf for PDF Compress/Optimisation.",
"grayscale": {
"label": "Apply Grayscale for Compression"

View File

@ -385,13 +385,6 @@
"moduleLicense": "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",
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss",
@ -749,13 +742,6 @@
"moduleLicense": "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",
"moduleUrl": "https://github.com/isaacs/core-util-is",
@ -938,13 +924,6 @@
"moduleLicense": "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",
"moduleUrl": "https://github.com/react-dropzone/file-selector",
@ -1554,20 +1533,6 @@
"moduleLicense": "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",
"moduleUrl": "https://github.com/facebook/jest",
@ -1963,7 +1928,7 @@
{
"moduleName": "typescript",
"moduleUrl": "https://github.com/microsoft/TypeScript",
"moduleVersion": "5.9.2",
"moduleVersion": "5.8.3",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
@ -2030,13 +1995,6 @@
"moduleLicense": "Apache-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",
"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 {
parameters: AddPasswordParameters;
onParameterChange: <K extends keyof AddPasswordParameters>(key: K, value: AddPasswordParameters[K]) => void;
onParameterChange: (key: keyof AddPasswordParameters, value: any) => void;
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 ButtonSelector from "../../shared/ButtonSelector";
interface WatermarkTypeSettingsProps {
watermarkType?: 'text' | 'image';
@ -11,25 +10,32 @@ interface WatermarkTypeSettingsProps {
const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => {
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 (
<Stack gap="sm">
<ButtonSelector
value={watermarkType}
onChange={onWatermarkTypeChange}
options={options}
disabled={disabled}
/>
<div style={{ display: 'flex', gap: '4px' }}>
<Button
variant={watermarkType === 'text' ? 'filled' : 'outline'}
color={watermarkType === 'text' ? 'blue' : 'var(--text-muted)'}
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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ import { StirlingFile } from "../../../types/fileContext";
interface ConvertSettingsProps {
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}>;
selectedFiles: StirlingFile[];
disabled?: boolean;

View File

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

View File

@ -7,7 +7,7 @@ import { StirlingFile } from '../../../types/fileContext';
interface ConvertToPdfaSettingsProps {
parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
selectedFiles: StirlingFile[];
disabled?: boolean;
}

View File

@ -16,7 +16,7 @@ interface AdvancedOption {
interface AdvancedOCRSettingsProps {
advancedOptions: string[];
ocrRenderType?: string;
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
onParameterChange: (key: keyof OCRParameters, value: any) => void;
disabled?: boolean;
}

View File

@ -6,7 +6,7 @@ import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
interface OCRSettingsProps {
parameters: OCRParameters;
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
onParameterChange: (key: keyof OCRParameters, value: any) => void;
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 { Stack, Text } from '@mantine/core';
import { Button, Stack, Text } from '@mantine/core';
import { RedactMode } from '../../../hooks/tools/redact/useRedactParameters';
import ButtonSelector from '../../shared/ButtonSelector';
interface RedactModeSelectorProps {
mode: RedactMode;
@ -12,30 +11,36 @@ interface RedactModeSelectorProps {
export default function RedactModeSelector({ mode, onModeChange, disabled }: RedactModeSelectorProps) {
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 (
<Stack gap="sm">
<Text size="sm" fw={600}>
{t('redact.modeSelector.mode', 'Mode')}
</Text>
<ButtonSelector
value={mode}
onChange={onModeChange}
options={options}
disabled={disabled}
/>
<div style={{ display: 'flex', gap: '4px' }}>
<Button
variant={mode === 'automatic' ? 'filled' : 'outline'}
color={mode === 'automatic' ? 'blue' : 'var(--text-muted)'}
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>
);
}

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
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';
export interface SplitSettingsProps {
parameters: SplitParameters;
onParameterChange: <K extends keyof SplitParameters>(key: K, value: SplitParameters[K]) => void;
onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void;
disabled?: boolean;
}
@ -62,7 +62,7 @@ const SplitSettings = ({
<Select
label={t("split-by-size-or-count.type.label", "Split Type")}
value={parameters.splitType}
onChange={(v) => isSplitType(v) && onParameterChange('splitType', v)}
onChange={(v) => v && onParameterChange('splitType', v)}
disabled={disabled}
data={[
{ 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"),
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: [
{
title: t("redact.tooltip.advanced.color.title", "Box Colour & 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."),
title: t("redact.tooltip.advanced.color.title", "Box Color & Padding"),
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"),

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({
fallbackLng: 'en-GB',
supportedLngs: Object.keys(supportedLanguages),
load: 'currentOnly',
nonExplicitSupportedLngs: false,
debug: process.env.NODE_ENV === 'development',