@salesforce/design-system-react
Version:
Salesforce Lightning Design System for React
604 lines (521 loc) • 18.5 kB
JSX
/* eslint-disable max-lines */
// Import your external dependencies
import React from 'react';
import ReactDOM from 'react-dom';
import chai, { expect } from 'chai';
import chaiEnzyme from 'chai-enzyme';
import assign from 'lodash.assign';
import { Simulate } from 'react-dom/test-utils';
/* Enzyme Helpers that can mount and unmount React component instances to
* the DOM and set `this.wrapper` and `this.dom` within Mocha's `this`
* context [full source here](tests/enzyme-helpers.js). `this` can
* only be referenced if inside `function () {}`.
*/
import {
mountComponent,
unmountComponent,
} from '../../../tests/enzyme-helpers';
// Import your internal dependencies (for example):
import Dropdown from '../../menu-dropdown';
import IconSettings from '../../icon-settings';
import Tooltip from '../../tooltip';
import List from '../../utilities/menu-list';
import { keyObjects } from '../../../utilities/key-code';
import EventUtil from '../../../utilities/event';
/* Set Chai to use chaiEnzyme for enzyme compatible assertions:
* https://github.com/producthunt/chai-enzyme
*/
chai.use(chaiEnzyme());
const menuOptions = [
{ label: 'A super short', value: 'A0' },
{ label: 'B Option Super Super Long', value: 'B0' },
{ label: 'C Option', value: 'C0' },
{ disabled: true, label: 'D Option', value: 'D0' },
];
const defaultProps = {
iconCategory: 'utility',
iconName: 'down',
id: 'sample-dropdown',
label: 'Test',
menuPosition: 'relative',
openOn: 'click',
options: menuOptions,
placeholder: 'Select a contact',
value: 'B0',
};
/* eslint-disable react/prop-types */
const DropdownCustomContent = (props) => (
<div id="custom-dropdown-menu-content">
<div className="slds-m-around_medium">
<div className="slds-tile slds-tile_board slds-m-horizontal_small">
<p className="tile__title slds-text-heading_small">Art Vandelay</p>
<div className="slds-tile__detail">
<p className="slds-truncate">
<a
id="custom-dropdown-menu-content-link"
className="slds-m-right_medium"
href="#"
onClick={EventUtil.trappedHandler(props.onClick)}
>
Settings
</a>
<a href="#" onClick={EventUtil.trappedHandler(props.onClick)}>
Log Out
</a>
</p>
</div>
</div>
</div>
</div>
);
DropdownCustomContent.displayName = 'DropdownCustomContent';
/* A re-usable demo component fixture outside of `describe` sections
* can accept props within each test and be unmounted after each tests.
* This wrapping component will be similar to your wrapping component
* you will create in the React Storybook for manual testing.
*/
class DemoComponent extends React.Component {
render() {
return (
<IconSettings iconPath="/assets/icons">
<Dropdown {...defaultProps} {...this.props}>
{this.props.children}
</Dropdown>
</IconSettings>
);
}
}
DemoComponent.displayName = 'DropdownDemoComponent';
DemoComponent.defaultProps = defaultProps;
const getNodes = ({ wrapper }) => ({
trigger: wrapper.find('.slds-dropdown-trigger'),
button: wrapper.find('.slds-dropdown-trigger button'),
menu: wrapper.find('.slds-dropdown'),
customContent: wrapper.find('#custom-dropdown-menu-content'),
customContentLink: wrapper.find(
'#custom-dropdown-menu-content #custom-dropdown-menu-content-link'
),
});
/* All tests for component being tested should be wrapped in a root `describe`,
* which should be named after the component being tested.
* When read aloud, the cumulative `describe` and `it` names should form a coherent
* sentence, eg "Date Picker default structure and css is present with expected
* attributes set". If you are having trouble constructing a cumulative short
* sentence, this may be an indicator that your test is poorly structured.
* String provided as first parameter names the `describe` section. Limit to nouns
* as much as possible/appropriate.`
*/
describe('SLDSMenuDropdown', function () {
describe('Styling', () => {
beforeEach(
mountComponent(
<DemoComponent menuStyle={{ height: '500px' }} width="small" />
)
);
afterEach(unmountComponent);
it('has correct CSS classes and style', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.button.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu).to.exist;
expect(openNodes.menu).to.have.style('height', '500px');
expect(openNodes.menu.hasClass('slds-dropdown_small')).to.equal(true);
});
});
describe('Inverse', () => {
beforeEach(mountComponent(<DemoComponent inverse />));
afterEach(unmountComponent);
it('has correct CSS class for inverse', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.button.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu.hasClass('slds-dropdown_inverse')).to.equal(true);
});
});
describe('Custom Content Present', () => {
beforeEach(
mountComponent(
<DemoComponent nubbinPosition="top left" openOn="click">
<DropdownCustomContent />
<List
options={[{ label: 'Custom Content Option' }, ...menuOptions]}
/>
</DemoComponent>
)
);
afterEach(unmountComponent);
it('has content with custom ID is present', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.button.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.customContent.length).to.equal(1);
});
it('closes when custom content is clicked', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.button.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
openNodes.customContentLink.simulate('click', {});
const closedNodes = getNodes({ wrapper: this.wrapper });
expect(closedNodes.customContent.length).to.equal(0);
});
it("has additional ListItem from list child's options prop", function () {
const nodes = getNodes({ wrapper: this.wrapper });
const buttonId = nodes.trigger.prop('id');
nodes.button.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu.find(`li#${buttonId}-item-0`).text()).to.equal(
'Custom Content Option'
);
});
});
describe('Clickable', () => {
const onClick = sinon.spy();
beforeEach(mountComponent(<DemoComponent onClick={onClick} />));
afterEach(unmountComponent);
it('does not expand on hover', function () {
const nodes = getNodes({ wrapper: this.wrapper });
expect(nodes.menu.length).to.equal(0);
nodes.trigger.simulate('mouseEnter', {});
const hoverNodes = getNodes({ wrapper: this.wrapper });
expect(hoverNodes.menu.length).to.equal(0);
});
it('expands/contracts on click', function () {
const nodes = getNodes({ wrapper: this.wrapper });
expect(nodes.menu.length).to.equal(0);
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu.length).to.equal(1);
openNodes.trigger.simulate('click', {});
const closedNodes = getNodes({ wrapper: this.wrapper });
expect(closedNodes.menu.length).to.equal(0);
});
it('preserves click behavior', function () {
onClick.resetHistory();
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('click', {});
expect(onClick.calledOnce);
});
});
describe('Expanded', () => {
let selected;
beforeEach(
mountComponent(
<DemoComponent
onSelect={(selectedOption) => {
selected = selectedOption;
}}
/>
)
);
afterEach(unmountComponent);
it('selects an item on click', function () {
const nodes = getNodes({ wrapper: this.wrapper });
expect(nodes.menu.length).to.equal(0);
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
openNodes.menu.find('li a').first().simulate('click', {});
expect(selected.value).to.equal('A0');
});
});
describe('accessible markup for label Dropdowns', () => {
beforeEach(mountComponent(<DemoComponent />));
afterEach(unmountComponent);
it('<ul> has role menu & aria-labelledby', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu.find('ul')).to.have.attr('role', 'menu');
const nodeId = openNodes.trigger.prop('id');
expect(openNodes.menu.find('ul')).attr('aria-labelledby', nodeId);
});
it('<a> inside <li> has role menuitem', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
const anchorRole = openNodes.menu.find('li a').first().prop('role');
const match =
anchorRole === 'menuitem' ||
anchorRole === 'menuitemradio' ||
anchorRole === 'menuitemcheckbox';
expect(match).to.be.true;
});
it('if option.disabled, add aria-disabled to <a> that has role menuitem', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
const lastItemAriaDisabledRole = openNodes.menu
.find('li a')
.at(3)
.prop('aria-disabled');
expect(lastItemAriaDisabledRole).to.be.true;
});
});
describe('accessible markup for Icon Only Dropdowns', () => {
beforeEach(
mountComponent(
<DemoComponent
assistiveText={{ icon: 'more options' }}
buttonVariant="icon"
checkmark
iconCategory="utility"
iconName="down"
iconVariant="border-filled"
/>
)
);
afterEach(unmountComponent);
it('<button> has assistiveText', function () {
const nodes = getNodes({ wrapper: this.wrapper });
expect(nodes.button.find('.slds-assistive-text').text()).to.equal(
'more options'
);
});
});
describe('Keyboard behavior', () => {
let selected;
beforeEach(
mountComponent(
<DemoComponent
onSelect={(selectedOption) => {
selected = selectedOption;
}}
/>
)
);
afterEach(unmountComponent);
it('opens menu with enter', function () {
const nodes = getNodes({ wrapper: this.wrapper });
expect(nodes.menu.length).to.equal(0);
nodes.button.simulate('keyDown', keyObjects.ENTER);
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu.length).to.equal(1);
});
it('opens menu with down arrow key', function () {
const nodes = getNodes({ wrapper: this.wrapper });
expect(nodes.menu.length).to.equal(0);
nodes.button.simulate('keyDown', keyObjects.DOWN);
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu.length).to.equal(1);
});
it('selects an item with keyboard', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
openNodes.menu.simulate('keyDown', keyObjects.DOWN);
openNodes.menu.simulate('keyDown', keyObjects.DOWN);
openNodes.menu.simulate('keyDown', keyObjects.ENTER);
expect(selected.value).to.equal('B0');
});
it('closes Menu on esc', function () {
const nodes = getNodes({ wrapper: this.wrapper });
expect(nodes.menu.length).to.equal(0);
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
expect(openNodes.menu.length).to.equal(1);
openNodes.menu
.find('.slds-dropdown__item a')
.first()
.simulate('keyDown', keyObjects.ESCAPE);
const closedNodes = getNodes({ wrapper: this.wrapper });
expect(closedNodes.menu.length).to.equal(0);
});
});
describe('multiple selection', function () {
beforeEach(mountComponent(<DemoComponent multiple checkmark />));
afterEach(unmountComponent);
it('selects multiple items and renders checkmarks', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('click', {});
let openNodes = getNodes({ wrapper: this.wrapper });
const firstNode = openNodes.menu.find('.slds-dropdown__item a').at(0);
firstNode.simulate('click');
const secondNode = openNodes.menu.find('.slds-dropdown__item a').at(2);
secondNode.simulate('click');
const thirdNode = openNodes.menu.find('.slds-dropdown__item a').at(3);
openNodes = getNodes({ wrapper: this.wrapper });
// item with checkmark has proper aria markup
expect(firstNode.getDOMNode().getAttribute('aria-checked')).to.equal(
'true'
);
expect(secondNode.getDOMNode().getAttribute('aria-checked')).to.equal(
'true'
);
expect(thirdNode.getDOMNode().getAttribute('aria-checked')).to.equal(
null
);
expect(firstNode).attr('role', 'menuitemcheckbox');
});
it('moves focus to next item after keyboard selection', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('click', {});
const openNodes = getNodes({ wrapper: this.wrapper });
openNodes.menu.simulate('keyDown', keyObjects.DOWN);
openNodes.menu.simulate('keyDown', keyObjects.ENTER);
openNodes.menu.simulate('keyDown', keyObjects.DOWN);
openNodes.menu.simulate('keyDown', keyObjects.ENTER);
const secondNode = openNodes.menu.find('.slds-dropdown__item a').at(1);
expect(secondNode.getDOMNode().getAttribute('aria-checked')).to.not.equal(
'true'
);
});
});
// Hover and hybrid hover UX patterns are not approved UX patterns due to accessibility concerns
describe('Hoverable', () => {
let body;
const renderDropdown = (inst) => {
body = document.createElement('div');
document.body.appendChild(body);
/* deepscan-disable REACT_ASYNC_RENDER_RETURN_VALUE */
// eslint-disable-next-line react/no-render-return-value
return ReactDOM.render(
<div>
<IconSettings iconPath="/assets/icons">{inst}</IconSettings>
</div>,
body
);
/* deepscan-enable REACT_ASYNC_RENDER_RETURN_VALUE */
};
function removeDropdownTrigger() {
ReactDOM.unmountComponentAtNode(body);
document.body.removeChild(body);
}
const createDropdown = (props) =>
React.createElement(Dropdown, assign({}, defaultProps, props));
createDropdown.displayName = 'createDropdown';
const dropItDown = (props) => renderDropdown(createDropdown(props));
const getMenu = (dom) => dom.querySelector('.slds-dropdown');
let cmp;
let btn;
beforeEach(() => {
cmp = dropItDown({
buttonClassName: 'dijkstrafied',
openOn: 'hover',
hoverCloseDelay: 2,
});
[btn] = cmp.getElementsByClassName('slds-dropdown-trigger');
});
afterEach((done) => {
// due to hover-close delay, removal from DOM must be delayed
setTimeout(() => {
removeDropdownTrigger();
done();
}, 100);
});
it('gives the button correct aria properties', () => {
expect(btn.firstChild.getAttribute('aria-haspopup')).to.equal('true');
});
it('sets the label', () => {
expect(btn.textContent).to.equal('Test');
});
it('expands the dropdown on hover', () => {
expect(getMenu(body)).to.equal(null);
Simulate.mouseEnter(btn, {});
expect(getMenu(body).className).to.include('slds-dropdown');
Simulate.mouseLeave(btn, {});
});
it('closes on blur based on timeout delay', (done) => {
expect(getMenu(body)).to.equal(null);
Simulate.mouseEnter(btn, {});
Simulate.mouseLeave(btn);
expect(getMenu(body)).to.not.equal(null);
setTimeout(() => {
expect(getMenu(body)).to.equal(null);
done();
}, 3);
});
it("doesn't close on quick hover outside", (done) => {
expect(getMenu(body)).to.equal(null);
Simulate.mouseEnter(btn, {});
Simulate.mouseLeave(btn);
setTimeout(() => {
Simulate.mouseEnter(btn, {});
expect(getMenu(body)).to.not.equal(null);
setTimeout(() => {
expect(getMenu(body)).to.not.equal(null);
done();
}, 3);
}, 1);
});
});
describe('Hybrid-able', () => {
let body;
const renderDropdown = (inst) => {
body = document.createElement('div');
document.body.appendChild(body);
/* deepscan-disable REACT_ASYNC_RENDER_RETURN_VALUE */
// eslint-disable-next-line react/no-render-return-value
return ReactDOM.render(
<div>
<IconSettings iconPath="/assets/icons">{inst}</IconSettings>
</div>,
body
);
/* deepscan-enable REACT_ASYNC_RENDER_RETURN_VALUE */
};
function removeDropdownTrigger() {
ReactDOM.unmountComponentAtNode(body);
document.body.removeChild(body);
}
const createDropdown = (props) =>
React.createElement(Dropdown, assign({}, defaultProps, props));
createDropdown.displayName = 'createDropdown';
const dropItDown = (props) => renderDropdown(createDropdown(props));
const getMenu = (dom) => dom.querySelector('.slds-dropdown');
let cmp;
let btn;
const onClick = sinon.spy();
beforeEach(() => {
cmp = dropItDown({ openOn: 'hybrid', onClick, hoverCloseDelay: 1 });
[btn] = cmp.getElementsByClassName('slds-dropdown-trigger');
});
afterEach(() => {
removeDropdownTrigger();
});
it('doesnt expand on hover', () => {
expect(getMenu(body)).to.equal(null);
Simulate.mouseEnter(btn, {});
expect(getMenu(body)).to.equal(null);
});
it('opens on click, closes on mouseLeave', (done) => {
// open
expect(getMenu(body)).to.equal(null);
Simulate.click(btn, {});
expect(getMenu(body).className).to.include('slds-dropdown');
// close
Simulate.mouseEnter(btn, {});
Simulate.mouseLeave(btn);
expect(getMenu(body)).to.not.equal(null);
setTimeout(() => {
expect(getMenu(body)).to.equal(null);
done();
}, 2);
});
});
describe('Tooltips function as expected', () => {
beforeEach(
mountComponent(
<DemoComponent
options={[
{ label: 'Test item A', value: 'A0' },
{
label: 'Test item B',
value: 'B0',
tooltipContent: 'Testing tooltip content',
},
{ label: 'Test item C', value: 'C0' },
]}
tooltipMenuItem={<Tooltip />}
/>
)
);
afterEach(unmountComponent);
it('Tooltip component shows when focused on menu item.', function () {
const nodes = getNodes({ wrapper: this.wrapper });
nodes.trigger.simulate('focus');
nodes.trigger.simulate('keyDown', keyObjects.ENTER);
nodes.trigger.simulate('keyDown', keyObjects.DOWN);
const tooltip = this.wrapper.find('#sample-dropdown-item-1-tooltip');
expect(tooltip.length).to.equal(1);
});
});
});