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 {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from './menubar'
// Base UI's popup animation toggles pointer-events while entering; disable
// user-event's safety check (same as dropdown-menu / select tests).
const setupUser = () => userEvent.setup({ pointerEventsCheck: 0 })
interface BasicMenubarProps {
variant?: 'pill' | 'line'
size?: 'sm' | 'md' | 'lg'
orientation?: 'horizontal' | 'vertical'
onNew?: () => void
}
function BasicMenubar({ variant, size, orientation, onNew }: BasicMenubarProps = {}) {
return (
File
New
Open
Disabled
More
Nested
Edit
Copy
)
}
describe('Menubar', () => {
it('renders a horizontal menubar with the expected slot and orientation', () => {
render()
const bar = screen.getByRole('menubar', { name: 'Main' })
expect(bar.getAttribute('data-slot')).toBe('menubar')
expect(bar.getAttribute('data-orientation')).toBe('horizontal')
})
it('applies flex-col styling when orientation is vertical', () => {
render()
const bar = screen.getByRole('menubar', { name: 'Main' })
expect(bar.getAttribute('data-orientation')).toBe('vertical')
expect(bar.className).toContain('flex-col')
})
it('mirrors orientation onto each trigger so the line variant selectors apply', () => {
render()
const trigger = screen.getByRole('menuitem', { name: 'File' })
expect(trigger.getAttribute('data-orientation')).toBe('vertical')
})
it('styles triggers with the pill variant by default', () => {
render()
const trigger = screen.getByRole('menuitem', { name: 'File' })
// pill variant in navigationLinkVariants paints the background via a `before:` layer
expect(trigger.className).toContain('before:bg-background-muted')
})
it('styles triggers with the line variant when requested', () => {
render()
const trigger = screen.getByRole('menuitem', { name: 'File' })
// line variant uses an `after:` underline rendered from a gradient
expect(trigger.className).toContain('after:bg-no-repeat')
})
it('propagates size down to triggers and popup content', async () => {
const user = setupUser()
render()
const trigger = screen.getByRole('menuitem', { name: 'File' })
expect(trigger.className).toContain('text-base')
await user.click(trigger)
const item = await screen.findByRole('menuitem', { name: 'New' })
const popup = item.closest('[data-slot="menubar-content"]') as HTMLElement
expect(popup).not.toBeNull()
expect(popup.className).toContain('rounded-xl')
})
it('opens a menu on trigger click and shows items', async () => {
const user = setupUser()
render()
await user.click(screen.getByRole('menuitem', { name: 'File' }))
expect(await screen.findByRole('menuitem', { name: 'New' })).toBeInTheDocument()
})
it('fires item onClick and closes the menu', async () => {
const user = setupUser()
const onNew = vi.fn()
render()
await user.click(screen.getByRole('menuitem', { name: 'File' }))
await user.click(await screen.findByRole('menuitem', { name: 'New' }))
expect(onNew).toHaveBeenCalledTimes(1)
await waitFor(() => {
expect(screen.queryByRole('menuitem', { name: 'New' })).toBeNull()
})
})
it('closes the open menu on Escape', async () => {
const user = setupUser()
render()
await user.click(screen.getByRole('menuitem', { name: 'File' }))
await screen.findByRole('menuitem', { name: 'New' })
await user.keyboard('{Escape}')
await waitFor(() => {
expect(screen.queryByRole('menuitem', { name: 'New' })).toBeNull()
})
})
it('moves focus between triggers with arrow keys', async () => {
const user = setupUser()
render()
const file = screen.getByRole('menuitem', { name: 'File' })
file.focus()
expect(file).toHaveFocus()
await user.keyboard('{ArrowRight}')
expect(screen.getByRole('menuitem', { name: 'Edit' })).toHaveFocus()
})
it('marks disabled items with data-disabled', async () => {
const user = setupUser()
render()
await user.click(screen.getByRole('menuitem', { name: 'File' }))
const disabled = await screen.findByRole('menuitem', { name: 'Disabled' })
expect(disabled.getAttribute('data-disabled')).not.toBeNull()
})
it('has no a11y violations in the closed state', async () => {
const { container } = render()
expect(await axe(container)).toHaveNoViolations()
})
})