All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 11m56s
522 lines
23 KiB
TypeScript
522 lines
23 KiB
TypeScript
// src/features/flyer/FlyerUploader.test.tsx
|
|
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
|
|
import { FlyerUploader } from './FlyerUploader';
|
|
import * as aiApiClientModule from '../../services/aiApiClient';
|
|
import * as checksumModule from '../../utils/checksum';
|
|
import { useNavigate, MemoryRouter } from 'react-router-dom';
|
|
import { QueryClient, QueryClientProvider, onlineManager } from '@tanstack/react-query';
|
|
|
|
// Mock dependencies
|
|
vi.mock('../../services/aiApiClient', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../../services/aiApiClient')>();
|
|
return {
|
|
...actual,
|
|
uploadAndProcessFlyer: vi.fn(),
|
|
getJobStatus: vi.fn(),
|
|
};
|
|
});
|
|
vi.mock('../../services/logger.client', () => ({
|
|
// Keep the original logger.info/error but also spy on it for test assertions if needed
|
|
logger: {
|
|
info: vi.fn((...args) => console.log('[LOGGER.INFO]', ...args)),
|
|
error: vi.fn((...args) => console.error('[LOGGER.ERROR]', ...args)),
|
|
warn: vi.fn((...args) => console.warn('[LOGGER.WARN]', ...args)),
|
|
debug: vi.fn((...args) => console.debug('[LOGGER.DEBUG]', ...args)),
|
|
},
|
|
}));
|
|
vi.mock('../../utils/checksum', () => ({
|
|
generateFileChecksum: vi.fn(),
|
|
}));
|
|
|
|
// Mock react-router-dom
|
|
vi.mock('react-router-dom', async () => {
|
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
|
return {
|
|
...actual,
|
|
useNavigate: vi.fn(),
|
|
};
|
|
});
|
|
|
|
const mockedAiApiClient = aiApiClientModule as unknown as {
|
|
uploadAndProcessFlyer: Mock;
|
|
getJobStatus: Mock;
|
|
};
|
|
const mockedChecksumModule = checksumModule as unknown as {
|
|
generateFileChecksum: Mock;
|
|
};
|
|
|
|
const renderComponent = (onProcessingComplete = vi.fn()) => {
|
|
console.log('--- [TEST LOG] ---: Rendering component inside MemoryRouter.');
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
},
|
|
});
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<MemoryRouter>
|
|
<FlyerUploader onProcessingComplete={onProcessingComplete} />
|
|
</MemoryRouter>
|
|
</QueryClientProvider>,
|
|
);
|
|
};
|
|
|
|
describe('FlyerUploader', () => {
|
|
const navigateSpy = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
// Disable react-query's online manager to prevent it from interfering with fake timers
|
|
onlineManager.setEventListener((setOnline) => {
|
|
return () => {};
|
|
});
|
|
console.log(`\n--- [TEST LOG] ---: Starting test: "${expect.getState().currentTestName}"`);
|
|
vi.resetAllMocks(); // Resets mock implementations AND call history.
|
|
console.log('--- [TEST LOG] ---: Mocks reset.');
|
|
mockedChecksumModule.generateFileChecksum.mockResolvedValue('mock-checksum');
|
|
(useNavigate as Mock).mockReturnValue(navigateSpy);
|
|
});
|
|
|
|
afterEach(() => {
|
|
console.log(`--- [TEST LOG] ---: Finished test: "${expect.getState().currentTestName}"\n`);
|
|
});
|
|
|
|
it('should render the initial state correctly', () => {
|
|
renderComponent();
|
|
expect(screen.getByText('Upload New Flyer')).toBeInTheDocument();
|
|
expect(screen.getByText(/click to select a file/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should handle file upload and start polling', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mocks for upload and polling.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-123' });
|
|
mockedAiApiClient.getJobStatus.mockResolvedValue({
|
|
state: 'active',
|
|
progress: { message: 'Checking...' },
|
|
});
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Rendering component and preparing file.');
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
console.log('--- [TEST LOG] ---: 4. File change event fired. Now AWAITING UI update.');
|
|
|
|
try {
|
|
console.log('--- [TEST LOG] ---: 5a. Awaiting screen.findByText("Checking...")');
|
|
await screen.findByText('Checking...');
|
|
console.log('--- [TEST LOG] ---: 5b. SUCCESS: UI updated to polling state.');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 5c. ERROR: findByText("Checking...") timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug(); // Print the DOM when the error occurs
|
|
throw error; // Re-throw the error to fail the test
|
|
}
|
|
|
|
console.log('--- [TEST LOG] ---: 6. Asserting mock calls after UI update.');
|
|
expect(mockedAiApiClient.uploadAndProcessFlyer).toHaveBeenCalledWith(file, 'mock-checksum');
|
|
expect(mockedAiApiClient.getJobStatus).toHaveBeenCalledTimes(1);
|
|
console.log('--- [TEST LOG] ---: 7. Mocks verified. Advancing timers now...');
|
|
|
|
// With real timers, we now wait for the polling interval to elapse.
|
|
console.log(
|
|
`--- [TEST LOG] ---: 9. Act block finished. Now checking if getJobStatus was called again.`,
|
|
);
|
|
|
|
try {
|
|
// The polling interval is 3s, so we wait for a bit longer.
|
|
await waitFor(() => {
|
|
const calls = mockedAiApiClient.getJobStatus.mock.calls.length;
|
|
console.log(`--- [TEST LOG] ---: 10. waitFor check: getJobStatus calls = ${calls}`);
|
|
expect(mockedAiApiClient.getJobStatus).toHaveBeenCalledTimes(2);
|
|
}, { timeout: 4000 });
|
|
console.log('--- [TEST LOG] ---: 11. SUCCESS: Second poll confirmed.');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 11. ERROR: waitFor for second poll timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug();
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
it('should handle file upload via drag and drop', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mocks for drag and drop.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-dnd' });
|
|
mockedAiApiClient.getJobStatus.mockResolvedValue({
|
|
state: 'active',
|
|
progress: { message: 'Dropped...' },
|
|
});
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Rendering component and preparing file for drop.');
|
|
renderComponent();
|
|
const file = new File(['dnd-content'], 'dnd-flyer.pdf', { type: 'application/pdf' });
|
|
// The dropzone is the label element
|
|
const dropzone = screen.getByText(/click to select a file/i).closest('label')!;
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Firing drop event.');
|
|
// Simulate the drop event
|
|
fireEvent.drop(dropzone, {
|
|
dataTransfer: { files: [file] },
|
|
});
|
|
|
|
console.log('--- [TEST LOG] ---: 4. Awaiting UI update to "Dropped...".');
|
|
await screen.findByText('Dropped...');
|
|
console.log('--- [TEST LOG] ---: 5. Asserting upload was called.');
|
|
expect(mockedAiApiClient.uploadAndProcessFlyer).toHaveBeenCalledWith(file, 'mock-checksum');
|
|
});
|
|
|
|
it('should poll for status, complete successfully, and redirect', async () => {
|
|
const onProcessingComplete = vi.fn();
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mock sequence for polling.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-123' });
|
|
mockedAiApiClient.getJobStatus
|
|
.mockResolvedValueOnce({ state: 'active', progress: { message: 'Analyzing...' } })
|
|
.mockResolvedValueOnce({ state: 'completed', returnValue: { flyerId: 42 } });
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Rendering component and uploading file.');
|
|
renderComponent(onProcessingComplete);
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Fired event. Now AWAITING UI update to "Analyzing...".');
|
|
try {
|
|
await screen.findByText('Analyzing...');
|
|
console.log('--- [TEST LOG] ---: 4. SUCCESS: UI is showing "Analyzing...".');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 4. ERROR: findByText("Analyzing...") timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug();
|
|
throw error;
|
|
}
|
|
expect(mockedAiApiClient.getJobStatus).toHaveBeenCalledTimes(1);
|
|
console.log('--- [TEST LOG] ---: 5. First poll confirmed. Now AWAITING timer advancement.');
|
|
|
|
try {
|
|
console.log(
|
|
'--- [TEST LOG] ---: 8a. waitFor check: Waiting for completion text and job status count.',
|
|
);
|
|
// Wait for the second poll to occur and the UI to update.
|
|
await waitFor(() => {
|
|
console.log(
|
|
`--- [TEST LOG] ---: 8b. waitFor interval: calls=${
|
|
mockedAiApiClient.getJobStatus.mock.calls.length
|
|
}`,
|
|
);
|
|
expect(
|
|
screen.getByText('Processing complete! Redirecting to flyer 42...'),
|
|
).toBeInTheDocument();
|
|
}, { timeout: 4000 });
|
|
console.log('--- [TEST LOG] ---: 9. SUCCESS: Completion message found.');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 9. ERROR: waitFor for completion message timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug();
|
|
throw error;
|
|
}
|
|
expect(mockedAiApiClient.getJobStatus).toHaveBeenCalledTimes(2);
|
|
|
|
// Wait for the redirect timer (1.5s in component) to fire.
|
|
await act(() => new Promise((r) => setTimeout(r, 2000)));
|
|
console.log(`--- [TEST LOG] ---: 11. Timers advanced. Now asserting navigation.`);
|
|
expect(onProcessingComplete).toHaveBeenCalled();
|
|
expect(navigateSpy).toHaveBeenCalledWith('/flyers/42');
|
|
console.log('--- [TEST LOG] ---: 12. Callback and navigation confirmed.');
|
|
});
|
|
|
|
it('should handle a failed job', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mocks for a failed job.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-fail' });
|
|
// The getJobStatus function throws a specific error when the job fails,
|
|
// which is then caught by react-query and placed in the `error` state.
|
|
const jobFailedError = new aiApiClientModule.JobFailedError('AI model exploded', 'UNKNOWN_ERROR');
|
|
mockedAiApiClient.getJobStatus.mockRejectedValue(jobFailedError);
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Rendering and uploading.');
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
console.log('--- [TEST LOG] ---: 3. File upload triggered.');
|
|
|
|
try {
|
|
console.log('--- [TEST LOG] ---: 4. AWAITING failure message...');
|
|
// The UI should now display the error from the `pollError` state, which includes the "Polling failed" prefix.
|
|
expect(await screen.findByText(/Polling failed: AI model exploded/i)).toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 5. SUCCESS: Failure message found.');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 5. ERROR: findByText for failure message timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug();
|
|
throw error;
|
|
}
|
|
|
|
expect(screen.getByText('Upload Another Flyer')).toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 6. "Upload Another" button confirmed.');
|
|
});
|
|
|
|
it('should clear the polling timeout when a job fails', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mocks for failed job timeout clearance.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-fail-timeout' });
|
|
|
|
// We need at least one 'active' response to establish a timeout loop so we have something to clear
|
|
// The second call should be a rejection, as this is how getJobStatus signals a failure.
|
|
mockedAiApiClient.getJobStatus
|
|
.mockResolvedValueOnce({
|
|
state: 'active',
|
|
progress: { message: 'Working...' },
|
|
} as aiApiClientModule.JobStatus)
|
|
.mockRejectedValueOnce(new aiApiClientModule.JobFailedError('Fatal Error', 'UNKNOWN_ERROR'));
|
|
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
// Wait for the first poll to complete and UI to update to "Working..."
|
|
await screen.findByText('Working...');
|
|
|
|
// Wait for the failure UI
|
|
await waitFor(() => expect(screen.getByText(/Polling failed: Fatal Error/i)).toBeInTheDocument(), { timeout: 4000 });
|
|
});
|
|
|
|
it('should stop polling for job status when the component unmounts', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mocks for unmount polling stop.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-unmount' });
|
|
// Mock getJobStatus to always return 'active' to keep polling
|
|
mockedAiApiClient.getJobStatus.mockResolvedValue({
|
|
state: 'active',
|
|
progress: { message: 'Polling...' },
|
|
});
|
|
|
|
const { unmount } = renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
// Wait for the first poll to complete and UI to update
|
|
await screen.findByText('Polling...');
|
|
|
|
// Wait for exactly one call to be sure polling has started.
|
|
await waitFor(() => {
|
|
expect(mockedAiApiClient.getJobStatus).toHaveBeenCalledTimes(1);
|
|
});
|
|
console.log('--- [TEST LOG] ---: 2. First poll confirmed.');
|
|
|
|
// Record the number of calls before unmounting.
|
|
const callsBeforeUnmount = mockedAiApiClient.getJobStatus.mock.calls.length;
|
|
|
|
// Now unmount the component, which should stop the polling.
|
|
console.log('--- [TEST LOG] ---: 3. Unmounting component.');
|
|
unmount();
|
|
|
|
// Wait for a duration longer than the polling interval (3s) to see if more calls are made.
|
|
console.log('--- [TEST LOG] ---: 4. Waiting for 4 seconds to check for further polling.');
|
|
await act(() => new Promise((resolve) => setTimeout(resolve, 4000)));
|
|
|
|
// Verify that getJobStatus was not called again after unmounting.
|
|
console.log('--- [TEST LOG] ---: 5. Asserting no new polls occurred.');
|
|
expect(mockedAiApiClient.getJobStatus).toHaveBeenCalledTimes(callsBeforeUnmount);
|
|
});
|
|
|
|
it('should handle a duplicate flyer error (409)', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mock for 409 duplicate error.');
|
|
// The API client throws a structured error, which useFlyerUploader now parses
|
|
// to set both the errorMessage and the duplicateFlyerId.
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockRejectedValue({
|
|
status: 409,
|
|
body: { flyerId: 99, message: 'This flyer has already been processed.' },
|
|
});
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Rendering and uploading.');
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
console.log('--- [TEST LOG] ---: 3. File upload triggered.');
|
|
|
|
try {
|
|
console.log('--- [TEST LOG] ---: 4. AWAITING duplicate flyer message...');
|
|
// With the fix, the duplicate error message and the link are combined into a single paragraph.
|
|
// We now look for this combined message.
|
|
const errorMessage = await screen.findByText(/This flyer has already been processed. You can view it here:/i);
|
|
expect(errorMessage).toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 5. SUCCESS: Duplicate message found.');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 5. ERROR: findByText for duplicate message timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug();
|
|
throw error;
|
|
}
|
|
|
|
const link = screen.getByRole('link', { name: /Flyer #99/i });
|
|
expect(link).toHaveAttribute('href', '/flyers/99');
|
|
console.log('--- [TEST LOG] ---: 6. Duplicate link confirmed.');
|
|
});
|
|
|
|
it('should allow the user to stop watching progress', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mocks for infinite polling.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-stop' });
|
|
mockedAiApiClient.getJobStatus.mockResolvedValue({
|
|
state: 'active',
|
|
progress: { message: 'Analyzing...' },
|
|
} as any);
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Rendering and uploading.');
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
console.log('--- [TEST LOG] ---: 3. File upload triggered.');
|
|
|
|
let stopButton;
|
|
try {
|
|
console.log('--- [TEST LOG] ---: 4. AWAITING polling UI...');
|
|
stopButton = await screen.findByRole('button', { name: 'Stop Watching Progress' });
|
|
console.log('--- [TEST LOG] ---: 5. SUCCESS: Polling UI is visible.');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 5. ERROR: findByRole for stop button timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug();
|
|
throw error;
|
|
}
|
|
|
|
console.log('--- [TEST LOG] ---: 6. Clicking "Stop Watching Progress" button.');
|
|
fireEvent.click(stopButton);
|
|
console.log('--- [TEST LOG] ---: 7. Click event fired.');
|
|
|
|
try {
|
|
console.log('--- [TEST LOG] ---: 8. AWAITING UI reset to idle state...');
|
|
expect(await screen.findByText(/click to select a file/i)).toBeInTheDocument();
|
|
// Fix typo: queryText -> queryByText
|
|
expect(screen.queryByText('Analyzing...')).not.toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 9. SUCCESS: UI has reset and message removed.');
|
|
} catch (error) {
|
|
console.error('--- [TEST LOG] ---: 9. ERROR: findByText for idle state timed out.');
|
|
console.log('--- [DEBUG] ---: DOM at time of failure:');
|
|
screen.debug();
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
describe('Error Handling and Edge Cases', () => {
|
|
it('should handle checksum generation failure', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mock for checksum failure.');
|
|
mockedChecksumModule.generateFileChecksum.mockRejectedValue(
|
|
new Error('Checksum generation failed'),
|
|
);
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Awaiting error message.');
|
|
expect(await screen.findByText(/Checksum generation failed/i)).toBeInTheDocument();
|
|
expect(mockedAiApiClient.uploadAndProcessFlyer).not.toHaveBeenCalled();
|
|
console.log('--- [TEST LOG] ---: 4. Assertions passed.');
|
|
});
|
|
|
|
it('should handle a generic network error during upload', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mock for generic upload error.');
|
|
// Simulate a structured error from the API client
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockRejectedValue({
|
|
status: 500,
|
|
body: { message: 'Network Error During Upload' },
|
|
});
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Awaiting error message.');
|
|
expect(await screen.findByText(/Network Error During Upload/i)).toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 4. Assertions passed.');
|
|
});
|
|
|
|
it('should handle a generic network error during polling', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mock for polling error.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-poll-fail' });
|
|
mockedAiApiClient.getJobStatus.mockRejectedValue(new Error('Polling Network Error'));
|
|
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Awaiting error message.');
|
|
expect(await screen.findByText(/Polling failed: Polling Network Error/i)).toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 4. Assertions passed.');
|
|
});
|
|
|
|
it('should handle a completed job with a missing flyerId', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mock for malformed completion payload.');
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-no-flyerid' });
|
|
mockedAiApiClient.getJobStatus.mockResolvedValue(
|
|
{ state: 'completed', returnValue: {} }, // No flyerId
|
|
);
|
|
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Awaiting error message.');
|
|
expect(
|
|
await screen.findByText(/Job completed but did not return a flyer ID/i),
|
|
).toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 4. Assertions passed.');
|
|
});
|
|
|
|
it('should handle a non-JSON response during polling', async () => {
|
|
console.log('--- [TEST LOG] ---: 1. Setting up mock for non-JSON response.');
|
|
// The actual function would throw, so we mock the rejection.
|
|
// The new getJobStatus would throw an error like "Failed to parse JSON..."
|
|
mockedAiApiClient.uploadAndProcessFlyer.mockResolvedValue({ jobId: 'job-bad-json' });
|
|
mockedAiApiClient.getJobStatus.mockRejectedValue(
|
|
new Error('Failed to parse JSON response from server. Body: <html>502 Bad Gateway</html>'),
|
|
);
|
|
|
|
renderComponent();
|
|
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
|
|
console.log('--- [TEST LOG] ---: 2. Firing file change event.');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
console.log('--- [TEST LOG] ---: 3. Awaiting error message.');
|
|
expect(
|
|
await screen.findByText(/Polling failed: Failed to parse JSON response from server/i),
|
|
).toBeInTheDocument();
|
|
console.log('--- [TEST LOG] ---: 4. Assertions passed.');
|
|
});
|
|
|
|
it('should do nothing if the file input is cancelled', () => {
|
|
renderComponent();
|
|
const input = screen.getByLabelText(/click to select a file/i);
|
|
fireEvent.change(input, { target: { files: [] } }); // Empty file list
|
|
expect(mockedAiApiClient.uploadAndProcessFlyer).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|