veza/apps/web/src/components/ui/dropdown.test.tsx
senke 37981c2c17 chore(refactor/sumi-migration): commit pending changes — tests, stream server, dist_verification
- apps/web: test updates (Vitest/setup), playbackAnalyticsService, TrackGrid, serviceErrorHandler
- veza-common: logging, metrics, traits, validation, random
- veza-stream-server: audio pipeline, codecs, cache, monitoring, routes
- apps/web/dist_verification: refresh build assets (content-hashed filenames)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 19:39:18 +01:00

395 lines
11 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={<span>Open Menu</span>}>
<div>Menu content</div>
</Dropdown>,
);
expect(screen.getByText('Open Menu')).toBeInTheDocument();
});
it('does not show menu initially', () => {
render(
<Dropdown trigger={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<div>Menu content</div>
</Dropdown>,
);
// The trigger is wrapped in a native <button>, so click it
const triggerButton = screen.getByText('Open Menu').closest('button')!;
fireEvent.click(triggerButton);
await waitFor(() => {
expect(screen.getByText('Menu content')).toBeInTheDocument();
});
});
it('opens menu when pressing Space on trigger', async () => {
render(
<Dropdown trigger={<span>Open Menu</span>}>
<div>Menu content</div>
</Dropdown>,
);
// The trigger is wrapped in a native <button>, so click it
const triggerButton = screen.getByText('Open Menu').closest('button')!;
fireEvent.click(triggerButton);
await waitFor(() => {
expect(screen.getByText('Menu content')).toBeInTheDocument();
});
});
it('navigates menu items with ArrowDown', async () => {
render(
<Dropdown trigger={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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();
});
// After open, focusedIndex is 0 (Item 1)
// ArrowDown → 1 (Item 2)
// ArrowDown → 0 (wrap to Item 1)
// ArrowDown → 1 (Item 2)
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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>}>
<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={<span>Open Menu</span>} 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={<span>Open Menu</span>} 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={<span>Open Menu</span>}
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={<span>Open Menu</span>}
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={<span>Open Menu</span>}>
<div>Menu content</div>
</Dropdown>,
);
// The trigger is wrapped in a native <button> element
const triggerButton = screen.getByText('Open Menu').closest('button');
expect(triggerButton).toHaveAttribute('aria-haspopup', 'true');
expect(triggerButton).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={<span>Open Menu</span>}>
<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();
});
});