From 9d622f016f3fcce9d3ae97c29ba0714fdfddda5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Takahashi?= Date: Wed, 21 Oct 2020 23:58:25 -0300 Subject: [PATCH] fix: support listbox options in selectOptions (#473) Co-authored-by: Kent C. Dodds --- src/__tests__/helpers/utils.js | 37 ++++++++++++++++++++- src/__tests__/select-options.js | 58 ++++++++++++++++++++++++++++++++- src/select-options.js | 35 +++++++++++++------- 3 files changed, 116 insertions(+), 14 deletions(-) diff --git a/src/__tests__/helpers/utils.js b/src/__tests__/helpers/utils.js index 5c773788..9d5e7c59 100644 --- a/src/__tests__/helpers/utils.js +++ b/src/__tests__/helpers/utils.js @@ -55,6 +55,32 @@ function setupSelect({ } } +function setupListbox() { + const wrapper = document.createElement('div') + wrapper.innerHTML = ` + + + ` + document.body.append(wrapper) + const listbox = wrapper.querySelector('[role="listbox"]') + const options = Array.from(wrapper.querySelectorAll('[role="option"]')) + return { + ...addListeners(listbox), + listbox, + options, + } +} + const eventLabelGetters = { KeyboardEvent(event) { return [ @@ -91,6 +117,12 @@ function addEventListener(el, type, listener, options) { function getElementValue(element) { if (element.tagName === 'SELECT' && element.multiple) { return JSON.stringify(Array.from(element.selectedOptions).map(o => o.value)) + } else if (element.getAttribute('role') === 'listbox') { + return JSON.stringify( + element.querySelector('[aria-selected="true"]')?.innerHTML, + ) + } else if (element.getAttribute('role') === 'option') { + return JSON.stringify(element.innerHTML) } else if ( element.type === 'checkbox' || element.type === 'radio' || @@ -114,6 +146,9 @@ function getElementDisplayName(element) { value ? `[value=${value}]` : null, hasChecked ? `[checked=${element.checked}]` : null, element.tagName === 'OPTION' ? `[selected=${element.selected}]` : null, + element.getAttribute('role') === 'option' + ? `[aria-selected=${element.getAttribute('aria-selected')}]` + : null, ] .filter(Boolean) .join('') @@ -278,4 +313,4 @@ afterEach(() => { document.body.innerHTML = '' }) -export {setup, setupSelect, addEventListener, addListeners} +export {setup, setupSelect, setupListbox, addEventListener, addListeners} diff --git a/src/__tests__/select-options.js b/src/__tests__/select-options.js index c28c27de..24259ba0 100644 --- a/src/__tests__/select-options.js +++ b/src/__tests__/select-options.js @@ -1,5 +1,5 @@ import userEvent from '../' -import {setupSelect, addListeners} from './helpers/utils' +import {setupSelect, addListeners, setupListbox} from './helpers/utils' test('fires correct events', () => { const {select, options, getEventSnapshot} = setupSelect() @@ -29,6 +29,53 @@ test('fires correct events', () => { expect(o3.selected).toBe(false) }) +test('fires correct events on listBox select', () => { + const {listbox, options, getEventSnapshot} = setupListbox() + userEvent.selectOptions(listbox, '2') + expect(getEventSnapshot()).toMatchInlineSnapshot(` + Events fired on: ul[value="2"] + + ul - pointerover + ul - pointerenter + ul - 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=true] - click: 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] - pointerout + ul[value="2"] - pointerleave + li#2[value="2"][aria-selected=true] - mouseout: Left (0) + ul[value="2"] - mouseleave: Left (0) + `) + const [o1, o2, o3] = options + expect(o1).toHaveAttribute('aria-selected', 'false') + expect(o2).toHaveAttribute('aria-selected', 'true') + expect(o3).toHaveAttribute('aria-selected', 'false') +}) + test('fires correct events on multi-selects', () => { const {select, options, getEventSnapshot} = setupSelect({multiple: true}) userEvent.selectOptions(select, ['1', '3']) @@ -79,6 +126,15 @@ test('sets the selected prop on the selected option using option html elements', expect(o3.selected).toBe(false) }) +test('sets the selected prop on the selected listbox option using option html elements', () => { + const {listbox, options} = setupListbox() + const [o1, o2, o3] = options + userEvent.selectOptions(listbox, o1) + expect(o1).toHaveAttribute('aria-selected', 'true') + expect(o2).toHaveAttribute('aria-selected', 'false') + expect(o3).toHaveAttribute('aria-selected', 'false') +}) + test('a previously focused input gets blurred', () => { const button = document.createElement('button') document.body.append(button) diff --git a/src/select-options.js b/src/select-options.js index 5ef82330..967a34b3 100644 --- a/src/select-options.js +++ b/src/select-options.js @@ -1,6 +1,7 @@ import {createEvent, getConfig, fireEvent} from '@testing-library/dom' import {click} from './click' import {focus} from './focus' +import {hover, unhover} from './hover' function selectOptionsBase(newValue, select, values, init) { if (!newValue && !select.multiple) { @@ -18,7 +19,9 @@ function selectOptionsBase(newValue, select, values, init) { if (allOptions.includes(val)) { return val } else { - const matchingOption = allOptions.find(o => o.value === val) + const matchingOption = allOptions.find( + o => o.value === val || o.innerHTML === val, + ) if (matchingOption) { return matchingOption } else { @@ -61,17 +64,25 @@ function selectOptionsBase(newValue, select, values, init) { } function selectOption(option) { - option.selected = newValue - fireEvent( - select, - createEvent('input', select, { - bubbles: true, - cancelable: false, - composed: true, - ...init, - }), - ) - fireEvent.change(select, init) + 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) + } } }