import * as React from 'react'; import { act } from 'react-dom/test-utils'; import { renderDropdown, items, getItemIdRegexByIndex } from './test-utils'; import { Dropdown } from 'src/components/Dropdown/Dropdown'; import { dropdownSelectedItemSlotClassNames } from 'src/components/Dropdown/DropdownSelectedItem'; import { implementsShorthandProp, isConformant } from 'test/specs/commonTests'; import { findIntrinsicElement } from 'test/utils'; import { DropdownItemProps } from 'src/components/Dropdown/DropdownItem'; import { ShorthandValue } from 'src/types'; import { List } from 'src/components/List/List'; jest.dontMock('@fluentui/keyboard-key'); jest.useFakeTimers(); describe('Dropdown', () => { isConformant(Dropdown, { hasAccessibilityProp: false, constructorName: 'Dropdown', autoControlledProps: ['highlightedIndex', 'open', 'searchQuery', 'activeSelectedIndex', 'value'], }); implementsShorthandProp(Dropdown)('list', List, { implementsPopper: true, requiredProps: { open: true }, }); describe('clearable', () => { it('value is cleared at Icon click', () => { const { triggerButtonNode, clickOnClearIndicator } = renderDropdown({ clearable: true, defaultValue: items[0], }); clickOnClearIndicator(); expect(triggerButtonNode).toHaveTextContent(''); }); it('calls onChange on Icon click with an `empty` value', () => { const onChange = jest.fn(); const { clickOnClearIndicator } = renderDropdown({ onChange, defaultValue: items[0], clearable: true, }); clickOnClearIndicator(); expect(onChange).toBeCalledTimes(1); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ type: 'click' }), expect.objectContaining({ activeSelectedIndex: undefined, highlightedIndex: null, open: false, searchQuery: undefined, value: null, }), ); }); it('should have the indicator tabbable if not a search', () => { const { getClearIndicatorNode } = renderDropdown({ clearable: true, defaultValue: items[0], }); expect(getClearIndicatorNode()).toHaveAttribute('tabindex', '0'); expect(getClearIndicatorNode()).toHaveAttribute('role', 'button'); }); it('should not have the indicator tabbable if a search', () => { const { getClearIndicatorNode } = renderDropdown({ clearable: true, defaultValue: items[0], search: true, }); expect(getClearIndicatorNode()).not.toHaveAttribute('tabindex'); expect(getClearIndicatorNode()).not.toHaveAttribute('role', 'button'); }); }); describe('open', () => { it('it takes the value of the controlled prop', () => { const { getItemNodes, clickOnItemAtIndex } = renderDropdown({ open: true }); expect(getItemNodes()).toHaveLength(items.length); clickOnItemAtIndex(0); expect(getItemNodes()).toHaveLength(items.length); }); it('it takes the value of the default prop but can be changed', () => { const { getItemNodes, clickOnItemAtIndex } = renderDropdown({ defaultOpen: true }); expect(getItemNodes()).toHaveLength(items.length); clickOnItemAtIndex(0); expect(getItemNodes()).toHaveLength(0); }); it('is "true" when opened by trigger button click', () => { const { getItemNodes, clickOnTriggerButton } = renderDropdown(); clickOnTriggerButton(); expect(getItemNodes()).toHaveLength(items.length); }); it('is "false" when closed by trigger button click', () => { const { clickOnTriggerButton, getItemNodes } = renderDropdown({ defaultOpen: true, }); clickOnTriggerButton(); expect(getItemNodes()).toHaveLength(0); }); it('calls onOpenChange with a value that represents the open state', () => { const onOpenChange = jest.fn(); const { clickOnTriggerButton } = renderDropdown({ onOpenChange, }); clickOnTriggerButton(); expect(onOpenChange).toBeCalledTimes(1); expect(onOpenChange).toHaveBeenLastCalledWith( null, expect.objectContaining({ open: true, }), ); clickOnTriggerButton(); expect(onOpenChange).toBeCalledTimes(2); expect(onOpenChange).toHaveBeenLastCalledWith( null, expect.objectContaining({ open: false, }), ); }); it('is "true" when opened by toggle indicator click', () => { const { clickOnToggleIndicator, getItemNodes } = renderDropdown(); clickOnToggleIndicator(); expect(getItemNodes()).toHaveLength(items.length); }); it('is "false" when closed by toggle indicator click', () => { const { clickOnToggleIndicator, getItemNodes } = renderDropdown({ defaultOpen: true, }); clickOnToggleIndicator(); expect(getItemNodes()).toHaveLength(0); }); it('is "false" when closed by hitting Escape in search input', () => { const { keyDownOnSearchInput, getItemNodes } = renderDropdown({ search: true, defaultOpen: true, }); keyDownOnSearchInput('Escape'); expect(getItemNodes()).toHaveLength(0); }); it('is "false" when closed by hitting Escape in items list', () => { const { keyDownOnItemsList, getItemNodes } = renderDropdown({ defaultOpen: true }); expect(items).toHaveLength(items.length); keyDownOnItemsList('Escape'); expect(getItemNodes()).toHaveLength(0); }); it('is "false" when an item has been selected', () => { const { clickOnItemAtIndex, getItemNodes } = renderDropdown({ defaultOpen: true }); clickOnItemAtIndex(0); expect(getItemNodes()).toHaveLength(0); }); it('when set to "true" by trigger button click will move focus to the items list', () => { const { clickOnTriggerButton, itemsListNode } = renderDropdown(); clickOnTriggerButton(); expect(itemsListNode).toHaveFocus(); }); it('is "false" when blurred by Tab on items list', () => { const { getItemNodes, keyDownOnItemsList } = renderDropdown({ defaultOpen: true }); keyDownOnItemsList('Tab'); expect(getItemNodes()).toHaveLength(0); }); it('is "false" when blurred by Shift+Tab on items list', () => { const { getItemNodes, keyDownOnItemsList } = renderDropdown({ defaultOpen: true }); keyDownOnItemsList('Tab', { shiftKey: true }); expect(getItemNodes()).toHaveLength(0); }); it('is "false" when blurred by Tab on search input', () => { const { getItemNodes, keyDownOnSearchInput } = renderDropdown({ defaultOpen: true, search: true, }); keyDownOnSearchInput('Tab'); expect(getItemNodes()).toHaveLength(0); }); it('is "false" when blurred by Shift+Tab on search input', () => { const { getItemNodes, keyDownOnSearchInput } = renderDropdown({ defaultOpen: true, search: true, }); keyDownOnSearchInput('Tab', { shiftKey: true }); expect(getItemNodes()).toHaveLength(0); }); it('is "true" when you start typing in the search input', () => { const { getItemNodes, changeSearchInput } = renderDropdown({ search: true, }); changeSearchInput('item'); expect(getItemNodes()).toHaveLength(items.length); }); it('is "false" when you remove the query from the search input', () => { const { getItemNodes, changeSearchInput } = renderDropdown({ search: true, defaultOpen: true, defaultSearchQuery: 'item', }); changeSearchInput(''); expect(getItemNodes()).toHaveLength(0); }); it('is "true" when opened by space bar on trigger button', () => { const { getItemNodes, keyDownOnTriggerButton } = renderDropdown({}); keyDownOnTriggerButton(' '); expect(getItemNodes()).toHaveLength(items.length); }); it('is "true" when opened by arrow down on trigger button', () => { const { getItemNodes, keyDownOnTriggerButton } = renderDropdown({}); keyDownOnTriggerButton('ArrowDown'); expect(getItemNodes()).toHaveLength(items.length); }); it('is "true" when opened by arrow up on trigger button', () => { const { getItemNodes, keyDownOnTriggerButton } = renderDropdown({}); keyDownOnTriggerButton('ArrowUp'); expect(getItemNodes()).toHaveLength(items.length); }); }); describe('highlightedIndex', () => { afterEach(() => { act(() => { jest.runAllTimers(); }); }); it('is null when opened by click', () => { const { clickOnTriggerButton, itemsListNode } = renderDropdown(); clickOnTriggerButton(); expect(itemsListNode).not.toHaveAttribute('aria-activedescendant'); }); it('is null when opened by toggle indicator click', () => { const { clickOnToggleIndicator, itemsListNode } = renderDropdown(); clickOnToggleIndicator(); expect(itemsListNode).not.toHaveAttribute('aria-activedescendant'); }); it('is first item index when opened by arrow down key', () => { const { keyDownOnTriggerButton, itemsListNode } = renderDropdown(); keyDownOnTriggerButton('ArrowDown'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('is last item index when opened by arrow up key', () => { const { keyDownOnTriggerButton, itemsListNode } = renderDropdown(); keyDownOnTriggerButton('ArrowUp'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(items.length - 1)), ); }); it('has the provided prop value when opened by click', () => { const highlightedIndex = 2; const { clickOnTriggerButton, itemsListNode } = renderDropdown({ highlightedIndex, }); clickOnTriggerButton(); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(highlightedIndex)), ); }); it('has the provided prop value when opened by arrow down key', () => { const highlightedIndex = 1; const { keyDownOnTriggerButton, itemsListNode } = renderDropdown({ highlightedIndex, }); keyDownOnTriggerButton('ArrowDown'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(highlightedIndex)), ); }); it('has the provided prop value when opened by arrow up key', () => { const highlightedIndex = 1; const { keyDownOnTriggerButton, itemsListNode } = renderDropdown({ highlightedIndex, }); keyDownOnTriggerButton('ArrowUp'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(highlightedIndex)), ); }); it('is defaultHighlightedIndex prop value at first opening, then null', () => { const defaultHighlightedIndex = 2; const { clickOnTriggerButton, itemsListNode } = renderDropdown({ defaultHighlightedIndex, }); clickOnTriggerButton(); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(defaultHighlightedIndex)), ); clickOnTriggerButton(); clickOnTriggerButton(); expect(itemsListNode).not.toHaveAttribute('aria-activedescendant'); }); it('is 0 on every open when highlightFirstItemOnOpen prop is provided', () => { const { clickOnTriggerButton, itemsListNode } = renderDropdown({ highlightFirstItemOnOpen: true, }); clickOnTriggerButton(); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); clickOnTriggerButton(); clickOnTriggerButton(); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('is set to 0 on searchQuery change and when highlightFirstItemOnOpen prop is provided', () => { const { changeSearchInput, keyDownOnSearchInput, searchInputNode } = renderDropdown({ highlightFirstItemOnOpen: true, search: true, }); changeSearchInput('i'); expect(searchInputNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); keyDownOnSearchInput('ArrowDown'); expect(searchInputNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(1))); changeSearchInput('it'); expect(searchInputNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('is null on searchQuery change and when highlightFirstItemOnOpen prop is not provided', () => { const { changeSearchInput, keyDownOnSearchInput, searchInputNode } = renderDropdown({ search: true, }); changeSearchInput('i'); expect(searchInputNode).not.toHaveAttribute('aria-activedescendant'); keyDownOnSearchInput('ArrowDown'); expect(searchInputNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); changeSearchInput('it'); expect(searchInputNode).not.toHaveAttribute('aria-activedescendant'); }); it('is the index of the value previously selected when opened', () => { const highlightedIndex = 2; const { clickOnTriggerButton, itemsListNode } = renderDropdown({ value: items[highlightedIndex], }); clickOnTriggerButton(); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(highlightedIndex)), ); }); it('is the index of the (value previously selected + 1) when opened by arrow down', () => { const highlightedIndex = 2; const { keyDownOnTriggerButton, itemsListNode } = renderDropdown({ value: items[highlightedIndex], }); keyDownOnTriggerButton('ArrowDown'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(highlightedIndex + 1)), ); }); it('is the index of the (value previously selected - 1) when opened by arrow up', () => { const highlightedIndex = 2; const { keyDownOnTriggerButton, itemsListNode } = renderDropdown({ value: items[highlightedIndex], }); keyDownOnTriggerButton('ArrowUp'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(highlightedIndex - 1)), ); }); it('is changed correctly on arrow down navigation', () => { const { keyDownOnItemsList, itemsListNode } = renderDropdown({ defaultOpen: true }); for (let index = 0; index < items.length; index++) { keyDownOnItemsList('ArrowDown'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(index)), ); } }); it('is changed correctly on arrow up navigation', () => { const { keyDownOnItemsList, itemsListNode } = renderDropdown({ defaultOpen: true }); for (let index = items.length - 1; index >= 0; index--) { keyDownOnItemsList('ArrowUp'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(index)), ); } }); it('is changed correctly on arrow down and shift navigation', () => { const { keyDownOnTriggerButton, itemsListNode } = renderDropdown({ defaultOpen: true }); keyDownOnTriggerButton('ArrowDown', { shiftKey: true }); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(4))); }); it('is changed correctly on arrow up and shift navigation', () => { const { keyDownOnTriggerButton, itemsListNode } = renderDropdown({ defaultHighlightedIndex: items.length - 1, defaultOpen: true, }); keyDownOnTriggerButton('ArrowUp', { shiftKey: true }); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('is changed correctly on home key navigation', () => { const { keyDownOnItemsList, itemsListNode } = renderDropdown({ defaultHighlightedIndex: 2, defaultOpen: true, }); keyDownOnItemsList('Home'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('is changed correctly on end key navigation', () => { const { keyDownOnItemsList, itemsListNode } = renderDropdown({ defaultHighlightedIndex: 2, defaultOpen: true, }); keyDownOnItemsList('End'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(items.length - 1)), ); }); it('wraps to start and end on navigation', () => { const { keyDownOnItemsList, itemsListNode } = renderDropdown({ defaultHighlightedIndex: 0, defaultOpen: true, }); keyDownOnItemsList('ArrowUp'); expect(itemsListNode).toHaveAttribute( 'aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(items.length - 1)), ); keyDownOnItemsList('ArrowDown'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('is updated correctly when hovering over items', () => { const { mouseOverItemAtIndex, itemsListNode } = renderDropdown({ defaultOpen: true, }); mouseOverItemAtIndex(1); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(1))); mouseOverItemAtIndex(3); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(3))); }); it('is updated correctly when hovering over items and using arrow keys to navigate', () => { const { mouseOverItemAtIndex, keyDownOnItemsList, itemsListNode } = renderDropdown({ defaultOpen: true, }); mouseOverItemAtIndex(1); keyDownOnItemsList('ArrowDown'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(2))); mouseOverItemAtIndex(4); keyDownOnItemsList('ArrowUp'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(3))); }); it('jumps to the item starting with the character key pressed', () => { const items = ['Athos', 'Porthos', 'Aramis', `D'Artagnan`]; const { keyDownOnItemsList, itemsListNode } = renderDropdown({ items, defaultOpen: true, }); keyDownOnItemsList('P'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(1))); }); it('jumps starting from the current highlightedIndex on character key press', () => { const items = ['Athos', 'Porthos', 'Aramis', `D'Artagnan`]; const { keyDownOnItemsList, itemsListNode } = renderDropdown({ items, defaultHighlightedIndex: 1, defaultOpen: true, }); keyDownOnItemsList('A'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(2))); }); it('wraps to the start of the list when no options remain', () => { const items = ['Athos', 'Porthos', 'Aramis', `D'Artagnan`]; const { keyDownOnItemsList, itemsListNode } = renderDropdown({ items, defaultHighlightedIndex: 2, defaultOpen: true, }); keyDownOnItemsList('A'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('jumps from item to item when pressing the same key with enough time in between', () => { const items = ['Athos', 'Porthos', 'Aramis', `D'Artagnan`]; const { keyDownOnItemsList, itemsListNode } = renderDropdown({ items, defaultOpen: true, }); keyDownOnItemsList('A'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); act(() => { jest.runAllTimers(); }); keyDownOnItemsList('A'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(2))); act(() => { jest.runAllTimers(); }); keyDownOnItemsList('A'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); }); it('jumps to the item starting with the keys tapped in rapid succession', () => { const items = ['Albert', 'Alfred', 'Alena', 'Ali']; const { keyDownOnItemsList, itemsListNode } = renderDropdown({ items, defaultOpen: true, }); keyDownOnItemsList('A'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); jest.advanceTimersByTime(500 /* charKeyPressedCleanupTime */ / 2); keyDownOnItemsList('L'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(0))); jest.advanceTimersByTime(500 /* charKeyPressedCleanupTime */ / 2); keyDownOnItemsList('E'); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(2))); act(() => { jest.runAllTimers(); }); }); it('does not open with highlightedIndex after selecting item in multiple mode', () => { const itemSelectedIndex = 2; const { clickOnItemAtIndex, clickOnTriggerButton, itemsListNode } = renderDropdown({ defaultOpen: true, multiple: true, }); clickOnItemAtIndex(itemSelectedIndex); clickOnTriggerButton(); expect(itemsListNode).not.toHaveAttribute('aria-activedescendant'); }); it('opens with highlightedIndex after selecting item in non-multiple mode', () => { const itemSelectedIndex = 2; const { clickOnItemAtIndex, clickOnTriggerButton, itemsListNode } = renderDropdown({ defaultOpen: true, }); clickOnItemAtIndex(itemSelectedIndex); clickOnTriggerButton(); expect(itemsListNode).toHaveAttribute('aria-activedescendant', expect.stringMatching(getItemIdRegexByIndex(2))); }); }); describe('value', () => { it('it takes the value of the controlled prop', () => { const value = items[2]; const { triggerButtonNode, clickOnItemAtIndex } = renderDropdown({ value, defaultOpen: true }); expect(triggerButtonNode).toHaveTextContent(value); clickOnItemAtIndex(0); expect(triggerButtonNode).toHaveTextContent(value); }); it('it takes the value of the default prop but can be changed', () => { const defaultValue = items[2]; const itemToBeClickedIndex = 1; const { triggerButtonNode, clickOnItemAtIndex } = renderDropdown({ defaultValue, defaultOpen: true, }); expect(triggerButtonNode).toHaveTextContent(defaultValue); clickOnItemAtIndex(itemToBeClickedIndex); expect(triggerButtonNode).toHaveTextContent(items[itemToBeClickedIndex]); }); it('has onChange called when item is added', () => { const itemToClickIndex = 2; const onChange = jest.fn(); const { clickOnItemAtIndex } = renderDropdown({ open: true, onChange }); clickOnItemAtIndex(itemToClickIndex); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith( null, expect.objectContaining({ value: items[itemToClickIndex], searchQuery: items[itemToClickIndex], }), ); }); it('has onChange called when item is added or removed on multiple', () => { // it will actually be the third, since one is already removed from the list due to defaultValue. const itemToClickIndex = 2; const defaultSelectedItemIndex = 1; const onChange = jest.fn(); const { clickOnItemAtIndex, keyDownOnSelectedItemAtIndex } = renderDropdown({ open: true, defaultValue: [items[defaultSelectedItemIndex]], onChange, multiple: true, }); clickOnItemAtIndex(itemToClickIndex - 1); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith( null, expect.objectContaining({ value: [items[defaultSelectedItemIndex], items[itemToClickIndex]], }), ); keyDownOnSelectedItemAtIndex(0, 'Delete'); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenLastCalledWith( null, expect.objectContaining({ value: [items[itemToClickIndex]], }), ); }); it('It shows no matches message when all iems are selected', () => { // it will actually be the third, since one is already removed from the list due to defaultValue. const noResultsMessage = 'no items'; const { clickOnItemAtIndex, clickOnToggleIndicator, itemsListNode } = renderDropdown({ items: ['item0', 'item1'], open: true, search: true, multiple: true, noResultsMessage, }); // Select all clickOnItemAtIndex(0); clickOnItemAtIndex(0); // open clickOnToggleIndicator(); expect(itemsListNode.textContent).toBe(noResultsMessage); }); it('has onChange called with null value by hitting Escape in search input', () => { const onChange = jest.fn(); const { keyDownOnSearchInput } = renderDropdown({ search: true, onChange, defaultValue: items[2], defaultSearchQuery: items[2], }); keyDownOnSearchInput('Escape'); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenLastCalledWith( null, expect.objectContaining({ value: null, searchQuery: '', }), ); }); it('onChange is called after onSearchQueryChange', () => { const onChange = jest.fn(); const onSearchQueryChange = jest.fn(); const { keyDownOnSearchInput } = renderDropdown({ defaultValue: items[2], defaultSearchQuery: items[2], onChange, onSearchQueryChange, search: true, }); keyDownOnSearchInput('Escape'); expect(onChange.mock.invocationCallOrder[0]).toBeGreaterThan(onSearchQueryChange.mock.invocationCallOrder[0]); }); it('is set by clicking on item', () => { const itemSelectedIndex = 2; const { triggerButtonNode, clickOnItemAtIndex } = renderDropdown({ defaultOpen: true }); clickOnItemAtIndex(itemSelectedIndex); expect(triggerButtonNode).toHaveTextContent(items[itemSelectedIndex]); }); it('is set by using Enter on highlighted item', () => { const itemSelectedIndex = 1; const { triggerButtonNode, keyDownOnItemsList } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: itemSelectedIndex, }); keyDownOnItemsList('Enter'); expect(triggerButtonNode).toHaveTextContent(items[itemSelectedIndex]); }); it('is set by using Tab on highlighted item', () => { const itemSelectedIndex = 3; const { triggerButtonNode, keyDownOnItemsList } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: itemSelectedIndex, }); keyDownOnItemsList('Tab'); expect(triggerButtonNode).toHaveTextContent(items[itemSelectedIndex]); }); it('is set by using Shift+Tab on highlighted item', () => { const itemSelectedIndex = 2; const { triggerButtonNode, keyDownOnItemsList } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: itemSelectedIndex, }); keyDownOnItemsList('Tab', { shiftKey: true }); expect(triggerButtonNode).toHaveTextContent(items[itemSelectedIndex]); }); it('is not set by clicking on disabled item', () => { const inputItems = [{ header: 'item1' }, { header: 'item2', disabled: true }]; const { triggerButtonNode, clickOnItemAtIndex, getItemNodes } = renderDropdown({ items: inputItems, defaultOpen: true, }); clickOnItemAtIndex(1); expect(triggerButtonNode).toHaveTextContent(''); expect(getItemNodes()).toHaveLength(2); }); // ToDo: investigate why 'Enter' still selects disabled item, manually it does not. it.skip('is not set by using Enter on highlighted disabled item', () => { const inputItems = [{ header: 'item1' }, { header: 'item2', disabled: true }]; const { triggerButtonNode, keyDownOnItemsList, getItemNodes } = renderDropdown({ items: inputItems, defaultOpen: true, defaultHighlightedIndex: 1, }); keyDownOnItemsList('Enter'); expect(triggerButtonNode).toHaveTextContent(''); expect(getItemNodes()).toHaveLength(2); }); it('is not set by using Spacebar on highlighted disabled item', () => { const inputItems = [{ header: 'item1' }, { header: 'item2', disabled: true }]; const { triggerButtonNode, keyDownOnItemsList, getItemNodes } = renderDropdown({ defaultOpen: true, items: inputItems, defaultHighlightedIndex: 1, }); keyDownOnItemsList(' '); expect(triggerButtonNode).toHaveTextContent(''); expect(getItemNodes()).toHaveLength(2); }); it('is not set by using Tab on highlighted selected item', () => { const inputItems = [{ header: 'item1' }, { header: 'item2', disabled: true }]; const { triggerButtonNode, keyDownOnItemsList, getItemNodes } = renderDropdown({ defaultOpen: true, items: inputItems, defaultHighlightedIndex: 1, }); keyDownOnItemsList('Tab'); expect(triggerButtonNode).toHaveTextContent(''); expect(getItemNodes()).toHaveLength(0); }); it('is not set by using Shift+Tab on highlighted disabled item', () => { const inputItems = [{ header: 'item1' }, { header: 'item2', disabled: true }]; const { triggerButtonNode, keyDownOnItemsList, getItemNodes } = renderDropdown({ defaultOpen: true, items: inputItems, defaultHighlightedIndex: 1, }); keyDownOnItemsList('Tab', { shiftKey: true }); expect(triggerButtonNode).toHaveTextContent(''); expect(getItemNodes()).toHaveLength(0); }); it('is set correctly in multiple selection by using Tab on highlighted item', () => { const itemSelectedIndex = 3; const { triggerButtonNode, keyDownOnItemsList, getSelectedItemNodeAtIndex, getSelectedItemNodes, } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: itemSelectedIndex, defaultValue: items[4], multiple: true, }); keyDownOnItemsList('Tab'); expect(triggerButtonNode).toHaveTextContent(''); expect(getSelectedItemNodes()).toHaveLength(2); expect(getSelectedItemNodeAtIndex(1)).toHaveTextContent(items[3]); expect(getSelectedItemNodeAtIndex(0)).toHaveTextContent(items[4]); }); it('is set correctly in multiple selection by using Shift+Tab on highlighted item', () => { const itemSelectedIndex = 2; const { triggerButtonNode, keyDownOnItemsList, getSelectedItemNodeAtIndex, getSelectedItemNodes, } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: itemSelectedIndex, defaultValue: items[4], multiple: true, }); keyDownOnItemsList('Tab', { shiftKey: true }); expect(triggerButtonNode).toHaveTextContent(''); expect(getSelectedItemNodes()).toHaveLength(2); expect(getSelectedItemNodeAtIndex(1)).toHaveTextContent(items[2]); expect(getSelectedItemNodeAtIndex(0)).toHaveTextContent(items[4]); }); it('is not cleared when hitting Escape if not search', () => { const defaultValue = items[0]; const { triggerButtonNode, keyDownOnTriggerButton } = renderDropdown({ defaultValue, }); keyDownOnTriggerButton('Escape'); expect(triggerButtonNode).toHaveTextContent(defaultValue); }); it('is not cleared when hitting Escape if search but multiple', () => { const defaultValue = [items[0], items[1]]; const { getSelectedItemNodes, keyDownOnSearchInput, searchInputNode } = renderDropdown({ defaultValue, search: true, multiple: true, defaultSearchQuery: 'test', }); keyDownOnSearchInput('Escape'); expect(searchInputNode).toHaveTextContent(''); expect(getSelectedItemNodes()).toHaveLength(2); }); it('is replaced when another item is selected', () => { const defaultValue = items[0]; const itemSelectedIndex = 2; const { triggerButtonNode, clickOnItemAtIndex } = renderDropdown({ defaultOpen: true, defaultValue, }); expect(triggerButtonNode).toHaveTextContent(defaultValue); clickOnItemAtIndex(itemSelectedIndex); expect(triggerButtonNode).toHaveTextContent(items[itemSelectedIndex]); }); it('has an array of items if more items are selected and the multiple prop is supplied', () => { const { getSelectedItemNodes, getSelectedItemNodeAtIndex } = renderDropdown({ multiple: true, defaultValue: [items[0], items[1]], }); expect(getSelectedItemNodes()).toHaveLength(2); expect(getSelectedItemNodeAtIndex(0)).toHaveTextContent(items[0]); expect(getSelectedItemNodeAtIndex(1)).toHaveTextContent(items[1]); }); it('removes last item on backspace when query is emtpy', () => { const { getSelectedItemNodes, getSelectedItemNodeAtIndex, keyDownOnSearchInput } = renderDropdown({ multiple: true, search: true, defaultValue: [items[0], items[1]], }); keyDownOnSearchInput('Backspace'); expect(getSelectedItemNodes()).toHaveLength(1); expect(getSelectedItemNodeAtIndex(0)).toHaveTextContent(items[0]); }); it('does not remove last item on backspace when query is not empty', () => { const { getSelectedItemNodes, keyDownOnSearchInput, searchInputNode } = renderDropdown({ multiple: true, search: true, defaultSearchQuery: 'bla', defaultValue: [items[0], items[1]], }); searchInputNode.setSelectionRange(1, 2); keyDownOnSearchInput('Backspace'); expect(getSelectedItemNodes()).toHaveLength(2); }); it('removes last item on backspace when selection range is 0, 0', () => { const { getSelectedItemNodes, getSelectedItemNodeAtIndex, keyDownOnSearchInput, searchInputNode, } = renderDropdown({ multiple: true, search: true, defaultSearchQuery: 'bla', defaultValue: [items[0], items[1]], }); searchInputNode.setSelectionRange(0, 0); keyDownOnSearchInput('Backspace'); expect(getSelectedItemNodes()).toHaveLength(1); expect(getSelectedItemNodeAtIndex(0)).toHaveTextContent(items[0]); }); it('does not remove last item on backspace when selection range is 0, (y>0)', () => { const { getSelectedItemNodes, keyDownOnSearchInput, searchInputNode } = renderDropdown({ multiple: true, search: true, defaultSearchQuery: 'bla', defaultValue: [items[0], items[1]], }); searchInputNode.setSelectionRange(0, 1); keyDownOnSearchInput('Backspace'); expect(getSelectedItemNodes()).toHaveLength(2); }); it('has the item removed if it receives delete key down', () => { const { getSelectedItemNodes, getSelectedItemNodeAtIndex, keyDownOnSelectedItemAtIndex } = renderDropdown({ multiple: true, search: true, defaultValue: [items[0], items[1]], }); keyDownOnSelectedItemAtIndex(0, 'Delete'); expect(getSelectedItemNodes()).toHaveLength(1); expect(getSelectedItemNodeAtIndex(0)).toHaveTextContent(items[1]); }); it('has the item removed if it receives click on remove icon', () => { const { getSelectedItemNodes, getSelectedItemNodeAtIndex, wrapper } = renderDropdown({ multiple: true, search: true, defaultValue: [items[0], items[1]], }); findIntrinsicElement(wrapper, `.${dropdownSelectedItemSlotClassNames.icon}`) .at(0) .simulate('click'); expect(getSelectedItemNodes()).toHaveLength(1); expect(getSelectedItemNodeAtIndex(0)).toHaveTextContent(items[1]); }); it('keeps selection when the same item is selected', () => { const selectedItemIndex = 0; const selectedItem = items[selectedItemIndex]; const { clickOnItemAtIndex, triggerButtonNode, clickOnTriggerButton, keyDownOnItemsList } = renderDropdown({ defaultValue: selectedItem, defaultOpen: true, }); clickOnItemAtIndex(selectedItemIndex); expect(triggerButtonNode).toHaveTextContent(selectedItem); clickOnTriggerButton(); keyDownOnItemsList('Enter'); expect(triggerButtonNode).toHaveTextContent(selectedItem); clickOnTriggerButton(); keyDownOnItemsList('Tab'); expect(triggerButtonNode).toHaveTextContent(selectedItem); }); }); describe('getA11ySelectionMessage', () => { afterEach(() => { jest.runAllTimers(); }); it('creates message container element', () => { const { getA11yMessageContainerNode } = renderDropdown({ getA11ySelectionMessage: {} }); expect(getA11yMessageContainerNode()).toMatchInlineSnapshot(`
`); }); it('has the onAdd message inserted and cleared after an item has been added to selection', () => { const itemToBeClickedIndex = 1; const { getA11yMessageContainerNode, clickOnItemAtIndex } = renderDropdown({ defaultOpen: true, getA11ySelectionMessage: { onAdd: item => `${item} has been added` }, }); clickOnItemAtIndex(itemToBeClickedIndex); expect(getA11yMessageContainerNode()).toHaveTextContent(`${items[itemToBeClickedIndex]} has been added`); act(() => { jest.runAllTimers(); }); expect(getA11yMessageContainerNode()).toHaveTextContent(''); }); it('has the onRemove message inserted and cleared after an item has been removed from selection', () => { const itemSelectedByDefaultIndex = 2; const { getA11yMessageContainerNode, keyDownOnSelectedItemAtIndex } = renderDropdown({ defaultOpen: true, multiple: true, defaultValue: [items[itemSelectedByDefaultIndex]], getA11ySelectionMessage: { onRemove: item => `${item} has been removed` }, }); keyDownOnSelectedItemAtIndex(0, 'Delete'); expect(getA11yMessageContainerNode()).toHaveTextContent(`${items[itemSelectedByDefaultIndex]} has been removed`); act(() => { jest.runAllTimers(); }); expect(getA11yMessageContainerNode()).toHaveTextContent(''); }); }); describe('searchQuery', () => { it('it takes the value of the controlled prop', () => { const searchQuery = "can't touch this"; const { changeSearchInput, searchInputNode } = renderDropdown({ searchQuery, search: true }); expect(searchInputNode).toHaveValue(searchQuery); changeSearchInput('but I can try!'); expect(searchInputNode).toHaveValue(searchQuery); }); it('it takes the value of the default prop but can be changed', () => { const defaultSearchQuery = "maybe you can't touch this"; const finalSearchQuery = 'you underestimate my power'; const { changeSearchInput, searchInputNode } = renderDropdown({ defaultSearchQuery, search: true, }); expect(searchInputNode).toHaveValue(defaultSearchQuery); changeSearchInput(finalSearchQuery); expect(searchInputNode).toHaveValue(finalSearchQuery); }); it("updates component's state on props updates", () => { const newSearchQueryProp = 'bar'; const { wrapper, searchInputNode } = renderDropdown({ searchQuery: 'foo', search: true, }); wrapper.setProps({ searchQuery: newSearchQueryProp }); expect(searchInputNode).toHaveValue(newSearchQueryProp); }); it('closes dropdown when changed to empty string', () => { const { getItemNodes, changeSearchInput } = renderDropdown({ defaultSearchQuery: 'foo', defaultOpen: true, search: true, }); changeSearchInput(''); expect(getItemNodes()).toHaveLength(0); }); it('has onSearchQueryChange called each time the input is changed', () => { const onSearchQueryChange = jest.fn(); const { changeSearchInput } = renderDropdown({ search: true, onSearchQueryChange }); changeSearchInput('ala'); expect(onSearchQueryChange).toHaveBeenCalledTimes(1); expect(onSearchQueryChange).toHaveBeenLastCalledWith( null, expect.objectContaining({ searchQuery: 'ala', }), ); changeSearchInput('alladin'); expect(onSearchQueryChange).toHaveBeenCalledTimes(2); expect(onSearchQueryChange).toHaveBeenLastCalledWith( null, expect.objectContaining({ searchQuery: 'alladin', }), ); }); it('has onSearchQueryChange called with empty string by hitting Escape in search input', () => { const onSearchQueryChange = jest.fn(); const { keyDownOnSearchInput } = renderDropdown({ search: true, onSearchQueryChange, defaultSearchQuery: 'foo' }); keyDownOnSearchInput('Escape'); expect(onSearchQueryChange).toHaveBeenCalledTimes(1); expect(onSearchQueryChange).toHaveBeenLastCalledWith( null, expect.objectContaining({ searchQuery: '', value: null, open: false, }), ); }); it('has onChange called with null when changed to empty string and there was item selected', () => { const onChange = jest.fn(); const { changeSearchInput, getClearIndicatorWrapper } = renderDropdown({ defaultValue: items[0], defaultOpen: true, search: true, clearable: true, onChange, defaultSearchQuery: items[0], }); changeSearchInput(''); expect(getClearIndicatorWrapper().length).toEqual(0); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith( null, expect.objectContaining({ value: null, searchQuery: '', open: false, }), ); }); it('is the string equivalent of selected item in single search', () => { const itemSelectedIndex = 2; const itemsAsObjects = items.map(item => ({ value: item, key: item })); const { searchInputNode, clickOnItemAtIndex } = renderDropdown({ search: true, defaultOpen: true, items: itemsAsObjects, itemToString: (itemObject: { value: string }) => itemObject.value, }); clickOnItemAtIndex(itemSelectedIndex); expect(searchInputNode).toHaveValue(itemsAsObjects[itemSelectedIndex].value); }); it('is set to empty by hitting Escape in search input', () => { const { keyDownOnSearchInput, searchInputNode } = renderDropdown({ defaultSearchQuery: 'foo', search: true, }); keyDownOnSearchInput('Escape'); expect(searchInputNode).toHaveValue(''); }); it('is set to empty when item is selected in multiple search', () => { const { clickOnItemAtIndex, searchInputNode, getSelectedItemNodes } = renderDropdown({ search: true, multiple: true, defaultOpen: true, }); clickOnItemAtIndex(2); expect(searchInputNode).toHaveValue(''); expect(getSelectedItemNodes()).toHaveLength(1); }); }); describe('activeSelectedIndex', () => { it('is set on active item click', () => { const { getSelectedItemNodeAtIndex, clickOnSelectedItemAtIndex } = renderDropdown({ multiple: true, value: [items[2]], }); clickOnSelectedItemAtIndex(0); expect(getSelectedItemNodeAtIndex(0)).toHaveFocus(); }); it('is set as last index on left arrow from the search query', () => { const { getSelectedItemNodeAtIndex, keyDownOnSearchInput } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], search: true, }); keyDownOnSearchInput('ArrowLeft'); expect(getSelectedItemNodeAtIndex(2)).toHaveFocus(); }); it('is set as last index on left arrow from the trigger button', () => { const { getSelectedItemNodeAtIndex, keyDownOnTriggerButton } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], }); keyDownOnTriggerButton('ArrowLeft'); expect(getSelectedItemNodeAtIndex(2)).toHaveFocus(); }); it('is updated on arrow navigation after being set by click', () => { const { getSelectedItemNodeAtIndex, keyDownOnSelectedItemAtIndex, clickOnSelectedItemAtIndex } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], }); clickOnSelectedItemAtIndex(2); keyDownOnSelectedItemAtIndex(2, 'ArrowLeft'); expect(getSelectedItemNodeAtIndex(1)).toHaveFocus(); }); it('stays as "0" on left arrow from the first selected item', () => { const { getSelectedItemNodeAtIndex, keyDownOnSelectedItemAtIndex, clickOnSelectedItemAtIndex } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], }); clickOnSelectedItemAtIndex(0); keyDownOnSelectedItemAtIndex(0, 'ArrowLeft'); expect(getSelectedItemNodeAtIndex(0)).toHaveFocus(); }); it('gets unset on right arrow from the last selected item and moves focus to trigger button', () => { const { triggerButtonNode, keyDownOnSelectedItemAtIndex, clickOnSelectedItemAtIndex } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], }); clickOnSelectedItemAtIndex(2); keyDownOnSelectedItemAtIndex(2, 'ArrowRight'); expect(triggerButtonNode).toHaveFocus(); }); it('gets unset on the removal of selected item and moves focus to trigger button', () => { const { triggerButtonNode, keyDownOnSelectedItemAtIndex, clickOnSelectedItemAtIndex } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], }); clickOnSelectedItemAtIndex(2); keyDownOnSelectedItemAtIndex(2, 'Delete'); expect(triggerButtonNode).toHaveFocus(); }); it('gets unset on right arrow from the last selected item and moves focus to search input', () => { const { searchInputNode, keyDownOnSelectedItemAtIndex, clickOnSelectedItemAtIndex } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], search: true, }); clickOnSelectedItemAtIndex(2); keyDownOnSelectedItemAtIndex(2, 'ArrowRight'); expect(searchInputNode).toHaveFocus(); }); it('gets unset on the removal of selected item and moves focus to search input', () => { const { searchInputNode, keyDownOnSelectedItemAtIndex, clickOnSelectedItemAtIndex } = renderDropdown({ multiple: true, value: [items[0], items[1], items[2]], search: true, }); clickOnSelectedItemAtIndex(2); keyDownOnSelectedItemAtIndex(2, 'Delete'); expect(searchInputNode).toHaveFocus(); }); }); describe('focused', () => { it('is "true" when focus is on trigger button', () => { const { rootNode, focusTriggerButton } = renderDropdown(); focusTriggerButton(); expect(rootNode).toHaveAttribute('data-test-focused', 'true'); }); it('is "true" when focus is on search input', () => { const { rootNode, focusSearchInput } = renderDropdown({ search: true }); focusSearchInput(); expect(rootNode).toHaveAttribute('data-test-focused', 'true'); }); it('is "true" when focus is on the list', () => { const { rootNode, focusItemsList } = renderDropdown({ open: true }); focusItemsList(); expect(rootNode).toHaveAttribute('data-test-focused', 'true'); }); }); describe('toggleIndicatorNode', () => { it('moves focus to list at click', () => { const { clickOnToggleIndicator, itemsListNode } = renderDropdown(); clickOnToggleIndicator(); expect(itemsListNode).toHaveFocus(); }); it('moves focus to input in search mode', () => { const { clickOnToggleIndicator, searchInputNode } = renderDropdown({ search: true }); clickOnToggleIndicator(); expect(searchInputNode).toHaveFocus(); }); }); describe('moveFocusOnTab', () => { // only way to check Tab is prevented in unit tests. const preventDefault = jest.fn(); afterEach(() => { preventDefault.mockReset(); }); it('keeps focus on trigger button when not passed', () => { const { keyDownOnItemsList } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: 0, multiple: true, }); keyDownOnItemsList('Tab', { preventDefault }); expect(preventDefault).toBeCalled(); }); it('keeps focus on input when not passed', () => { const { keyDownOnSearchInput } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: 0, multiple: true, search: true, }); keyDownOnSearchInput('Tab', { preventDefault }); expect(preventDefault).toBeCalled(); }); it('allows focus to move to next item from search input when passed', () => { const { keyDownOnSearchInput } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: 0, multiple: true, search: true, moveFocusOnTab: true, }); keyDownOnSearchInput('Tab', { preventDefault }); expect(preventDefault).not.toBeCalled(); }); it('allows focus to move to next item from items list when passed', () => { const { keyDownOnItemsList } = renderDropdown({ defaultOpen: true, defaultHighlightedIndex: 0, multiple: true, moveFocusOnTab: true, }); keyDownOnItemsList('Tab', { preventDefault }); expect(preventDefault).not.toBeCalled(); }); }); describe('multiple', () => { it('can be switched to "multiple"', () => { const { wrapper, getSelectedItemNodes } = renderDropdown({ value: items[0] }); expect(getSelectedItemNodes()).toHaveLength(0); wrapper.setProps({ multiple: true }); expect(getSelectedItemNodes()).toHaveLength(1); }); it('does not contain duplicates after an item is selected', () => { const { getSelectedItemNodes, getItemNodes, clickOnItemAtIndex } = renderDropdown({ multiple: true, open: true, }); clickOnItemAtIndex(0); expect(getSelectedItemNodes()).toHaveLength(1); expect(getItemNodes()).toHaveLength(items.length - 1); clickOnItemAtIndex(0); expect(getSelectedItemNodes()).toHaveLength(2); expect(getItemNodes()).toHaveLength(items.length - 2); }); it('does not contain duplicates when value is set', () => { const { getSelectedItemNodes, getItemNodes } = renderDropdown({ multiple: true, open: true, value: items[0], }); expect(getSelectedItemNodes()).toHaveLength(1); expect(getItemNodes()).toHaveLength(items.length - 1); }); it('contains duplicates by default', () => { const items = [{ key: '1', header: 'James Smith' }]; const value = [{ key: '1', header: 'John Locke' }]; const { getSelectedItemNodes, getItemNodes } = renderDropdown({ multiple: true, open: true, value, items, }); expect(getSelectedItemNodes()).toHaveLength(1); expect(getItemNodes()).toHaveLength(items.length); }); it('does not contain duplicates when proper itemToValue prop is used', () => { const items = [{ id: '1', header: 'James Smith' }]; const value = [{ id: '1', header: 'John Locke' }]; const itemToValue = (item: ShorthandValue): string => { if (!item || !React.isValidElement(item)) { return ''; } return (item as any).id; }; const { getSelectedItemNodes, getItemNodes } = renderDropdown({ multiple: true, open: true, value, items, itemToValue, }); expect(getSelectedItemNodes()).toHaveLength(1); expect(getItemNodes()).toHaveLength(items.length - 1); }); }); describe('items', () => { it('have onClick called when passed stop event from being propagated', () => { const onClick = jest.fn(); const stopPropagation = jest.fn(); const mockedEvent = { stopPropagation }; const items = [{ header: 'Venom', onClick }]; const { clickOnItemAtIndex } = renderDropdown({ items, defaultOpen: true }); clickOnItemAtIndex(0, mockedEvent); expect(onClick).toBeCalledTimes(1); expect(onClick).toHaveBeenCalledWith( expect.objectContaining(mockedEvent), expect.objectContaining({ header: 'Venom', }), ); expect(stopPropagation).toBeCalledTimes(1); }); it('when selected have onClick called when passed stop event from being propagated', () => { const onClick = jest.fn(); const stopPropagation = jest.fn(); const mockedEvent = { stopPropagation }; const items = [{ header: 'Venom', onClick }]; const { clickOnSelectedItemAtIndex } = renderDropdown({ items, multiple: true, value: items, defaultOpen: true, }); clickOnSelectedItemAtIndex(0, mockedEvent); expect(onClick).toBeCalledTimes(1); expect(onClick).toHaveBeenCalledWith( expect.objectContaining(mockedEvent), expect.objectContaining({ header: 'Venom', }), ); expect(stopPropagation).toBeCalledTimes(1); }); }); describe('renderSelectedItem', () => { it('is called in multiple selection', () => { const renderSelectedItem = jest.fn(); const value = [items[0], items[1]]; renderDropdown({ renderSelectedItem, multiple: true, value }); expect(renderSelectedItem).toBeCalledTimes(value.length); }); }); describe('searchInput', () => { it("merges refs from user's input", () => { const inputRef = React.createRef(); const { keyDownOnSearchInput } = renderDropdown({ defaultSearchQuery: 'Foo', multiple: true, search: true, searchInput: { inputRef }, }); keyDownOnSearchInput('Backspace'); // This test asserts also on internals that a condition that uses internal `inputRef` will pass. expect(inputRef.current).toBeInstanceOf(HTMLInputElement); }); }); describe('disabled', () => { it('allows no action on the trigger button', () => { const { clickOnTriggerButton, focusTriggerButton, getItemNodes, triggerButtonNode, keyDownOnTriggerButton, } = renderDropdown({ disabled: true, }); expect(triggerButtonNode).toHaveAttribute('disabled'); clickOnTriggerButton(); expect(getItemNodes()).toHaveLength(0); focusTriggerButton(); expect(triggerButtonNode).not.toHaveFocus(); keyDownOnTriggerButton('ArrowDown'); expect(getItemNodes()).toHaveLength(0); }); it('allows no action on the search input', () => { const { keyDownOnSearchInput, clickOnSearchInput, focusSearchInput, getItemNodes, searchInputNode, } = renderDropdown({ disabled: true, search: true, }); expect(searchInputNode).toHaveAttribute('disabled'); keyDownOnSearchInput('ArrowDown'); expect(getItemNodes()).toHaveLength(0); focusSearchInput(); expect(searchInputNode).not.toHaveFocus(); clickOnSearchInput(); expect(searchInputNode).not.toHaveFocus(); }); it('allows no action on the toggle indicator', () => { const { clickOnToggleIndicator, toggleIndicatorNode, getItemNodes } = renderDropdown({ disabled: true, }); clickOnToggleIndicator(); expect(getItemNodes()).toHaveLength(0); toggleIndicatorNode.focus(); expect(toggleIndicatorNode).not.toHaveFocus(); }); }); describe('footer and header messages', () => { it('shows loadingMessage when status is loading', () => { const loadingMessage = 'loading results'; const { getItemNodeAtIndex } = renderDropdown({ open: true, loadingMessage, loading: true, }); expect(getItemNodeAtIndex(items.length)).toHaveTextContent(loadingMessage); }); it('shows noResultsMessage when status is no results', () => { const noResultsMessage = 'oups we found nothing'; const { getItemNodeAtIndex } = renderDropdown({ open: true, noResultsMessage, items: [], }); expect(getItemNodeAtIndex(0)).toHaveTextContent(noResultsMessage); }); it('shows headerMessage when status is custom', () => { const headerMessage = 'just some status'; const { getItemNodeAtIndex } = renderDropdown({ open: true, headerMessage, }); expect(getItemNodeAtIndex(0)).toHaveTextContent(headerMessage); }); it('can juggle between messages depending on the status', () => { const headerMessage = 'just some status'; const noResultsMessage = 'oups we found nothing'; const loadingMessage = 'loading results'; const { getItemNodeAtIndex, getItemNodes, rerender } = renderDropdown({ open: true, noResultsMessage, loadingMessage, }); expect(getItemNodes()).toHaveLength(items.length); rerender({ headerMessage }); expect(getItemNodes()).toHaveLength(items.length + 1); expect(getItemNodeAtIndex(0)).toHaveTextContent(headerMessage); rerender({ loading: true }); expect(getItemNodes()).toHaveLength(items.length + 2); expect(getItemNodeAtIndex(0)).toHaveTextContent(headerMessage); expect(getItemNodeAtIndex(items.length + 1)).toHaveTextContent(loadingMessage); rerender({ items: [] }); expect(getItemNodes()).toHaveLength(2); expect(getItemNodeAtIndex(0)).toHaveTextContent(headerMessage); expect(getItemNodeAtIndex(1)).toHaveTextContent(loadingMessage); rerender({ loading: false }); expect(getItemNodes()).toHaveLength(2); expect(getItemNodeAtIndex(0)).toHaveTextContent(headerMessage); expect(getItemNodeAtIndex(1)).toHaveTextContent(noResultsMessage); rerender({ items: [items[0]] }); expect(getItemNodes()).toHaveLength(2); expect(getItemNodeAtIndex(0)).toHaveTextContent(headerMessage); expect(getItemNodeAtIndex(1)).toHaveTextContent(items[0]); rerender({ headerMessage: undefined }); expect(getItemNodes()).toHaveLength(1); expect(getItemNodeAtIndex(0)).toHaveTextContent(items[0]); }); }); });