Files
erp-nutzfahrzeuge/frontend/tests/retouch.test.tsx
T

249 lines
8.3 KiB
TypeScript
Raw Normal View History

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<string, string> = {};
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(<RetouchUpload />);
expect(screen.getByTestId('retouch-dropzone')).toBeInTheDocument();
expect(screen.getByText('Drag and drop vehicle image here')).toBeInTheDocument();
});
it('shows file input with image accept', () => {
render(<RetouchUpload />);
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(<RetouchUpload />);
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(<RetouchUpload onUploadComplete={(id) => { 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(<RetouchUpload />);
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(
<BeforeAfterSlider
beforeSrc="https://example.com/before.png"
afterSrc="https://example.com/after.png"
/>
);
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(
<BeforeAfterSlider
beforeSrc="https://example.com/before.png"
afterSrc="https://example.com/after.png"
beforeLabel="Original"
afterLabel="Retuschiert"
/>
);
expect(screen.getByText('Original')).toBeInTheDocument();
expect(screen.getByText('Retuschiert')).toBeInTheDocument();
});
it('renders slider handle', () => {
render(
<BeforeAfterSlider
beforeSrc="https://example.com/before.png"
afterSrc="https://example.com/after.png"
/>
);
expect(screen.getByTestId('slider-handle')).toBeInTheDocument();
});
});
describe('PriceComparison', () => {
it('renders vehicle ID input and compare button', () => {
render(<PriceComparison />);
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(<PriceComparison />);
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(<PriceComparison />);
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(<PriceComparison />);
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(<PriceComparison />);
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();
});
});
});