Files
flyer-crawler.projectium.com/src/pages/admin/components/CorrectionRow.test.tsx
Torben Sorensen 80d2b1ffe6
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m28s
Add user database service and unit tests
- Implement user database service with functions for user management (create, find, update, delete).
- Add comprehensive unit tests for user database service using Vitest.
- Mock database interactions to ensure isolated testing.
- Create setup files for unit tests to handle database connections and global mocks.
- Introduce error handling for unique constraints and foreign key violations.
- Enhance logging for better traceability during database operations.
2025-12-04 15:30:27 -08:00

176 lines
6.4 KiB
TypeScript

// 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<typeof apiClient>;
// 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 <div> inside a <tbody>, 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(
<div data-testid="confirmation-modal">
<button onClick={onConfirm}>Confirm</button>
<button onClick={onClose}>Cancel</button>
</div>,
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(
<table>
<tbody>
<CorrectionRow {...props} />
</tbody>
</table>
);
};
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();
});
});