import * as React from 'react' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, expect, it, vi } from 'vitest' import { axe } from 'vitest-axe' import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuGroupLabel, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from './dropdown-menu' // Base UI's popup animation toggles pointer-events while entering; disable // user-event's safety check the same way the Select test does. const setupUser = () => userEvent.setup({ pointerEventsCheck: 0 }) function BasicMenu({ size, onSelect }: { size?: 'sm' | 'md' | 'lg'; onSelect?: () => void } = {}) { return ( Open Actions Profile Settings Disabled Docs ) } describe('DropdownMenu', () => { it('does not render content before being opened', () => { render() expect(screen.queryByText('Profile')).toBeNull() }) it('opens on trigger click and shows items', async () => { const user = setupUser() render() await user.click(screen.getByRole('button', { name: 'Open' })) expect(await screen.findByRole('menuitem', { name: 'Profile' })).toBeInTheDocument() }) it('fires onClick and closes on item click', async () => { const user = setupUser() const onSelect = vi.fn() render() await user.click(screen.getByRole('button', { name: 'Open' })) await user.click(await screen.findByRole('menuitem', { name: 'Profile' })) expect(onSelect).toHaveBeenCalledTimes(1) await waitFor(() => { expect(screen.queryByRole('menuitem', { name: 'Profile' })).toBeNull() }) }) it('closes on Escape', async () => { const user = setupUser() render() await user.click(screen.getByRole('button', { name: 'Open' })) await screen.findByRole('menuitem', { name: 'Profile' }) await user.keyboard('{Escape}') await waitFor(() => { expect(screen.queryByRole('menuitem', { name: 'Profile' })).toBeNull() }) }) it('propagates size to items via navigationLinkVariants', async () => { const user = setupUser() render() await user.click(screen.getByRole('button', { name: 'Open' })) const item = await screen.findByRole('menuitem', { name: 'Profile' }) expect(item.className).toContain('text-base') }) it('renders the popup with size-driven radius', async () => { const user = setupUser() render() await user.click(screen.getByRole('button', { name: 'Open' })) const item = await screen.findByRole('menuitem', { name: 'Profile' }) const popup = item.closest('[data-slot="dropdown-menu-content"]') as HTMLElement expect(popup).not.toBeNull() expect(popup.className).toContain('rounded-md') expect(popup.className).toContain('bg-background') }) it('renders a disabled item with data-disabled', async () => { const user = setupUser() render() await user.click(screen.getByRole('button', { name: 'Open' })) const disabled = await screen.findByRole('menuitem', { name: 'Disabled' }) expect(disabled.getAttribute('data-disabled')).not.toBeNull() }) it('renders a LinkItem as an anchor with href', async () => { const user = setupUser() render() await user.click(screen.getByRole('button', { name: 'Open' })) const link = (await screen.findByText('Docs')).closest('a') as HTMLAnchorElement expect(link).not.toBeNull() expect(link.getAttribute('href')).toBe('https://example.com') }) it('navigates items with the keyboard', async () => { const user = setupUser() render() await user.click(screen.getByRole('button', { name: 'Open' })) await screen.findByRole('menuitem', { name: 'Profile' }) await user.keyboard('{ArrowDown}') await waitFor(() => { const profile = screen.getByRole('menuitem', { name: 'Profile' }) expect(profile.getAttribute('data-highlighted')).not.toBeNull() }) }) it('RadioGroup: controlled value round-trips and only one indicator is checked', async () => { const user = setupUser() const onValueChange = vi.fn() function Wrapped() { const [value, setValue] = React.useState('a') return ( Open { onValueChange(next) setValue(next as string) }} > A B ) } render() await user.click(screen.getByRole('button', { name: 'Open' })) const itemB = await screen.findByRole('menuitemradio', { name: 'B' }) await user.click(itemB) expect(onValueChange).toHaveBeenCalledWith('b') // Radio items keep the menu open (Base UI `closeOnClick` defaults to false); // the checked indicator moves to B in place. const items = await screen.findAllByRole('menuitemradio') const checked = items.filter((el) => el.getAttribute('data-checked') !== null) expect(checked).toHaveLength(1) expect(checked[0]?.textContent).toBe('B') }) it('CheckboxItem: controlled checked toggles via onCheckedChange', async () => { const user = setupUser() const onCheckedChange = vi.fn() function Wrapped() { const [checked, setChecked] = React.useState(false) return ( Open { onCheckedChange(next) setChecked(next) }} > Notifications ) } render() await user.click(screen.getByRole('button', { name: 'Open' })) const item = await screen.findByRole('menuitemcheckbox', { name: 'Notifications' }) await user.click(item) expect(onCheckedChange).toHaveBeenCalledWith(true) }) it('Submenu: SubTrigger renders chevron and opens on hover', async () => { const user = setupUser() render( Open Top More Nested , ) await user.click(screen.getByRole('button', { name: 'Open' })) const subTrigger = await screen.findByRole('menuitem', { name: /More/ }) expect(subTrigger.querySelector('svg')).not.toBeNull() await user.hover(subTrigger) expect(await screen.findByRole('menuitem', { name: 'Nested' })).toBeInTheDocument() }) it('has no a11y violations in closed state', async () => { const { container } = render() expect(await axe(container)).toHaveNoViolations() }) })