393 lines
10 KiB
TypeScript
393 lines
10 KiB
TypeScript
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import userEvent from '@testing-library/user-event';
|
||
|
|
import { Dropdown } from './dropdown';
|
||
|
|
|
||
|
|
describe('Dropdown Component', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders trigger correctly', () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(screen.getByText('Open Menu')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not show menu initially', () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(screen.queryByText('Menu content')).not.toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('opens menu when trigger is clicked', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
expect(screen.getByText('Menu content')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('closes menu when trigger is clicked again', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
await user.click(trigger);
|
||
|
|
expect(screen.getByText('Menu content')).toBeInTheDocument();
|
||
|
|
|
||
|
|
await user.click(trigger);
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.queryByText('Menu content')).not.toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('closes menu when clicking outside', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<div>
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
<div>Outside content</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
await user.click(trigger);
|
||
|
|
expect(screen.getByText('Menu content')).toBeInTheDocument();
|
||
|
|
|
||
|
|
const outside = screen.getByText('Outside content');
|
||
|
|
await user.click(outside);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.queryByText('Menu content')).not.toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('closes menu when pressing Escape', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
expect(screen.getByText('Menu content')).toBeInTheDocument();
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.queryByText('Menu content')).not.toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('opens menu when pressing Enter on trigger', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.keyDown(trigger, { key: 'Enter' });
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Menu content')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('opens menu when pressing Space on trigger', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.keyDown(trigger, { key: ' ' });
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Menu content')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('navigates menu items with ArrowDown', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<button>Item 1</button>
|
||
|
|
<button>Item 2</button>
|
||
|
|
<button>Item 3</button>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
||
|
|
const item2 = screen.getByText('Item 2');
|
||
|
|
expect(document.activeElement).toBe(item2);
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
||
|
|
const item3 = screen.getByText('Item 3');
|
||
|
|
expect(document.activeElement).toBe(item3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('navigates menu items with ArrowUp', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<button>Item 1</button>
|
||
|
|
<button>Item 2</button>
|
||
|
|
<button>Item 3</button>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowUp' });
|
||
|
|
|
||
|
|
const item2 = screen.getByText('Item 2');
|
||
|
|
expect(document.activeElement).toBe(item2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('wraps navigation at the end', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<button>Item 1</button>
|
||
|
|
<button>Item 2</button>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
||
|
|
|
||
|
|
const item1 = screen.getByText('Item 1');
|
||
|
|
expect(document.activeElement).toBe(item1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('wraps navigation at the beginning', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<button>Item 1</button>
|
||
|
|
<button>Item 2</button>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: 'ArrowUp' });
|
||
|
|
|
||
|
|
const item2 = screen.getByText('Item 2');
|
||
|
|
expect(document.activeElement).toBe(item2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('activates item when pressing Enter', async () => {
|
||
|
|
const handleClick = vi.fn();
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<button onClick={handleClick}>Item 1</button>
|
||
|
|
<button>Item 2</button>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: 'Enter' });
|
||
|
|
|
||
|
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('activates item when pressing Space', async () => {
|
||
|
|
const handleClick = vi.fn();
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<button onClick={handleClick}>Item 1</button>
|
||
|
|
<button>Item 2</button>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
fireEvent.keyDown(document, { key: ' ' });
|
||
|
|
|
||
|
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('applies left alignment by default', () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
const menu = screen.getByText('Menu content').closest('.left-0');
|
||
|
|
expect(menu).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('applies right alignment', () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>} align="right">
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
const menu = screen.getByText('Menu content').closest('.right-0');
|
||
|
|
expect(menu).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('applies center alignment', () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>} align="center">
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
const menu = screen.getByText('Menu content').closest('.left-1\\/2');
|
||
|
|
expect(menu).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onOpenChange when menu opens', () => {
|
||
|
|
const onOpenChange = vi.fn();
|
||
|
|
render(
|
||
|
|
<Dropdown
|
||
|
|
trigger={<button>Open Menu</button>}
|
||
|
|
onOpenChange={onOpenChange}
|
||
|
|
>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
expect(onOpenChange).toHaveBeenCalledWith(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onOpenChange when menu closes', async () => {
|
||
|
|
const onOpenChange = vi.fn();
|
||
|
|
render(
|
||
|
|
<Dropdown
|
||
|
|
trigger={<button>Open Menu</button>}
|
||
|
|
onOpenChange={onOpenChange}
|
||
|
|
>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('has correct ARIA attributes', () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<div>Menu content</div>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const triggerWrapper = screen
|
||
|
|
.getByText('Open Menu')
|
||
|
|
.closest('[role="button"]');
|
||
|
|
expect(triggerWrapper).toHaveAttribute('aria-haspopup', 'true');
|
||
|
|
expect(triggerWrapper).toHaveAttribute('aria-expanded', 'false');
|
||
|
|
|
||
|
|
fireEvent.click(screen.getByText('Open Menu'));
|
||
|
|
|
||
|
|
const menu = screen.getByRole('menu');
|
||
|
|
expect(menu).toBeInTheDocument();
|
||
|
|
expect(menu).toHaveAttribute('aria-orientation', 'vertical');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('focuses first item when menu opens', async () => {
|
||
|
|
render(
|
||
|
|
<Dropdown trigger={<button>Open Menu</button>}>
|
||
|
|
<button>Item 1</button>
|
||
|
|
<button>Item 2</button>
|
||
|
|
</Dropdown>
|
||
|
|
);
|
||
|
|
|
||
|
|
const trigger = screen.getByText('Open Menu');
|
||
|
|
fireEvent.click(trigger);
|
||
|
|
|
||
|
|
// Attendre que le menu soit rendu
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Attendre un peu plus pour que le focus soit appliqué
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||
|
|
|
||
|
|
const item1 = screen.getByText('Item 1');
|
||
|
|
// Le focus devrait être sur le premier élément ou le menu devrait être présent
|
||
|
|
expect(item1).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|