import { AppstoreOutlined, InboxOutlined, MailOutlined, PieChartOutlined, UserOutlined, } from '@ant-design/icons'; import React, { useState, useMemo } from 'react'; import type { MenuProps, MenuRef } from '..'; import Menu from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; import { fireEvent, render, act } from '../../../tests/utils'; import Layout from '../../layout'; import initCollapseMotion from '../../_util/motion'; import { noop } from '../../_util/warning'; Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', { writable: true, value: true, }); type MouseEvent = 'click' | 'mouseEnter' | 'mouseLeave'; const { SubMenu } = Menu; describe('Menu', () => { function triggerAllTimer() { for (let i = 0; i < 10; i += 1) { act(() => { jest.runAllTimers(); }); } } const expectSubMenuBehavior = ( defaultTestProps: MenuProps, instance: ReturnType, enter = noop, leave = noop, ) => { const { container } = instance; expect(container.querySelectorAll('ul.ant-menu-sub')).toHaveLength(0); const AnimationClassNames = { horizontal: 'ant-slide-up-leave', inline: 'ant-motion-collapse-leave', vertical: 'ant-zoom-big-leave', }; const mode = defaultTestProps.mode || 'horizontal'; act(() => { enter(); }); // React concurrent may delay creat this triggerAllTimer(); function getSubMenu() { return container.querySelector( mode === 'inline' ? 'ul.ant-menu-sub.ant-menu-inline' : 'div.ant-menu-submenu-popup', ); } expect( getSubMenu()?.classList.contains('ant-menu-hidden') || getSubMenu()?.classList.contains(AnimationClassNames[mode]), ).toBeFalsy(); act(() => { leave(); }); // React concurrent may delay creat this triggerAllTimer(); if (getSubMenu()) { expect( getSubMenu()?.classList.contains('ant-menu-hidden') || getSubMenu()?.classList.contains(AnimationClassNames[mode]), ).toBeTruthy(); } }; // window.requestAnimationFrame = callback => window.setTimeout(callback, 16); // window.cancelAnimationFrame = window.clearTimeout; beforeEach(() => { jest.useFakeTimers(); jest.clearAllTimers(); }); afterEach(() => { jest.useRealTimers(); }); mountTest(() => ( )); mountTest(() => ( <> {null} {/* eslint-disable-next-line react/jsx-no-useless-fragment */} <> {undefined} {/* eslint-disable-next-line react/jsx-no-useless-fragment */} <> {/* eslint-disable-next-line react/jsx-no-useless-fragment */} <> )); const RtlDemo: React.FC = () => ( ); rtlTest(RtlDemo, { componentName: 'menu' }); let div: HTMLDivElement; beforeEach(() => { div = document.createElement('div'); document.body.appendChild(div); }); afterEach(() => { document.body.removeChild(div); }); it('If has select nested submenu item ,the menu items on the grandfather level should be highlight', () => { const { container } = render( Option 1 Option 2 Option 3 Option 4 menu2 , ); expect(container.querySelectorAll('li.ant-menu-submenu-selected').length).toBe(1); }); it('forceSubMenuRender', () => { const { container, rerender } = render( , ); expect(container.querySelector('.bamboo')).toBeFalsy(); rerender( , ); expect(container.querySelector('.bamboo')).toBeTruthy(); }); it('should accept defaultOpenKeys in mode horizontal', () => { const { container } = render( Option 1 Option 2 menu2 , ); expect( container.querySelector('.ant-menu-submenu-open')?.querySelector('.ant-menu-submenu-title') ?.textContent, ).toEqual('submenu1'); }); it('should accept defaultOpenKeys in mode inline', () => { const { container } = render( Option 1 Option 2 menu2 , ); expect( container.querySelector('.ant-menu-submenu-open')?.querySelector('.ant-menu-submenu-title') ?.textContent, ).toEqual('submenu1'); }); it('should accept defaultOpenKeys in mode vertical', () => { const { container } = render( Option 1 Option 2 menu2 , ); expect(container.querySelector('.ant-menu-sub')).toBeFalsy(); }); it('should accept openKeys in mode horizontal', () => { const { container } = render( Option 1 Option 2 menu2 , ); triggerAllTimer(); expect(container.querySelector('div.ant-menu-submenu-popup')).not.toHaveClass( 'ant-menu-submenu-hidden', ); }); it('should accept openKeys in mode inline', () => { const { container } = render( Option 1 Option 2 menu2 , ); expect(container.querySelector('ul.ant-menu-sub')).not.toHaveClass('ant-menu-hidden'); }); it('should accept openKeys in mode vertical', () => { const { container } = render( Option 1 Option 2 menu2 , ); triggerAllTimer(); expect(container.querySelector('div.ant-menu-submenu-popup')).not.toHaveClass( 'ant-menu-submenu-hidden', ); }); it('test submenu in mode horizontal', async () => { const defaultTestProps: MenuProps = { mode: 'horizontal' }; const Demo: React.FC = (props) => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => instance.rerender(), () => instance.rerender(), ); instance.rerender(); }); it('test submenu in mode inline', () => { const defaultTestProps: MenuProps = { mode: 'inline' }; const Demo: React.FC = (props) => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => instance.rerender(), () => instance.rerender(), ); }); it('test submenu in mode vertical', () => { const defaultTestProps: MenuProps = { mode: 'vertical' }; const Demo: React.FC = (props) => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => instance.rerender(), () => instance.rerender(), ); }); describe('allows the overriding of theme at the popup submenu level', () => { const menuModesWithPopupSubMenu: MenuProps['mode'][] = ['horizontal', 'vertical']; menuModesWithPopupSubMenu.forEach((menuMode) => { it(`when menu is mode ${menuMode}`, () => { const { container } = render( Option 1 Option 2 menu2 , ); act(() => { jest.runAllTimers(); }); expect(container.querySelector('ul.ant-menu-root')).toHaveClass('ant-menu-dark'); expect(container.querySelector('div.ant-menu-submenu-popup')).toHaveClass('ant-menu-light'); }); }); }); // https://github.com/ant-design/ant-design/pulls/4677 // https://github.com/ant-design/ant-design/issues/4692 // TypeError: Cannot read property 'indexOf' of undefined it('pr #4677 and issue #4692', () => { render( menu1 menu2 , ); act(() => { jest.runAllTimers(); }); // just expect no error emit }); it('should always follow openKeys when mode is switched', () => { const Demo: React.FC = (props) => ( Option 1 Option 2 menu2 ); const { container, rerender } = render(); expect(container.querySelector('ul.ant-menu-sub')).not.toHaveClass('ant-menu-hidden'); rerender(); expect(container.querySelector('ul.ant-menu-sub')).not.toHaveClass('ant-menu-hidden'); rerender(); expect(container.querySelector('ul.ant-menu-sub')).not.toHaveClass('ant-menu-hidden'); }); it('should always follow openKeys when inlineCollapsed is switched', () => { const Demo: React.FC = (props) => ( }> Option Option Option ); const { container, rerender } = render(); expect(container.querySelector('li.ant-menu-submenu-inline')).toHaveClass( 'ant-menu-submenu-open', ); // inlineCollapsed rerender(); act(() => { jest.runAllTimers(); }); expect(container.querySelector('ul.ant-menu-root')).toHaveClass('ant-menu-vertical'); expect(container.querySelector('.ant-menu-submenu-popup')).toBeFalsy(); // !inlineCollapsed rerender(); act(() => { jest.runAllTimers(); }); expect(container.querySelector('ul.ant-menu-sub')).toHaveClass('ant-menu-inline'); expect(container.querySelector('li.ant-menu-submenu-inline')).toHaveClass( 'ant-menu-submenu-open', ); }); it('inlineCollapsed should works well when specify a not existed default openKeys', () => { const Demo: React.FC = (props) => ( }> Option Option Option ); const { container, rerender } = render(); expect(container.querySelectorAll('.ant-menu-sub')).toHaveLength(0); rerender(); act(() => { jest.runAllTimers(); }); const transitionEndEvent = new Event('transitionend'); fireEvent(container.querySelector('ul')!, transitionEndEvent); act(() => { jest.runAllTimers(); }); fireEvent.mouseEnter(container.querySelector('.ant-menu-submenu-title')!); triggerAllTimer(); expect(container.querySelector('.ant-menu-submenu')).toHaveClass('ant-menu-submenu-vertical'); expect(container.querySelector('.ant-menu-submenu')).toHaveClass('ant-menu-submenu-open'); expect(container.querySelector('ul.ant-menu-sub')).toHaveClass('ant-menu-vertical'); expect(container.querySelector('ul.ant-menu-sub')).not.toHaveClass('ant-menu-hidden'); }); it('inlineCollapsed Menu.Item Tooltip can be removed', () => { const { container } = render( node.parentNode as HTMLElement} > item item item item item item , ); fireEvent.mouseEnter(container.querySelectorAll('li.ant-menu-item')[0]); fireEvent.mouseEnter(container.querySelectorAll('li.ant-menu-item')[1]); fireEvent.mouseEnter(container.querySelectorAll('li.ant-menu-item')[2]); fireEvent.mouseEnter(container.querySelectorAll('li.ant-menu-item')[3]); fireEvent.mouseEnter(container.querySelectorAll('li.ant-menu-item')[4]); fireEvent.mouseEnter(container.querySelectorAll('li.ant-menu-item')[5]); triggerAllTimer(); // when title is null or '' and false, tooltip will not render. expect(container.querySelectorAll('.ant-tooltip-inner').length).toBe(3); expect(container.querySelectorAll('.ant-tooltip-inner')[0].textContent).toBe('item'); expect(container.querySelectorAll('.ant-tooltip-inner')[1].textContent).toBe('title'); expect(container.querySelectorAll('.ant-tooltip-inner')[2].textContent).toBe('item'); }); describe('open submenu when click submenu title', () => { const toggleMenu = ( instance: ReturnType, index: number, event: MouseEvent, ): void => { fireEvent[event](instance.container.querySelectorAll('.ant-menu-submenu-title')[index]); triggerAllTimer(); }; it('inline', () => { const defaultTestProps: MenuProps = { mode: 'inline' }; const Demo: React.FC = (props) => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => toggleMenu(instance, 0, 'click'), () => toggleMenu(instance, 0, 'click'), ); }); it('inline menu collapseMotion should be triggered', async () => { const cloneMotion = { ...initCollapseMotion(), motionDeadline: 1, }; const onOpenChange = jest.fn(); const onEnterEnd = jest.spyOn(cloneMotion, 'onEnterEnd'); const { container } = render( Option 1 Option 2 menu2 , ); fireEvent.click(container.querySelector('.ant-menu-submenu-title')!); triggerAllTimer(); expect(onOpenChange).toHaveBeenCalled(); expect(onEnterEnd).toHaveBeenCalledTimes(1); }); it('vertical with hover(default)', () => { const defaultTestProps: MenuProps = { mode: 'vertical' }; const Demo: React.FC = () => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => toggleMenu(instance, 0, 'mouseEnter'), () => toggleMenu(instance, 0, 'mouseLeave'), ); }); it('vertical with click', () => { const defaultTestProps: MenuProps = { mode: 'vertical', triggerSubMenuAction: 'click' }; const Demo: React.FC = () => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => toggleMenu(instance, 0, 'click'), () => toggleMenu(instance, 0, 'click'), ); }); it('horizontal with hover(default)', () => { const defaultTestProps: MenuProps = { mode: 'horizontal' }; const Demo: React.FC = () => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => toggleMenu(instance, 0, 'mouseEnter'), () => toggleMenu(instance, 0, 'mouseLeave'), ); }); it('horizontal with click', () => { const defaultTestProps: MenuProps = { mode: 'horizontal', triggerSubMenuAction: 'click' }; const Demo: React.FC = () => ( Option 1 Option 2 menu2 ); const instance = render(); expectSubMenuBehavior( defaultTestProps, instance, () => toggleMenu(instance, 0, 'click'), () => toggleMenu(instance, 0, 'click'), ); }); }); it('inline title', () => { const { container } = render( }> Option 1 test , ); fireEvent.mouseEnter(container.querySelector('.ant-menu-item')!); triggerAllTimer(); expect(container.querySelector('.ant-tooltip-inner')?.textContent).toBe('bamboo lucky'); }); it('render correctly when using with Layout.Sider', () => { const Demo: React.FC = () => { const [collapsed, setCollapsed] = useState(false); return ( setCollapsed(!collapsed)} >
} title="User"> Tom Bill Alex ); }; const { container } = render(); expect(container.querySelector('ul.ant-menu-root')).toHaveClass('ant-menu-inline'); fireEvent.click(container.querySelector('.ant-menu-submenu-title')!); fireEvent.click(container.querySelector('.ant-layout-sider-trigger')!); triggerAllTimer(); expect(container.querySelector('ul.ant-menu-root')).toHaveClass('ant-menu-inline-collapsed'); fireEvent.mouseEnter(container.querySelector('ul.ant-menu-root')!); expect(container.querySelector('ul.ant-menu-root')).not.toHaveClass('ant-menu-inline'); expect(container.querySelector('ul.ant-menu-root')).toHaveClass('ant-menu-vertical'); }); it('onMouseEnter should work', () => { const onMouseEnter = jest.fn(); const { container } = render( Navigation One Navigation Two , ); fireEvent.mouseEnter(container.querySelector('ul.ant-menu-root')!); expect(onMouseEnter).toHaveBeenCalled(); }); it('MenuItem should not render Tooltip when inlineCollapsed is false', () => { const { container } = render( }> Navigation One }> Navigation Two Navigation Four - Link , ); fireEvent.mouseEnter(container.querySelector('li.ant-menu-item')!); act(() => { jest.runAllTimers(); }); expect(container.querySelectorAll('.ant-tooltip-inner').length).toBe(0); }); it('MenuItem should render icon and icon should be the first child when icon exists', () => { const { container } = render( }> Navigation One , ); expect(container.querySelector('.ant-menu-item .anticon')).toHaveClass('anticon-mail'); }); it('should controlled collapse work', () => { const { asFragment, rerender } = render( }> Option 1 , ); expect(asFragment()).toMatchSnapshot(); rerender( }> Option 1 , ); expect(asFragment()).toMatchSnapshot(); }); it('not title if not collapsed', () => { jest.useFakeTimers(); const { container } = render( }> Option 1 , ); fireEvent.mouseEnter(container.querySelector('.ant-menu-item')!); act(() => { jest.runAllTimers(); }); expect(container.querySelectorAll('.ant-tooltip-inner').length).toBeFalsy(); jest.useRealTimers(); }); it('props#onOpen and props#onClose do not warn anymore', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const onOpen = jest.fn(); const onClose = jest.fn(); const Demo: React.FC = () => { const menuProps = useMemo(() => ({ onOpen, onClose }) as MenuProps, []); return ( ); }; render(); expect(errorSpy.mock.calls.length).toBe(1); expect(errorSpy.mock.calls[0][0]).not.toContain( '`onOpen` and `onClose` are removed, please use `onOpenChange` instead, see: https://u.ant.design/menu-on-open-change.', ); expect(onOpen).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled(); errorSpy.mockRestore(); }); // https://github.com/ant-design/ant-design/issues/18825 // https://github.com/ant-design/ant-design/issues/8587 it('should keep selectedKeys in state when collapsed to 0px', () => { jest.useFakeTimers(); const Demo: React.FC = (props) => { const menuProps = useMemo(() => ({ collapsedWidth: 0 }) as MenuProps, []); return ( Option 1 Option 2 Option 4 ); }; const { container, rerender } = render(); expect(container.querySelector('li.ant-menu-item-selected')?.textContent).toBe('Option 1'); fireEvent.click(container.querySelectorAll('li.ant-menu-item')[1]); expect(container.querySelector('li.ant-menu-item-selected')?.textContent).toBe('Option 2'); rerender(); act(() => { jest.runAllTimers(); }); expect(container.querySelector('li.ant-menu-item-selected')?.textContent).toBe('O'); rerender(); expect(container.querySelector('li.ant-menu-item-selected')?.textContent).toBe('Option 2'); jest.useRealTimers(); }); it('Menu.Item with icon children auto wrap span', () => { const { asFragment } = render( }> Navigation One }> Navigation One } title="Navigation One" /> } title={Navigation One} /> , ); expect(asFragment()).toMatchSnapshot(); }); // https://github.com/ant-design/ant-design/issues/23755 it('should trigger onOpenChange when collapse inline menu', () => { const onOpenChange = jest.fn(); function App() { const [inlineCollapsed, setInlineCollapsed] = useState(false); return ( <> menu menu ); } const { container } = render(); fireEvent.click(container.querySelector('button')!); expect(onOpenChange).toHaveBeenCalledWith([]); }); it('Use first char as Icon when collapsed', () => { const { container } = render( Bamboo , ); expect(container.querySelectorAll('.ant-menu-inline-collapsed-noicon')[0]?.textContent).toEqual( 'L', ); expect(container.querySelectorAll('.ant-menu-inline-collapsed-noicon')[1]?.textContent).toEqual( 'B', ); }); it('divider should show', () => { const { container } = render( Option 1 Option 2 Option 3 , ); expect(container.querySelectorAll('li.ant-menu-item-divider').length).toBe(2); expect(container.querySelectorAll('li.ant-menu-item-divider-dashed').length).toBe(1); }); it('should support ref', async () => { const ref = React.createRef(); const { container } = render( Option 1 , ); expect(ref.current?.menu?.list).toBe(container.querySelector('ul')); ref.current?.focus(); expect(document.activeElement).toBe(container.querySelector('li')); }); it('expandIcon', () => { const { container } = render( }> Option 1 , ); expect(container.querySelector('.bamboo')).toBeTruthy(); }); it('all types must be available in the "items" syntax', () => { const { asFragment } = render( , ); expect(asFragment()).toMatchSnapshot(); }); it('should not warning deprecated message when items={undefined}', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); render(); expect(errorSpy).not.toHaveBeenCalledWith( expect.stringContaining('`children` will be removed in next major version'), ); errorSpy.mockRestore(); }); it('expandIconClassName', () => { const { container } = render( } inlineCollapsed items={[ { label: 'Option 1', key: '1', icon: '112', children: [ { label: 'Option 1-1', key: '1-1', }, ], }, ]} />, ); expect(container.querySelector('.custom-expand-icon')).toBeTruthy(); }); // https://github.com/ant-design/ant-design/issues/40041 it('should not show icon when inlineCollapsed', () => { const { container } = render( I} inlineCollapsed items={[ { label: 'Option 1', key: '1', icon: '112', children: [ { label: 'Option 1-1', key: '1-1', }, ], }, ]} />, ); expect(container.querySelector('.bamboo')).toBeTruthy(); expect(getComputedStyle(container.querySelector('.bamboo') as HTMLElement)).toHaveProperty( 'opacity', '0', ); }); });