- Add NavigationRail component (desktop vertical icon sidebar + mobile bottom tab bar) - Add ConfirmDialog with promise-based useConfirmDialog hook for async confirmation flow - Add WelcomeBanner onboarding component (one-time display, localStorage persistence) - Polish login form UX (shake on error, TOTP slide animation, password visibility toggle, session expired banner) - Add inline form validation with shake animation in email composer and contacts - Add empty state patterns for contacts (no data vs no search results with contextual actions) - Improve toast notification system with undo action support and typed durations - Add WCAG AA prefers-reduced-motion media query, safe area insets, sr-only live regions - Add template settings tab and keyboard shortcut integration - Update all 8 locale translations
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { ContactDetail } from '../contact-detail';
|
|
import type { ContactCard } from '@/lib/jmap/types';
|
|
|
|
beforeEach(() => {
|
|
Object.assign(navigator, {
|
|
clipboard: {
|
|
writeText: vi.fn().mockResolvedValue(undefined),
|
|
},
|
|
});
|
|
});
|
|
|
|
const contact: ContactCard = {
|
|
id: '1',
|
|
addressBookIds: {},
|
|
name: { components: [{ kind: 'given', value: 'Alice' }, { kind: 'surname', value: 'Smith' }], isOrdered: true },
|
|
emails: { e0: { address: 'alice@example.com' } },
|
|
phones: { p0: { number: '+33612345678' } },
|
|
organizations: { o0: { name: 'Acme Corp' } },
|
|
addresses: { a0: { street: '123 Main St', locality: 'Paris', country: 'France' } },
|
|
notes: { n0: { note: 'VIP customer' } },
|
|
};
|
|
|
|
describe('ContactDetail', () => {
|
|
it('shows empty state when contact is null', () => {
|
|
render(<ContactDetail contact={null} onEdit={vi.fn()} onDelete={vi.fn()} />);
|
|
expect(screen.getByText('detail.no_contact_selected')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays the contact name', () => {
|
|
render(<ContactDetail contact={contact} onEdit={vi.fn()} onDelete={vi.fn()} />);
|
|
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays email addresses as mailto links', () => {
|
|
render(<ContactDetail contact={contact} onEdit={vi.fn()} onDelete={vi.fn()} />);
|
|
const link = screen.getByText('alice@example.com');
|
|
expect(link.closest('a')).toHaveAttribute('href', 'mailto:alice@example.com');
|
|
});
|
|
|
|
it('displays phone numbers', () => {
|
|
render(<ContactDetail contact={contact} onEdit={vi.fn()} onDelete={vi.fn()} />);
|
|
expect(screen.getByText('+33612345678')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays organization name', () => {
|
|
render(<ContactDetail contact={contact} onEdit={vi.fn()} onDelete={vi.fn()} />);
|
|
const matches = screen.getAllByText('Acme Corp');
|
|
expect(matches.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('calls onEdit when edit button is clicked', () => {
|
|
const onEdit = vi.fn();
|
|
render(<ContactDetail contact={contact} onEdit={onEdit} onDelete={vi.fn()} />);
|
|
fireEvent.click(screen.getByText('form.edit_title'));
|
|
expect(onEdit).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('calls onDelete when delete button is clicked', () => {
|
|
const onDelete = vi.fn();
|
|
render(<ContactDetail contact={contact} onEdit={vi.fn()} onDelete={onDelete} />);
|
|
const deleteButton = screen.getAllByRole('button').find(
|
|
btn => btn.className.includes('text-red')
|
|
);
|
|
fireEvent.click(deleteButton!);
|
|
expect(onDelete).toHaveBeenCalledOnce();
|
|
});
|
|
});
|