feat: enhance contacts management with sidebar and selection features

This commit is contained in:
Linus Rath
2026-03-16 15:15:58 +01:00
parent ed4be96706
commit 1b35c9e3d6
7 changed files with 410 additions and 197 deletions
@@ -23,33 +23,62 @@ const _emptyContact: ContactCard = {
};
describe('ContactListItem', () => {
const baseProps = {
isSelected: false,
isChecked: false,
hasSelection: false,
density: 'regular' as const,
onClick: vi.fn(),
onCheckboxClick: vi.fn(),
};
it('renders contact name and email', () => {
render(<ContactListItem contact={contact} isSelected={false} onClick={vi.fn()} />);
render(<ContactListItem contact={contact} {...baseProps} />);
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
it('renders organization', () => {
render(<ContactListItem contact={contact} isSelected={false} onClick={vi.fn()} />);
it('renders organization in comfortable density', () => {
render(<ContactListItem contact={contact} {...baseProps} density="comfortable" />);
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
});
it('hides organization in regular density', () => {
render(<ContactListItem contact={contact} {...baseProps} density="regular" />);
expect(screen.queryByText('Acme Corp')).not.toBeInTheDocument();
});
it('applies selected styling', () => {
const { container } = render(<ContactListItem contact={contact} isSelected={true} onClick={vi.fn()} />);
const button = container.querySelector('button');
expect(button?.className).toContain('bg-accent');
const { container } = render(<ContactListItem contact={contact} {...baseProps} isSelected={true} />);
const div = container.firstElementChild;
expect(div?.className).toContain('bg-blue-200');
});
it('shows email as display name when no name exists', () => {
render(<ContactListItem contact={noNameContact} isSelected={false} onClick={vi.fn()} />);
render(<ContactListItem contact={noNameContact} {...baseProps} />);
const matches = screen.getAllByText('nobody@example.com');
expect(matches.length).toBeGreaterThanOrEqual(1);
});
it('calls onClick when clicked', () => {
const onClick = vi.fn();
render(<ContactListItem contact={contact} isSelected={false} onClick={onClick} />);
render(<ContactListItem contact={contact} {...baseProps} onClick={onClick} />);
fireEvent.click(screen.getByText('Alice Smith'));
expect(onClick).toHaveBeenCalledOnce();
});
it('does not show checkbox when hasSelection is false', () => {
const { container } = render(<ContactListItem contact={contact} {...baseProps} hasSelection={false} />);
expect(container.querySelector('button')).not.toBeInTheDocument();
});
it('shows checkbox when hasSelection is true', () => {
const { container } = render(<ContactListItem contact={contact} {...baseProps} hasSelection={true} />);
expect(container.querySelector('button')).toBeInTheDocument();
});
it('hides avatar in extra-compact density', () => {
const { container } = render(<ContactListItem contact={contact} {...baseProps} density="extra-compact" />);
expect(container.querySelector('[data-testid="avatar"]') || container.querySelector('.rounded-full')).toBeNull();
});
});
@@ -36,15 +36,15 @@ const defaultProps = {
onSearchChange: vi.fn(),
onSelectContact: vi.fn(),
onCreateNew: vi.fn(),
supportsSync: true,
categoryLabel: 'All Contacts',
selectedContactIds: new Set<string>(),
onToggleSelection: vi.fn(),
onSelectRangeContacts: vi.fn(),
onSelectAll: vi.fn(),
onClearSelection: vi.fn(),
onBulkDelete: vi.fn(),
onBulkAddToGroup: vi.fn(),
onBulkExport: vi.fn(),
groups: [],
};
describe('ContactList', () => {
@@ -70,32 +70,14 @@ describe('ContactList', () => {
expect(screen.getByText('empty_search')).toBeInTheDocument();
});
it('shows local mode banner when supportsSync is false', () => {
render(<ContactList {...defaultProps} supportsSync={false} />);
expect(screen.getByText('local_mode')).toBeInTheDocument();
});
it('hides local mode banner when supportsSync is true', () => {
render(<ContactList {...defaultProps} supportsSync={true} />);
expect(screen.queryByText('local_mode')).not.toBeInTheDocument();
});
it('calls onCreateNew when create button is clicked', () => {
const onCreateNew = vi.fn();
render(<ContactList {...defaultProps} onCreateNew={onCreateNew} />);
fireEvent.click(screen.getByText('create_new'));
expect(onCreateNew).toHaveBeenCalledOnce();
});
it('shows bulk action bar when contacts are selected', () => {
render(<ContactList {...defaultProps} selectedContactIds={new Set(['1'])} />);
expect(screen.getByText('bulk.delete')).toBeInTheDocument();
expect(screen.getByText('bulk.export')).toBeInTheDocument();
});
it('excludes groups from the list', () => {
render(<ContactList {...defaultProps} contacts={[alice, bob, group]} />);
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
expect(screen.queryByText('Team')).not.toBeInTheDocument();
it('shows category label with count', () => {
render(<ContactList {...defaultProps} />);
expect(screen.getByText('All Contacts (2)')).toBeInTheDocument();
});
});