244 lines
7.5 KiB
TypeScript
244 lines
7.5 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||
|
|
import { ContactList } from '@/components/contacts/ContactList';
|
||
|
|
import { ContactForm } from '@/components/contacts/ContactForm';
|
||
|
|
import { validateVatIdFormat } from '@/lib/contacts';
|
||
|
|
|
||
|
|
// Mock the contacts API module
|
||
|
|
vi.mock('@/lib/contacts', async (importOriginal) => {
|
||
|
|
const actual = await importOriginal<typeof import('@/lib/contacts')>();
|
||
|
|
return {
|
||
|
|
...actual,
|
||
|
|
listContacts: vi.fn(),
|
||
|
|
createContact: vi.fn(),
|
||
|
|
updateContact: vi.fn(),
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
import { listContacts, createContact } from '@/lib/contacts';
|
||
|
|
|
||
|
|
const mockContacts = {
|
||
|
|
items: [
|
||
|
|
{
|
||
|
|
id: 'test-id-1',
|
||
|
|
company_name: 'Müller Transport GmbH',
|
||
|
|
address_city: 'Berlin',
|
||
|
|
address_country: 'DE',
|
||
|
|
role: 'kaeufer',
|
||
|
|
vat_id: 'DE123456789',
|
||
|
|
vat_id_status: 'ungeprueft',
|
||
|
|
is_private: false,
|
||
|
|
contact_persons: [],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'test-id-2',
|
||
|
|
company_name: 'Van der Berg B.V.',
|
||
|
|
address_city: 'Amsterdam',
|
||
|
|
address_country: 'NL',
|
||
|
|
role: 'verkaeufer',
|
||
|
|
vat_id: 'NL123456789B01',
|
||
|
|
vat_id_status: 'geprueft',
|
||
|
|
is_private: false,
|
||
|
|
contact_persons: [{ id: 'p1', contact_id: 'test-id-2', name: 'Jan' }],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
total: 2,
|
||
|
|
page: 1,
|
||
|
|
page_size: 20,
|
||
|
|
};
|
||
|
|
|
||
|
|
describe('ContactList', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders contact list with search and filter controls', async () => {
|
||
|
|
vi.mocked(listContacts).mockResolvedValue(mockContacts);
|
||
|
|
render(<ContactList />);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('contact-list')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByTestId('filter-search')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('filter-role')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('filter-eu')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('filter-sort')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('displays contacts in table after loading', async () => {
|
||
|
|
vi.mocked(listContacts).mockResolvedValue(mockContacts);
|
||
|
|
render(<ContactList />);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Müller Transport GmbH')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByText('Van der Berg B.V.')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('Berlin')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('Amsterdam')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows new contact button', async () => {
|
||
|
|
vi.mocked(listContacts).mockResolvedValue(mockContacts);
|
||
|
|
render(<ContactList />);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('new-contact-btn')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows error message on API failure', async () => {
|
||
|
|
vi.mocked(listContacts).mockRejectedValue({
|
||
|
|
error: { message: 'Network error' },
|
||
|
|
});
|
||
|
|
render(<ContactList />);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByTestId('contact-error')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByText('Network error')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows loading state initially', async () => {
|
||
|
|
vi.mocked(listContacts).mockImplementation(
|
||
|
|
() => new Promise((resolve) => setTimeout(() => resolve(mockContacts), 100))
|
||
|
|
);
|
||
|
|
render(<ContactList />);
|
||
|
|
|
||
|
|
expect(screen.getByTestId('contact-loading')).toBeInTheDocument();
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Müller Transport GmbH')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('ContactForm', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders form with all required fields', () => {
|
||
|
|
render(<ContactForm mode="create" />);
|
||
|
|
|
||
|
|
expect(screen.getByTestId('contact-form')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('input-company_name')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('input-role')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('input-address_country')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('input-vat_id')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('input-phone')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('input-email')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('submit-button')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows EU/Inland toggle', () => {
|
||
|
|
render(<ContactForm mode="create" />);
|
||
|
|
|
||
|
|
expect(screen.getByTestId('eu-inland-toggle')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('toggle-inland')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('toggle-eu')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('defaults to Inland (DE) and shows DE placeholder for VAT ID', () => {
|
||
|
|
render(<ContactForm mode="create" />);
|
||
|
|
|
||
|
|
const inlandRadio = screen.getByTestId('toggle-inland') as HTMLInputElement;
|
||
|
|
expect(inlandRadio.checked).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('switches to EU mode and shows VAT ID hint when EU is selected', () => {
|
||
|
|
render(<ContactForm mode="create" />);
|
||
|
|
|
||
|
|
const euRadio = screen.getByTestId('toggle-eu');
|
||
|
|
fireEvent.click(euRadio);
|
||
|
|
|
||
|
|
expect(screen.getByTestId('vat-id-hint')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('validates VAT ID format and shows error for invalid DE VAT', () => {
|
||
|
|
render(<ContactForm mode="create" />);
|
||
|
|
|
||
|
|
const vatInput = screen.getByTestId('input-vat_id') as HTMLInputElement;
|
||
|
|
fireEvent.change(vatInput, { target: { value: 'DE123' } });
|
||
|
|
|
||
|
|
const submitBtn = screen.getByTestId('submit-button');
|
||
|
|
fireEvent.click(submitBtn);
|
||
|
|
|
||
|
|
expect(screen.getByText(/Invalid VAT ID format/)).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('accepts valid DE VAT ID without error', async () => {
|
||
|
|
vi.mocked(createContact).mockResolvedValue({
|
||
|
|
id: 'new-id',
|
||
|
|
company_name: 'Test GmbH',
|
||
|
|
address_country: 'DE',
|
||
|
|
role: 'kaeufer',
|
||
|
|
vat_id_status: 'ungeprueft',
|
||
|
|
is_private: false,
|
||
|
|
contact_persons: [],
|
||
|
|
});
|
||
|
|
|
||
|
|
render(<ContactForm mode="create" />);
|
||
|
|
|
||
|
|
const companyInput = screen.getByTestId('input-company_name');
|
||
|
|
fireEvent.change(companyInput, { target: { value: 'Test GmbH' } });
|
||
|
|
|
||
|
|
const vatInput = screen.getByTestId('input-vat_id');
|
||
|
|
fireEvent.change(vatInput, { target: { value: 'DE123456789' } });
|
||
|
|
|
||
|
|
const submitBtn = screen.getByTestId('submit-button');
|
||
|
|
fireEvent.click(submitBtn);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(createContact).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('requires company name', () => {
|
||
|
|
render(<ContactForm mode="create" />);
|
||
|
|
|
||
|
|
const submitBtn = screen.getByTestId('submit-button');
|
||
|
|
fireEvent.click(submitBtn);
|
||
|
|
|
||
|
|
expect(screen.getByText('Company name is required')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('validateVatIdFormat', () => {
|
||
|
|
it('returns null for empty VAT ID', () => {
|
||
|
|
expect(validateVatIdFormat('', 'DE')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns null for valid DE VAT ID', () => {
|
||
|
|
expect(validateVatIdFormat('DE123456789', 'DE')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns error for invalid DE VAT ID (too short)', () => {
|
||
|
|
const result = validateVatIdFormat('DE12345678', 'DE');
|
||
|
|
expect(result).toContain('Invalid');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns null for valid AT VAT ID', () => {
|
||
|
|
expect(validateVatIdFormat('ATU12345678', 'AT')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns null for valid NL VAT ID', () => {
|
||
|
|
expect(validateVatIdFormat('NL123456789B01', 'NL')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns error for non-EU country', () => {
|
||
|
|
const result = validateVatIdFormat('US123456789', 'US');
|
||
|
|
expect(result).toContain('not supported');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles lowercase input', () => {
|
||
|
|
expect(validateVatIdFormat('de123456789', 'DE')).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles spaces in VAT ID', () => {
|
||
|
|
expect(validateVatIdFormat('DE 123 456 789', 'DE')).toBeNull();
|
||
|
|
});
|
||
|
|
});
|