box-ui-elements-mlh
Version:
649 lines (565 loc) • 23.6 kB
JavaScript
import React from 'react';
import { mount, shallow } from 'enzyme';
import sinon from 'sinon';
import DropdownMenu from '../DropdownMenu';
import { KEYS } from '../../../constants';
const sandbox = sinon.sandbox.create();
describe('components/dropdown-menu/DropdownMenu', () => {
// eslint-disable-next-line react/button-has-type
const FakeButton = props => <button {...props}>Some Button</button>;
FakeButton.displayName = 'FakeButton';
/* eslint-disable */
const FakeMenu = ({ initialFocusIndex = 0, onClose = () => {}, ...rest }) => (
<ul {...rest} role="menu">
Some Menu
</ul>
);
FakeMenu.displayName = 'FakeMenu';
/* eslint-enable */
const getWrapper = (props = {}) =>
shallow(
<DropdownMenu {...props}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
afterEach(() => {
sandbox.verifyAndRestore();
});
describe('render()', () => {
test('should throw an error when passed less than 2 children', () => {
expect(() => {
shallow(
<DropdownMenu>
<FakeButton />
</DropdownMenu>,
);
}).toThrow();
});
test('should throw an error when passed more than 2 children', () => {
expect(() => {
shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
<div />
</DropdownMenu>,
);
}).toThrow();
});
test('should correctly render a single child button with correct props', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
const button = wrapper.find(FakeButton);
expect(button.length).toBe(1);
expect(button.prop('id')).toEqual(instance.menuButtonID);
expect(button.key()).toEqual(instance.menuButtonID);
expect(button.prop('aria-haspopup')).toEqual('true');
expect(button.prop('aria-expanded')).toEqual('false');
expect(button.prop('aria-controls')).toBeFalsy();
});
test('should set aria-expanded="true" and aria-controls=menuID when menu is open', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(0);
wrapper.update();
const button = wrapper.find(FakeButton);
expect(button.prop('aria-expanded')).toEqual('true');
expect(button.prop('aria-controls')).toEqual(instance.menuID);
});
test('should not render child menu when menu is closed', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
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(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(1);
wrapper.update();
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(1);
expect(menu.prop('aria-labelledby')).toEqual(instance.menuButtonID);
});
test('should render TetherComponent with correct props with correct default values', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.is('TetherComponent')).toBe(true);
expect(wrapper.prop('attachment')).toEqual('top left');
expect(wrapper.prop('bodyElement')).toEqual(document.body);
expect(wrapper.prop('classPrefix')).toEqual('dropdown-menu');
expect(wrapper.prop('targetAttachment')).toEqual('bottom left');
expect(wrapper.prop('constraints')).toEqual([]);
expect(wrapper.prop('enabled')).toBe(false);
});
test('should render TetherComponent in the body if invalid body element is specified', () => {
const wrapper = shallow(
<DropdownMenu bodyElement="foo">
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.is('TetherComponent')).toBe(true);
expect(wrapper.prop('attachment')).toEqual('top left');
expect(wrapper.prop('bodyElement')).toEqual(document.body);
expect(wrapper.prop('classPrefix')).toEqual('dropdown-menu');
expect(wrapper.prop('targetAttachment')).toEqual('bottom left');
expect(wrapper.prop('constraints')).toEqual([]);
expect(wrapper.prop('enabled')).toBe(false);
});
test('should render className in the className is specified', () => {
const wrapper = shallow(
<DropdownMenu className="foo">
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.is('TetherComponent')).toBe(true);
expect(wrapper.prop('className')).toEqual('foo');
});
test('should render TetherComponent with a specific body element', () => {
const bodyEl = document.createElement('div');
const wrapper = shallow(
<DropdownMenu bodyElement={bodyEl}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.is('TetherComponent')).toBe(true);
expect(wrapper.prop('attachment')).toEqual('top left');
expect(wrapper.prop('bodyElement')).toEqual(bodyEl);
expect(wrapper.prop('classPrefix')).toEqual('dropdown-menu');
expect(wrapper.prop('targetAttachment')).toEqual('bottom left');
expect(wrapper.prop('constraints')).toEqual([]);
expect(wrapper.prop('enabled')).toBe(false);
});
test('should render TetherComponent with correct props when right aligned', () => {
const wrapper = shallow(
<DropdownMenu isRightAligned>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.prop('attachment')).toEqual('top right');
expect(wrapper.prop('targetAttachment')).toEqual('bottom right');
expect(wrapper.prop('enabled')).toBe(false);
});
test('should render TetherComponent with enabled prop when menu is open', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(0);
wrapper.update();
expect(wrapper.prop('enabled')).toBe(true);
});
test('should render TetherComponent with scrollParent constraint when constrainToScrollParent=true', () => {
const wrapper = shallow(
<DropdownMenu constrainToScrollParent>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.prop('constraints')).toEqual([
{
to: 'scrollParent',
attachment: 'together',
},
]);
});
test('should render TetherComponent with window constraint when constrainToScrollParent=true', () => {
const wrapper = shallow(
<DropdownMenu constrainToWindow>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.prop('constraints')).toEqual([
{
to: 'window',
attachment: 'together',
},
]);
});
test('should render TetherComponent with scrollParent and window constraints when constrainToScrollParent=true and constrainToWindow=true', () => {
const wrapper = shallow(
<DropdownMenu constrainToScrollParent constrainToWindow>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
expect(wrapper.prop('constraints')).toEqual([
{
to: 'scrollParent',
attachment: 'together',
},
{
to: 'window',
attachment: 'together',
},
]);
});
});
describe('openMenuAndSetFocusIndex()', () => {
test('should call setState() with correct values', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
sandbox
.mock(instance)
.expects('setState')
.withArgs({
isOpen: true,
initialFocusIndex: 1,
});
instance.openMenuAndSetFocusIndex(1);
});
});
describe('closeMenu()', () => {
test('should call setState() with correct values', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
sandbox
.mock(instance)
.expects('setState')
.withArgs({
isOpen: false,
});
instance.closeMenu();
});
});
describe('handleButtonClick()', () => {
test('should call openMenuAndSetFocusIndex(null) when menu is currently closed', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
sandbox
.mock(instance)
.expects('openMenuAndSetFocusIndex')
.withArgs(null);
wrapper.find(FakeButton).simulate('click', {
preventDefault: sandbox.mock(),
stopPropagation: sandbox.mock(),
});
});
test('should call closeMenu() when menu is currently open', () => {
const event = {
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
};
const onMenuClose = jest.fn();
const wrapper = shallow(
<DropdownMenu onMenuClose={onMenuClose}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(1);
wrapper.find(FakeButton).simulate('click', event);
expect(event.stopPropagation).toBeCalled();
expect(event.preventDefault).toBeCalled();
expect(onMenuClose).toBeCalledWith(event);
});
});
describe('handleButtonKeyDown()', () => {
[
{
key: KEYS.space,
},
{
key: KEYS.enter,
},
{
key: KEYS.arrowDown,
},
].forEach(({ key }) => {
test('should call openMenuAndSetFocus(0) when an open keystroke is pressed', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
sandbox
.mock(instance)
.expects('openMenuAndSetFocusIndex')
.withArgs(0);
wrapper.find(FakeButton).simulate('keydown', {
key,
preventDefault: sandbox.mock(),
stopPropagation: sandbox.mock(),
});
});
});
test('shoud not stop esc propagation if dropdown is closed', () => {
const onMenuClose = jest.fn();
const wrapper = getWrapper({ onMenuClose });
wrapper.setState({ isOpen: false });
wrapper.find(FakeButton).simulate('keydown', {
key: KEYS.escape,
preventDefault: sandbox.mock(),
stopPropagation: sandbox.mock().never(),
});
expect(onMenuClose).toBeCalled();
});
test('should stop esc propagation if dropdown is open', () => {
const onMenuClose = jest.fn();
const wrapper = getWrapper({ onMenuClose });
wrapper.setState({ isOpen: true });
wrapper.find(FakeButton).simulate('keydown', {
key: KEYS.escape,
preventDefault: sandbox.mock(),
stopPropagation: sandbox.mock(),
});
expect(onMenuClose).toBeCalled();
});
test('should call openMenuAndSetFocus(-1) to last item when "up" is pressed', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
sandbox
.mock(instance)
.expects('openMenuAndSetFocusIndex')
.withArgs(-1);
wrapper.find(FakeButton).simulate('keydown', {
key: 'ArrowUp',
preventDefault: sandbox.mock(),
stopPropagation: sandbox.mock(),
});
});
});
describe('handleMenuClose()', () => {
test('should call closeMenu() and focusButton() when called', () => {
const wrapper = shallow(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
sandbox.mock(instance).expects('closeMenu');
sandbox.mock(instance).expects('focusButton');
instance.handleMenuClose();
});
});
describe('componentDidUpdate()', () => {
describe.each([[false], [true]])('when useBubble=%o', useBubble => {
test('should add click and contextmenu listeners when opening menu', () => {
const wrapper = mount(
<DropdownMenu useBubble={useBubble}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
const documentMock = sandbox.mock(document);
documentMock.expects('addEventListener').withArgs('click', sinon.match.any, !useBubble);
documentMock.expects('addEventListener').withArgs('contextmenu', sinon.match.any, !useBubble);
documentMock.expects('removeEventListener').never();
instance.openMenuAndSetFocusIndex(0);
});
test('should remove click and contextmenu listeners when closing menu', () => {
const wrapper = mount(
<DropdownMenu useBubble={useBubble}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(0);
const documentMock = sandbox.mock(document);
documentMock.expects('removeEventListener').withArgs('contextmenu', sinon.match.any, !useBubble);
documentMock.expects('removeEventListener').withArgs('click', sinon.match.any, !useBubble);
documentMock.expects('addEventListener').never();
instance.closeMenu();
});
test('should not do anything opening a menu when menu is already open', () => {
const wrapper = mount(
<DropdownMenu useBubble={useBubble}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
const documentMock = sandbox.mock(document);
instance.openMenuAndSetFocusIndex(0);
documentMock.expects('addEventListener').never();
documentMock.expects('removeEventListener').never();
instance.openMenuAndSetFocusIndex(1);
});
});
});
describe('componentWillUnmount()', () => {
describe.each([[false], [true]])('when useBubble=%o', useBubble => {
test('should not do anything when menu is closed', () => {
const wrapper = mount(
<DropdownMenu useBubble={useBubble}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const documentMock = sandbox.mock(document);
documentMock.expects('removeEventListener').never();
wrapper.unmount();
});
test('should remove listeners when menu is open', () => {
const wrapper = mount(
<DropdownMenu useBubble={useBubble}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const documentMock = sandbox.mock(document);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(0);
documentMock.expects('removeEventListener').withArgs('contextmenu', sinon.match.any, !useBubble);
documentMock.expects('removeEventListener').withArgs('click', sinon.match.any, !useBubble);
wrapper.unmount();
});
});
});
describe('tests requiring body mounting', () => {
let attachTo;
let wrapper = 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 => {
wrapper = mount(component, { attachTo });
};
beforeEach(() => {
// Set up a place to mount
attachTo = document.createElement('div');
attachTo.setAttribute('data-mounting-point', '');
document.body.appendChild(attachTo);
});
afterEach(() => {
sandbox.verifyAndRestore();
// Unmount and remove the mounting point after each test
if (wrapper) {
wrapper.unmount();
wrapper = null;
}
document.body.removeChild(attachTo);
});
describe('handleDocumentClick()', () => {
const closeMenuSpy = jest.fn();
test('should call closeMenu() when event target is not within the menu or button', () => {
mountToBody(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(0);
instance.closeMenu = closeMenuSpy;
const handleDocumentClickEvent = {
target: document.createElement('div'),
};
instance.handleDocumentClick(handleDocumentClickEvent);
expect(closeMenuSpy).toHaveBeenCalled();
});
test('should call onMenuClose() when provided', () => {
const onMenuCloseSpy = jest.fn();
mountToBody(
<DropdownMenu onMenuClose={onMenuCloseSpy}>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(0);
const handleDocumentClickEvent = {
target: document.createElement('div'),
};
instance.handleDocumentClick(handleDocumentClickEvent);
expect(onMenuCloseSpy).toHaveBeenCalledWith(handleDocumentClickEvent);
});
test.each`
elementID | description
${'menuButtonID'} | ${'button'}
${'menuID'} | ${'menu'}
`('should not call handleMenuClose() when event target is within the $description', ({ elementID }) => {
mountToBody(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
instance.openMenuAndSetFocusIndex(0);
instance.closeMenu = closeMenuSpy;
const handleDocumentClickEvent = {
target: document.getElementById(instance[elementID]),
};
instance.handleDocumentClick(handleDocumentClickEvent);
expect(closeMenuSpy).not.toHaveBeenCalled();
});
});
describe('focusButton()', () => {
test('should focus the menu button when called', () => {
mountToBody(
<DropdownMenu>
<FakeButton />
<FakeMenu />
</DropdownMenu>,
);
const instance = wrapper.instance();
const menuButtonEl = document.getElementById(instance.menuButtonID);
sandbox.mock(menuButtonEl).expects('focus');
instance.focusButton();
});
});
});
});