veza/apps/web/src/components/data/List.test.tsx
2025-12-12 21:34:34 -05:00

258 lines
6.6 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import { List, ListItem } from './List';
describe('List Component', () => {
const mockItems: ListItem[] = [
{
id: '1',
content: 'Item 1',
onClick: vi.fn(),
},
{
id: '2',
content: 'Item 2',
actions: <button>Action</button>,
},
{
id: '3',
content: 'Item 3',
},
];
beforeEach(() => {
vi.clearAllMocks();
});
it('renders list items', () => {
render(<List items={mockItems} />);
expect(screen.getByText('Item 1')).toBeInTheDocument();
expect(screen.getByText('Item 2')).toBeInTheDocument();
expect(screen.getByText('Item 3')).toBeInTheDocument();
});
it('renders empty message when items array is empty', () => {
render(<List items={[]} emptyMessage="No items found" />);
expect(screen.getByText('No items found')).toBeInTheDocument();
});
it('returns null when items array is empty and no empty message', () => {
const { container } = render(<List items={[]} />);
expect(container.firstChild).toBeNull();
});
it('calls onClick when item is clicked', async () => {
const user = userEvent.setup();
const mockOnClick = vi.fn();
const itemsWithClick: ListItem[] = [
{
id: '1',
content: 'Clickable Item',
onClick: mockOnClick,
},
];
render(<List items={itemsWithClick} />);
const item = screen.getByText('Clickable Item');
await user.click(item);
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when item is disabled', async () => {
const user = userEvent.setup();
const mockOnClick = vi.fn();
const itemsWithDisabled: ListItem[] = [
{
id: '1',
content: 'Disabled Item',
onClick: mockOnClick,
disabled: true,
},
];
render(<List items={itemsWithDisabled} />);
const item = screen.getByText('Disabled Item');
await user.click(item);
expect(mockOnClick).not.toHaveBeenCalled();
});
it('renders actions on items', () => {
render(<List items={mockItems} />);
const actionButton = screen.getByText('Action');
expect(actionButton).toBeInTheDocument();
});
it('does not trigger item onClick when clicking actions', async () => {
const user = userEvent.setup();
const mockOnClick = vi.fn();
const itemsWithActions: ListItem[] = [
{
id: '1',
content: 'Item with Actions',
actions: <button>Action</button>,
onClick: mockOnClick,
},
];
render(<List items={itemsWithActions} />);
const actionButton = screen.getByText('Action');
await user.click(actionButton);
expect(mockOnClick).not.toHaveBeenCalled();
});
it('applies default variant styles', () => {
const { container } = render(<List items={mockItems} variant="default" />);
const list = container.querySelector('ul');
expect(list).toHaveClass('space-y-1');
});
it('applies bordered variant styles', () => {
const { container } = render(<List items={mockItems} variant="bordered" />);
const list = container.querySelector('ul');
expect(list).toHaveClass('border');
expect(list).toHaveClass('divide-y');
expect(list).toHaveClass('rounded-md');
});
it('applies spaced variant styles', () => {
const { container } = render(<List items={mockItems} variant="spaced" />);
const list = container.querySelector('ul');
expect(list).toHaveClass('space-y-2');
});
it('applies custom className', () => {
const { container } = render(
<List items={mockItems} className="custom-class" />,
);
const list = container.querySelector('ul');
expect(list).toHaveClass('custom-class');
});
it('applies custom itemClassName', () => {
const { container } = render(
<List items={mockItems} itemClassName="custom-item-class" />,
);
const firstItem = container.querySelector('li');
expect(firstItem).toHaveClass('custom-item-class');
});
it('applies hover styles when item is clickable', () => {
const clickableItems: ListItem[] = [
{
id: '1',
content: 'Clickable',
onClick: vi.fn(),
},
];
const { container } = render(<List items={clickableItems} />);
const item = container.querySelector('li');
expect(item).toHaveClass('cursor-pointer');
expect(item).toHaveClass('hover:bg-accent');
});
it('applies disabled styles when item is disabled', () => {
const disabledItems: ListItem[] = [
{
id: '1',
content: 'Disabled',
disabled: true,
},
];
const { container } = render(<List items={disabledItems} />);
const item = container.querySelector('li');
expect(item).toHaveClass('cursor-not-allowed');
expect(item).toHaveClass('opacity-50');
});
it('renders items with complex content', () => {
const complexItems: ListItem[] = [
{
id: '1',
content: (
<div>
<h3>Title</h3>
<p>Description</p>
</div>
),
},
];
render(<List items={complexItems} />);
expect(screen.getByText('Title')).toBeInTheDocument();
expect(screen.getByText('Description')).toBeInTheDocument();
});
it('renders items with multiple actions', () => {
const itemsWithMultipleActions: ListItem[] = [
{
id: '1',
content: 'Item',
actions: (
<>
<button>Edit</button>
<button>Delete</button>
</>
),
},
];
render(<List items={itemsWithMultipleActions} />);
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Delete')).toBeInTheDocument();
});
it('handles items without onClick', () => {
const itemsWithoutClick: ListItem[] = [
{
id: '1',
content: 'Static Item',
},
];
const { container } = render(<List items={itemsWithoutClick} />);
const item = container.querySelector('li');
expect(item).not.toHaveClass('cursor-pointer');
});
it('handles items with onClick but no actions', () => {
const itemsWithClickOnly: ListItem[] = [
{
id: '1',
content: 'Clickable Item',
onClick: vi.fn(),
},
];
const { container } = render(<List items={itemsWithClickOnly} />);
const item = container.querySelector('li');
expect(item).toHaveClass('cursor-pointer');
// Actions container should not be rendered
const actionsContainer = item?.querySelector('.ml-4');
expect(actionsContainer).not.toBeInTheDocument();
});
});