All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m28s
- 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.
176 lines
6.4 KiB
TypeScript
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();
|
|
});
|
|
}); |