// src/pages/admin/components/CorrectionRow.test.tsx import React from 'react'; import ReactDOM from 'react-dom'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest'; import { CorrectionRow } from './CorrectionRow'; import * as apiClient from '../../../services/apiClient'; import type { SuggestedCorrection, MasterGroceryItem, Category } from '../../../types'; // Cast the mocked module to its mocked type to retain type safety and autocompletion. // The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts. const mockedApiClient = apiClient as Mocked; // Mock the logger vi.mock('../../../services/logger', () => ({ logger: { info: vi.fn(), error: vi.fn() }, })); // Mock the ConfirmationModal to test its props and interactions // The ConfirmationModal is now in a different directory. // We use ReactDOM.createPortal to avoid rendering a
inside a , which is invalid HTML. vi.mock('../../../components/ConfirmationModal', () => ({ ConfirmationModal: vi.fn( ({ isOpen, onConfirm, onClose }: { isOpen: boolean; onConfirm: () => void; onClose: () => void }) => { if (!isOpen) return null; // Use a portal to render the modal at the body level, avoiding invalid nesting. return ReactDOM.createPortal(
, document.body ); } ), })); const mockCorrection: SuggestedCorrection = { suggested_correction_id: 1, flyer_item_id: 101, user_id: 'user-1', correction_type: 'WRONG_PRICE', suggested_value: '250', // $2.50 status: 'pending', created_at: new Date().toISOString(), flyer_item_name: 'Bananas', flyer_item_price_display: '$1.99', user_email: 'test@example.com', }; const mockMasterItems: MasterGroceryItem[] = [ { master_grocery_item_id: 1, name: 'Bananas', created_at: '', category_id: 1, category_name: 'Produce' }, ]; const mockCategories: Category[] = [ { category_id: 1, name: 'Produce' }, ]; const mockOnProcessed = vi.fn(); const defaultProps = { correction: mockCorrection, masterItems: mockMasterItems, categories: mockCategories, onProcessed: mockOnProcessed, }; // Helper to render the component inside a table structure const renderInTable = (props = defaultProps) => { return render(
); }; describe('CorrectionRow', () => { beforeEach(() => { vi.clearAllMocks(); mockedApiClient.approveCorrection.mockResolvedValue(new Response(null, { status: 204 })); mockedApiClient.rejectCorrection.mockResolvedValue(new Response(null, { status: 204 })); mockedApiClient.updateSuggestedCorrection.mockResolvedValue(new Response(JSON.stringify({ ...mockCorrection, suggested_value: '300' }))); }); it('should render correction data correctly', () => { renderInTable(); expect(screen.getByText('Bananas')).toBeInTheDocument(); expect(screen.getByText('Original Price: $1.99')).toBeInTheDocument(); expect(screen.getByText('WRONG_PRICE')).toBeInTheDocument(); expect(screen.getByText('$2.50')).toBeInTheDocument(); // Formatted price expect(screen.getByText('test@example.com')).toBeInTheDocument(); }); it('should open the confirmation modal on approve click', async () => { renderInTable(); fireEvent.click(screen.getByTitle('Approve')); await waitFor(() => { expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument(); }); }); it('should call approveCorrection when approval is confirmed', async () => { renderInTable(); fireEvent.click(screen.getByTitle('Approve')); await waitFor(() => { fireEvent.click(screen.getByRole('button', { name: 'Confirm' })); }); await waitFor(() => { expect(mockedApiClient.approveCorrection).toHaveBeenCalledWith(mockCorrection.suggested_correction_id); expect(mockOnProcessed).toHaveBeenCalledWith(mockCorrection.suggested_correction_id); }); }); it('should call rejectCorrection when rejection is confirmed', async () => { renderInTable(); fireEvent.click(screen.getByTitle('Reject')); await waitFor(() => { expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Confirm' })); }); await waitFor(() => { expect(mockedApiClient.rejectCorrection).toHaveBeenCalledWith(mockCorrection.suggested_correction_id); expect(mockOnProcessed).toHaveBeenCalledWith(mockCorrection.suggested_correction_id); }); }); it('should display an error message if an action fails', async () => { mockedApiClient.approveCorrection.mockRejectedValue(new Error('API Error')); renderInTable(); fireEvent.click(screen.getByTitle('Approve')); await waitFor(() => { fireEvent.click(screen.getByRole('button', { name: 'Confirm' })); }); await waitFor(() => { expect(screen.getByText('API Error')).toBeInTheDocument(); }); }); it('should enter and exit editing mode', async () => { renderInTable(); // Enter editing mode fireEvent.click(screen.getByTitle('Edit')); await waitFor(() => { expect(screen.getByRole('spinbutton')).toBeInTheDocument(); // For input type=number expect(screen.getByTitle('Save')).toBeInTheDocument(); expect(screen.getByTitle('Cancel')).toBeInTheDocument(); }); // Exit editing mode fireEvent.click(screen.getByTitle('Cancel')); await waitFor(() => { expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument(); expect(screen.getByTitle('Approve')).toBeInTheDocument(); }); }); it('should save an edited value', async () => { renderInTable(); fireEvent.click(screen.getByTitle('Edit')); const input = await screen.findByRole('spinbutton'); fireEvent.change(input, { target: { value: '300' } }); fireEvent.click(screen.getByTitle('Save')); await waitFor(() => { expect(mockedApiClient.updateSuggestedCorrection).toHaveBeenCalledWith(mockCorrection.suggested_correction_id, '300'); // The component should now display the updated value from the mock response expect(screen.getByText('$3.00')).toBeInTheDocument(); }); // Check that it exited editing mode expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument(); }); });