import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { RetouchUpload } from '@/components/retouch/RetouchUpload'; import { BeforeAfterSlider } from '@/components/retouch/BeforeAfterSlider'; import { PriceComparison } from '@/components/retouch/PriceComparison'; // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; // Mock localStorage const localStorageMock = (() => { let store: Record = {}; return { getItem: (key: string) => store[key] || null, setItem: (key: string, value: string) => { store[key] = value; }, removeItem: (key: string) => { delete store[key]; }, clear: () => { store = {}; }, }; })(); Object.defineProperty(window, 'localStorage', { value: localStorageMock }); // Mock apiFetch vi.mock('@/lib/api', () => ({ apiFetch: vi.fn(), API_BASE_URL: 'http://localhost:8000/api/v1', })); const { apiFetch } = await import('@/lib/api'); beforeEach(() => { vi.clearAllMocks(); localStorageMock.clear(); }); describe('RetouchUpload', () => { it('renders drag-and-drop zone', () => { render(); expect(screen.getByTestId('retouch-dropzone')).toBeInTheDocument(); expect(screen.getByText('Drag and drop vehicle image here')).toBeInTheDocument(); }); it('shows file input with image accept', () => { render(); const input = screen.getByTestId('retouch-file-input'); expect(input).toHaveAttribute('type', 'file'); expect(input).toHaveAttribute('accept', 'image/*'); }); it('shows error for non-image file', async () => { render(); const input = screen.getByTestId('retouch-file-input') as HTMLInputElement; const file = new File(['data'], 'doc.pdf', { type: 'application/pdf' }); Object.defineProperty(input, 'files', { value: [file], writable: false }); fireEvent.change(input); await waitFor(() => { expect(screen.getByTestId('retouch-upload-error')).toBeInTheDocument(); expect(screen.getByText('Only image files are allowed')).toBeInTheDocument(); }); }); it('uploads image file successfully', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ message: 'Retouch processing queued', retouch_id: 'new-retouch-id', status: 'pending', }), }); let uploadedId = ''; render( { uploadedId = id; }} />); const input = screen.getByTestId('retouch-file-input') as HTMLInputElement; const file = new File(['image-data'], 'truck.png', { type: 'image/png' }); Object.defineProperty(input, 'files', { value: [file], writable: false }); fireEvent.change(input); await waitFor(() => { expect(screen.getByTestId('retouch-upload-success')).toBeInTheDocument(); expect(uploadedId).toBe('new-retouch-id'); }); expect(mockFetch).toHaveBeenCalledTimes(1); const call = mockFetch.mock.calls[0]; expect(call[0]).toContain('/retouch/process'); expect(call[1].method).toBe('POST'); }); it('shows error on upload failure', async () => { mockFetch.mockResolvedValueOnce({ ok: false, json: async () => ({ error: { code: 'INVALID_MIME_TYPE', message: 'Invalid MIME type' }, }), }); render(); const input = screen.getByTestId('retouch-file-input') as HTMLInputElement; const file = new File(['image-data'], 'truck.png', { type: 'image/png' }); Object.defineProperty(input, 'files', { value: [file], writable: false }); fireEvent.change(input); await waitFor(() => { expect(screen.getByTestId('retouch-upload-error')).toBeInTheDocument(); expect(screen.getByText('Invalid MIME type')).toBeInTheDocument(); }); }); }); describe('BeforeAfterSlider', () => { it('renders both images', () => { render( ); expect(screen.getByTestId('before-after-slider')).toBeInTheDocument(); expect(screen.getByTestId('before-image')).toHaveAttribute('src', 'https://example.com/before.png'); expect(screen.getByTestId('after-image')).toHaveAttribute('src', 'https://example.com/after.png'); }); it('renders labels', () => { render( ); expect(screen.getByText('Original')).toBeInTheDocument(); expect(screen.getByText('Retuschiert')).toBeInTheDocument(); }); it('renders slider handle', () => { render( ); expect(screen.getByTestId('slider-handle')).toBeInTheDocument(); }); }); describe('PriceComparison', () => { it('renders vehicle ID input and compare button', () => { render(); expect(screen.getByTestId('price-compare-vehicle-input')).toBeInTheDocument(); expect(screen.getByTestId('price-compare-button')).toBeInTheDocument(); }); it('shows error when comparing without vehicle ID', async () => { render(); const button = screen.getByTestId('price-compare-button'); fireEvent.click(button); await waitFor(() => { expect(screen.getByTestId('price-compare-error')).toBeInTheDocument(); expect(screen.getByText('Please enter a vehicle ID')).toBeInTheDocument(); }); }); it('fetches and displays comparable listings', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ vehicle_id: 'veh-123', comparable_listings: [ { title: 'Mercedes Actros 2020', make: 'Mercedes', model: 'Actros', year: 2020, price: 42000, mileage_km: 110000, location: 'Berlin', url: 'https://mobile.de/listing/123', source: 'mobile.de', }, { title: 'Mercedes Actros 2020', make: 'Mercedes', model: 'Actros', year: 2020, price: 48000, mileage_km: 95000, location: 'Hamburg', url: 'https://mobile.de/listing/456', source: 'mobile.de', }, ], average_price: 45000, listing_count: 2, }); render(); const input = screen.getByTestId('price-compare-vehicle-input') as HTMLInputElement; fireEvent.change(input, { target: { value: 'veh-123' } }); fireEvent.click(screen.getByTestId('price-compare-button')); await waitFor(() => { expect(screen.getByTestId('price-compare-results')).toBeInTheDocument(); expect(screen.getAllByText('Mercedes Actros 2020').length).toBe(2); expect(screen.getByTestId('price-compare-row-0')).toBeInTheDocument(); expect(screen.getByTestId('price-compare-row-1')).toBeInTheDocument(); }); }); it('shows empty state when no listings found', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ vehicle_id: 'veh-456', comparable_listings: [], average_price: null, listing_count: 0, }); render(); const input = screen.getByTestId('price-compare-vehicle-input') as HTMLInputElement; fireEvent.change(input, { target: { value: 'veh-456' } }); fireEvent.click(screen.getByTestId('price-compare-button')); await waitFor(() => { expect(screen.getByTestId('price-compare-empty')).toBeInTheDocument(); expect(screen.getByText('No comparable listings found.')).toBeInTheDocument(); }); }); it('shows error on fetch failure', async () => { vi.mocked(apiFetch).mockRejectedValueOnce({ error: { code: 'VEHICLE_NOT_FOUND', message: 'Vehicle not found' }, }); render(); const input = screen.getByTestId('price-compare-vehicle-input') as HTMLInputElement; fireEvent.change(input, { target: { value: 'bad-id' } }); fireEvent.click(screen.getByTestId('price-compare-button')); await waitFor(() => { expect(screen.getByTestId('price-compare-error')).toBeInTheDocument(); expect(screen.getByText('Vehicle not found')).toBeInTheDocument(); }); }); });