diff --git a/src/utility/selectOptions.ts b/src/utility/selectOptions.ts index 29d4f3ca..dd93780d 100644 --- a/src/utility/selectOptions.ts +++ b/src/utility/selectOptions.ts @@ -2,6 +2,7 @@ import {getConfig} from '@testing-library/dom' import {hasPointerEvents, isDisabled, isElementType, wait} from '../utils' import {type Instance} from '../setup' import {focusElement} from '../event' +import {getVisibleText} from '../utils/misc/getVisibleText' export async function selectOptions( this: Instance, @@ -43,7 +44,7 @@ async function selectOptionsBase( const matchingOption = allOptions.find( o => (o as HTMLInputElement | HTMLTextAreaElement).value === val || - o.innerHTML === val, + getVisibleText(o as HTMLElement) === val, ) if (matchingOption) { return matchingOption diff --git a/src/utils/index.ts b/src/utils/index.ts index 8ab15cc8..b4d751e6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -25,6 +25,7 @@ export * from './misc/findClosest' export * from './misc/getDocumentFromNode' export * from './misc/getTreeDiff' export * from './misc/getWindow' +export * from './misc/getVisibleText' export * from './misc/isDescendantOrSelf' export * from './misc/isElementType' export * from './misc/isVisible' diff --git a/src/utils/misc/getVisibleText.ts b/src/utils/misc/getVisibleText.ts new file mode 100644 index 00000000..89cdfde5 --- /dev/null +++ b/src/utils/misc/getVisibleText.ts @@ -0,0 +1,25 @@ +import {getWindow} from './getWindow' + +const removeNotVisibleChildren = (element: Element) => { + const window = getWindow(element) + for (const child of Array.from(element.children)) { + const {display, visibility} = window.getComputedStyle(child) + if ( + child.getAttribute('aria-hidden') || + display === 'none' || + visibility === 'hidden' + ) { + child.remove() + } else { + removeNotVisibleChildren(child) + } + } +} + +export const getVisibleText = (element: Element | null) => { + if (!element) return + + const clone = element.cloneNode(true) as Element + removeNotVisibleChildren(clone) + return clone.textContent?.trim().replace(/\s+/gm, ' ') +} diff --git a/tests/_helpers/listeners.ts b/tests/_helpers/listeners.ts index e66e7022..d4cd2b9e 100644 --- a/tests/_helpers/listeners.ts +++ b/tests/_helpers/listeners.ts @@ -1,5 +1,5 @@ import {eventMap} from '#src/event/eventMap' -import {isElementType} from '#src/utils' +import {isElementType, getVisibleText} from '#src/utils' import {MouseButton, MouseButtonFlip} from '#src/system/pointer/buttons' let eventListeners: Array<{ @@ -215,10 +215,12 @@ function getElementValue(element: Element) { 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, + getVisibleText( + element.querySelector('[aria-selected="true"]') as Element, + ), ) } else if (element.getAttribute('role') === 'option') { - return JSON.stringify(element.innerHTML) + return JSON.stringify(getVisibleText(element)) } else if ('value' in element) { return JSON.stringify((element as HTMLInputElement).value) } diff --git a/tests/utility/selectOptions/_setup.ts b/tests/utility/selectOptions/_setup.ts index 5c72a9d2..432fbc1a 100644 --- a/tests/utility/selectOptions/_setup.ts +++ b/tests/utility/selectOptions/_setup.ts @@ -78,3 +78,56 @@ export function setupListbox() { user: userEvent.setup(), } } + +export function setupListboxWithComplexOptions() { + const wrapper = document.createElement('div') + wrapper.innerHTML = ` + + + ` + document.body.append(wrapper) + const listbox = wrapper.querySelector('[role="listbox"]') as HTMLUListElement + 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 => { + const target = e.currentTarget as HTMLElement + target.setAttribute( + 'aria-selected', + JSON.stringify( + !JSON.parse(String(target.getAttribute('aria-selected'))), + ), + ) + }), + ) + + return { + ...addListeners(listbox), + listbox, + options, + user: userEvent.setup(), + } +} diff --git a/tests/utility/selectOptions/select.ts b/tests/utility/selectOptions/select.ts index 51b13cb2..a8b39120 100644 --- a/tests/utility/selectOptions/select.ts +++ b/tests/utility/selectOptions/select.ts @@ -1,4 +1,8 @@ -import {setupListbox, setupSelect} from './_setup' +import { + setupListbox, + setupListboxWithComplexOptions, + setupSelect, +} from './_setup' import {PointerEventsCheckLevel} from '#src' import {addListeners, setup} from '#testHelpers' @@ -65,6 +69,35 @@ test('fires correct events on listBox select', async () => { expect(o3).toHaveAttribute('aria-selected', 'false') }) +test('fires correct events on listBox select with complex options', async () => { + const {listbox, options, getEventSnapshot, user} = + setupListboxWithComplexOptions() + await user.selectOptions(listbox, '2 is the best option') + expect(getEventSnapshot()).toMatchInlineSnapshot(` + Events fired on: ul[value="2 is the best option"] + + li#2[value="2 is the best option"][aria-selected=false] - pointerover + ul - pointerenter + li#2[value="2 is the best option"][aria-selected=false] - mouseover + ul - mouseenter + li#2[value="2 is the best option"][aria-selected=false] - pointermove + li#2[value="2 is the best option"][aria-selected=false] - mousemove + li#2[value="2 is the best option"][aria-selected=false] - pointerdown + li#2[value="2 is the best option"][aria-selected=false] - mousedown: primary + li#2[value="2 is the best option"][aria-selected=false] - pointerup + li#2[value="2 is the best option"][aria-selected=false] - mouseup: primary + li#2[value="2 is the best option"][aria-selected=true] - click: primary + li#2[value="2 is the best option"][aria-selected=true] - pointerout + ul[value="2 is the best option"] - pointerleave + li#2[value="2 is the best option"][aria-selected=true] - mouseout + ul[value="2 is the best option"] - mouseleave + `) + 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', async () => { const {select, options, getEventSnapshot, user} = setupSelect({ multiple: true,