- 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>
395 lines
11 KiB
TypeScript
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();
|
|
});
|
|
});
|