diff --git a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js index f5726a02686d..1558bf24be47 100644 --- a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js +++ b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js @@ -46,7 +46,14 @@ describe('MultiSelect.Filterable', () => { it('should initially have the menu open when open prop is provided', () => { const wrapper = mount(); - expect(wrapper.state('isOpen')).toBe(true); + assertMenuOpen(wrapper, mockProps); + }); + + it('should open the menu with a down arrow', () => { + const wrapper = mount(); + + findMenuIconNode(wrapper).simulate('keyDown', { key: 'ArrowDown' }); + assertMenuOpen(wrapper, mockProps); }); it('should let the user toggle the menu by the menu icon', () => { @@ -71,7 +78,12 @@ describe('MultiSelect.Filterable', () => { const wrapper = mount(); openMenu(wrapper); expect(wrapper.find(listItemName).length).toBe(mockProps.items.length); - wrapper.setState({ inputValue: '3' }); + + wrapper + .find('[role="combobox"]') + .at(0) + .simulate('change', { target: { value: '3' } }); + expect(wrapper.find(listItemName).length).toBe(1); }); @@ -154,12 +166,17 @@ describe('MultiSelect.Filterable', () => { const wrapper = mount(); const inputValue = 'Item'; openMenu(wrapper); - wrapper.setState({ inputValue }); + + wrapper + .find('[role="combobox"]') + .at(0) + .simulate('change', { target: { value: 'Item' } }); + wrapper .find(listItemName) .at(0) .simulate('click'); - expect(wrapper.state('inputValue')).toEqual(inputValue); + expect(wrapper.find('[role="combobox"]').props().value).toEqual(inputValue); }); }); diff --git a/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js b/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js index d52cc2ab215b..c08e7413ab90 100644 --- a/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js +++ b/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js @@ -5,378 +5,438 @@ * LICENSE file in the root directory of this source tree. */ +import { getByText, isElementVisible } from '@carbon/test-utils/dom'; +import { pressEnter, pressSpace, pressTab } from '@carbon/test-utils/keyboard'; +import { render, cleanup } from '@carbon/test-utils/react'; import React from 'react'; -import { mount } from 'enzyme'; -import MultiSelect from '../../MultiSelect'; -import { - openMenu, - generateItems, - generateGenericItem, -} from '../../ListBox/test-helpers'; -import { settings } from 'carbon-components'; - -const { prefix } = settings; - -const mouseDownAndUp = node => { - node.dispatchEvent(new window.MouseEvent('mousedown', { bubbles: true })); - node.dispatchEvent(new window.MouseEvent('mouseup', { bubbles: true })); -}; +import { Simulate } from 'react-dom/test-utils'; +import MultiSelect from '../'; +import { generateItems, generateGenericItem } from '../../ListBox/test-helpers'; +import { keys } from '../../../internal/keyboard'; describe('MultiSelect', () => { - it('should render', () => { - const wrapper = mount( - + afterEach(cleanup); + + describe.skip('automated accessibility tests', () => { + it('should have no axe violations', async () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + + ); + await expect(container).toHaveNoAxeViolations(); + }); + + it('should have no DAP violations', async () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + + ); + await expect(container).toHaveNoDAPViolations(); + }); + }); + + it('should initially render with a given label', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + ); - expect(wrapper).toMatchSnapshot(); + + const labelNode = getByText(container, label); + expect(isElementVisible(labelNode)).toBe(true); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeNull(); }); - it('should initialize with no selected items if no `initialSelectedItems` are given', () => { - const items = generateItems(5, generateGenericItem); - const wrapper = mount( - + it('should open the menu when a user clicks on the label', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + ); - expect(wrapper.find('Selection').instance().state.selectedItems).toEqual( - [] + + const labelNode = getByText(container, label); + Simulate.click(labelNode); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeInstanceOf(HTMLElement); + }); + + it('should open the menu when a user hits space while the field is focused', () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + ); + + pressTab(); + pressSpace(); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeInstanceOf(HTMLElement); }); - it('should initialize with the menu not open', () => { - const items = generateItems(5, generateGenericItem); - const wrapper = mount( - + + it.skip('should open the menu when a user hits enter while the field is focused', () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + ); - expect(wrapper.state('isOpen')).toEqual(false); + + pressTab(); + pressEnter(); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeInstanceOf(HTMLElement); }); - it('should initialize with the menu open', () => { - const items = generateItems(5, generateGenericItem); - const wrapper = mount( - + it('should let the user toggle item selection with a mouse', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + ); - expect(wrapper.state('isOpen')).toEqual(true); + + const labelNode = getByText(container, label); + Simulate.click(labelNode); + + const [item] = items; + const itemNode = getByText(container, item.label); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeNull(); + + Simulate.click(itemNode); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeInstanceOf(HTMLElement); + + Simulate.click(itemNode); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeNull(); }); - describe('#handleOnToggleMenu', () => { - it('should toggle the boolean `isOpen` field', () => { - const items = generateItems(5, generateGenericItem); - const wrapper = mount( - - ); - expect(wrapper.state('isOpen')).toBe(false); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - expect(wrapper.state('isOpen')).toBe(true); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - expect(wrapper.state('isOpen')).toBe(false); + it('should close the menu when the user hits the Escape key', () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + + ); + + pressTab(); + pressSpace(); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeInstanceOf(HTMLElement); + + Simulate.keyDown(document.activeElement, { + key: 'Escape', }); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeNull(); + }); + + it.skip('close menu with click outside of field', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + + ); + const labelNode = getByText(container, label); + const button = document.createElement('BUTTON'); + button.id = 'button-id'; + document.body.appendChild(button); + const buttonNode = document.getElementById('button-id'); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeFalsy(); + + Simulate.click(labelNode); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeTruthy(); + + Simulate.click(buttonNode); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeFalsy(); + + document.body.removeChild(button); + }); + + it.skip('should toggle selection with enter', () => { + // yeah focus is on the field, you hit the arrows to change the active index, and keydown is on the field since it has focus + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + + ); + + const labelNode = getByText(container, label); + Simulate.click(labelNode); + + const [item] = items; + const itemNode = getByText(container, item.label); + console.log(item.label); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeNull(); + + Simulate.keyDown(itemNode, { key: 'Enter' }); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeInstanceOf(HTMLElement); + + Simulate.keyDown(itemNode, { key: 'Enter' }); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeNull(); + }); + + it.skip('toggle selection with space', () => { + // yeah focus is on the field, you hit the arrows to change the active index, and keydown is on the field since it has focus + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + + ); + + const labelNode = getByText(container, label); + Simulate.click(labelNode); + + const [item] = items; + const itemNode = getByText(container, item.label); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeNull(); + + Simulate.keyDown(itemNode, keys.Space); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeInstanceOf(HTMLElement); + + Simulate.keyDown(itemNode, keys.Space); + + expect( + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeNull(); + }); + + it('should clear selected items when the user clicks the clear selection button', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + + ); + const labelNode = getByText(container, label); + Simulate.click(labelNode); + + const [item] = items; + const itemNode = getByText(container, item.label); + Simulate.click(itemNode); + + expect( + document.querySelector('[aria-label="Clear Selection"]') + ).toBeTruthy(); + + Simulate.click(document.querySelector('[aria-label="Clear Selection"]')); + + expect( + document.querySelector('[aria-label="Clear Selection"]') + ).toBeFalsy(); }); - describe('when `initialSelectedItems` is given', () => { - it('should initialize `selectedItems` with the given initial selected items', () => { - const items = generateItems(5, generateGenericItem); - const wrapper = mount( + it('should not be interactive if disabled', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + + ); + const labelNode = getByText(container, label); + Simulate.click(labelNode); + + expect( + container.querySelector('[aria-expanded="true"][aria-haspopup="true"]') + ).toBeFalsy(); + }); + + describe('Component API', () => { + it('should set the default selected items with the `initialSelectedItems` prop', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( ); - expect(wrapper.find('Selection').instance().state.selectedItems).toEqual([ - items[0], - items[1], - ]); - }); - }); - describe('MultiSelect with InitialSelectedItems', () => { - let mockProps; - const items = generateItems(5, generateGenericItem); - - beforeEach(() => { - mockProps = { - items: items, - initialSelectedItems: [items[0], items[1], items[2]], - itemToString: ({ label }) => label, - onChange: jest.fn(), - label: 'Label', - }; - }); - - it('should allow a user to de-select an item by clicking on initial selected items', () => { - const wrapper = mount( - - ); expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(3); + document.querySelector('[aria-label="Clear Selection"]') + ).toBeTruthy(); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(0) - .simulate('click'); - - expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(2); - }); + const labelNode = getByText(container, label); - it('should allow a user to de-select an initial selected item by hitting enter on initial selected item', () => { - const wrapper = mount( - - ); - const simulateArrowDown = wrapper => - wrapper.find(`.${prefix}--list-box__field`).simulate('keydown', { - key: 'ArrowDown', - }); + Simulate.click(labelNode); expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(3); - openMenu(wrapper); - simulateArrowDown(wrapper); - wrapper - .find(`.${prefix}--list-box__field`) - .at(0) - .simulate('keydown', { - key: 'Enter', - }); - expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(2); + document.querySelector('[aria-selected="true"][role="option"]') + ).toBeInstanceOf(HTMLElement); }); - it('should allow a user to de-select an item after calling setState by clicking on selected item', () => { - const wrapper = mount( + it('should place the given id on the ___ node when passed in as a prop', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + + render( ); - expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(1); - wrapper.setState({ foo: 'bar' }); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(0) - .simulate('click'); - expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(0); + expect(document.getElementById('custom-id')).toBeTruthy(); }); - it('should select an item when a user clicks on an item', () => { - const wrapper = mount( - + it('should support a custom itemToString with object items', () => { + const items = [ + { text: 'joey' }, + { text: 'johnny' }, + { text: 'tommy' }, + { text: 'dee dee' }, + { text: 'marky' }, + ]; + const label = 'test-label'; + const { container } = render( + (item ? item.text : '')} + /> ); - expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(3); + const labelNode = getByText(container, label); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(4) - .simulate('click'); + Simulate.click(labelNode); - expect( - wrapper.find('Selection').instance().state.selectedItems.length - ).toBe(4); + expect(getByText(container, 'joey')).toBeTruthy(); + expect(getByText(container, 'johnny')).toBeTruthy(); + expect(getByText(container, 'tommy')).toBeTruthy(); + expect(getByText(container, 'dee dee')).toBeTruthy(); + expect(getByText(container, 'marky')).toBeTruthy(); }); - }); - describe('e2e', () => { - let mockProps; - - beforeEach(() => { - mockProps = { - items: generateItems(5, generateGenericItem), - initialSelectedItems: [], - itemToString: ({ label }) => label, - onChange: jest.fn(), - label: 'Label', - }; - }); + it('should support custom translation with translateWithId', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const translateWithId = jest.fn(() => 'message'); - it('should open the menu when a user clicks on the ListBox field', () => { - const wrapper = mount( - - ); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - expect(wrapper.find(`.${prefix}--list-box__menu`).length).toBe(1); - expect(wrapper.find(`.${prefix}--list-box__menu-item`).length).toBe( - mockProps.items.length + render( + ); - }); - it('should open the menu when a user focuses and hits space on the ListBox field', () => { - const wrapper = mount( - - ); - wrapper.find(`.${prefix}--list-box__field`).simulate('keydown', { - key: ' ', - }); - expect(wrapper.find(`.${prefix}--list-box__menu`).length).toBe(1); - expect(wrapper.find(`.${prefix}--list-box__menu-item`).length).toBe( - mockProps.items.length - ); + expect(translateWithId).toHaveBeenCalled(); }); - it('should select an item when a user clicks on an item', () => { - const wrapper = mount( - - ); - openMenu(wrapper); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .first() - .simulate('click'); - expect(wrapper.find('Selection').instance().state.selectedItems).toEqual([ - mockProps.items[0], - ]); - }); + it('should call onChange when the selection changes', () => { + const testFunction = jest.fn(); + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; - it('should allow a user to highlight items with the up and down arrow keys', () => { - const wrapper = mount( - + const { container } = render( + ); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - const simulateArrowDown = () => - wrapper.find(`.${prefix}--list-box__field`).simulate('keydown', { - key: 'ArrowDown', - }); - const simulateArrowUp = () => - wrapper.find(`.${prefix}--list-box__field`).simulate('keydown', { - key: 'ArrowUp', - }); - const getHighlightedId = () => - wrapper.find(`.${prefix}--list-box__menu-item--highlighted`).prop('id'); - simulateArrowDown(); - expect(getHighlightedId()).toBe('downshift-13-item-0'); - simulateArrowDown(); - expect(getHighlightedId()).toBe('downshift-13-item-1'); - // Simulate "wrap" behavior - simulateArrowDown(); - simulateArrowDown(); - simulateArrowDown(); - simulateArrowDown(); - expect(getHighlightedId()).toBe('downshift-13-item-0'); - simulateArrowUp(); - expect(getHighlightedId()).toBe('downshift-13-item-4'); - }); - it('should close the menu when a user clicks outside of the control', () => { - const wrapper = mount( - - ); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - mouseDownAndUp(document.body); - expect(wrapper.state('isOpen')).toBe(false); - }); + const labelNode = getByText(container, label); + Simulate.click(labelNode); - it('should show a badge that mirrors the number of selected items', () => { - const wrapper = mount( - - ); - openMenu(wrapper); - for (let i = 0; i < mockProps.items.length; i++) { - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(i) - .simulate('click'); - expect(wrapper.find(`.${prefix}--list-box__selection`).text()).toEqual( - expect.stringContaining(`${i + 1}`) - ); - } - expect(wrapper.find('Selection').instance().state.selectedItems).toEqual( - mockProps.items - ); - }); + const [item] = items; + const itemNode = getByText(container, item.label); + Simulate.click(itemNode); - it('should allow a user to de-select an item by clicking on a selected item', () => { - const wrapper = mount( - - ); - wrapper.find(`.${prefix}--list-box__field`).simulate('click'); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(0) - .simulate('click'); - expect( - wrapper.find(`.${prefix}--list-box__menu-item--active`).length - ).toBe(1); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(0) - .simulate('click'); - expect( - wrapper.find(`.${prefix}--list-box__menu-item--active`).length - ).toBe(0); + expect(testFunction).toHaveBeenCalledTimes(1); }); - it('should allow a user to de-select an item by hitting enter on a selected item', () => { - const wrapper = mount( - - ); - const simulateArrowDown = wrapper => - wrapper.find(`.${prefix}--list-box__field`).simulate('keydown', { - key: 'ArrowDown', - }); - openMenu(wrapper); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(0) - .simulate('click'); - expect( - wrapper.find(`.${prefix}--list-box__menu-item--active`).length - ).toBe(1); - simulateArrowDown(wrapper); - wrapper.find(`.${prefix}--list-box__field`).simulate('keydown', { - key: 'Enter', - }); - expect( - wrapper.find(`.${prefix}--list-box__menu-item--active`).length - ).toBe(0); - }); + it('should support an invalid state with invalidText that describes the field', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; - it('should allow a user to click on the clear icon to clear all selected items', () => { - const wrapper = mount( - + const { container } = render( + ); - openMenu(wrapper); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(0) - .simulate('click'); - wrapper.find(`.${prefix}--list-box__selection`).simulate('click'); - expect(wrapper.find('Selection').instance().state.selectedItems).toEqual( - [] + + expect(getByText(container, 'Fool of a Took!')).toBeTruthy(); + + expect(document.querySelector('[data-invalid="true"')).toBeInstanceOf( + HTMLElement ); }); - it('should allow a user to focus the clear icon and hit enter to clear all selected items', () => { - const wrapper = mount( - + it('should support different feedback modes with selectionFeedback', () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const [_firstItem, _secondItem, thirdItem] = items; + const { container } = render( + ); - openMenu(wrapper); - wrapper - .find(`.${prefix}--list-box__menu-item`) - .at(0) - .simulate('click'); - wrapper.find(`.${prefix}--list-box__selection`).simulate('keydown', { - keyCode: 13, - }); - expect(wrapper.find('Selection').instance().state.selectedItems).toEqual( - [] + + // click the label to open the multiselect options menu + const labelNode = getByText(container, label); + Simulate.click(labelNode); + + // click the third option down in the list + const itemNode = getByText(container, thirdItem.label); + Simulate.click(itemNode); + + // get an array of all the options + const optionsArray = Array.from( + document.querySelectorAll('[role="option"]') ); + + // the first option in the list to the the former third option in the list + expect(optionsArray[0].title).toBe('Item 2'); }); }); }); diff --git a/packages/react/src/components/MultiSelect/__tests__/__snapshots__/MultiSelect-test.js.snap b/packages/react/src/components/MultiSelect/__tests__/__snapshots__/MultiSelect-test.js.snap deleted file mode 100644 index 3a8e765bb454..000000000000 --- a/packages/react/src/components/MultiSelect/__tests__/__snapshots__/MultiSelect-test.js.snap +++ /dev/null @@ -1,426 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MultiSelect should render 1`] = ` - -
- - - -
- -
- - Field - - -
- - - - - - Open menu - - - - -
-
-
-
- -
- -
-
-
- - - Item 0 - - -
-
-
-
- -
-
-
- - - Item 1 - - -
-
-
-
- -
-
-
- - - Item 2 - - -
-
-
-
- -
-
-
- - - Item 3 - - -
-
-
-
- -
-
-
- - - Item 4 - - -
-
-
-
-
-
-
-
-
-
-
-
-`; diff --git a/packages/test-utils/keyboard.js b/packages/test-utils/keyboard.js index 5735363b61b4..a05d7e4ffd4c 100644 --- a/packages/test-utils/keyboard.js +++ b/packages/test-utils/keyboard.js @@ -132,7 +132,7 @@ export function pressSpace(node = document.activeElement) { }), ]; - if (node.tagName === 'BUTTON') { + if (node.tagName === 'BUTTON' || node.getAttribute('role') === 'button') { events.push( new MouseEvent('click', { bubbles: true,