304 lines
9 KiB
TypeScript
304 lines
9 KiB
TypeScript
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> },
|
|
{
|
|
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> },
|
|
{
|
|
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> },
|
|
{
|
|
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', () => {
|
|
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 = [
|
|
{
|
|
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();
|
|
});
|
|
});
|
|
});
|