Files
headlessui/packages/@headlessui-react/src/components/menu/menu.test.tsx
T
Robin Malfait ae8c253c21 Fix typos (#3086)
* fix a bunch of typos

* fix typos in `@headlessui/vue`
2024-04-08 23:31:50 +02:00

3540 lines
97 KiB
TypeScript

import { render } from '@testing-library/react'
import React, { createElement, useEffect } from 'react'
import {
MenuState,
assertActiveElement,
assertMenu,
assertMenuButton,
assertMenuButtonLinkedWithMenu,
assertMenuItem,
assertMenuLinkedWithMenuItem,
assertNoActiveMenuItem,
getByText,
getMenu,
getMenuButton,
getMenuButtons,
getMenuItems,
getMenus,
} from '../../test-utils/accessibility-assertions'
import {
Keys,
MouseButton,
click,
focus,
mouseLeave,
mouseMove,
press,
rawClick,
shift,
type,
word,
} from '../../test-utils/interactions'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
import { Transition } from '../transition/transition'
import { Menu } from './menu'
jest.mock('../../hooks/use-id')
// @ts-expect-error
global.ResizeObserver = class FakeResizeObserver {
observe() {}
disconnect() {}
}
beforeAll(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any)
jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any)
})
afterAll(() => jest.restoreAllMocks())
describe('Safe guards', () => {
it.each([
['Menu.Button', Menu.Button],
['Menu.Items', Menu.Items],
['Menu.Item', Menu.Item],
])(
'should error when we are using a <%s /> without a parent <Menu />',
suppressConsoleLogs((name, Component) => {
expect(() => render(createElement(Component as any))).toThrow(
`<${name} /> is missing a parent <Menu /> component.`
)
})
)
it(
'should be possible to render a Menu without crashing',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
})
describe('Rendering', () => {
describe('Menu', () => {
it(
'should be possible to render a Menu using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
{({ open }) => (
<>
<Menu.Button>Trigger</Menu.Button>
{open && (
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
)}
</>
)}
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.Visible })
})
)
it(
'should be possible to manually close the Menu using the exposed close function',
suppressConsoleLogs(async () => {
render(
<Menu>
{({ close }) => (
<>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item>
<button
onClick={(e) => {
e.preventDefault()
close()
}}
>
Close
</button>
</Menu.Item>
</Menu.Items>
</>
)}
</Menu>
)
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
await click(getByText('Close'))
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
})
describe('Menu.Button', () => {
it(
'should be possible to render a Menu.Button using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>{(slot) => <>{JSON.stringify(slot)}</>}</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({
open: false,
active: false,
disabled: false,
hover: false,
focus: false,
autofocus: false,
}),
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({
open: true,
active: true,
disabled: false,
hover: false,
focus: false,
autofocus: false,
}),
})
assertMenu({ state: MenuState.Visible })
})
)
it(
'should be possible to render a Menu.Button using a render prop and an `as` prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button as="div" role="button">
{(slot) => <>{JSON.stringify(slot)}</>}
</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({
open: false,
active: false,
disabled: false,
hover: false,
focus: false,
autofocus: false,
}),
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({
open: true,
active: true,
disabled: false,
hover: false,
focus: false,
autofocus: false,
}),
})
assertMenu({ state: MenuState.Visible })
})
)
describe('`type` attribute', () => {
it('should set the `type` to "button" by default', async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
</Menu>
)
expect(getMenuButton()).toHaveAttribute('type', 'button')
})
it('should not set the `type` to "button" if it already contains a `type`', async () => {
render(
<Menu>
<Menu.Button type="submit">Trigger</Menu.Button>
</Menu>
)
expect(getMenuButton()).toHaveAttribute('type', 'submit')
})
it('should set the `type` to "button" when using the `as` prop which resolves to a "button"', async () => {
let CustomButton = React.forwardRef<HTMLButtonElement>((props, ref) => (
<button ref={ref} {...props} />
))
render(
<Menu>
<Menu.Button as={CustomButton}>Trigger</Menu.Button>
</Menu>
)
expect(getMenuButton()).toHaveAttribute('type', 'button')
})
it('should not set the type if the "as" prop is not a "button"', async () => {
render(
<Menu>
<Menu.Button as="div">Trigger</Menu.Button>
</Menu>
)
expect(getMenuButton()).not.toHaveAttribute('type')
})
it('should not set the `type` to "button" when using the `as` prop which resolves to a "div"', async () => {
let CustomButton = React.forwardRef<HTMLDivElement>((props, ref) => (
<div ref={ref} {...props} />
))
render(
<Menu>
<Menu.Button as={CustomButton}>Trigger</Menu.Button>
</Menu>
)
expect(getMenuButton()).not.toHaveAttribute('type')
})
})
})
describe('Menu.Items', () => {
it(
'should be possible to render Menu.Items using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
{(data) => (
<>
<Menu.Item as="a">{JSON.stringify(data)}</Menu.Item>
</>
)}
</Menu.Items>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({
state: MenuState.Visible,
textContent: JSON.stringify({ open: true }),
})
})
)
it('should be possible to always render the Menu.Items if we provide it a `static` prop', () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items static>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
// Let's verify that the Menu is already there
expect(getMenu()).not.toBe(null)
})
it('should be possible to use a different render strategy for the Menu.Items', async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items unmount={false}>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenu({ state: MenuState.InvisibleHidden })
// Let's open the Menu, to see if it is not hidden anymore
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
})
})
describe('Menu.Item', () => {
it(
'should be possible to render a Menu.Item using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">{(slot) => <>{JSON.stringify(slot)}</>}</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({
state: MenuState.Visible,
textContent: JSON.stringify({ active: false, focus: false, disabled: false }),
})
})
)
it(
'should be possible to manually close the Menu using the exposed close function',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item>
{({ close }) => (
<button
onClick={(e) => {
e.preventDefault()
close()
}}
>
Close
</button>
)}
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
await click(getByText('Close'))
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it('should not override an explicit disabled prop on MenuItems child', async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item>{({ disabled }) => <button disabled={disabled}>Item A</button>}</Menu.Item>
<Menu.Item>{({ disabled }) => <button disabled={disabled}>Item B</button>}</Menu.Item>
<Menu.Item disabled>
{({ disabled }) => <button disabled={disabled}>Item C</button>}
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
})
assertMenu({ state: MenuState.InvisibleUnmounted })
getMenuButton()?.focus()
await press(Keys.Enter)
assertMenuButton({
state: MenuState.Visible,
})
assertMenu({ state: MenuState.Visible })
assertMenuItem(getMenuItems()[0], {
tag: 'button',
attributes: { 'data-focus': '' },
})
assertMenuItem(getMenuItems()[1], {
tag: 'button',
attributes: {},
})
assertMenuItem(getMenuItems()[2], {
tag: 'button',
attributes: { disabled: '' },
})
})
})
it('should guarantee the order of DOM nodes when performing actions', async () => {
function Example({ hide = false }) {
return (
<>
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="button">Item 1</Menu.Item>
{!hide && <Menu.Item as="button">Item 2</Menu.Item>}
<Menu.Item as="button">Item 3</Menu.Item>
</Menu.Items>
</Menu>
</>
)
}
let { rerender } = render(<Example />)
// Open the Menu
await click(getByText('Trigger'))
rerender(<Example hide={true} />) // Remove Menu.Item 2
rerender(<Example hide={false} />) // Re-add Menu.Item 2
assertMenu({ state: MenuState.Visible })
let items = getMenuItems()
// Focus the first item
await press(Keys.ArrowDown)
// Verify that the first menu item is active
assertMenuLinkedWithMenuItem(items[0])
await press(Keys.ArrowDown)
// Verify that the second menu item is active
assertMenuLinkedWithMenuItem(items[1])
await press(Keys.ArrowDown)
// Verify that the third menu item is active
assertMenuLinkedWithMenuItem(items[2])
})
})
describe('Rendering composition', () => {
it(
'should be possible to conditionally render classNames (aka className can be a function?!)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" className={(bag) => JSON.stringify(bag)}>
Item A
</Menu.Item>
<Menu.Item as="a" disabled className={(bag) => JSON.stringify(bag)}>
Item B
</Menu.Item>
<Menu.Item as="a" className="no-special-treatment">
Item C
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// Verify correct classNames
expect('' + items[0].classList).toEqual(
JSON.stringify({ active: false, focus: false, disabled: false })
)
expect('' + items[1].classList).toEqual(
JSON.stringify({ active: false, focus: false, disabled: true })
)
expect('' + items[2].classList).toEqual('no-special-treatment')
// Double check that nothing is active
assertNoActiveMenuItem()
// Make the first item active
await press(Keys.ArrowDown)
// Verify the classNames
expect('' + items[0].classList).toEqual(
JSON.stringify({ active: true, focus: true, disabled: false })
)
expect('' + items[1].classList).toEqual(
JSON.stringify({ active: false, focus: false, disabled: true })
)
expect('' + items[2].classList).toEqual('no-special-treatment')
// Double check that the first item is the active one
assertMenuLinkedWithMenuItem(items[0])
// Let's go down, this should go to the third item since the second item is disabled!
await press(Keys.ArrowDown)
// Verify the classNames
expect('' + items[0].classList).toEqual(
JSON.stringify({ active: false, focus: false, disabled: false })
)
expect('' + items[1].classList).toEqual(
JSON.stringify({ active: false, focus: false, disabled: true })
)
expect('' + items[2].classList).toEqual('no-special-treatment')
// Double check that the last item is the active one
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to swap the menu item with a button for example',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="button">Item A</Menu.Item>
<Menu.Item as="button">Item B</Menu.Item>
<Menu.Item as="button">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify items are buttons now
let items = getMenuItems()
items.forEach((item) => assertMenuItem(item, { tag: 'button' }))
})
)
it(
'should mark all the elements between Menu.Items and Menu.Item with role none',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<div className="outer">
<Menu.Items>
<div className="inner py-1">
<Menu.Item as="button">Item A</Menu.Item>
<Menu.Item as="button">Item B</Menu.Item>
</div>
<div className="inner py-1">
<Menu.Item as="button">Item C</Menu.Item>
<Menu.Item>
<div>
<div className="outer">Item D</div>
</div>
</Menu.Item>
</div>
<div className="inner py-1">
<form className="inner">
<Menu.Item as="button">Item E</Menu.Item>
</form>
</div>
</Menu.Items>
</div>
</Menu>
)
// Open menu
await click(getMenuButton())
expect.hasAssertions()
document.querySelectorAll('.outer').forEach((element) => {
expect(element).not.toHaveAttribute('role', 'none')
})
document.querySelectorAll('.inner').forEach((element) => {
expect(element).toHaveAttribute('role', 'none')
})
})
)
})
describe('Composition', () => {
function Debug({ fn, name }: { fn: (text: string) => void; name: string }) {
useEffect(() => {
fn(`Mounting - ${name}`)
return () => {
fn(`Unmounting - ${name}`)
}
}, [fn, name])
return null
}
it(
'should be possible to wrap the Menu.Items with a Transition component',
suppressConsoleLogs(async () => {
let orderFn = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Debug name="Menu" fn={orderFn} />
<Transition>
<Debug name="Transition" fn={orderFn} />
<Menu.Items>
<Menu.Item as="a">
{(data) => (
<>
{JSON.stringify(data)}
<Debug name="Menu.Item" fn={orderFn} />
</>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await rawClick(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({
state: MenuState.Visible,
textContent: JSON.stringify({ active: false, focus: false, disabled: false }),
})
await rawClick(getMenuButton())
// Verify that we tracked the `mounts` and `unmounts` in the correct order
expect(orderFn.mock.calls).toEqual([
['Mounting - Menu'],
['Mounting - Transition'],
['Mounting - Menu.Item'],
['Unmounting - Transition'],
['Unmounting - Menu.Item'],
])
})
)
it(
'should be possible to wrap the Menu.Items with a Transition.Child component',
suppressConsoleLogs(async () => {
let orderFn = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Debug name="Menu" fn={orderFn} />
<Transition.Child>
<Debug name="Transition" fn={orderFn} />
<Menu.Items>
<Menu.Item as="a">
{(data) => (
<>
{JSON.stringify(data)}
<Debug name="Menu.Item" fn={orderFn} />
</>
)}
</Menu.Item>
</Menu.Items>
</Transition.Child>
</Menu>
)
assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })
await rawClick(getMenuButton())
assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({
state: MenuState.Visible,
textContent: JSON.stringify({ active: false, focus: false, disabled: false }),
})
await rawClick(getMenuButton())
// Verify that we tracked the `mounts` and `unmounts` in the correct order
expect(orderFn.mock.calls).toEqual([
['Mounting - Menu'],
['Mounting - Transition'],
['Mounting - Menu.Item'],
['Unmounting - Transition'],
['Unmounting - Menu.Item'],
])
})
)
})
describe('Keyboard interactions', () => {
describe('`Enter` key', () => {
it(
'should be possible to open the menu with Enter',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
// Verify that the first menu item is active
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should not be possible to open the menu with Enter when the button is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button disabled>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Try to open the menu
await press(Keys.Enter)
// Verify it is still closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
)
it(
'should focus the first non disabled menu item when opening with Enter',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should focus the first non disabled menu item when opening with Enter (jump over multiple disabled ones)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should have no active menu item upon Enter key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
assertNoActiveMenuItem()
})
)
it(
'should be possible to close the menu with Enter when there is no active menuitem',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
// Close menu
await press(Keys.Enter)
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
it(
'should be possible to close the menu with Enter and invoke the active menu item',
suppressConsoleLogs(async () => {
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" onClick={clickHandler}>
Item A
</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
let items = getMenuItems()
await mouseMove(items[0])
// Close menu, and invoke the item
await press(Keys.Enter)
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
// Verify the "click" went through on the `a` tag
expect(clickHandler).toHaveBeenCalled()
})
)
})
it(
'should be possible to use a button as a menu item and invoke it upon Enter',
suppressConsoleLogs(async () => {
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="button" onClick={clickHandler}>
Item B
</Menu.Item>
<Menu.Item>
<button onClick={clickHandler}>Item C</button>
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
// Activate the second menu item
let items = getMenuItems()
await mouseMove(items[1])
// Close menu, and invoke the item
await press(Keys.Enter)
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button got "clicked"
expect(clickHandler).toHaveBeenCalledTimes(1)
// Verify the button is focused again
assertActiveElement(getMenuButton())
// Click the menu button again
await click(getMenuButton())
// Activate the last menu item
await mouseMove(getMenuItems()[2])
// Close menu, and invoke the item
await press(Keys.Enter)
// Verify the button got "clicked"
expect(clickHandler).toHaveBeenCalledTimes(2)
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
describe('`Space` key', () => {
it(
'should be possible to open the menu with Space',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Space)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should not be possible to open the menu with Space when the button is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button disabled>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Try to open the menu
await press(Keys.Space)
// Verify it is still closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Space)
assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
)
it(
'should focus the first non disabled menu item when opening with Space',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Space)
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should focus the first non disabled menu item when opening with Space (jump over multiple disabled ones)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Space)
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should have no active menu item upon Space key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Space)
assertNoActiveMenuItem()
})
)
it(
'should be possible to close the menu with Space when there is no active menuitem',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
// Close menu
await press(Keys.Space)
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
it(
'should be possible to close the menu with Space and invoke the active menu item',
suppressConsoleLogs(async () => {
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" onClick={clickHandler}>
Item A
</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
let items = getMenuItems()
await mouseMove(items[0])
// Close menu, and invoke the item
await press(Keys.Space)
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the "click" went through on the `a` tag
expect(clickHandler).toHaveBeenCalled()
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
})
describe('`Escape` key', () => {
it(
'should be possible to close an open menu with Escape',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Space)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Close menu
await press(Keys.Escape)
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
})
describe('`Tab` key', () => {
it(
'should close when we use Tab',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
// Try to tab
await press(Keys.Tab)
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should focus trap when we use Shift+Tab',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
// Try to Shift+Tab
await press(shift(Keys.Tab))
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
})
describe('`ArrowDown` key', () => {
it(
'should be possible to open the menu with ArrowDown',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowDown)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
// Verify that the first menu item is active
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should not be possible to open the menu with ArrowDown when the button is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button disabled>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Try to open the menu
await press(Keys.ArrowDown)
// Verify it is still closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowDown)
assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
)
it(
'should be possible to use ArrowDown to navigate the menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go down once
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(items[1])
// We should be able to go down again
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(items[2])
// We should NOT be able to go down again (because last item). Current implementation won't go around.
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to use ArrowDown to navigate the menu items and skip the first disabled one',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[1])
// We should be able to go down once
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to use ArrowDown to navigate the menu items and jump to the first non-disabled one',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
})
)
})
describe('`ArrowUp` key', () => {
it(
'should be possible to open the menu with ArrowUp and the last item should be active',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
// ! ALERT: The LAST item should now be active
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should not be possible to open the menu with ArrowUp and the last item should be active when the button is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button disabled>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Try to open the menu
await press(Keys.ArrowUp)
// Verify it is still closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
)
it(
'should be possible to use ArrowUp to navigate the menu items and jump to the first non-disabled one',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should not be possible to navigate up or down if there is only a single non-disabled item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
// We should not be able to go up (because those are disabled)
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(items[2])
// We should not be able to go down (because this is the last item)
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to use ArrowUp to navigate the menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
// We should be able to go down once
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(items[1])
// We should be able to go down again
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(items[0])
// We should NOT be able to go up again (because first item). Current implementation won't go around.
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(items[0])
})
)
})
describe('`End` key', () => {
it(
'should be possible to use the End key to go to the last menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last item
await press(Keys.End)
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to use the End key to go to the last non disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last non-disabled item
await press(Keys.End)
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should be possible to use the End key to go to the first menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.End)
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should have no active menu item upon End key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.End)
assertNoActiveMenuItem()
})
)
})
describe('`PageDown` key', () => {
it(
'should be possible to use the PageDown key to go to the last menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last item
await press(Keys.PageDown)
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to use the PageDown key to go to the last non disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.Enter)
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last non-disabled item
await press(Keys.PageDown)
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should be possible to use the PageDown key to go to the first menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.PageDown)
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should have no active menu item upon PageDown key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.PageDown)
assertNoActiveMenuItem()
})
)
})
describe('`Home` key', () => {
it(
'should be possible to use the Home key to go to the first menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
// We should be able to go to the first item
await press(Keys.Home)
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should be possible to use the Home key to go to the first non disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.Home)
let items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to use the Home key to go to the last menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.Home)
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[3])
})
)
it(
'should have no active menu item upon Home key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.Home)
assertNoActiveMenuItem()
})
)
})
describe('`PageUp` key', () => {
it(
'should be possible to use the PageUp key to go to the first menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
// We should be able to go to the first item
await press(Keys.PageUp)
assertMenuLinkedWithMenuItem(items[0])
})
)
it(
'should be possible to use the PageUp key to go to the first non disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.PageUp)
let items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to use the PageUp key to go to the last menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.PageUp)
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[3])
})
)
it(
'should have no active menu item upon PageUp key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem()
// We should not be able to go to the end
await press(Keys.PageUp)
assertNoActiveMenuItem()
})
)
})
describe('`Any` key aka search', () => {
it(
'should be possible to type a full word that has a perfect match',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// We should be able to go to the second item
await type(word('bob'))
assertMenuLinkedWithMenuItem(items[1])
// We should be able to go to the first item
await type(word('alice'))
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last item
await type(word('charlie'))
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to type a partial of a word',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
// We should be able to go to the second item
await type(word('bo'))
assertMenuLinkedWithMenuItem(items[1])
// We should be able to go to the first item
await type(word('ali'))
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last item
await type(word('char'))
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to type words with spaces',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">value a</Menu.Item>
<Menu.Item as="a">value b</Menu.Item>
<Menu.Item as="a">value c</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
// We should be able to go to the second item
await type(word('value b'))
assertMenuLinkedWithMenuItem(items[1])
// We should be able to go to the first item
await type(word('value a'))
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last item
await type(word('value c'))
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should not be possible to search for a disabled item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
// We should not be able to go to the disabled item
await type(word('bo'))
// We should still be on the last item
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should be possible to search for a word (case insensitive)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
await focus(getMenuButton())
// Open menu
await press(Keys.ArrowUp)
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
// Search for bob in a different casing
await type(word('BO'))
// We should be on `bob`
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should be possible to search for the next occurrence',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// Search for bob
await type(word('b'))
// We should be on the first `bob`
assertMenuLinkedWithMenuItem(items[1])
// Search for bob again
await type(word('b'))
// We should be on the second `bob`
assertMenuLinkedWithMenuItem(items[3])
})
)
it(
'should stay on the same item while keystrokes still match',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// ---
// Reset: Go to first item
await press(Keys.Home)
// Search for "b" in "bob"
await type(word('b'))
// We should be on the first `bob`
assertMenuLinkedWithMenuItem(items[1])
// Search for "b" in "bob" again
await type(word('b'))
// We should be on the next `bob`
assertMenuLinkedWithMenuItem(items[3])
// ---
// Reset: Go to first item
await press(Keys.Home)
// Search for "bo" in "bob"
await type(word('bo'))
// We should be on the first `bob`
assertMenuLinkedWithMenuItem(items[1])
// Search for "bo" in "bob" again
await type(word('bo'))
// We should be on the next `bob`
assertMenuLinkedWithMenuItem(items[3])
// ---
// Reset: Go to first item
await press(Keys.Home)
// Search for "bob" in "bob"
await type(word('bob'))
// We should be on the first `bob`
assertMenuLinkedWithMenuItem(items[1])
// Search for "bob" in "bob" again
await type(word('bob'))
// We should be on the next `bob`
assertMenuLinkedWithMenuItem(items[3])
})
)
})
})
describe('Mouse interactions', () => {
it(
'should be possible to open a menu on click',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
assertMenu({ state: MenuState.Visible })
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach((item) => assertMenuItem(item))
})
)
it(
'should not be possible to open a menu on right click',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Try to open the menu
await click(getMenuButton(), MouseButton.Right)
// Verify it is still closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should not be possible to open a menu on click when the button is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button disabled>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
// Try to open the menu
await click(getMenuButton())
// Verify it is still closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should be possible to close a menu on click',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton({ state: MenuState.Visible })
// Click to close
await click(getMenuButton())
// Verify it is closed
assertMenuButton({ state: MenuState.InvisibleUnmounted })
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should be a no-op when we click outside of a closed menu',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Verify that the window is closed
assertMenu({ state: MenuState.InvisibleUnmounted })
// Click something that is not related to the menu
await click(document.body)
// Should still be closed
assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
it(
'should be possible to click outside of the menu which should close the menu',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
// Click something that is not related to the menu
await click(document.body)
// Should be closed now
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
it(
'should be possible to click outside of the menu which should close the menu (even if we press the menu button)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
// Click the menu button again
await click(getMenuButton())
// Should be closed now
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
it(
'should be possible to click outside of the menu on another menu button which should close the current menu and open the new menu',
suppressConsoleLogs(async () => {
render(
<div>
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
</div>
)
let [button1, button2] = getMenuButtons()
// Click the first menu button
await click(button1)
expect(getMenus()).toHaveLength(1) // Only 1 menu should be visible
// Ensure the open menu is linked to the first button
assertMenuButtonLinkedWithMenu(button1, getMenu())
// Click the second menu button
await click(button2)
expect(getMenus()).toHaveLength(1) // Only 1 menu should be visible
// Ensure the open menu is linked to the second button
assertMenuButtonLinkedWithMenu(button2, getMenu())
})
)
it(
'should be possible to click outside of the menu, on an element which is within a focusable element, which closes the menu',
suppressConsoleLogs(async () => {
let focusFn = jest.fn()
render(
<div>
<Menu>
<Menu.Button onFocus={focusFn}>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
<button id="btn">
<span>Next</span>
</button>
</div>
)
// Click the menu button
await click(getMenuButton())
// Ensure the menu is open
assertMenu({ state: MenuState.Visible })
// Click the span inside the button
await click(getByText('Next'))
// Ensure the menu is closed
assertMenu({ state: MenuState.InvisibleUnmounted })
// Ensure the outside button is focused
assertActiveElement(document.getElementById('btn'))
// Ensure that the focus button only got focus once (first click)
expect(focusFn).toHaveBeenCalledTimes(1)
})
)
// TODO: This test doesn't work — and it would be more suited for browser testing anyway
it.skip(
'should be possible to click outside of the menu into an iframe and which should close the menu',
suppressConsoleLogs(async () => {
render(
<div>
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
<iframe srcDoc={'<button>Trigger</button>'} width="300" height="300"></iframe>
</div>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
// Click the input element in the iframe
await click(document.querySelector('iframe')?.contentDocument!.querySelector('button')!)
// Should be closed now
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)
it(
'should be possible to hover an item and make it active',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
// We should be able to go to the first item
await mouseMove(items[0])
assertMenuLinkedWithMenuItem(items[0])
// We should be able to go to the last item
await mouseMove(items[2])
assertMenuLinkedWithMenuItem(items[2])
})
)
it(
'should make a menu item active when you move the mouse over it',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should be a no-op when we move the mouse and the menu item is already active',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
await mouseMove(items[1])
// Nothing should be changed
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should be a no-op when we move the mouse and the menu item is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
await mouseMove(items[1])
assertNoActiveMenuItem()
})
)
it(
'should not be possible to hover an item that is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// Try to hover over item 1, which is disabled
await mouseMove(items[1])
// We should not have an active item now
assertNoActiveMenuItem()
})
)
it(
'should be possible to mouse leave an item and make it inactive',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
await mouseLeave(items[1])
assertNoActiveMenuItem()
// We should be able to go to the first item
await mouseMove(items[0])
assertMenuLinkedWithMenuItem(items[0])
await mouseLeave(items[0])
assertNoActiveMenuItem()
// We should be able to go to the last item
await mouseMove(items[2])
assertMenuLinkedWithMenuItem(items[2])
await mouseLeave(items[2])
assertNoActiveMenuItem()
})
)
it(
'should be possible to mouse leave a disabled item and be a no-op',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
let items = getMenuItems()
// Try to hover over item 1, which is disabled
await mouseMove(items[1])
assertNoActiveMenuItem()
await mouseLeave(items[1])
assertNoActiveMenuItem()
})
)
it(
'should be possible to click a menu item, which closes the menu',
suppressConsoleLogs(async () => {
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" onClick={clickHandler}>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
let items = getMenuItems()
// We should be able to click the first item
await click(items[1])
assertMenu({ state: MenuState.InvisibleUnmounted })
expect(clickHandler).toHaveBeenCalled()
})
)
it(
'should be possible to click a menu item, which closes the menu and invokes the @click handler',
suppressConsoleLogs(async () => {
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="button" onClick={clickHandler}>
bob
</Menu.Item>
<Menu.Item>
<button onClick={clickHandler}>charlie</button>
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
// We should be able to click the first item
await click(getMenuItems()[1])
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(1)
// Let's re-open the window for now
await click(getMenuButton())
// Click the last item, which should close and invoke the handler
await click(getMenuItems()[2])
assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(2)
})
)
it(
'should be possible to click a disabled menu item, which is a no-op',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
let items = getMenuItems()
// We should be able to click the first item
await click(items[1])
assertMenu({ state: MenuState.Visible })
})
)
it(
'should be possible focus a menu item, so that it becomes active',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
let items = getMenuItems()
// Verify that nothing is active yet
assertNoActiveMenuItem()
// We should be able to focus the first item
await focus(items[1])
assertMenuLinkedWithMenuItem(items[1])
})
)
it(
'should not be possible to focus a menu item which is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
let items = getMenuItems()
// We should not be able to focus the first item
await focus(items[1])
assertNoActiveMenuItem()
})
)
it(
'should not be possible to activate a disabled item',
suppressConsoleLogs(async () => {
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" onClick={clickHandler}>
alice
</Menu.Item>
<Menu.Item as="a" onClick={clickHandler} disabled>
bob
</Menu.Item>
<Menu.Item disabled>
<button onClick={clickHandler}>charlie</button>
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
let items = getMenuItems()
await focus(items[0])
await click(items[1])
expect(clickHandler).not.toHaveBeenCalled()
// Activate the last item
await click(getMenuItems()[2])
expect(clickHandler).not.toHaveBeenCalled()
})
)
})