74b5e6afb8
- Vehicle model: 25+ fields, 5 vehicle types, soft-delete - Vehicle CRUD: 7 API endpoints with JWT auth, filter/sort/paginate - mobile.de: push/update/delete listings, field mapping, retry logic - MobileDeListing model for sync status tracking - Frontend: VehicleList, VehicleForm, VehicleDetail, MobileDeStatus - 73 backend tests (82% coverage), 16 frontend tests
381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { ToastProvider } from '@/components/ui/Toast';
|
|
import { I18nProvider } from '@/lib/i18n';
|
|
import { VehicleList } from '@/components/vehicles/VehicleList';
|
|
import { VehicleForm } from '@/components/vehicles/VehicleForm';
|
|
import { VehicleDetail } from '@/components/vehicles/VehicleDetail';
|
|
import { MobileDeStatus } from '@/components/vehicles/MobileDeStatus';
|
|
|
|
// Mock next/navigation
|
|
vi.mock('next/navigation', () => ({
|
|
useRouter: () => ({
|
|
push: vi.fn(),
|
|
replace: vi.fn(),
|
|
back: vi.fn(),
|
|
refresh: vi.fn(),
|
|
}),
|
|
redirect: vi.fn(),
|
|
usePathname: () => '/de/fahrzeuge',
|
|
useSearchParams: () => new URLSearchParams(),
|
|
}));
|
|
|
|
// Mock the vehicles API module
|
|
vi.mock('@/lib/vehicles', () => ({
|
|
listVehicles: vi.fn(),
|
|
getVehicle: vi.fn(),
|
|
createVehicle: vi.fn(),
|
|
updateVehicle: vi.fn(),
|
|
deleteVehicle: vi.fn(),
|
|
pushToMobileDe: vi.fn(),
|
|
getMobileDeStatus: vi.fn(),
|
|
}));
|
|
|
|
import {
|
|
listVehicles as mockListVehicles,
|
|
getVehicle as mockGetVehicle,
|
|
createVehicle as mockCreateVehicle,
|
|
deleteVehicle as mockDeleteVehicle,
|
|
pushToMobileDe as mockPushToMobileDe,
|
|
getMobileDeStatus as mockGetMobileDeStatus,
|
|
} from '@/lib/vehicles';
|
|
|
|
const mockVehicle = {
|
|
id: 'vehicle-123',
|
|
make: 'Mercedes-Benz',
|
|
model: 'Actros',
|
|
fin: 'WDB9066351L123456',
|
|
year: 2020,
|
|
first_registration: '2020-03-15',
|
|
power_kw: 300,
|
|
power_hp: 408,
|
|
fuel_type: 'Diesel',
|
|
transmission: 'Manual',
|
|
color: 'White',
|
|
condition: 'used',
|
|
location: 'Berlin',
|
|
availability: 'available',
|
|
price: 45000.00,
|
|
vehicle_type: 'lkw',
|
|
lkw_type: 'sattelzugmaschine',
|
|
machine_type: null,
|
|
body_type: null,
|
|
operating_hours: null,
|
|
operating_hours_unit: null,
|
|
mileage_km: 120000,
|
|
description: 'Well maintained truck',
|
|
created_at: '2025-01-01T12:00:00Z',
|
|
updated_at: '2025-01-01T12:00:00Z',
|
|
deleted_at: null,
|
|
};
|
|
|
|
function renderWithProviders(ui: React.ReactElement) {
|
|
return render(
|
|
<I18nProvider initialLocale="de">
|
|
<ToastProvider>
|
|
{ui}
|
|
</ToastProvider>
|
|
</I18nProvider>
|
|
);
|
|
}
|
|
|
|
describe('VehicleList', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('renders the vehicle list with filters and table', async () => {
|
|
(mockListVehicles as any).mockResolvedValue({
|
|
items: [mockVehicle],
|
|
total: 1,
|
|
page: 1,
|
|
page_size: 20,
|
|
});
|
|
|
|
renderWithProviders(<VehicleList />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('vehicle-list')).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByTestId('vehicle-filters')).toBeInTheDocument();
|
|
expect(screen.getByTestId('filter-search')).toBeInTheDocument();
|
|
expect(screen.getByTestId('filter-type')).toBeInTheDocument();
|
|
expect(screen.getByTestId('filter-availability')).toBeInTheDocument();
|
|
expect(screen.getByTestId('filter-sort')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays vehicles in the table after loading', async () => {
|
|
(mockListVehicles as any).mockResolvedValue({
|
|
items: [mockVehicle],
|
|
total: 1,
|
|
page: 1,
|
|
page_size: 20,
|
|
});
|
|
|
|
renderWithProviders(<VehicleList />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Mercedes-Benz')).toBeInTheDocument();
|
|
expect(screen.getByText('Actros')).toBeInTheDocument();
|
|
expect(screen.getByText('WDB9066351L123456')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows pagination when total > page_size', async () => {
|
|
(mockListVehicles as any).mockResolvedValue({
|
|
items: [mockVehicle],
|
|
total: 40,
|
|
page: 1,
|
|
page_size: 20,
|
|
});
|
|
|
|
renderWithProviders(<VehicleList />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('vehicle-pagination')).toBeInTheDocument();
|
|
expect(screen.getByText(/Page 1 of 2/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows error message on API failure', async () => {
|
|
(mockListVehicles as any).mockRejectedValue({
|
|
error: { code: 'UNKNOWN', message: 'Failed to load' },
|
|
});
|
|
|
|
renderWithProviders(<VehicleList />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('vehicle-error')).toBeInTheDocument();
|
|
expect(screen.getByText('Failed to load')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls listVehicles with type filter when changed', async () => {
|
|
(mockListVehicles as any).mockResolvedValue({
|
|
items: [],
|
|
total: 0,
|
|
page: 1,
|
|
page_size: 20,
|
|
});
|
|
|
|
renderWithProviders(<VehicleList />);
|
|
|
|
await waitFor(() => {
|
|
expect(mockListVehicles).toHaveBeenCalled();
|
|
});
|
|
|
|
const typeSelect = screen.getByTestId('filter-type');
|
|
fireEvent.change(typeSelect, { target: { value: 'lkw' } });
|
|
|
|
await waitFor(() => {
|
|
expect(mockListVehicles).toHaveBeenCalledWith(
|
|
expect.objectContaining({ type: 'lkw', page: 1 })
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('VehicleForm', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('renders the form with all required fields', () => {
|
|
renderWithProviders(<VehicleForm mode="create" />);
|
|
|
|
expect(screen.getByTestId('vehicle-form')).toBeInTheDocument();
|
|
expect(screen.getByTestId('input-make')).toBeInTheDocument();
|
|
expect(screen.getByTestId('input-model')).toBeInTheDocument();
|
|
expect(screen.getByTestId('input-fin')).toBeInTheDocument();
|
|
expect(screen.getByTestId('input-price')).toBeInTheDocument();
|
|
expect(screen.getByTestId('input-vehicle_type')).toBeInTheDocument();
|
|
expect(screen.getByTestId('submit-button')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows validation errors for empty required fields', async () => {
|
|
renderWithProviders(<VehicleForm mode="create" />);
|
|
|
|
const submitButton = screen.getByTestId('submit-button');
|
|
fireEvent.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Make is required')).toBeInTheDocument();
|
|
expect(screen.getByText('Model is required')).toBeInTheDocument();
|
|
expect(screen.getByText('FIN is required')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows error for FIN not 17 characters', async () => {
|
|
renderWithProviders(<VehicleForm mode="create" />);
|
|
|
|
const finInput = screen.getByTestId('input-fin');
|
|
fireEvent.change(finInput, { target: { value: 'SHORT' } });
|
|
|
|
const submitButton = screen.getByTestId('submit-button');
|
|
fireEvent.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('FIN must be exactly 17 characters')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls createVehicle on submit with valid data', async () => {
|
|
(mockCreateVehicle as any).mockResolvedValue(mockVehicle);
|
|
|
|
renderWithProviders(<VehicleForm mode="create" />);
|
|
|
|
fireEvent.change(screen.getByTestId('input-make'), { target: { value: 'Volvo' } });
|
|
fireEvent.change(screen.getByTestId('input-model'), { target: { value: 'FH16' } });
|
|
fireEvent.change(screen.getByTestId('input-fin'), { target: { value: 'WDB9066351L123456' } });
|
|
fireEvent.change(screen.getByTestId('input-price'), { target: { value: '85000' } });
|
|
|
|
fireEvent.click(screen.getByTestId('submit-button'));
|
|
|
|
await waitFor(() => {
|
|
expect(mockCreateVehicle).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
make: 'Volvo',
|
|
model: 'FH16',
|
|
fin: 'WDB9066351L123456',
|
|
price: 85000,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('VehicleDetail', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('renders vehicle details after loading', async () => {
|
|
(mockGetVehicle as any).mockResolvedValue(mockVehicle);
|
|
(mockGetMobileDeStatus as any).mockResolvedValue({
|
|
synced: false,
|
|
sync_status: 'pending',
|
|
});
|
|
|
|
renderWithProviders(<VehicleDetail vehicleId="vehicle-123" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('vehicle-detail')).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByTestId('vehicle-detail-title')).toHaveTextContent('Mercedes-Benz Actros');
|
|
const fieldsContainer = screen.getByTestId('vehicle-detail-fields');
|
|
expect(fieldsContainer).toHaveTextContent('WDB9066351L123456');
|
|
expect(fieldsContainer).toHaveTextContent('lkw');
|
|
expect(fieldsContainer).toHaveTextContent('available');
|
|
});
|
|
|
|
it('shows mobile.de status section', async () => {
|
|
(mockGetVehicle as any).mockResolvedValue(mockVehicle);
|
|
(mockGetMobileDeStatus as any).mockResolvedValue({
|
|
synced: true,
|
|
ad_id: 'ad-123',
|
|
synced_at: '2025-01-01T12:00:00Z',
|
|
sync_status: 'synced',
|
|
});
|
|
|
|
renderWithProviders(<VehicleDetail vehicleId="vehicle-123" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('mobile-de-status')).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByTestId('mobile-de-sync-status')).toHaveTextContent('Synced');
|
|
});
|
|
|
|
it('shows error message on API failure', async () => {
|
|
(mockGetVehicle as any).mockRejectedValue({
|
|
error: { code: 'VEHICLE_NOT_FOUND', message: 'Vehicle not found' },
|
|
});
|
|
|
|
renderWithProviders(<VehicleDetail vehicleId="nonexistent" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('vehicle-detail-error')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('MobileDeStatus', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('renders with pending status when no listing exists', async () => {
|
|
(mockGetMobileDeStatus as any).mockResolvedValue({
|
|
synced: false,
|
|
sync_status: 'pending',
|
|
});
|
|
|
|
renderWithProviders(<MobileDeStatus vehicleId="vehicle-123" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('mobile-de-sync-status')).toHaveTextContent('Pending');
|
|
});
|
|
|
|
expect(screen.getByTestId('mobile-de-synced')).toHaveTextContent('No');
|
|
});
|
|
|
|
it('shows synced status after successful push', async () => {
|
|
(mockGetMobileDeStatus as any).mockResolvedValue({
|
|
synced: true,
|
|
ad_id: 'ad-456',
|
|
synced_at: '2025-06-01T10:00:00Z',
|
|
sync_status: 'synced',
|
|
});
|
|
|
|
renderWithProviders(<MobileDeStatus vehicleId="vehicle-123" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('mobile-de-sync-status')).toHaveTextContent('Synced');
|
|
});
|
|
|
|
expect(screen.getByTestId('mobile-de-synced')).toHaveTextContent('Yes');
|
|
expect(screen.getByText('ad-456')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows error log when sync failed', async () => {
|
|
(mockGetMobileDeStatus as any).mockResolvedValue({
|
|
synced: false,
|
|
sync_status: 'fehler',
|
|
error_log: 'HTTP 500: Internal Server Error',
|
|
});
|
|
|
|
renderWithProviders(<MobileDeStatus vehicleId="vehicle-123" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('mobile-de-sync-status')).toHaveTextContent('Error');
|
|
expect(screen.getByTestId('mobile-de-error-log')).toBeInTheDocument();
|
|
expect(screen.getByText(/HTTP 500/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls pushToMobileDe when push button is clicked', async () => {
|
|
(mockGetMobileDeStatus as any).mockResolvedValue({
|
|
synced: false,
|
|
sync_status: 'pending',
|
|
});
|
|
(mockPushToMobileDe as any).mockResolvedValue({
|
|
message: 'Push queued',
|
|
vehicle_id: 'vehicle-123',
|
|
});
|
|
|
|
renderWithProviders(<MobileDeStatus vehicleId="vehicle-123" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('mobile-de-push-button')).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByTestId('mobile-de-push-button'));
|
|
|
|
await waitFor(() => {
|
|
expect(mockPushToMobileDe).toHaveBeenCalledWith('vehicle-123');
|
|
});
|
|
});
|
|
});
|