import React from 'react'; import { mount, shallow, ReactWrapper } from 'enzyme'; import sinon from 'sinon'; import ContextMenu, { ContextMenuProps, ContextMenuState } from '../ContextMenu'; import Button, { ButtonType } from '../../button/Button'; import Menu from '../../menu/Menu'; const sandbox = sinon.sandbox.create(); describe('components/context-menu/ContextMenu', () => { const FakeButton = (props: Record) => ( ); FakeButton.displayName = 'FakeButton'; const FakeMenu = (props: Record) => (
    Some Menu
); FakeMenu.displayName = 'FakeMenu'; afterEach(() => { sandbox.verifyAndRestore(); }); describe('render()', () => { test('should throw an error when passed less than 2 children', () => { expect(() => { shallow( , ); }).toThrow(); }); test('should throw an error when passed more than 2 children', () => { expect(() => { shallow(
, ); }).toThrow(); }); test('should correctly render a single child button with correct props', () => { const wrapper = shallow( , ); const instance = wrapper.instance(); const button = wrapper.find(FakeButton); expect(button.length).toBe(1); expect(button.prop('id')).toEqual(instance.menuTargetID); expect(button.key()).toEqual(instance.menuTargetID); }); test('should not render child menu when menu is closed', () => { const wrapper = shallow( , ); const menu = wrapper.find(FakeMenu); expect(menu.length).toBe(0); }); test('should correctly render a single child menu with correct props when menu is open', () => { const wrapper = shallow( , ); wrapper.setState({ isOpen: true }); const instance = wrapper.instance(); const menu = wrapper.find(FakeMenu); expect(menu.length).toBe(1); expect(menu.prop('id')).toEqual(instance.menuID); expect(menu.key()).toEqual(instance.menuID); expect(menu.prop('initialFocusIndex')).toEqual(null); }); test('should render TetherComponent with correct props with correct default values', () => { const wrapper = shallow( , ); expect(wrapper.is('TetherComponent')).toBe(true); expect(wrapper.prop('attachment')).toEqual('top left'); expect(wrapper.prop('targetAttachment')).toEqual('top left'); expect(wrapper.prop('constraints')).toEqual([]); }); test('should render TetherComponent with constraints when specified', () => { const constraints = [ { to: 'window', attachment: 'together', }, ]; const wrapper = shallow( , ); expect(wrapper.prop('constraints')).toEqual(constraints); }); test('should render TetherComponent with correct target offset when set', () => { const targetOffset = '10px 20px'; const wrapper = shallow( , ); wrapper.setState({ targetOffset }); expect(wrapper.prop('targetOffset')).toEqual(targetOffset); }); }); describe('closeMenu()', () => { test('should call setState() with correct values', () => { const wrapper = shallow( , ); const instance = wrapper.instance(); sandbox .mock(instance) .expects('setState') .withArgs({ isOpen: false, }); instance.closeMenu(); }); test('should call onMenuClose() if onMenuClose prop is set', () => { const onMenuCloseSpy = jest.fn(); const wrapper = shallow( , ); wrapper.instance().closeMenu(); expect(onMenuCloseSpy).toBeCalled(); }); }); describe('handleMenuClose()', () => { test('should call closeMenu() and focusTarget() when called', () => { const onMenuCloseSpy = jest.fn(); const onFocusTargetSpy = jest.fn(); const wrapper = shallow( , ); wrapper.setState({ isOpen: true }); const instance = wrapper.instance(); instance.focusTarget = onFocusTargetSpy; instance.handleMenuClose(); expect(onMenuCloseSpy).toHaveBeenCalled(); expect(onFocusTargetSpy).toHaveBeenCalled(); }); }); describe('componentDidUpdate()', () => { test('should add click and contextmenu listeners when opening menu', () => { const wrapper = mount( , ); document.addEventListener = jest.fn(); wrapper.setState({ isOpen: true }); expect(document.addEventListener).toHaveBeenCalledWith('click', expect.anything(), expect.anything()); expect(document.addEventListener).toHaveBeenCalledWith('contextmenu', expect.anything(), expect.anything()); }); test('should remove click and contextmenu listeners when closing menu', () => { const wrapper = mount( , ); wrapper.setState({ isOpen: true }); const instance = wrapper.instance(); document.removeEventListener = jest.fn(); instance.closeMenu(); expect(document.removeEventListener).toHaveBeenCalledWith( 'contextmenu', expect.anything(), expect.anything(), ); expect(document.removeEventListener).toHaveBeenCalledWith('click', expect.anything(), expect.anything()); }); test('should not do anything opening a menu when menu is already open', () => { const wrapper = mount( , ); wrapper.setState({ isOpen: true }); const instance = wrapper.instance(); document.addEventListener = jest.fn(); document.removeEventListener = jest.fn(); instance.setState({ isOpen: true }); expect(document.addEventListener).not.toHaveBeenCalledWith('click', expect.anything(), expect.anything()); expect(document.addEventListener).not.toHaveBeenCalledWith( 'contextmenu', expect.anything(), expect.anything(), ); expect(document.removeEventListener).not.toHaveBeenCalledWith( 'contextmenu', expect.anything(), expect.anything(), ); expect(document.removeEventListener).not.toHaveBeenCalledWith( 'click', expect.anything(), expect.anything(), ); }); test('should close menu when context menu becomes disabled and the menu is currently open', () => { const wrapper = shallow( , ); wrapper.setState({ isOpen: true }); const instance = wrapper.instance() as ContextMenu; sandbox.mock(instance).expects('handleMenuClose'); instance.componentDidUpdate({ isDisabled: true } as ContextMenuProps, { isOpen: true } as ContextMenuState); }); }); describe('componentWillUnmount()', () => { test('should not do anything when menu is closed', () => { const wrapper = mount( , ); const documentMock = sandbox.mock(document); documentMock .expects('removeEventListener') .withArgs('contextmenu') .never(); documentMock .expects('removeEventListener') .withArgs('click') .never(); wrapper.unmount(); }); test('should remove listeners when menu is open', () => { const wrapper = mount( , ); wrapper.setState({ isOpen: true }); const documentMock = sandbox.mock(document); documentMock.expects('removeEventListener').withArgs('contextmenu'); documentMock.expects('removeEventListener').withArgs('click'); wrapper.unmount(); }); }); describe('tests requiring body mounting', () => { let attachTo: HTMLDivElement; let wrapper: ReactWrapper | null = null; /** * Helper method to mount things to the correct DOM element * This makes it easier to clean up after ourselves after each test */ const mountToBody = (component: React.ReactElement) => mount(component, { attachTo }); const preventDefaultSpy = jest.fn(); beforeEach(() => { // Set up a place to mount attachTo = document.createElement('div'); attachTo.setAttribute('data-mounting-point', ''); document.body.appendChild(attachTo); }); afterEach(() => { // Unmount and remove the mounting point after each test if (wrapper) { wrapper.unmount(); wrapper = null; } document.body.removeChild(attachTo); }); describe('handleContextMenu()', () => { test('should be no-op when props.isDisabled is true', () => { wrapper = mountToBody( , ); wrapper.find(FakeButton).simulate('contextmenu', { preventDefault: preventDefaultSpy, }); expect(wrapper.state('isOpen')).toBe(false); expect(preventDefaultSpy).not.toHaveBeenCalled(); }); test('should call setState() with correct values', () => { wrapper = mountToBody( , ); const instance = wrapper.instance() as ContextMenu; const menuTargetEl = document.getElementById(instance.menuTargetID) as HTMLDivElement; menuTargetEl.getBoundingClientRect = jest.fn(() => { return { left: 5, top: 10 } as DOMRect; }); wrapper.find(FakeButton).simulate('contextmenu', { clientX: 10, clientY: 20, preventDefault: preventDefaultSpy, }); expect(wrapper.state('isOpen')).toBe(true); expect(wrapper.state('targetOffset')).toEqual('10px 5px'); }); test('should call onMenuOpen handler when given', () => { const onMenuOpenSpy = jest.fn(); wrapper = mountToBody( , ); wrapper.find(FakeButton).simulate('contextmenu', { preventDefault: () => null, }); expect(onMenuOpenSpy).toBeCalled(); }); }); describe('handleDocumentClick()', () => { test('should call closeMenu() when event target is not within the menu', () => { const closeMenuSpy = jest.fn(); wrapper = mountToBody( , ); const instance = wrapper.instance() as ContextMenu; instance.closeMenu = closeMenuSpy; const handleContextMenuEvent = ({ clientX: 10, clientY: 15, preventDefault: preventDefaultSpy, } as unknown) as MouseEvent; instance.handleContextMenu(handleContextMenuEvent); const documentClickEvent = ({ target: document.createElement('div'), } as unknown) as MouseEvent; instance.handleDocumentClick(documentClickEvent); expect(closeMenuSpy).toHaveBeenCalled(); }); test('should not call closeMenu() when event target is within the menu', () => { const closeMenuSpy = jest.fn(); wrapper = mountToBody( , ); const instance = wrapper.instance() as ContextMenu; instance.closeMenu = closeMenuSpy; const handleContextMenuEvent = ({ clientX: 10, clientY: 15, preventDefault: preventDefaultSpy, } as unknown) as MouseEvent; instance.handleContextMenu(handleContextMenuEvent); const documentClickEvent = ({ target: document.getElementById(instance.menuID), } as unknown) as MouseEvent; instance.handleDocumentClick(documentClickEvent); expect(closeMenuSpy).not.toHaveBeenCalled(); }); }); describe('focusTarget()', () => { test('should focus the menu target when called', () => { const onFocusTargetSpy = jest.fn(); wrapper = mountToBody( , ); const instance = wrapper.instance() as ContextMenu; const menuTargetEl = document.getElementById(instance.menuTargetID) as HTMLButtonElement; menuTargetEl.focus = onFocusTargetSpy; instance.focusTarget(); expect(onFocusTargetSpy).toHaveBeenCalled(); }); }); }); });