feat: add 410 error handling, progress messages, touch targets, and tests (refs #145)
- Handle poll errors including 410 Gone in useManualExtraction hook - Add specific progress stage messages (Preparing/Processing/Mapping/Complete) - Enforce 44px minimum touch targets on all interactive elements - Add tests for inline editing, mobile fullscreen, and desktop modal layouts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,23 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MaintenanceScheduleReviewScreen } from './MaintenanceScheduleReviewScreen';
|
||||
import type { MaintenanceScheduleItem } from '../../documents/hooks/useManualExtraction';
|
||||
|
||||
// Mock matchMedia for responsive tests
|
||||
function mockMatchMedia(matches: boolean) {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query: string) => ({
|
||||
matches,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Mock the create hook
|
||||
const mockMutateAsync = jest.fn();
|
||||
jest.mock('../hooks/useCreateSchedulesFromExtraction', () => ({
|
||||
@@ -222,4 +239,70 @@ describe('MaintenanceScheduleReviewScreen', () => {
|
||||
await screen.findByText('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Editing', () => {
|
||||
it('should update item data via inline editing', async () => {
|
||||
mockMutateAsync.mockResolvedValue([{ id: '1' }]);
|
||||
|
||||
render(<MaintenanceScheduleReviewScreen {...defaultProps} />);
|
||||
|
||||
// Click on the Months field of the third item (unique value: 12 mo)
|
||||
const monthsField = screen.getByText('12 mo');
|
||||
fireEvent.click(monthsField);
|
||||
|
||||
// Find the input that appeared and change its value
|
||||
const monthsInput = screen.getByDisplayValue('12');
|
||||
fireEvent.change(monthsInput, { target: { value: '24' } });
|
||||
fireEvent.keyDown(monthsInput, { key: 'Enter' });
|
||||
|
||||
// Deselect items 1 and 2 to only create the edited item
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
fireEvent.click(checkboxes[0]);
|
||||
fireEvent.click(checkboxes[1]);
|
||||
|
||||
// Create the schedule and verify updated value is used
|
||||
fireEvent.click(screen.getByRole('button', { name: /create 1 schedule$/i }));
|
||||
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith({
|
||||
vehicleId: 'vehicle-123',
|
||||
items: [expect.objectContaining({
|
||||
service: 'Cabin Air Filter Replacement',
|
||||
intervalMonths: 24,
|
||||
})],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Responsive layout', () => {
|
||||
afterEach(() => {
|
||||
// Reset matchMedia after each test
|
||||
mockMatchMedia(false);
|
||||
});
|
||||
|
||||
it('should render in fullscreen mode on mobile viewports', () => {
|
||||
// Simulate mobile: breakpoints.down('sm') returns true
|
||||
mockMatchMedia(true);
|
||||
|
||||
render(<MaintenanceScheduleReviewScreen {...defaultProps} />);
|
||||
|
||||
// On mobile, dialog renders with fullScreen prop - check that the MuiDialog-paperFullScreen
|
||||
// class is applied. MUI renders the dialog in a portal, so query from document.
|
||||
const paper = document.querySelector('.MuiDialog-paperFullScreen');
|
||||
expect(paper).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should render as modal dialog on desktop viewports', () => {
|
||||
// Simulate desktop: breakpoints.down('sm') returns false
|
||||
mockMatchMedia(false);
|
||||
|
||||
render(<MaintenanceScheduleReviewScreen {...defaultProps} />);
|
||||
|
||||
// On desktop, dialog should NOT have fullScreen class
|
||||
const fullScreenPaper = document.querySelector('.MuiDialog-paperFullScreen');
|
||||
expect(fullScreenPaper).toBeNull();
|
||||
|
||||
// But the dialog should still render
|
||||
expect(screen.getByText('Extracted Maintenance Schedules')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,10 +117,10 @@ const InlineField: React.FC<InlineFieldProps> = ({ label, value, type = 'text',
|
||||
if (e.key === 'Escape') handleCancel();
|
||||
}}
|
||||
/>
|
||||
<IconButton size="small" onClick={handleSave} color="primary">
|
||||
<IconButton size="small" onClick={handleSave} color="primary" sx={{ minWidth: 44, minHeight: 44 }}>
|
||||
<CheckIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={handleCancel}>
|
||||
<IconButton size="small" onClick={handleCancel} sx={{ minWidth: 44, minHeight: 44 }}>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@@ -134,6 +134,7 @@ const InlineField: React.FC<InlineFieldProps> = ({ label, value, type = 'text',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
cursor: 'pointer',
|
||||
minHeight: 44,
|
||||
'&:hover .edit-icon': { opacity: 1 },
|
||||
}}
|
||||
onClick={() => setIsEditing(true)}
|
||||
@@ -293,7 +294,7 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
|
||||
<Checkbox
|
||||
checked={item.selected}
|
||||
onChange={() => handleToggle(index)}
|
||||
sx={{ mt: -0.5, mr: 1 }}
|
||||
sx={{ mt: -0.5, mr: 1, '& .MuiSvgIcon-root': { fontSize: 24 }, minWidth: 44, minHeight: 44 }}
|
||||
inputProps={{ 'aria-label': `Select ${item.service}` }}
|
||||
/>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
@@ -379,7 +380,7 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
|
||||
onClick={handleCreate}
|
||||
disabled={selectedCount === 0 || createMutation.isPending}
|
||||
startIcon={createMutation.isPending ? <CircularProgress size={16} /> : <CheckIcon />}
|
||||
sx={{ order: isMobile ? 1 : 2, width: isMobile ? '100%' : 'auto' }}
|
||||
sx={{ minHeight: 44, order: isMobile ? 1 : 2, width: isMobile ? '100%' : 'auto' }}
|
||||
>
|
||||
{createMutation.isPending
|
||||
? 'Creating...'
|
||||
|
||||
Reference in New Issue
Block a user