import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; import { vi } from 'vitest'; import ItemDetailModal from '@/components/ItemDetailModal'; import { Item } from '@/lib/db'; import * as api from '@/lib/api'; import { toast } from 'react-hot-toast'; vi.mock('react-hot-toast'); vi.mock('@/lib/api'); vi.mock('@/components/ItemPhotoUpload', () => ({ default: ({ itemId, onUploadSuccess, onError }: any) => (
), })); describe('ItemDetailModal', () => { const mockItem: Item = { id: 1, name: 'Test Component', category: 'Electronics', quantity: 10, part_number: 'PN-001', barcode: 'BAR-001', type: 'IC', specs: 'Test specs', min_quantity: 5, image_url: 'http://test.com/photo.jpg', }; const mockItemNoPhoto: Item = { id: 2, name: 'No Photo Item', category: 'Electronics', quantity: 5, part_number: 'PN-002', barcode: 'BAR-002', type: 'Resistor', specs: 'No photo specs', min_quantity: 2, }; beforeEach(() => { vi.clearAllMocks(); }); describe('Rendering', () => { it('should render modal with item details', () => { render( ); expect(screen.getByText('Test Component')).toBeInTheDocument(); expect(screen.getByText('Electronics')).toBeInTheDocument(); expect(screen.getByText('IC')).toBeInTheDocument(); }); it('should display item photo when image_url exists', () => { render( ); const photoImg = screen.getByAltText('Test Component') as HTMLImageElement; expect(photoImg).toBeInTheDocument(); expect(photoImg.src).toContain('photo.jpg'); }); it('should show "No photo uploaded" when image_url is missing', () => { render( ); expect(screen.getByText('No photo uploaded')).toBeInTheDocument(); }); it('should render item specifications in detail grid', () => { render( ); expect(screen.getByText('PN-001')).toBeInTheDocument(); expect(screen.getByText('BAR-001')).toBeInTheDocument(); }); }); describe('Photo Replacement Button', () => { it('should show "Replace Photo" button when photo exists', () => { render( ); expect(screen.getByText('Replace Photo')).toBeInTheDocument(); }); it('should show "Upload Photo" button when no photo exists', () => { render( ); expect(screen.getByText('Upload Photo')).toBeInTheDocument(); }); it('should toggle photo upload UI on "Replace Photo" click', () => { render( ); const replaceButton = screen.getByText('Replace Photo'); fireEvent.click(replaceButton); expect(screen.getByTestId('item-photo-upload')).toBeInTheDocument(); expect(screen.getByText('Upload New Photo')).toBeInTheDocument(); }); it('should hide photo upload UI on cancel', () => { render( ); fireEvent.click(screen.getByText('Replace Photo')); expect(screen.getByTestId('item-photo-upload')).toBeInTheDocument(); const closeButton = screen.getByLabelText('Cancel upload'); fireEvent.click(closeButton); expect(screen.queryByTestId('item-photo-upload')).not.toBeInTheDocument(); }); }); describe('Photo Upload Success', () => { it('should update photo and close upload UI on success', async () => { const onPhotoUpdated = vi.fn(); render( ); fireEvent.click(screen.getByText('Replace Photo')); fireEvent.click(screen.getByTestId('mock-upload-success')); await waitFor(() => { expect(onPhotoUpdated).toHaveBeenCalledWith( expect.objectContaining({ thumbnail_url: 'http://test.com/thumb.jpg', }) ); }, { timeout: 1000 }); expect(screen.queryByTestId('item-photo-upload')).not.toBeInTheDocument(); }); it('should call onItemRefresh callback on upload success', async () => { const onItemRefresh = vi.fn(); render( ); fireEvent.click(screen.getByText('Replace Photo')); fireEvent.click(screen.getByTestId('mock-upload-success')); await waitFor(() => { expect(onItemRefresh).toHaveBeenCalled(); }, { timeout: 1000 }); }); }); describe('Photo Upload Error', () => { it('should keep upload UI open on error', async () => { render( ); fireEvent.click(screen.getByText('Replace Photo')); fireEvent.click(screen.getByTestId('mock-upload-error')); await waitFor(() => { expect(screen.getByTestId('item-photo-upload')).toBeInTheDocument(); }, { timeout: 1000 }); }); }); describe('Photo Deletion', () => { it('should show delete button when photo exists', () => { render( ); const buttons = screen.getAllByRole('button'); const hasDeleteButton = buttons.some(b => b.className?.includes('rose-500')); expect(hasDeleteButton).toBe(true); }); it('should call deleteItemPhoto API on delete confirmation', async () => { vi.mocked(api.inventoryApi.deleteItemPhoto).mockResolvedValue({ status: 'ok' }); window.confirm = vi.fn(() => true); render( ); const buttons = screen.getAllByRole('button'); const deleteButton = buttons.find(b => b.className?.includes('rose-500')); if (deleteButton) { fireEvent.click(deleteButton); await waitFor(() => { expect(api.inventoryApi.deleteItemPhoto).toHaveBeenCalledWith(mockItem.id); }, { timeout: 1000 }); } }); it('should show success toast on delete', async () => { vi.mocked(api.inventoryApi.deleteItemPhoto).mockResolvedValue({ status: 'ok' }); window.confirm = vi.fn(() => true); render( ); const buttons = screen.getAllByRole('button'); const deleteButton = buttons.find(b => b.className?.includes('rose-500')); if (deleteButton) { fireEvent.click(deleteButton); await waitFor(() => { expect(toast.success).toHaveBeenCalledWith('Photo deleted successfully'); }, { timeout: 1000 }); } }); it('should not delete without confirmation', async () => { window.confirm = vi.fn(() => false); render( ); const buttons = screen.getAllByRole('button'); const deleteButton = buttons.find(b => b.className?.includes('rose-500')); if (deleteButton) { fireEvent.click(deleteButton); expect(api.inventoryApi.deleteItemPhoto).not.toHaveBeenCalled(); } }); it('should call onItemRefresh on delete success', async () => { vi.mocked(api.inventoryApi.deleteItemPhoto).mockResolvedValue({ status: 'ok' }); window.confirm = vi.fn(() => true); const onItemRefresh = vi.fn(); render( ); const buttons = screen.getAllByRole('button'); const deleteButton = buttons.find(b => b.className?.includes('rose-500')); if (deleteButton) { fireEvent.click(deleteButton); await waitFor(() => { expect(onItemRefresh).toHaveBeenCalled(); }, { timeout: 1000 }); } }); it('should show error toast on delete failure', async () => { vi.mocked(api.inventoryApi.deleteItemPhoto).mockRejectedValue( new Error('Delete failed') ); window.confirm = vi.fn(() => true); render( ); const buttons = screen.getAllByRole('button'); const deleteButton = buttons.find(b => b.className?.includes('rose-500')); if (deleteButton) { fireEvent.click(deleteButton); await waitFor(() => { expect(toast.error).toHaveBeenCalledWith('Delete failed'); }, { timeout: 1000 }); } }); }); describe('Modal Controls', () => { it('should call onClose when close button is clicked', () => { const onClose = vi.fn(); render( ); const closeButton = screen.getByLabelText('Close modal'); fireEvent.click(closeButton); expect(onClose).toHaveBeenCalled(); }); it('should be scrollable when content exceeds viewport', () => { render( ); const modalContent = screen.getByText('Test Component').closest('div'); expect(modalContent?.parentElement?.className).toContain('max-h-[90vh]'); expect(modalContent?.parentElement?.className).toContain('overflow-y-auto'); }); }); });