diff --git a/src/__tests__/helpers/utils.js b/src/__tests__/helpers/utils.js index 9d5e7c59..69946df9 100644 --- a/src/__tests__/helpers/utils.js +++ b/src/__tests__/helpers/utils.js @@ -74,6 +74,17 @@ function setupListbox() { document.body.append(wrapper) const listbox = wrapper.querySelector('[role="listbox"]') const options = Array.from(wrapper.querySelectorAll('[role="option"]')) + + // the user is responsible for handling aria-selected on listbox options + options.forEach(el => + el.addEventListener('click', e => + e.target.setAttribute( + 'aria-selected', + JSON.stringify(!JSON.parse(e.target.getAttribute('aria-selected'))), + ), + ), + ) + return { ...addListeners(listbox), listbox, diff --git a/src/__tests__/select-options.js b/src/__tests__/select-options.js index 24259ba0..9be6ef22 100644 --- a/src/__tests__/select-options.js +++ b/src/__tests__/select-options.js @@ -1,5 +1,5 @@ import userEvent from '../' -import {setupSelect, addListeners, setupListbox} from './helpers/utils' +import {setupSelect, addListeners, setupListbox, setup} from './helpers/utils' test('fires correct events', () => { const {select, options, getEventSnapshot} = setupSelect() @@ -22,6 +22,13 @@ test('fires correct events', () => { select[name="select"][value="1"] - click: Left (0) select[name="select"][value="2"] - input select[name="select"][value="2"] - change + select[name="select"][value="2"] - pointerover + select[name="select"][value="2"] - pointerenter + select[name="select"][value="2"] - mouseover: Left (0) + select[name="select"][value="2"] - mouseenter: Left (0) + select[name="select"][value="2"] - pointerup + select[name="select"][value="2"] - mouseup: Left (0) + select[name="select"][value="2"] - click: Left (0) `) const [o1, o2, o3] = options expect(o1.selected).toBe(false) @@ -35,33 +42,22 @@ test('fires correct events on listBox select', () => { expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: ul[value="2"] - ul - pointerover + li#2[value="2"][aria-selected=false] - pointerover ul - pointerenter - ul - mouseover: Left (0) + li#2[value="2"][aria-selected=false] - mouseover: Left (0) ul - mouseenter: Left (0) - ul - pointermove - ul - mousemove: Left (0) - ul - pointerdown - ul - mousedown: Left (0) - ul - pointerup - ul - mouseup: Left (0) - ul - click: Left (0) - li#2[value="2"][aria-selected=true] - pointerover - ul[value="2"] - pointerenter - li#2[value="2"][aria-selected=true] - mouseover: Left (0) - ul[value="2"] - mouseenter: Left (0) - li#2[value="2"][aria-selected=true] - pointermove - li#2[value="2"][aria-selected=true] - mousemove: Left (0) - li#2[value="2"][aria-selected=true] - pointerover - ul[value="2"] - pointerenter - li#2[value="2"][aria-selected=true] - mouseover: Left (0) - ul[value="2"] - mouseenter: Left (0) - li#2[value="2"][aria-selected=true] - pointermove - li#2[value="2"][aria-selected=true] - mousemove: Left (0) - li#2[value="2"][aria-selected=true] - pointerdown - li#2[value="2"][aria-selected=true] - mousedown: Left (0) - li#2[value="2"][aria-selected=true] - pointerup - li#2[value="2"][aria-selected=true] - mouseup: Left (0) + li#2[value="2"][aria-selected=false] - pointermove + li#2[value="2"][aria-selected=false] - mousemove: Left (0) + li#2[value="2"][aria-selected=false] - pointerover + ul - pointerenter + li#2[value="2"][aria-selected=false] - mouseover: Left (0) + ul - mouseenter: Left (0) + li#2[value="2"][aria-selected=false] - pointermove + li#2[value="2"][aria-selected=false] - mousemove: Left (0) + li#2[value="2"][aria-selected=false] - pointerdown + li#2[value="2"][aria-selected=false] - mousedown: Left (0) + li#2[value="2"][aria-selected=false] - pointerup + li#2[value="2"][aria-selected=false] - mouseup: Left (0) li#2[value="2"][aria-selected=true] - click: Left (0) li#2[value="2"][aria-selected=true] - pointermove li#2[value="2"][aria-selected=true] - mousemove: Left (0) @@ -150,6 +146,13 @@ test('a previously focused input gets blurred', () => { `) }) +test('throws an error if elements is neither select nor listbox', () => { + const {element} = setup(``) + expect(() => userEvent.selectOptions(element, ['foo'])).toThrowError( + /neither select nor listbox/i, + ) +}) + test('throws an error one selected option does not match', () => { const {select} = setupSelect({multiple: true}) expect(() => diff --git a/src/select-options.js b/src/select-options.js index 967a34b3..1ca6777d 100644 --- a/src/select-options.js +++ b/src/select-options.js @@ -36,53 +36,70 @@ function selectOptionsBase(newValue, select, values, init) { if (select.disabled || !selectedOptions.length) return - if (select.multiple) { - for (const option of selectedOptions) { - // events fired for multiple select are weird. Can't use hover... - fireEvent.pointerOver(option, init) + if (select instanceof HTMLSelectElement) { + if (select.multiple) { + for (const option of selectedOptions) { + // events fired for multiple select are weird. Can't use hover... + fireEvent.pointerOver(option, init) + fireEvent.pointerEnter(select, init) + fireEvent.mouseOver(option) + fireEvent.mouseEnter(select) + fireEvent.pointerMove(option, init) + fireEvent.mouseMove(option, init) + fireEvent.pointerDown(option, init) + fireEvent.mouseDown(option, init) + focus(select, init) + fireEvent.pointerUp(option, init) + fireEvent.mouseUp(option, init) + selectOption(option) + fireEvent.click(option, init) + } + } else if (selectedOptions.length === 1) { + // the click to open the select options + click(select, init) + + selectOption(selectedOptions[0]) + + // the browser triggers another click event on the select for the click on the option + // this second click has no 'down' phase + fireEvent.pointerOver(select, init) fireEvent.pointerEnter(select, init) - fireEvent.mouseOver(option) + fireEvent.mouseOver(select) fireEvent.mouseEnter(select) - fireEvent.pointerMove(option, init) - fireEvent.mouseMove(option, init) - fireEvent.pointerDown(option, init) - fireEvent.mouseDown(option, init) - focus(select, init) - fireEvent.pointerUp(option, init) - fireEvent.mouseUp(option, init) - selectOption(option) - fireEvent.click(option, init) + fireEvent.pointerUp(select, init) + fireEvent.mouseUp(select, init) + fireEvent.click(select, init) + } else { + throw getConfig().getElementError( + `Cannot select multiple options on a non-multiple select`, + select, + ) } - } else if (selectedOptions.length === 1) { - click(select, init) - selectOption(selectedOptions[0]) + } else if (select.getAttribute('role') === 'listbox') { + selectedOptions.forEach(option => { + hover(option, init) + click(option, init) + unhover(option, init) + }) } else { throw getConfig().getElementError( - `Cannot select multiple options on a non-multiple select`, + `Cannot select options on elements that are neither select nor listbox elements`, select, ) } function selectOption(option) { - if (option.getAttribute('role') === 'option') { - option?.setAttribute?.('aria-selected', newValue) - - hover(option, init) - click(option, init) - unhover(option, init) - } else { - option.selected = newValue - fireEvent( - select, - createEvent('input', select, { - bubbles: true, - cancelable: false, - composed: true, - ...init, - }), - ) - fireEvent.change(select, init) - } + option.selected = newValue + fireEvent( + select, + createEvent('input', select, { + bubbles: true, + cancelable: false, + composed: true, + ...init, + }), + ) + fireEvent.change(select, init) } }