mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useRotateOperation } from './useRotateOperation';
|
||||
import type { RotateParameters } from './useRotateParameters';
|
||||
|
||||
// Mock the useToolOperation hook
|
||||
vi.mock('../shared/useToolOperation', async () => {
|
||||
const actual = await vi.importActual('../shared/useToolOperation');
|
||||
return {
|
||||
...actual,
|
||||
useToolOperation: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the translation hook
|
||||
const mockT = vi.fn((key: string) => `translated-${key}`);
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: mockT })
|
||||
}));
|
||||
|
||||
// Mock the error handler
|
||||
vi.mock('../../../utils/toolErrorHandler', () => ({
|
||||
createStandardErrorHandler: vi.fn(() => 'error-handler-function')
|
||||
}));
|
||||
|
||||
// Import the mocked function
|
||||
import { SingleFileToolOperationConfig, ToolOperationHook, ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||
|
||||
describe('useRotateOperation', () => {
|
||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||
|
||||
const getToolConfig = () => mockUseToolOperation.mock.calls[0][0] as SingleFileToolOperationConfig<RotateParameters>;
|
||||
|
||||
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||
files: [],
|
||||
thumbnails: [],
|
||||
downloadUrl: null,
|
||||
downloadFilename: '',
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
status: '',
|
||||
isGeneratingThumbnails: false,
|
||||
progress: null,
|
||||
executeOperation: vi.fn(),
|
||||
resetResults: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
cancelOperation: vi.fn(),
|
||||
undoOperation: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ angle: 0, expectedNormalized: 0 },
|
||||
{ angle: 90, expectedNormalized: 90 },
|
||||
{ angle: 180, expectedNormalized: 180 },
|
||||
{ angle: 270, expectedNormalized: 270 },
|
||||
{ angle: 360, expectedNormalized: 0 },
|
||||
{ angle: -90, expectedNormalized: 270 },
|
||||
{ angle: -180, expectedNormalized: 180 },
|
||||
{ angle: -270, expectedNormalized: 90 },
|
||||
{ angle: 450, expectedNormalized: 90 },
|
||||
])('should create form data correctly with angle $angle (normalized to $expectedNormalized)', ({ angle, expectedNormalized }) => {
|
||||
renderHook(() => useRotateOperation());
|
||||
|
||||
const callArgs = getToolConfig();
|
||||
|
||||
const testParameters: RotateParameters = { angle };
|
||||
const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
|
||||
const formData = callArgs.buildFormData(testParameters, testFile);
|
||||
|
||||
// Verify the form data contains the file
|
||||
expect(formData.get('fileInput')).toBe(testFile);
|
||||
|
||||
// Verify angle parameter is normalized for backend
|
||||
expect(formData.get('angle')).toBe(expectedNormalized.toString());
|
||||
});
|
||||
|
||||
test('should use correct translation for error messages', () => {
|
||||
renderHook(() => useRotateOperation());
|
||||
|
||||
expect(mockT).toHaveBeenCalledWith(
|
||||
'rotate.error.failed',
|
||||
'An error occurred while rotating the PDF.'
|
||||
);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ property: 'toolType' as const, expectedValue: ToolType.singleFile },
|
||||
{ property: 'endpoint' as const, expectedValue: '/api/v1/general/rotate-pdf' },
|
||||
{ property: 'operationType' as const, expectedValue: 'rotate' }
|
||||
])('should configure $property correctly', ({ property, expectedValue }) => {
|
||||
renderHook(() => useRotateOperation());
|
||||
|
||||
const callArgs = getToolConfig();
|
||||
expect(callArgs[property]).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { RotateParameters, defaultParameters, normalizeAngle } from './useRotateParameters';
|
||||
|
||||
// Static configuration that can be used by both the hook and automation executor
|
||||
export const buildRotateFormData = (parameters: RotateParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
// Normalize angle for backend (0, 90, 180, 270)
|
||||
formData.append("angle", normalizeAngle(parameters.angle).toString());
|
||||
return formData;
|
||||
};
|
||||
|
||||
// Static configuration object
|
||||
export const rotateOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildRotateFormData,
|
||||
operationType: 'rotate',
|
||||
endpoint: '/api/v1/general/rotate-pdf',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useRotateOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<RotateParameters>({
|
||||
...rotateOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('rotate.error.failed', 'An error occurred while rotating the PDF.'))
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useRotateParameters, defaultParameters, normalizeAngle } from './useRotateParameters';
|
||||
|
||||
describe('useRotateParameters', () => {
|
||||
test('should initialize with default parameters', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
expect(result.current.parameters).toEqual(defaultParameters);
|
||||
expect(result.current.parameters.angle).toBe(0);
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should validate parameters correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
// Default should be valid
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
|
||||
// Set invalid angle
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', 45);
|
||||
});
|
||||
expect(result.current.validateParameters()).toBe(false);
|
||||
|
||||
// Set valid angle
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', 90);
|
||||
});
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
});
|
||||
|
||||
test('should rotate clockwise correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(90);
|
||||
expect(result.current.hasRotation).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(180);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(270);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(360);
|
||||
expect(normalizeAngle(result.current.parameters.angle)).toBe(0);
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should rotate anticlockwise correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(-90);
|
||||
expect(result.current.hasRotation).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(-180);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(-270);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateAnticlockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(-360);
|
||||
expect(normalizeAngle(result.current.parameters.angle)).toBe(0);
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should normalize angles correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
expect(result.current.normalizeAngle(360)).toBe(0);
|
||||
expect(result.current.normalizeAngle(450)).toBe(90);
|
||||
expect(result.current.normalizeAngle(-90)).toBe(270);
|
||||
expect(result.current.normalizeAngle(-180)).toBe(180);
|
||||
});
|
||||
|
||||
test('should reset parameters correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
// Set some rotation
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(90);
|
||||
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(180);
|
||||
|
||||
// Reset
|
||||
act(() => {
|
||||
result.current.resetParameters();
|
||||
});
|
||||
expect(result.current.parameters).toEqual(defaultParameters);
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
|
||||
test('should update parameters', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', 450);
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(450);
|
||||
expect(normalizeAngle(result.current.parameters.angle)).toBe(90);
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('angle', -90);
|
||||
});
|
||||
expect(result.current.parameters.angle).toBe(-90);
|
||||
expect(normalizeAngle(result.current.parameters.angle)).toBe(270);
|
||||
});
|
||||
|
||||
test('should return correct endpoint name', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
expect(result.current.getEndpointName()).toBe('rotate-pdf');
|
||||
});
|
||||
|
||||
test('should detect rotation state correctly', () => {
|
||||
const { result } = renderHook(() => useRotateParameters());
|
||||
|
||||
// Initially no rotation
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
|
||||
// After rotation
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
expect(result.current.hasRotation).toBe(true);
|
||||
|
||||
// After full rotation (360 degrees) - 3 more clicks to complete 360°
|
||||
for (let i = 0; i < 3; i++) {
|
||||
act(() => {
|
||||
result.current.rotateClockwise();
|
||||
});
|
||||
}
|
||||
expect(result.current.hasRotation).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
|
||||
// Normalize angle to number between 0 and 359
|
||||
export const normalizeAngle = (angle: number): number => {
|
||||
return ((angle % 360) + 360) % 360;
|
||||
};
|
||||
|
||||
export interface RotateParameters extends BaseParameters {
|
||||
angle: number; // Current rotation angle (0, 90, 180, 270)
|
||||
}
|
||||
|
||||
export const defaultParameters: RotateParameters = {
|
||||
angle: 0,
|
||||
};
|
||||
|
||||
export type RotateParametersHook = BaseParametersHook<RotateParameters> & {
|
||||
rotateClockwise: () => void;
|
||||
rotateAnticlockwise: () => void;
|
||||
hasRotation: boolean;
|
||||
normalizeAngle: (angle: number) => number;
|
||||
};
|
||||
|
||||
export const useRotateParameters = (): RotateParametersHook => {
|
||||
const baseHook = useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'rotate-pdf',
|
||||
validateFn: (params) => {
|
||||
// Angle must be a multiple of 90
|
||||
return params.angle % 90 === 0;
|
||||
},
|
||||
});
|
||||
|
||||
// Rotate clockwise by 90 degrees
|
||||
const rotateClockwise = useCallback(() => {
|
||||
baseHook.updateParameter('angle', baseHook.parameters.angle + 90);
|
||||
}, [baseHook]);
|
||||
|
||||
// Rotate anticlockwise by 90 degrees
|
||||
const rotateAnticlockwise = useCallback(() => {
|
||||
baseHook.updateParameter('angle', baseHook.parameters.angle - 90);
|
||||
}, [baseHook]);
|
||||
|
||||
// Check if rotation will actually change the document
|
||||
const hasRotation = useMemo(() => {
|
||||
const normalized = normalizeAngle(baseHook.parameters.angle);
|
||||
return normalized !== 0;
|
||||
}, [baseHook.parameters.angle, normalizeAngle]);
|
||||
|
||||
// Override updateParameter - no longer normalize angles here
|
||||
const updateParameter = useCallback(<K extends keyof RotateParameters>(
|
||||
parameter: K,
|
||||
value: RotateParameters[K]
|
||||
) => {
|
||||
baseHook.updateParameter(parameter, value);
|
||||
}, [baseHook]);
|
||||
|
||||
return {
|
||||
...baseHook,
|
||||
updateParameter,
|
||||
rotateClockwise,
|
||||
rotateAnticlockwise,
|
||||
hasRotation,
|
||||
normalizeAngle,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useFileSelection } from '../../../contexts/FileContext';
|
||||
import { useEndpointEnabled } from '../../useEndpointConfig';
|
||||
import { BaseToolProps } from '../../../types/tool';
|
||||
@@ -45,6 +45,7 @@ export function useBaseTool<TParams, TParamsHook extends BaseParametersHook<TPar
|
||||
|
||||
// File selection
|
||||
const { selectedFiles } = useFileSelection();
|
||||
const previousFileCount = useRef(selectedFiles.length);
|
||||
|
||||
// Tool-specific hooks
|
||||
const params = useParams();
|
||||
@@ -67,6 +68,18 @@ export function useBaseTool<TParams, TParamsHook extends BaseParametersHook<TPar
|
||||
}
|
||||
}, [selectedFiles.length]);
|
||||
|
||||
// Reset parameters when transitioning from 0 files to at least 1 file
|
||||
useEffect(() => {
|
||||
const currentFileCount = selectedFiles.length;
|
||||
const prevFileCount = previousFileCount.current;
|
||||
|
||||
if (prevFileCount === 0 && currentFileCount > 0) {
|
||||
params.resetParameters();
|
||||
}
|
||||
|
||||
previousFileCount.current = currentFileCount;
|
||||
}, [selectedFiles.length]);
|
||||
|
||||
// Standard handlers
|
||||
const handleExecute = useCallback(async () => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user