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 ', suppressConsoleLogs((name, Component) => { expect(() => render(createElement(Component as any))).toThrow( `<${name} /> is missing a parent component.` ) }) ) it( 'should be possible to render a Menu without crashing', suppressConsoleLogs(async () => { render( Trigger Item A Item B Item C ) 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( {({ open }) => ( <> Trigger {open && ( Item A Item B Item C )} )} ) 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( {({ close }) => ( <> Trigger )} ) 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( {(slot) => <>{JSON.stringify(slot)}} Item A Item B Item C ) 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( {(slot) => <>{JSON.stringify(slot)}} Item A Item B Item C ) 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( Trigger ) expect(getMenuButton()).toHaveAttribute('type', 'button') }) it('should not set the `type` to "button" if it already contains a `type`', async () => { render( Trigger ) 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((props, ref) => ( )} ) 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( Trigger {({ disabled }) => } {({ disabled }) => } {({ disabled }) => } ) 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 ( <> Trigger Item 1 {!hide && Item 2} Item 3 ) } let { rerender } = render() // Open the Menu await click(getByText('Trigger')) rerender() // Remove Menu.Item 2 rerender() // 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( Trigger JSON.stringify(bag)}> Item A JSON.stringify(bag)}> Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger
Item A Item B
Item C
Item D
Item E
) // 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( Trigger {(data) => ( <> {JSON.stringify(data)} )} ) 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( Trigger {(data) => ( <> {JSON.stringify(data)} )} ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) // 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger Item A Item B Item C Item D ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger value a value b value c ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie bob ) // 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( Trigger alice bob charlie bob ) // 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) 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( Trigger Item A Item B Item C ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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(
Trigger alice bob charlie Trigger alice bob charlie
) 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(
Trigger alice bob charlie
) // 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(
Trigger alice bob charlie
) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob charlie ) // 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( Trigger alice bob ) // 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() }) ) })