136 lines
4.5 KiB
TypeScript
136 lines
4.5 KiB
TypeScript
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { useFlyerUploader } from './useFlyerUploader';
|
|
import * as aiApiClient from '../services/aiApiClient';
|
|
import * as checksumUtil from '../utils/checksum';
|
|
|
|
// Import the actual error class because the module is mocked
|
|
const { JobFailedError } = await vi.importActual<typeof import('../services/aiApiClient')>(
|
|
'../services/aiApiClient',
|
|
);
|
|
|
|
// Mock dependencies
|
|
vi.mock('../services/aiApiClient');
|
|
vi.mock('../utils/checksum');
|
|
vi.mock('../services/logger.client', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const mockedAiApiClient = vi.mocked(aiApiClient);
|
|
const mockedChecksumUtil = vi.mocked(checksumUtil);
|
|
|
|
// Helper to wrap the hook with QueryClientProvider, which is required by react-query
|
|
const createWrapper = () => {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false, // Disable retries for tests for predictable behavior
|
|
},
|
|
},
|
|
});
|
|
return ({ children }: { children: React.ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
};
|
|
|
|
describe('useFlyerUploader Hook with React Query', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
mockedChecksumUtil.generateFileChecksum.mockResolvedValue('mock-checksum');
|
|
});
|
|
|
|
it('should handle a successful upload and polling flow', async () => {
|
|
// Arrange
|
|
const mockJobId = 'job-123';
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: mockJobId });
|
|
mockedAiApiClient.getJobStatus
|
|
.mockResolvedValueOnce({
|
|
// First poll: active
|
|
id: mockJobId,
|
|
state: 'active',
|
|
progress: { message: 'Processing...' },
|
|
returnValue: null,
|
|
failedReason: null,
|
|
} as aiApiClient.JobStatus)
|
|
.mockResolvedValueOnce({
|
|
// Second poll: completed
|
|
id: mockJobId,
|
|
state: 'completed',
|
|
progress: { message: 'Complete!' },
|
|
returnValue: { flyerId: 777 },
|
|
failedReason: null,
|
|
} as aiApiClient.JobStatus);
|
|
|
|
const { result } = renderHook(() => useFlyerUploader(), { wrapper: createWrapper() });
|
|
const mockFile = new File([''], 'flyer.pdf');
|
|
|
|
// Act
|
|
await act(async () => {
|
|
result.current.upload(mockFile);
|
|
});
|
|
|
|
// Assert initial upload state
|
|
await waitFor(() => expect(result.current.processingState).toBe('polling'));
|
|
expect(result.current.jobId).toBe(mockJobId);
|
|
|
|
// Assert polling state
|
|
await waitFor(() => expect(result.current.statusMessage).toBe('Processing...'));
|
|
|
|
// Assert completed state
|
|
await waitFor(() => expect(result.current.processingState).toBe('completed'), { timeout: 5000 });
|
|
expect(result.current.flyerId).toBe(777);
|
|
});
|
|
|
|
it('should handle an upload failure', async () => {
|
|
// Arrange
|
|
const uploadError = {
|
|
status: 409,
|
|
body: { message: 'Duplicate flyer detected.', flyerId: 99 },
|
|
};
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockRejectedValue(uploadError);
|
|
|
|
const { result } = renderHook(() => useFlyerUploader(), { wrapper: createWrapper() });
|
|
const mockFile = new File([''], 'flyer.pdf');
|
|
|
|
// Act
|
|
await act(async () => {
|
|
result.current.upload(mockFile);
|
|
});
|
|
|
|
// Assert error state
|
|
await waitFor(() => expect(result.current.processingState).toBe('error'));
|
|
expect(result.current.errorMessage).toBe('Duplicate flyer detected.');
|
|
expect(result.current.duplicateFlyerId).toBe(99);
|
|
expect(result.current.jobId).toBeNull();
|
|
});
|
|
|
|
it('should handle a job failure during polling', async () => {
|
|
// Arrange
|
|
const mockJobId = 'job-456';
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: mockJobId });
|
|
|
|
// Mock getJobStatus to throw a JobFailedError
|
|
mockedAiApiClient.getJobStatus.mockRejectedValue(
|
|
new JobFailedError('AI validation failed.', 'AI_VALIDATION_FAILED'),
|
|
);
|
|
|
|
const { result } = renderHook(() => useFlyerUploader(), { wrapper: createWrapper() });
|
|
const mockFile = new File([''], 'flyer.pdf');
|
|
|
|
// Act
|
|
await act(async () => {
|
|
result.current.upload(mockFile);
|
|
});
|
|
|
|
// Assert error state after polling fails
|
|
await waitFor(() => expect(result.current.processingState).toBe('error'));
|
|
expect(result.current.errorMessage).toBe('Polling failed: AI validation failed.');
|
|
expect(result.current.flyerId).toBeNull();
|
|
});
|
|
}); |