Compare commits

...
25 changed files with 339 additions and 105 deletions
@@ -353,6 +353,8 @@
"autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
"defaultToolPickerMode": "Default tool picker mode",
"defaultToolPickerModeDescription": "Choose whether the tool picker opens in fullscreen or sidebar by default",
"rememberInputs": "Remember tool inputs",
"rememberInputsDescription": "Store the last settings you used for each tool on this device",
"mode": {
"fullscreen": "Fullscreen",
"sidebar": "Sidebar"
@@ -323,6 +323,8 @@
"autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
"defaultToolPickerMode": "Default tool picker mode",
"defaultToolPickerModeDescription": "Choose whether the tool picker opens in fullscreen or sidebar by default",
"rememberInputs": "Remember tool inputs",
"rememberInputsDescription": "Store the last settings you used for each tool on this device",
"mode": {
"fullscreen": "Fullscreen",
"sidebar": "Sidebar"
@@ -114,6 +114,23 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
]}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Text fw={500} size="sm">
{t('settings.general.rememberInputs', 'Remember tool inputs')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
'settings.general.rememberInputsDescription',
'Store the last settings you used for each tool on this device'
)}
</Text>
</div>
<Switch
checked={preferences.rememberFormInputs}
onChange={(event) => updatePreference('rememberFormInputs', event.currentTarget.checked)}
/>
</div>
<Tooltip
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
multiline
@@ -2,10 +2,13 @@ import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useAddPasswordParameters, defaultParameters, AddPasswordParametersHook } from '@app/hooks/tools/addPassword/useAddPasswordParameters';
import { defaultParameters as defaultChangePermissionsParameters, ChangePermissionsParameters } from '@app/hooks/tools/changePermissions/useChangePermissionsParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderAddPasswordHook = () => renderHook(() => useAddPasswordParameters(), { wrapper: PreferencesTestWrapper });
describe('useAddPasswordParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -15,7 +18,7 @@ describe('useAddPasswordParameters', () => {
{ paramName: 'ownerPassword' as const, value: 'owner-password' },
{ paramName: 'keyLength' as const, value: 256 }
])('should update parameter $paramName', ({ paramName, value }) => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
act(() => {
result.current.updateParameter(paramName, value);
@@ -28,7 +31,7 @@ describe('useAddPasswordParameters', () => {
{ paramName: 'preventAssembly' as const },
{ paramName: 'preventPrinting' as const }
])('should update boolean permission parameter $paramName', ({ paramName }) => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
act(() => {
result.current.permissions.updateParameter(paramName, true);
@@ -38,7 +41,7 @@ describe('useAddPasswordParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
// First, change some parameters
act(() => {
@@ -60,7 +63,7 @@ describe('useAddPasswordParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
expect(result.current.getEndpointName()).toBe('add-password');
});
@@ -112,7 +115,7 @@ describe('useAddPasswordParameters', () => {
}
}
])('should validate parameters correctly $description', ({ setup }) => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
// Default state should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -126,7 +129,7 @@ describe('useAddPasswordParameters', () => {
});
test.each(Object.keys(defaultChangePermissionsParameters) as Array<keyof ChangePermissionsParameters>)('should handle boolean restriction parameter %s', (param) => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
act(() => {
result.current.resetParameters();
@@ -137,7 +140,7 @@ describe('useAddPasswordParameters', () => {
});
test('should handle mixed parameter types in updateParameter', () => {
const { result } = renderHook(() => useAddPasswordParameters());
const { result } = renderAddPasswordHook();
act(() => {
result.current.updateParameter('password', 'test-string');
@@ -1,10 +1,13 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useAdjustPageScaleParameters, defaultParameters, PageSize, AdjustPageScaleParametersHook } from '@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderAdjustPageScaleHook = () => renderHook(() => useAdjustPageScaleParameters(), { wrapper: PreferencesTestWrapper });
describe('useAdjustPageScaleParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
const { result } = renderAdjustPageScaleHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
expect(result.current.parameters.scaleFactor).toBe(1.0);
@@ -19,7 +22,7 @@ describe('useAdjustPageScaleParameters', () => {
{ paramName: 'pageSize' as const, value: PageSize.LETTER },
{ paramName: 'pageSize' as const, value: PageSize.LEGAL },
])('should update parameter $paramName to $value', ({ paramName, value }) => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
const { result } = renderAdjustPageScaleHook();
act(() => {
result.current.updateParameter(paramName, value);
@@ -29,7 +32,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
const { result } = renderAdjustPageScaleHook();
// First, change some parameters
act(() => {
@@ -49,7 +52,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
const { result } = renderAdjustPageScaleHook();
expect(result.current.getEndpointName()).toBe('scale-pages');
});
@@ -96,7 +99,7 @@ describe('useAdjustPageScaleParameters', () => {
expected: false
}
])('should validate parameters correctly $description', ({ setup, expected }) => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
const { result } = renderAdjustPageScaleHook();
act(() => {
setup(result.current);
@@ -106,7 +109,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should handle all PageSize enum values', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
const { result } = renderAdjustPageScaleHook();
Object.values(PageSize).forEach(pageSize => {
act(() => {
@@ -119,7 +122,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should handle scale factor edge cases', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
const { result } = renderAdjustPageScaleHook();
// Test very small valid scale factor
act(() => {
@@ -2,10 +2,13 @@ import { renderHook, act } from '@testing-library/react';
import { defaultParameters, useChangeMetadataParameters } from '@app/hooks/tools/changeMetadata/useChangeMetadataParameters';
import { TrappedStatus } from '@app/types/metadata';
import { describe, expect, test } from 'vitest';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderChangeMetadataHook = () => renderHook(() => useChangeMetadataParameters(), { wrapper: PreferencesTestWrapper });
describe('useChangeMetadataParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -23,7 +26,7 @@ describe('useChangeMetadataParameters', () => {
{ paramName: 'trapped', value: TrappedStatus.TRUE },
{ paramName: 'deleteAll', value: true },
] as const)('should update $paramName parameter', ({ paramName, value }) => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
act(() => {
result.current.updateParameter(paramName, value);
@@ -48,7 +51,7 @@ describe('useChangeMetadataParameters', () => {
{ description: 'no meaningful content', updates: {}, expected: false },
{ description: 'whitespace only', updates: { title: ' ', author: ' ' }, expected: false },
])('should validate correctly when $description', ({ updates, expected }) => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
act(() => {
Object.entries(updates).forEach(([key, value]) => {
@@ -64,7 +67,7 @@ describe('useChangeMetadataParameters', () => {
{ description: 'valid modification date', updates: { title: 'Test', modificationDate: new Date('2025/01/17 14:30:00') }, expected: true },
{ description: 'empty dates are valid', updates: { title: 'Test', creationDate: null, modificationDate: null }, expected: true },
])('should validate dates correctly with $description', ({ updates, expected }) => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
act(() => {
Object.entries(updates).forEach(([key, value]) => {
@@ -78,7 +81,7 @@ describe('useChangeMetadataParameters', () => {
describe('custom metadata', () => {
test('should add custom metadata with sequential IDs', () => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
act(() => {
result.current.addCustomMetadata();
@@ -93,7 +96,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should remove custom metadata by ID', () => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
act(() => {
result.current.addCustomMetadata();
@@ -109,7 +112,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should update custom metadata by ID', () => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
act(() => {
result.current.addCustomMetadata();
@@ -129,7 +132,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should validate with custom metadata', () => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
act(() => {
result.current.addCustomMetadata();
@@ -145,7 +148,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should generate unique IDs for multiple custom entries', () => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
for (let i = 0; i < 3; i++) {
act(() => {
@@ -161,7 +164,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useChangeMetadataParameters());
const { result } = renderChangeMetadataHook();
expect(result.current.getEndpointName()).toBe('update-metadata');
});
@@ -1,16 +1,19 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useChangePermissionsParameters, defaultParameters, ChangePermissionsParameters } from '@app/hooks/tools/changePermissions/useChangePermissionsParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderChangePermissionsHook = () => renderHook(() => useChangePermissionsParameters(), { wrapper: PreferencesTestWrapper });
describe('useChangePermissionsParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useChangePermissionsParameters());
const { result } = renderChangePermissionsHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should update individual boolean parameters', () => {
const { result } = renderHook(() => useChangePermissionsParameters());
const { result } = renderChangePermissionsHook();
act(() => {
result.current.updateParameter('preventAssembly', true);
@@ -28,7 +31,7 @@ describe('useChangePermissionsParameters', () => {
});
test('should update all permission parameters', () => {
const { result } = renderHook(() => useChangePermissionsParameters());
const { result } = renderChangePermissionsHook();
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
@@ -56,7 +59,7 @@ describe('useChangePermissionsParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useChangePermissionsParameters());
const { result } = renderChangePermissionsHook();
// First, change some parameters
act(() => {
@@ -78,13 +81,13 @@ describe('useChangePermissionsParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useChangePermissionsParameters());
const { result } = renderChangePermissionsHook();
expect(result.current.getEndpointName()).toBe('add-password');
});
test('should always validate as true', () => {
const { result } = renderHook(() => useChangePermissionsParameters());
const { result } = renderChangePermissionsHook();
// Default state should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -6,13 +6,16 @@ import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useConvertParameters } from '@app/hooks/tools/convert/useConvertParameters';
import { FIT_OPTIONS } from '@app/constants/convertConstants';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderConvertHook = () => renderHook(() => useConvertParameters(), { wrapper: PreferencesTestWrapper });
describe('useConvertParameters', () => {
describe('Parameter Management', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
expect(result.current.parameters.fromExtension).toBe('');
expect(result.current.parameters.toExtension).toBe('');
@@ -28,7 +31,7 @@ describe('useConvertParameters', () => {
});
test('should update individual parameters', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -39,7 +42,7 @@ describe('useConvertParameters', () => {
});
test('should update nested image options', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('imageOptions', {
@@ -61,7 +64,7 @@ describe('useConvertParameters', () => {
});
test('should update nested HTML options', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('htmlOptions', {
@@ -73,7 +76,7 @@ describe('useConvertParameters', () => {
});
test('should update nested email options', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('emailOptions', {
@@ -91,7 +94,7 @@ describe('useConvertParameters', () => {
});
test('should update nested PDF/A options', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('pdfaOptions', {
@@ -103,7 +106,7 @@ describe('useConvertParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -124,7 +127,7 @@ describe('useConvertParameters', () => {
describe('Parameter Validation', () => {
test('should validate parameters correctly', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
// No parameters - should be invalid
expect(result.current.validateParameters()).toBe(false);
@@ -143,7 +146,7 @@ describe('useConvertParameters', () => {
});
test('should validate unsupported conversions', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -158,7 +161,7 @@ describe('useConvertParameters', () => {
describe('Endpoint Generation', () => {
test('should generate correct endpoint names', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -170,7 +173,7 @@ describe('useConvertParameters', () => {
});
test('should generate correct endpoint URLs', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -182,7 +185,7 @@ describe('useConvertParameters', () => {
});
test('should return empty strings for invalid conversions', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
act(() => {
result.current.updateParameter('fromExtension', 'invalid');
@@ -197,7 +200,7 @@ describe('useConvertParameters', () => {
describe('Available Extensions', () => {
test('should return available extensions for valid source format', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const availableExtensions = result.current.getAvailableToExtensions('pdf');
@@ -207,7 +210,7 @@ describe('useConvertParameters', () => {
});
test('should return empty array for invalid source format', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const availableExtensions = result.current.getAvailableToExtensions('invalid');
@@ -219,7 +222,7 @@ describe('useConvertParameters', () => {
});
test('should return empty array for empty source format', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const availableExtensions = result.current.getAvailableToExtensions('');
@@ -137,6 +137,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
defaultParameters,
endpointName: getEndpointName,
validateFn: validateParameters,
storageKey: 'convert',
}), []);
const baseHook = useBaseParameters(config);
@@ -6,13 +6,16 @@
import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useConvertParameters } from '@app/hooks/tools/convert/useConvertParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderConvertHook = () => renderHook(() => useConvertParameters(), { wrapper: PreferencesTestWrapper });
describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Single File Detection', () => {
test('should detect single file extension and set auto-target', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const pdfFile = [{ name: 'document.pdf' }];
@@ -27,7 +30,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle unknown file types with file-to-pdf fallback', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const unknownFile = [{ name: 'document.xyz' }, { name: 'image.jpggg' }];
@@ -41,7 +44,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle files without extensions', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const noExtFile = [{ name: 'document' }];
@@ -59,7 +62,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Multiple Identical Files', () => {
test('should detect multiple PDF files and set auto-target', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const pdfFiles = [
{ name: 'doc1.pdf' },
@@ -78,7 +81,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle multiple unknown file types with fallback', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const unknownFiles = [
{ name: 'file1.xyz' },
@@ -98,7 +101,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection - All Images', () => {
test('should detect all image files and enable smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const imageFiles = [
{ name: 'photo1.jpg' },
@@ -117,7 +120,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle mixed case image extensions', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const imageFiles = [
{ name: 'photo1.JPG' },
@@ -136,7 +139,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection - All Web Files', () => {
test('should detect all web files and enable web smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const webFiles = [
{ name: 'page1.html' },
@@ -154,7 +157,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle mixed case web extensions', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const webFiles = [
{ name: 'page1.HTML' },
@@ -170,7 +173,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should detect multiple web files and enable web smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const zipFiles = [
{ name: 'site1.zip' },
@@ -191,7 +194,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection - Mixed File Types', () => {
test('should detect mixed file types and enable smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const mixedFiles = [
{ name: 'document.pdf' },
@@ -210,7 +213,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should detect mixed images and documents as mixed type', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const mixedFiles = [
{ name: 'photo.jpg' },
@@ -227,7 +230,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle mixed with unknown file types', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const mixedFiles = [
{ name: 'document.pdf' },
@@ -247,7 +250,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection Endpoint Resolution', () => {
test('should return correct endpoint for image smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const imageFiles = [
{ name: 'photo1.jpg' },
@@ -263,7 +266,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should return correct endpoint for web smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const webFiles = [
{ name: 'page1.html' },
@@ -279,7 +282,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should return correct endpoint for mixed smart detection', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const mixedFiles = [
{ name: 'document.pdf' },
@@ -298,7 +301,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Auto-Target Selection Logic', () => {
test('should select single available target automatically', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
// Markdown has only one conversion target (PDF)
const mdFile = [{ name: 'readme.md' }];
@@ -312,7 +315,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should not auto-select when multiple targets available', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
// PDF has multiple conversion targets, so no auto-selection
const pdfFile = [{ name: 'document.pdf' }];
@@ -330,7 +333,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Edge Cases', () => {
test('should handle empty file names', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const emptyFiles = [{ name: '' }];
@@ -343,7 +346,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle malformed file objects', () => {
const { result } = renderHook(() => useConvertParameters());
const { result } = renderConvertHook();
const malformedFiles: Array<{name: string}> = [
{ name: 'valid.pdf' },
@@ -1,10 +1,13 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useMergeParameters, defaultParameters } from '@app/hooks/tools/merge/useMergeParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderMergeHook = () => renderHook(() => useMergeParameters(), { wrapper: PreferencesTestWrapper });
describe('useMergeParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useMergeParameters());
const { result } = renderMergeHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -15,7 +18,7 @@ describe('useMergeParameters', () => {
{ paramName: 'generateTableOfContents' as const, value: true },
{ paramName: 'generateTableOfContents' as const, value: false }
])('should update parameter $paramName to $value', ({ paramName, value }) => {
const { result } = renderHook(() => useMergeParameters());
const { result } = renderMergeHook();
act(() => {
result.current.updateParameter(paramName, value);
@@ -25,7 +28,7 @@ describe('useMergeParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useMergeParameters());
const { result } = renderMergeHook();
// First, change some parameters
act(() => {
@@ -45,7 +48,7 @@ describe('useMergeParameters', () => {
});
test('should validate parameters correctly - always returns true', () => {
const { result } = renderHook(() => useMergeParameters());
const { result } = renderMergeHook();
// Default state should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -1,10 +1,13 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRedactParameters, defaultParameters } from '@app/hooks/tools/redact/useRedactParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderRedactHook = () => renderHook(() => useRedactParameters(), { wrapper: PreferencesTestWrapper });
describe('useRedactParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -18,7 +21,7 @@ describe('useRedactParameters', () => {
{ paramName: 'customPadding' as const, value: 0.5 },
{ paramName: 'convertPDFToImage' as const, value: false }
])('should update parameter $paramName', ({ paramName, value }) => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
act(() => {
result.current.updateParameter(paramName, value);
@@ -28,7 +31,7 @@ describe('useRedactParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
// Modify some parameters
act(() => {
@@ -52,7 +55,7 @@ describe('useRedactParameters', () => {
{ 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());
const { result } = renderRedactHook();
act(() => {
result.current.updateParameter('mode', 'automatic');
@@ -63,7 +66,7 @@ describe('useRedactParameters', () => {
});
test('should not validate in manual mode (not implemented)', () => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
act(() => {
result.current.updateParameter('mode', 'manual');
@@ -75,7 +78,7 @@ describe('useRedactParameters', () => {
describe('endpoint handling', () => {
test('should return correct endpoint for automatic mode', () => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
act(() => {
result.current.updateParameter('mode', 'automatic');
@@ -85,7 +88,7 @@ describe('useRedactParameters', () => {
});
test('should throw error for manual mode (not implemented)', () => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
act(() => {
result.current.updateParameter('mode', 'manual');
@@ -96,7 +99,7 @@ describe('useRedactParameters', () => {
});
test('should maintain parameter state across updates', () => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
act(() => {
result.current.updateParameter('redactColor', '#FF0000');
@@ -117,7 +120,7 @@ describe('useRedactParameters', () => {
});
test('should handle array parameter updates correctly', () => {
const { result } = renderHook(() => useRedactParameters());
const { result } = renderRedactHook();
act(() => {
result.current.updateParameter('wordsToRedact', ['initial']);
@@ -43,6 +43,7 @@ export const useRedactParameters = (): RedactParametersHook => {
}
// Manual mode validation would go here when implemented
return false;
}
},
storageKey: 'redact'
});
};
@@ -1,16 +1,19 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRemovePasswordParameters, defaultParameters } from '@app/hooks/tools/removePassword/useRemovePasswordParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderRemovePasswordHook = () => renderHook(() => useRemovePasswordParameters(), { wrapper: PreferencesTestWrapper });
describe('useRemovePasswordParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
const { result } = renderRemovePasswordHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should update password parameter', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
const { result } = renderRemovePasswordHook();
act(() => {
result.current.updateParameter('password', 'test-password');
@@ -20,7 +23,7 @@ describe('useRemovePasswordParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
const { result } = renderRemovePasswordHook();
// First, change the password
act(() => {
@@ -38,7 +41,7 @@ describe('useRemovePasswordParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
const { result } = renderRemovePasswordHook();
expect(result.current.getEndpointName()).toBe('remove-password');
});
@@ -70,7 +73,7 @@ describe('useRemovePasswordParameters', () => {
expectedValid: true
}
])('should validate parameters correctly $description', ({ password, expectedValid }) => {
const { result } = renderHook(() => useRemovePasswordParameters());
const { result } = renderRemovePasswordHook();
act(() => {
result.current.updateParameter('password', password);
@@ -1,10 +1,13 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRotateParameters, defaultParameters, normalizeAngle } from '@app/hooks/tools/rotate/useRotateParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderRotateHook = () => renderHook(() => useRotateParameters(), { wrapper: PreferencesTestWrapper });
describe('useRotateParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
expect(result.current.parameters).toEqual(defaultParameters);
expect(result.current.parameters.angle).toBe(0);
@@ -12,7 +15,7 @@ describe('useRotateParameters', () => {
});
test('should validate parameters correctly', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
// Default should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -31,7 +34,7 @@ describe('useRotateParameters', () => {
});
test('should rotate clockwise correctly', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
act(() => {
result.current.rotateClockwise();
@@ -58,7 +61,7 @@ describe('useRotateParameters', () => {
});
test('should rotate anticlockwise correctly', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
act(() => {
result.current.rotateAnticlockwise();
@@ -85,7 +88,7 @@ describe('useRotateParameters', () => {
});
test('should normalize angles correctly', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
expect(result.current.normalizeAngle(360)).toBe(0);
expect(result.current.normalizeAngle(450)).toBe(90);
@@ -94,7 +97,7 @@ describe('useRotateParameters', () => {
});
test('should reset parameters correctly', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
// Set some rotation
act(() => {
@@ -116,7 +119,7 @@ describe('useRotateParameters', () => {
});
test('should update parameters', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
act(() => {
result.current.updateParameter('angle', 450);
@@ -132,13 +135,13 @@ describe('useRotateParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
expect(result.current.getEndpointName()).toBe('rotate-pdf');
});
test('should detect rotation state correctly', () => {
const { result } = renderHook(() => useRotateParameters());
const { result } = renderRotateHook();
// Initially no rotation
expect(result.current.hasRotation).toBe(false);
@@ -1,16 +1,19 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { defaultParameters, useSanitizeParameters } from '@app/hooks/tools/sanitize/useSanitizeParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderSanitizeHook = () => renderHook(() => useSanitizeParameters(), { wrapper: PreferencesTestWrapper });
describe('useSanitizeParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useSanitizeParameters());
const { result } = renderSanitizeHook();
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should update individual parameters', () => {
const { result } = renderHook(() => useSanitizeParameters());
const { result } = renderSanitizeHook();
act(() => {
result.current.updateParameter('removeXMPMetadata', true);
@@ -23,7 +26,7 @@ describe('useSanitizeParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useSanitizeParameters());
const { result } = renderSanitizeHook();
// First, change some parameters
act(() => {
@@ -43,13 +46,13 @@ describe('useSanitizeParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useSanitizeParameters());
const { result } = renderSanitizeHook();
expect(result.current.getEndpointName()).toBe('sanitize-pdf');
});
test('should validate parameters correctly', () => {
const { result } = renderHook(() => useSanitizeParameters());
const { result } = renderSanitizeHook();
// Default state should be valid (has removeJavaScript and removeEmbeddedFiles enabled)
expect(result.current.validateParameters()).toBe(true);
@@ -71,7 +74,7 @@ describe('useSanitizeParameters', () => {
});
test('should handle all parameter types correctly', () => {
const { result } = renderHook(() => useSanitizeParameters());
const { result } = renderSanitizeHook();
const allParameters = Object.keys(defaultParameters) as (keyof typeof defaultParameters)[];
@@ -1,4 +1,6 @@
import { useState, useCallback, Dispatch, SetStateAction } from 'react';
import { useState, useCallback, Dispatch, SetStateAction, useRef, useEffect } from 'react';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { loadToolParameters, saveToolParameters } from '@app/services/toolParameterStorage';
export interface BaseParametersHook<T> {
parameters: T;
@@ -13,10 +15,31 @@ export interface BaseParametersConfig<T> {
defaultParameters: T;
endpointName: string | ((params: T) => string);
validateFn?: (params: T) => boolean;
storageKey?: string;
}
export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParametersHook<T> {
const [parameters, setParameters] = useState<T>(config.defaultParameters);
const { preferences } = usePreferences();
const storageKey = config.storageKey ?? (typeof config.endpointName === 'string' ? config.endpointName : undefined);
const shouldPersist = Boolean(storageKey && preferences.rememberFormInputs);
const hasHydratedFromStorage = useRef(false);
const mergeWithDefaults = useCallback((values?: Partial<T>): T => ({
...(config.defaultParameters as T),
...(values ?? {}),
}), [config.defaultParameters]);
const [parameters, setParameters] = useState<T>(() => {
if (shouldPersist && storageKey) {
const stored = loadToolParameters<T>(storageKey);
if (stored) {
hasHydratedFromStorage.current = true;
return mergeWithDefaults(stored);
}
}
return mergeWithDefaults();
});
const updateParameter = useCallback(<K extends keyof T>(parameter: K, value: T[K]) => {
setParameters(prev => ({
@@ -33,6 +56,34 @@ export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParam
return config.validateFn ? config.validateFn(parameters) : true;
}, [parameters, config.validateFn]);
useEffect(() => {
if (!storageKey) {
return;
}
if (!preferences.rememberFormInputs) {
hasHydratedFromStorage.current = false;
return;
}
if (hasHydratedFromStorage.current) {
return;
}
const stored = loadToolParameters<T>(storageKey);
if (stored) {
setParameters(mergeWithDefaults(stored));
}
hasHydratedFromStorage.current = true;
}, [mergeWithDefaults, preferences.rememberFormInputs, storageKey]);
useEffect(() => {
if (!storageKey || !preferences.rememberFormInputs) {
return;
}
saveToolParameters(storageKey, parameters);
}, [parameters, preferences.rememberFormInputs, storageKey]);
const endpointName = config.endpointName;
let getEndpointName: () => string;
if (typeof endpointName === "string") {
@@ -57,5 +57,6 @@ export const useSplitParameters = (): SplitParametersHook => {
return false;
}
},
storageKey: 'split',
});
};
@@ -9,6 +9,7 @@ export interface UserPreferences {
toolPanelModePromptSeen: boolean;
showLegacyToolDescriptions: boolean;
hasCompletedOnboarding: boolean;
rememberFormInputs: boolean;
}
export const DEFAULT_PREFERENCES: UserPreferences = {
@@ -19,6 +20,7 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
toolPanelModePromptSeen: false,
showLegacyToolDescriptions: false,
hasCompletedOnboarding: false,
rememberFormInputs: false,
};
const STORAGE_KEY = 'stirlingpdf_preferences';
@@ -0,0 +1,114 @@
const STORAGE_KEY = 'stirlingpdf_tool_parameters';
type SerializableValue = string | number | boolean | null | SerializableValue[] | { [key: string]: SerializableValue };
const isFile = (value: unknown): value is File => typeof File !== 'undefined' && value instanceof File;
const isBlob = (value: unknown): value is Blob => typeof Blob !== 'undefined' && value instanceof Blob;
function sanitizeValue(value: unknown): SerializableValue | undefined {
if (value === null) {
return null;
}
const valueType = typeof value;
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
if (valueType === 'number' && Number.isNaN(value)) {
return null;
}
return value as SerializableValue;
}
if (valueType === 'undefined' || valueType === 'function' || valueType === 'symbol' || valueType === 'bigint') {
return undefined;
}
if (Array.isArray(value)) {
const sanitizedArray = value
.map((item) => sanitizeValue(item))
.filter((item) => item !== undefined) as SerializableValue[];
return sanitizedArray;
}
if (isFile(value) || isBlob(value)) {
return undefined;
}
if (valueType === 'object' && value !== null) {
const entries = Object.entries(value as Record<string, unknown>);
const sanitizedEntries: Record<string, SerializableValue> = {};
for (const [key, entryValue] of entries) {
const sanitized = sanitizeValue(entryValue);
if (sanitized !== undefined) {
sanitizedEntries[key] = sanitized;
}
}
return sanitizedEntries;
}
return undefined;
}
function readStorage(): Record<string, SerializableValue> {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (typeof parsed === 'object' && parsed !== null) {
return parsed as Record<string, SerializableValue>;
}
} catch (error) {
console.error('[toolParameterStorage] Failed to read stored parameters', error);
}
return {};
}
function writeStorage(map: Record<string, SerializableValue>): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
} catch (error) {
console.error('[toolParameterStorage] Failed to write stored parameters', error);
}
}
export function loadToolParameters<T>(toolKey: string): Partial<T> | null {
const storage = readStorage();
const stored = storage[toolKey];
if (!stored || typeof stored !== 'object') {
return null;
}
return stored as Partial<T>;
}
export function saveToolParameters<T>(toolKey: string, parameters: T): void {
const sanitized = sanitizeValue(parameters);
if (sanitized === undefined) {
return;
}
const storage = readStorage();
storage[toolKey] = sanitized;
writeStorage(storage);
}
export function clearToolParameters(toolKey: string): void {
const storage = readStorage();
if (Object.prototype.hasOwnProperty.call(storage, toolKey)) {
delete storage[toolKey];
writeStorage(storage);
}
}
export function clearAllToolParameters(): void {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error('[toolParameterStorage] Failed to clear stored parameters', error);
}
}
@@ -0,0 +1,6 @@
import React from 'react';
import { PreferencesProvider } from '@app/contexts/PreferencesContext';
export const PreferencesTestWrapper = ({ children }: { children: React.ReactNode }) => (
<PreferencesProvider>{children}</PreferencesProvider>
);
+2 -1
View File
@@ -4,7 +4,8 @@
"paths": {
"@app/*": [
"src/core/*"
]
],
"@testing/*": ["src/testing/*"]
}
},
"exclude": [
+2 -1
View File
@@ -9,7 +9,8 @@
],
"@desktop/*": ["src/desktop/*"],
"@proprietary/*": ["src/proprietary/*"],
"@core/*": ["src/core/*"]
"@core/*": ["src/core/*"],
"@testing/*": ["src/testing/*"]
}
},
"exclude": [
+1
View File
@@ -36,6 +36,7 @@
"src/proprietary/*",
"src/core/*"
],
"@testing/*": ["src/testing/*"],
"@desktop/*": ["src/desktop/*"],
"@proprietary/*": ["src/proprietary/*"],
"@core/*": ["src/core/*"]
+2 -1
View File
@@ -6,7 +6,8 @@
"src/proprietary/*",
"src/core/*"
],
"@core/*": ["src/core/*"]
"@core/*": ["src/core/*"],
"@testing/*": ["src/testing/*"]
}
},
"exclude": [