import React from 'react'; import { mount, shallow } from 'enzyme'; import assert from 'assert'; import _, { forEach, has } from 'lodash'; import { common } from '../../util/generic-tests'; import { DropMenuDumb as DropMenu } from './DropMenu'; import ContextMenu from '../ContextMenu/ContextMenu'; import * as KEYCODE from '../../constants/key-code'; const { Control, Header, Option, OptionGroup, NullOption } = DropMenu as any; describe('DropMenu', () => { common(DropMenu); describe('render', () => { it('should render a ContextMenu', () => { const wrapper = shallow( control ); assert.equal(wrapper.find('ContextMenu').length, 1); }); }); describe('props', () => { describe('pass throughs', () => { let wrapper: any; const defaultProps = DropMenu.defaultProps; beforeEach(() => { const props = { ...defaultProps, initialState: { test: true }, callbackId: 1, 'data-testid': 10, }; wrapper = shallow(); }); afterEach(() => { wrapper.unmount(); }); it('passes through select props to the root div element.', () => { const rootProps = wrapper.find('div.lucid-DropMenu').props(); const dataTestId = wrapper.first().prop(['data-testid']); expect(dataTestId).toBe(10); // 'className' and 'style' are plucked from the pass through object // but still appear becuase they are also directly passed to the root element as a prop forEach(['className', 'data-testid', 'style', 'children'], (prop) => { expect(has(rootProps, prop)).toBe(true); }); }); it('omits the component props, "initialState" and "callbackId" from the root div element', () => { const rootProps = wrapper.find('div.lucid-DropMenu').props(); forEach( [ 'isDisabled', 'isExpanded', 'direction', 'alignment', 'selectedIndices', 'focusedIndex', 'portalId', 'flyOutStyle', 'optionContainerStyle', 'onExpand', 'onCollapse', 'onSelect', 'onFocusNext', 'onFocusPrev', 'onFocusOption', 'Control', 'Option', 'OptionGroup', 'NullOption', 'Header', 'ContextMenu', 'FixedOption', 'initialState', 'callbackId', ], (prop) => { expect(has(rootProps, prop)).toBe(false); } ); }); }); describe('children', () => { it('should not render any direct child elements which are not DropMenu-specific', () => { const wrapper = shallow( control italic

header

); assert.equal(wrapper.find('button').length, 0); assert.equal(wrapper.find('h1').length, 0); assert.equal(wrapper.find('i').length, 1); }); }); describe('className', () => { it('should pass the className prop thru to the root element', () => { const wrapper = shallow( control ); const dropMenuClassName = wrapper.first().prop('className'); assert(_.includes(dropMenuClassName, 'MyDropMenu')); }); describe('FlyOut', () => { let wrapper: any; afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it('should pass the className prop thru to the FlyOut (portal) element', () => { wrapper = mount( control ); const flyOutClassName = (document as any).querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut' ).className; assert(_.includes(flyOutClassName, 'MyDropMenu')); }); }); }); describe('style', () => { it('should pass the style prop thru to the root element', () => { const wrapper = shallow( control ); const dropMenuStyle = wrapper.first().prop('style'); assert( _.isEqual(dropMenuStyle, { flex: 2, }) ); }); }); describe('isDisabled', () => { it('should make the control handle `onClick`, `onKeyDown`, and have a `tabIndex` when `false`', () => { const wrapper = shallow( control ); const controlElement = wrapper.find('.lucid-DropMenu-Control'); assert(!_.isNil(controlElement.prop('tabIndex'))); assert(!_.isNil(controlElement.prop('onClick'))); assert(!_.isNil(controlElement.prop('onKeyDown'))); }); it('should make the control not handle `onClick`, `onKeydown`, or have a `tabIndex` when `true`', () => { const wrapper = shallow( control ); const controlElement = wrapper.find('.lucid-DropMenu-Control'); assert(_.isNil(controlElement.prop('tabIndex'))); assert(_.isNil(controlElement.prop('onClick'))); assert(_.isNil(controlElement.prop('onKeyDown'))); }); }); describe('isExpanded', () => { it('should pass isExpanded to the underlying ContextMenu component thru props', () => { const wrapper = shallow( control ); const contextMenuIsExpanded = wrapper .find('ContextMenu') .prop('isExpanded'); assert.equal(contextMenuIsExpanded, true); }); }); describe('direction', () => { it('should pass the direction to the underlying ContextMenu component thru props', () => { const wrapper = shallow( control ); const contextMenuDirection = wrapper .find('ContextMenu') .prop('direction'); assert.equal(contextMenuDirection, 'up'); }); }); describe('selectedIndices', () => { let wrapper: any; afterEach(() => { wrapper.unmount(); }); it('should render the selected options with appropriate className', () => { wrapper = mount( control ); const optionDOMNodes = document.querySelectorAll( '.lucid-ContextMenu-FlyOut .lucid-DropMenu-Option' ); assert( _.includes( optionDOMNodes[0].className, 'lucid-DropMenu-Option-is-selected' ) ); assert( !_.includes( optionDOMNodes[1].className, 'lucid-DropMenu-Option-is-selected' ) ); assert( _.includes( optionDOMNodes[2].className, 'lucid-DropMenu-Option-is-selected' ) ); }); }); describe('focusedIndex', () => { let wrapper: any; afterEach(() => { wrapper.unmount(); }); it('should render the focused option with appropriate className', () => { wrapper = mount( control ); const optionDOMNodes = document.querySelectorAll( '.lucid-ContextMenu-FlyOut .lucid-DropMenu-Option' ); assert( !_.includes( optionDOMNodes[0].className, 'lucid-DropMenu-Option-is-focused' ) ); assert( !_.includes( optionDOMNodes[1].className, 'lucid-DropMenu-Option-is-focused' ) ); assert( _.includes( optionDOMNodes[2].className, 'lucid-DropMenu-Option-is-focused' ) ); }); }); describe('portalId', () => { let wrapper: any; afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it('should render an element under document.body with the same id', () => { wrapper = mount( control ); const portalDOMNode: any = document.getElementById( 'test-dropmenu-portal' ); assert(_.isElement(portalDOMNode)); assert.equal(document.body, portalDOMNode.parentNode); }); }); describe('flyOutStyle', () => { it('should pass flyOutStyle style object to the ContextMenu.FlyOut', () => { const styleObj = { width: 123, height: 321, backgroundColor: 'tan', }; const wrapper = shallow( control ); assert.deepEqual( styleObj, wrapper.find(ContextMenu.FlyOut).prop('style'), 'style object must be passed through to ContextMenu.FlyOut' ); }); }); describe('onExpand', () => { describe('mouse', () => { it('should be called when DropMenu [not expanded, clicked]', () => { const onExpandMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('click'); expect(onExpandMock).toHaveBeenCalledTimes(1); }); it('should not be called when DropMenu [expanded, clicked]', () => { const onExpandMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('click'); expect(onExpandMock).not.toHaveBeenCalled(); }); }); describe('keyboard', () => { it('should be called when DropMenu [not expanded, has focus, Down Arrow key pressed]', () => { const onExpandMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.ArrowDown, preventDefault: _.noop, }); expect(onExpandMock).toHaveBeenCalledTimes(1); }); }); }); describe('onCollapse', () => { describe('mouse', () => { it('should be called when DropMenu [expanded, control clicked]', () => { const onCollapseMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('click'); expect(onCollapseMock).toHaveBeenCalledTimes(1); }); it('should not be called when DropMenu [not expanded, control clicked]', () => { const onCollapseMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('click'); expect(onCollapseMock).not.toHaveBeenCalled(); }); it('should be called when DropMenu [expanded, ContextMenu onClickOut called]', () => { const onCollapseMock: any = jest.fn(); const wrapper: any = shallow( control ); wrapper.find('ContextMenu').prop('onClickOut')(); expect(onCollapseMock).toHaveBeenCalledTimes(1); }); }); describe('keyboard', () => { it('should be called when DropMenu [expanded, Escape key pressed]', () => { const onCollapseMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.Escape, preventDefault: _.noop, }); expect(onCollapseMock).toHaveBeenCalledTimes(1); }); }); }); describe('onSelect', () => { describe('mouse', () => { let onSelectMock: any; let wrapper: any; beforeEach(() => { onSelectMock = jest.fn(); wrapper = mount( control ); }); afterEach(() => { wrapper.unmount(); onSelectMock.mockClear(); }); it('should be called when DropMenu [expanded, option clicked]', () => { const optionDOMNodes: any = document.querySelectorAll( '.lucid-ContextMenu-FlyOut .lucid-DropMenu-Option' ); optionDOMNodes[2].click(); const selectedIndex = onSelectMock.mock.calls[0][0]; expect(onSelectMock).toHaveBeenCalledTimes(1); expect(selectedIndex).toEqual(2); }); }); describe('keyboard', () => { it('should be called when DropMenu [expanded, option focused, Enter key pressed]', () => { const onSelectMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.Enter, preventDefault: _.noop, }); const selectedIndex = onSelectMock.mock.calls[0][0]; expect(onSelectMock).toHaveBeenCalledTimes(1); expect(selectedIndex).toEqual(2); }); }); }); describe('onFocusOption', () => { describe('keyboard', () => { it('should be called when DropMenu [expanded, focusedIndex=null, Down Arrow key pressed]', () => { const onFocusOptionMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.ArrowDown, preventDefault: _.noop, }); expect(onFocusOptionMock).toHaveBeenCalledTimes(1); }); it('should be called when DropMenu [expanded, focusedIndex={not last option}, Down Arrow key pressed]', () => { const onFocusOptionMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.ArrowDown, preventDefault: _.noop, }); expect(onFocusOptionMock).toHaveBeenCalledTimes(1); }); it('should not be called when DropMenu [expanded, focusedIndex={last option}, Down Arrow key pressed]', () => { const onFocusOptionMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.ArrowDown, preventDefault: _.noop, }); expect(onFocusOptionMock).not.toHaveBeenCalled(); }); it('should be called when DropMenu [expanded, focusedIndex={not first option}, Up Arrow key pressed]', () => { const onFocusOptionMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.ArrowUp, preventDefault: _.noop, }); expect(onFocusOptionMock).toHaveBeenCalledTimes(1); }); it('should not be called when DropMenu [expanded, focusedIndex={first option}, Up Arrow key pressed]', () => { const onFocusOptionMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.ArrowUp, preventDefault: _.noop, }); expect(onFocusOptionMock).not.toHaveBeenCalled(); }); it('should not be called when DropMenu [expanded, focusedIndex={null}, Up Arrow key pressed]', () => { const onFocusOptionMock: any = jest.fn(); const wrapper = shallow( control ); wrapper.find('.lucid-DropMenu-Control').simulate('keydown', { keyCode: KEYCODE.ArrowUp, preventDefault: _.noop, }); expect(onFocusOptionMock).not.toHaveBeenCalled(); }); }); describe('mouse', () => { let onFocusOptionMock: any; let wrapper: any; beforeEach(() => { onFocusOptionMock = jest.fn(); wrapper = mount( control ); }); afterEach(() => { wrapper.unmount(); onFocusOptionMock.mockClear(); }); it('should be called when user moves mouse over option', () => { const optionDOMNodes = document.querySelectorAll( '.lucid-ContextMenu-FlyOut .lucid-DropMenu-Option' ); const mouseMoveEvent = document.createEvent('MouseEvents'); mouseMoveEvent.initMouseEvent( 'mousemove', //event type : click, mousedown, mouseup, mouseover, mousemove, mouseout. true, //canBubble false, //cancelable window, //event's AbstractView : should be window 1, // detail : Event's mouse click count 50, // screenX 50, // screenY 50, // clientX 50, // clientY false, // ctrlKey false, // altKey false, // shiftKey false, // metaKey 0, // button : 0 = click, 1 = middle button, 2 = right button null // relatedTarget : Only used with some event types (e.g. mouseover and mouseout). In other cases, pass null. ); optionDOMNodes[2].dispatchEvent(mouseMoveEvent); const selectedIndex = onFocusOptionMock.mock.calls[0][0]; expect(onFocusOptionMock).toHaveBeenCalledTimes(1); expect(selectedIndex).toEqual(2); }); }); }); }); describe('child elements', () => { describe('Control', () => { it('should render the children of Control to the control container', () => { const wrapper = shallow( control ); assert.equal('control', wrapper.find('.lucid-DropMenu-Control').text()); }); }); describe('Option', () => { let wrapper: any; afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it('should render the children of each Option in option nodes of the flyout menu', () => { wrapper = mount( control ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut' ); const optionDOMNodes = flyOutDOMNode.querySelectorAll( '.lucid-DropMenu-Option' ); assert.equal(optionDOMNodes.length, 3); assert.equal(optionDOMNodes[0].innerHTML, 'option a'); assert.equal(optionDOMNodes[1].innerHTML, 'option b'); assert.equal(optionDOMNodes[2].innerHTML, 'option c'); }); it('should not render children with `isHidden`', () => { wrapper = mount( control ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut' ); const optionDOMNodes = flyOutDOMNode.querySelectorAll( '.lucid-DropMenu-Option' ); assert.equal(optionDOMNodes.length, 2); assert.equal(optionDOMNodes[0].innerHTML, 'option b'); assert.equal(optionDOMNodes[1].innerHTML, 'option c'); }); it('should render children options with `Selection` properties without generating React warnings', () => { wrapper = mount( control ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut' ); const optionDOMNodes = flyOutDOMNode.querySelectorAll( '.lucid-DropMenu-Option' ); assert.equal(optionDOMNodes.length, 3); assert.equal(optionDOMNodes[0].innerHTML, 'option a'); assert.equal(optionDOMNodes[1].innerHTML, 'option b'); assert.equal(optionDOMNodes[2].innerHTML, 'option c'); }); }); describe('OptionGroup', () => { let wrapper: any; afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it('should render the Options in each OptionGroup separated by dividers', () => { wrapper = mount( control ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut .lucid-DropMenu-option-container' ); assert.equal(flyOutDOMNode.children.length, 10); assert.equal(flyOutDOMNode.children[0].innerHTML, 'option a'); assert.equal(flyOutDOMNode.children[1].innerHTML, 'option b'); assert.equal(flyOutDOMNode.children[2].innerHTML, 'option c'); assert.equal( flyOutDOMNode.children[3].className, 'lucid-DropMenu-OptionGroup-divider' ); assert.equal(flyOutDOMNode.children[4].innerHTML, 'option p'); assert.equal(flyOutDOMNode.children[5].innerHTML, 'option q'); assert.equal( flyOutDOMNode.children[6].className, 'lucid-DropMenu-OptionGroup-divider' ); assert.equal(flyOutDOMNode.children[7].innerHTML, 'option x'); assert.equal(flyOutDOMNode.children[8].innerHTML, 'option y'); assert.equal(flyOutDOMNode.children[9].innerHTML, 'option z'); assert( _.includes( flyOutDOMNode.children[0].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[1].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[2].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[4].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[5].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[7].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[8].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[9].className, 'lucid-DropMenu-Option-is-grouped' ) ); }); it('should render the ungrouped and grouped Options separated by dividers', () => { wrapper = mount( control ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut .lucid-DropMenu-option-container' ); assert.equal(flyOutDOMNode.children.length, 7); assert.equal(flyOutDOMNode.children[0].innerHTML, 'option a'); assert.equal(flyOutDOMNode.children[1].innerHTML, 'option b'); assert.equal(flyOutDOMNode.children[2].innerHTML, 'option c'); assert.equal( flyOutDOMNode.children[3].className, 'lucid-DropMenu-OptionGroup-divider' ); assert.equal(flyOutDOMNode.children[4].innerHTML, 'option x'); assert.equal(flyOutDOMNode.children[5].innerHTML, 'option y'); assert.equal(flyOutDOMNode.children[6].innerHTML, 'option z'); assert( _.includes( flyOutDOMNode.children[0].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[1].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( _.includes( flyOutDOMNode.children[2].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( !_.includes( flyOutDOMNode.children[4].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( !_.includes( flyOutDOMNode.children[5].className, 'lucid-DropMenu-Option-is-grouped' ) ); assert( !_.includes( flyOutDOMNode.children[6].className, 'lucid-DropMenu-Option-is-grouped' ) ); }); it('should render non-Option children of OptionGroups as group labels', () => { wrapper = mount( control Preferred Available ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut .lucid-DropMenu-option-container' ); assert.equal(flyOutDOMNode.children.length, 9); assert.equal(flyOutDOMNode.children[0].textContent, 'Preferred'); assert.equal(flyOutDOMNode.children[1].innerHTML, 'option a'); assert.equal(flyOutDOMNode.children[2].innerHTML, 'option b'); assert.equal(flyOutDOMNode.children[3].innerHTML, 'option c'); assert.equal( flyOutDOMNode.children[4].className, 'lucid-DropMenu-OptionGroup-divider' ); assert.equal(flyOutDOMNode.children[5].textContent, 'Available'); assert.equal(flyOutDOMNode.children[6].innerHTML, 'option x'); assert.equal(flyOutDOMNode.children[7].innerHTML, 'option y'); assert.equal(flyOutDOMNode.children[8].innerHTML, 'option z'); assert( _.includes( flyOutDOMNode.children[0].className, 'lucid-DropMenu-label' ) ); assert( _.includes( flyOutDOMNode.children[5].className, 'lucid-DropMenu-label' ) ); }); it('should not render OptionGroups with `isHidden`', () => { wrapper = mount( control Preferred Available ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut .lucid-DropMenu-option-container' ); assert.equal(flyOutDOMNode.children.length, 4); assert.equal(flyOutDOMNode.children[0].textContent, 'Available'); assert.equal(flyOutDOMNode.children[1].innerHTML, 'option x'); assert.equal(flyOutDOMNode.children[2].innerHTML, 'option y'); assert.equal(flyOutDOMNode.children[3].innerHTML, 'option z'); assert( _.includes( flyOutDOMNode.children[0].className, 'lucid-DropMenu-label' ) ); }); }); describe('NullOption', () => { let wrapper: any; afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it('should render NullOption first with a divider immediately following', () => { wrapper = mount( control unselect ); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut .lucid-DropMenu-option-container' ); assert.equal(flyOutDOMNode.children.length, 5); assert.equal(flyOutDOMNode.children[0].innerHTML, 'unselect'); assert.equal( flyOutDOMNode.children[1].className, 'lucid-DropMenu-OptionGroup-divider' ); assert.equal(flyOutDOMNode.children[2].innerHTML, 'option a'); assert.equal(flyOutDOMNode.children[3].innerHTML, 'option b'); assert.equal(flyOutDOMNode.children[4].innerHTML, 'option c'); assert( _.includes( flyOutDOMNode.children[0].className, 'lucid-DropMenu-Option' ) ); assert( _.includes( flyOutDOMNode.children[0].className, 'lucid-DropMenu-Option-is-null' ) ); assert( _.includes( flyOutDOMNode.children[2].className, 'lucid-DropMenu-Option' ) ); assert( _.includes( flyOutDOMNode.children[3].className, 'lucid-DropMenu-Option' ) ); assert( _.includes( flyOutDOMNode.children[4].className, 'lucid-DropMenu-Option' ) ); }); }); describe('Header', () => { let wrapper: any; afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it('should render Header first if it is provided, followed by the option-container', () => { wrapper = mount(
HeyDer
control unselect
); const flyOutDOMNode: any = document.querySelector( '.lucid-DropMenu.lucid-ContextMenu-FlyOut' ); assert.equal(flyOutDOMNode.children.length, 2); assert.equal(flyOutDOMNode.children[0].innerHTML, 'HeyDer'); assert.equal( flyOutDOMNode.children[1].className, 'lucid-DropMenu-option-container' ); }); }); }); });