All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m11s
270 lines
11 KiB
TypeScript
270 lines
11 KiB
TypeScript
// src/features/flyer/ProcessingStatus.test.tsx
|
|
import React from 'react';
|
|
import { render, screen, act } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { ProcessingStatus } from './ProcessingStatus';
|
|
import type { ProcessingStage } from '../../types';
|
|
import { createMockProcessingStage } from '../../tests/utils/mockFactories';
|
|
|
|
describe('ProcessingStatus', () => {
|
|
const mockStages: ProcessingStage[] = [
|
|
createMockProcessingStage({ name: 'Uploading File', status: 'completed', detail: 'Done' }),
|
|
createMockProcessingStage({
|
|
name: 'Converting to Image',
|
|
status: 'in-progress',
|
|
detail: 'Page 2 of 5...',
|
|
}),
|
|
createMockProcessingStage({ name: 'Extracting Text', status: 'pending', detail: '' }),
|
|
createMockProcessingStage({
|
|
name: 'Analyzing with AI',
|
|
status: 'error',
|
|
detail: 'AI model timeout',
|
|
critical: false,
|
|
}),
|
|
createMockProcessingStage({
|
|
name: 'Saving to Database',
|
|
status: 'error',
|
|
detail: 'Connection failed',
|
|
critical: true,
|
|
}),
|
|
];
|
|
|
|
describe('Single File Layout', () => {
|
|
it('should render the title and initial time remaining', () => {
|
|
render(<ProcessingStatus stages={[]} estimatedTime={125} />);
|
|
expect(screen.getByRole('heading', { name: /processing your flyer/i })).toBeInTheDocument();
|
|
expect(screen.getByText(/estimated time remaining: 2m 5s/i)).toBeInTheDocument();
|
|
});
|
|
|
|
describe('with fake timers', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should count down the time remaining', () => {
|
|
render(<ProcessingStatus stages={[]} estimatedTime={125} />);
|
|
expect(screen.getByText(/estimated time remaining: 2m 5s/i)).toBeInTheDocument();
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(3000); // Advance time by 3 seconds
|
|
});
|
|
|
|
expect(screen.getByText(/estimated time remaining: 2m 2s/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should stop the countdown at 0', () => {
|
|
render(<ProcessingStatus stages={[]} estimatedTime={2} />);
|
|
expect(screen.getByText(/estimated time remaining: 0m 2s/i)).toBeInTheDocument();
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(5000); // Advance time by more than the remaining time
|
|
});
|
|
|
|
expect(screen.getByText(/estimated time remaining: 0m 0s/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should render all stages with correct statuses and icons', () => {
|
|
render(<ProcessingStatus stages={mockStages} estimatedTime={120} />);
|
|
|
|
// Completed stage
|
|
const completedStageText = screen.getByTestId('stage-text-0');
|
|
expect(completedStageText.className).toContain('text-gray-700');
|
|
expect(completedStageText).toHaveTextContent('Uploading File');
|
|
expect(screen.getByTestId('stage-icon-0').querySelector('svg')).toHaveClass('text-green-500');
|
|
|
|
// In-progress stage`
|
|
const inProgressStageText = screen.getByTestId('stage-text-1');
|
|
expect(inProgressStageText.className).toContain('text-brand-primary');
|
|
expect(screen.getByTestId('stage-icon-1').querySelector('svg')).toHaveClass('animate-spin');
|
|
|
|
// Pending stage
|
|
const pendingStageText = screen.getByTestId('stage-text-2');
|
|
expect(pendingStageText.className).toContain('text-gray-400');
|
|
expect(screen.getByTestId('stage-icon-2').querySelector('div')).toHaveClass(
|
|
'border-gray-400',
|
|
);
|
|
|
|
// Non-critical error stage
|
|
const nonCriticalErrorStageText = screen.getByTestId('stage-text-3');
|
|
expect(nonCriticalErrorStageText.className).toContain('text-yellow-600');
|
|
expect(screen.getByTestId('stage-icon-3').querySelector('svg')).toHaveClass(
|
|
'text-yellow-500',
|
|
);
|
|
expect(screen.getByText(/optional/i)).toBeInTheDocument();
|
|
|
|
// Critical error stage
|
|
const criticalErrorStageText = screen.getByTestId('stage-text-4');
|
|
expect(criticalErrorStageText.className).toContain('text-red-500');
|
|
expect(screen.getByTestId('stage-icon-4').querySelector('svg')).toHaveClass('text-red-500');
|
|
});
|
|
|
|
it('should render PDF conversion progress bar', () => {
|
|
render(
|
|
<ProcessingStatus
|
|
stages={[]}
|
|
estimatedTime={60}
|
|
pageProgress={{ current: 3, total: 10 }}
|
|
/>,
|
|
);
|
|
const progressBar = screen.getByText(/converting pdf: page 3 of 10/i).nextElementSibling
|
|
?.firstChild;
|
|
expect(progressBar).toBeInTheDocument();
|
|
expect(progressBar).toHaveStyle('width: 30%');
|
|
});
|
|
|
|
it('should render item extraction progress bar for a stage', () => {
|
|
const stagesWithProgress: ProcessingStage[] = [
|
|
createMockProcessingStage({
|
|
name: 'Extracting Items',
|
|
status: 'in-progress',
|
|
progress: { current: 4, total: 8 },
|
|
}),
|
|
];
|
|
render(<ProcessingStatus stages={stagesWithProgress} estimatedTime={60} />);
|
|
const progressBar = screen.getByText(/analyzing page 4 of 8/i).nextElementSibling?.firstChild;
|
|
expect(progressBar).toBeInTheDocument();
|
|
expect(progressBar).toHaveStyle('width: 50%');
|
|
});
|
|
});
|
|
|
|
describe('Bulk Processing Layout', () => {
|
|
const bulkProps = {
|
|
stages: mockStages,
|
|
estimatedTime: 300,
|
|
currentFile: 'flyer_batch_01.pdf',
|
|
bulkProgress: 25,
|
|
bulkFileCount: { current: 2, total: 8 },
|
|
};
|
|
|
|
it('should render the bulk processing layout with current file name', () => {
|
|
render(<ProcessingStatus {...bulkProps} />);
|
|
// The heading now includes the filename, so we can check for it in one assertion.
|
|
const heading = screen.getByRole('heading', { name: /Processing: flyer_batch_01.pdf/i });
|
|
expect(heading).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render the overall bulk progress bar', () => {
|
|
render(<ProcessingStatus {...bulkProps} />);
|
|
const progressBar = screen.getByText(/file 2 of 8/i).nextElementSibling?.firstChild;
|
|
expect(progressBar).toBeInTheDocument();
|
|
expect(progressBar).toHaveStyle('width: 25%');
|
|
});
|
|
|
|
it('should render the PDF conversion progress bar in bulk mode', () => {
|
|
render(<ProcessingStatus {...bulkProps} pageProgress={{ current: 1, total: 5 }} />);
|
|
const progressBar = screen.getByText(/converting pdf: page 1 of 5/i).nextElementSibling
|
|
?.firstChild;
|
|
expect(progressBar).toBeInTheDocument();
|
|
expect(progressBar).toHaveStyle('width: 20%');
|
|
});
|
|
|
|
it('should render the item extraction progress bar from the correct stage in bulk mode', () => {
|
|
const stagesWithProgress: ProcessingStage[] = [
|
|
createMockProcessingStage({ name: 'Some Other Step', status: 'completed' }),
|
|
createMockProcessingStage({
|
|
name: 'Extracting All Items from Flyer',
|
|
status: 'in-progress',
|
|
progress: { current: 3, total: 10 },
|
|
}),
|
|
];
|
|
render(<ProcessingStatus {...bulkProps} stages={stagesWithProgress} />);
|
|
const progressBar =
|
|
screen.getByText(/analyzing page 3 of 10/i).nextElementSibling?.firstChild;
|
|
expect(progressBar).toBeInTheDocument();
|
|
expect(progressBar).toHaveStyle('width: 30%');
|
|
});
|
|
|
|
it('should render the checklist of stages on the right', () => {
|
|
render(<ProcessingStatus {...bulkProps} />);
|
|
// Find the list of stages by its role. This is more robust than relying on specific CSS classes.
|
|
const stageList = screen.getByRole('list');
|
|
expect(stageList).toBeInTheDocument();
|
|
expect(stageList).toHaveTextContent('Uploading File');
|
|
expect(stageList).toHaveTextContent('Converting to Image');
|
|
});
|
|
});
|
|
|
|
describe('Conditional Rendering', () => {
|
|
it('should not render any progress bars if props are not provided', () => {
|
|
render(<ProcessingStatus stages={mockStages} estimatedTime={60} />);
|
|
expect(screen.queryByText(/converting pdf/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/overall progress/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/analyzing page/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render stage details and optional text', () => {
|
|
render(<ProcessingStatus stages={mockStages} estimatedTime={120} />);
|
|
|
|
// Stage with detail
|
|
const inProgressStage = screen.getByTestId('stage-text-1');
|
|
expect(inProgressStage).toHaveTextContent('Page 2 of 5...');
|
|
|
|
// Stage with non-critical error and optional text
|
|
const nonCriticalErrorStage = screen.getByTestId('stage-text-3');
|
|
expect(nonCriticalErrorStage).toHaveTextContent('AI model timeout');
|
|
expect(nonCriticalErrorStage).toHaveTextContent('(optional)');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should render null for unknown stage status icon', () => {
|
|
const stagesWithUnknownStatus: ProcessingStage[] = [
|
|
createMockProcessingStage({
|
|
name: 'Unknown Stage',
|
|
status: 'unknown-status' as any,
|
|
detail: '',
|
|
}),
|
|
];
|
|
render(<ProcessingStatus stages={stagesWithUnknownStatus} estimatedTime={60} />);
|
|
|
|
const stageIcon = screen.getByTestId('stage-icon-0');
|
|
// The icon container should be empty (no SVG or spinner rendered)
|
|
expect(stageIcon.querySelector('svg')).not.toBeInTheDocument();
|
|
expect(stageIcon.querySelector('.animate-spin')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should return empty string for unknown stage status text color', () => {
|
|
const stagesWithUnknownStatus: ProcessingStage[] = [
|
|
createMockProcessingStage({
|
|
name: 'Unknown Stage',
|
|
status: 'unknown-status' as any,
|
|
detail: '',
|
|
}),
|
|
];
|
|
render(<ProcessingStatus stages={stagesWithUnknownStatus} estimatedTime={60} />);
|
|
|
|
const stageText = screen.getByTestId('stage-text-0');
|
|
// Should not have any of the known status color classes
|
|
expect(stageText.className).not.toContain('text-brand-primary');
|
|
expect(stageText.className).not.toContain('text-gray-700');
|
|
expect(stageText.className).not.toContain('text-gray-400');
|
|
expect(stageText.className).not.toContain('text-red-500');
|
|
expect(stageText.className).not.toContain('text-yellow-600');
|
|
});
|
|
|
|
it('should not render page progress bar when total is 1', () => {
|
|
render(
|
|
<ProcessingStatus stages={[]} estimatedTime={60} pageProgress={{ current: 1, total: 1 }} />,
|
|
);
|
|
expect(screen.queryByText(/converting pdf/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should not render stage progress bar when total is 1', () => {
|
|
const stagesWithSinglePageProgress: ProcessingStage[] = [
|
|
createMockProcessingStage({
|
|
name: 'Extracting Items',
|
|
status: 'in-progress',
|
|
progress: { current: 1, total: 1 },
|
|
}),
|
|
];
|
|
render(<ProcessingStatus stages={stagesWithSinglePageProgress} estimatedTime={60} />);
|
|
expect(screen.queryByText(/analyzing page/i)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|