veza/apps/web/src/components/navigation/Tabs.test.tsx

305 lines
9 KiB
TypeScript
Raw Normal View History

import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import { Tabs } from './Tabs';
describe('Tabs Component', () => {
const mockItems = [
{ id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
{ id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
{ id: 'tab3', label: 'Tab 3', content: <div>Content 3</div> },
];
const mockOnChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it('renders tabs with labels', () => {
render(<Tabs items={mockItems} />);
expect(screen.getByText('Tab 1')).toBeInTheDocument();
expect(screen.getByText('Tab 2')).toBeInTheDocument();
expect(screen.getByText('Tab 3')).toBeInTheDocument();
});
it('renders content of first tab by default', () => {
render(<Tabs items={mockItems} />);
expect(screen.getByText('Content 1')).toBeInTheDocument();
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
});
it('renders content of defaultActiveId tab', () => {
render(<Tabs items={mockItems} defaultActiveId="tab2" />);
expect(screen.getByText('Content 2')).toBeInTheDocument();
expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
});
it('switches content when tab is clicked', async () => {
const user = userEvent.setup();
render(<Tabs items={mockItems} />);
const tab2 = screen.getByText('Tab 2');
await user.click(tab2);
expect(screen.getByText('Content 2')).toBeInTheDocument();
expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
});
it('calls onChange when tab is clicked', async () => {
const user = userEvent.setup();
render(<Tabs items={mockItems} onChange={mockOnChange} />);
const tab2 = screen.getByText('Tab 2');
await user.click(tab2);
expect(mockOnChange).toHaveBeenCalledWith('tab2');
});
it('uses controlled activeId when provided', () => {
const { rerender } = render(<Tabs items={mockItems} activeId="tab2" />);
expect(screen.getByText('Content 2')).toBeInTheDocument();
rerender(<Tabs items={mockItems} activeId="tab3" />);
expect(screen.getByText('Content 3')).toBeInTheDocument();
});
it('navigates to next tab with ArrowRight key', async () => {
render(<Tabs items={mockItems} defaultActiveId="tab1" />);
const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
tab1.focus();
fireEvent.keyDown(tab1, { key: 'ArrowRight', code: 'ArrowRight' });
await waitFor(() => {
expect(screen.getByText('Content 2')).toBeInTheDocument();
});
});
it('navigates to previous tab with ArrowLeft key', async () => {
render(<Tabs items={mockItems} defaultActiveId="tab2" />);
const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
tab2.focus();
fireEvent.keyDown(tab2, { key: 'ArrowLeft', code: 'ArrowLeft' });
await waitFor(() => {
expect(screen.getByText('Content 1')).toBeInTheDocument();
});
});
it('navigates to first tab with Home key', async () => {
render(<Tabs items={mockItems} defaultActiveId="tab3" />);
const tab3 = screen.getByRole('tab', { name: 'Tab 3' });
tab3.focus();
fireEvent.keyDown(tab3, { key: 'Home', code: 'Home' });
await waitFor(() => {
expect(screen.getByText('Content 1')).toBeInTheDocument();
});
});
it('navigates to last tab with End key', async () => {
render(<Tabs items={mockItems} defaultActiveId="tab1" />);
const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
tab1.focus();
fireEvent.keyDown(tab1, { key: 'End', code: 'End' });
await waitFor(() => {
expect(screen.getByText('Content 3')).toBeInTheDocument();
});
});
it('wraps around when navigating with ArrowRight at last tab', async () => {
render(<Tabs items={mockItems} defaultActiveId="tab3" />);
const tab3 = screen.getByRole('tab', { name: 'Tab 3' });
tab3.focus();
fireEvent.keyDown(tab3, { key: 'ArrowRight', code: 'ArrowRight' });
await waitFor(() => {
expect(screen.getByText('Content 1')).toBeInTheDocument();
});
});
it('wraps around when navigating with ArrowLeft at first tab', async () => {
render(<Tabs items={mockItems} defaultActiveId="tab1" />);
const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
tab1.focus();
fireEvent.keyDown(tab1, { key: 'ArrowLeft', code: 'ArrowLeft' });
await waitFor(() => {
expect(screen.getByText('Content 3')).toBeInTheDocument();
});
});
it('disables tab when disabled prop is true', () => {
const itemsWithDisabled = [
{ id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
2025-12-13 02:34:34 +00:00
{
id: 'tab2',
label: 'Tab 2',
content: <div>Content 2</div>,
disabled: true,
},
];
render(<Tabs items={itemsWithDisabled} />);
const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
expect(tab2).toBeDisabled();
});
it('skips disabled tabs when navigating with keyboard', async () => {
const itemsWithDisabled = [
{ id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
2025-12-13 02:34:34 +00:00
{
id: 'tab2',
label: 'Tab 2',
content: <div>Content 2</div>,
disabled: true,
},
{ id: 'tab3', label: 'Tab 3', content: <div>Content 3</div> },
];
render(<Tabs items={itemsWithDisabled} defaultActiveId="tab1" />);
const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
tab1.focus();
fireEvent.keyDown(tab1, { key: 'ArrowRight', code: 'ArrowRight' });
// Devrait aller à tab3, en sautant tab2 désactivé
await waitFor(() => {
expect(screen.getByText('Content 3')).toBeInTheDocument();
});
});
it('does not switch to disabled tab when clicked', async () => {
const user = userEvent.setup();
const itemsWithDisabled = [
{ id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
2025-12-13 02:34:34 +00:00
{
id: 'tab2',
label: 'Tab 2',
content: <div>Content 2</div>,
disabled: true,
},
];
render(<Tabs items={itemsWithDisabled} />);
const tab2 = screen.getByText('Tab 2');
await user.click(tab2);
expect(screen.getByText('Content 1')).toBeInTheDocument();
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
});
it('renders tab with icon', () => {
const itemsWithIcon = [
{
id: 'tab1',
label: 'Tab 1',
content: <div>Content 1</div>,
icon: <span data-testid="icon">📊</span>,
},
];
render(<Tabs items={itemsWithIcon} />);
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
it('applies default variant styles', () => {
render(<Tabs items={mockItems} variant="default" />);
const tabList = screen.getByRole('tablist');
expect(tabList).toHaveClass('border-b');
});
it('applies pills variant styles', () => {
render(<Tabs items={mockItems} variant="pills" />);
const tabList = screen.getByText('Tab 1').closest('[role="tablist"]');
expect(tabList).toHaveClass('bg-muted');
});
it('applies underline variant styles', () => {
render(<Tabs items={mockItems} variant="underline" />);
const tabList = screen.getByText('Tab 1').closest('[role="tablist"]');
expect(tabList).toHaveClass('border-b');
});
it('has correct ARIA attributes', () => {
render(<Tabs items={mockItems} defaultActiveId="tab1" />);
const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
expect(tab1).toHaveAttribute('role', 'tab');
expect(tab1).toHaveAttribute('aria-selected', 'true');
expect(tab1).toHaveAttribute('aria-controls', 'tabpanel-tab1');
const tabpanel = screen.getByRole('tabpanel');
expect(tabpanel).toHaveAttribute('id', 'tabpanel-tab1');
expect(tabpanel).toHaveAttribute('aria-labelledby', 'tab-tab1');
});
it('sets tabIndex correctly', () => {
render(<Tabs items={mockItems} defaultActiveId="tab1" />);
const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
expect(tab1).toHaveAttribute('tabindex', '0');
const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
expect(tab2).toHaveAttribute('tabindex', '-1');
});
it('applies custom className', () => {
2025-12-13 02:34:34 +00:00
const { container } = render(
<Tabs items={mockItems} className="custom-class" />,
);
const tabsContainer = container.firstChild;
expect(tabsContainer).toHaveClass('custom-class');
});
it('handles empty items array', () => {
render(<Tabs items={[]} />);
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
});
it('selects first enabled tab when defaultActiveId points to disabled tab', async () => {
const itemsWithDisabled = [
2025-12-13 02:34:34 +00:00
{
id: 'tab1',
label: 'Tab 1',
content: <div>Content 1</div>,
disabled: true,
},
{ id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
];
render(<Tabs items={itemsWithDisabled} defaultActiveId="tab1" />);
await waitFor(() => {
expect(screen.getByText('Content 2')).toBeInTheDocument();
});
});
});