diff --git a/src/utils/pointer/cssPointerEvents.ts b/src/utils/pointer/cssPointerEvents.ts index ae1d8772..88a31c80 100644 --- a/src/utils/pointer/cssPointerEvents.ts +++ b/src/utils/pointer/cssPointerEvents.ts @@ -2,22 +2,33 @@ import {PointerEventsCheckLevel} from '../../options' import {Config} from '../../setup' import {ApiLevel, getLevelRef} from '..' import {getWindow} from '../misc/getWindow' +import {isElementType} from '../misc/isElementType' export function hasPointerEvents(element: Element): boolean { + return closestPointerEventsDeclaration(element)?.pointerEvents !== 'none' +} + +function closestPointerEventsDeclaration(element: Element): + | { + pointerEvents: string + tree: Element[] + } + | undefined { const window = getWindow(element) for ( - let el: Element | null = element; + let el: Element | null = element, tree: Element[] = []; el?.ownerDocument; el = el.parentElement ) { + tree.push(el) const pointerEvents = window.getComputedStyle(el).pointerEvents if (pointerEvents && !['inherit', 'unset'].includes(pointerEvents)) { - return pointerEvents !== 'none' + return {pointerEvents, tree} } } - return true + return undefined } const PointerEventsCheck = Symbol('Last check for pointer-events') @@ -52,21 +63,84 @@ export function assertPointerEvents(config: Config, element: Element) { return } - const result = hasPointerEvents(element) + const declaration = closestPointerEventsDeclaration(element) element[PointerEventsCheck] = { [ApiLevel.Call]: getLevelRef(config, ApiLevel.Call), [ApiLevel.Trigger]: getLevelRef(config, ApiLevel.Trigger), - result, + result: declaration?.pointerEvents !== 'none', } - if (!result) { + if (declaration?.pointerEvents === 'none') { throw new Error( - 'Unable to perform pointer interaction as the element has or inherits pointer-events set to "none".', + [ + `Unable to perform pointer interaction as the element ${ + declaration.tree.length > 1 ? 'inherits' : 'has' + } \`pointer-events: none\`:`, + '', + printTree(declaration.tree), + ].join('\n'), ) } } +function printTree(tree: Element[]) { + return tree + .reverse() + .map((el, i) => + [ + ''.padEnd(i), + el.tagName, + el.id && `#${el.id}`, + el.hasAttribute('data-testid') && + `(testId=${el.getAttribute('data-testid')})`, + getLabelDescr(el), + tree.length > 1 && + i === 0 && + ' <-- This element declared `pointer-events: none`', + tree.length > 1 && + i === tree.length - 1 && + ' <-- Asserted pointer events here', + ] + .filter(Boolean) + .join(''), + ) + .join('\n') +} + +function getLabelDescr(element: Element) { + let label: string | undefined | null + if (element.hasAttribute('aria-label')) { + label = element.getAttribute('aria-label') as string + } else if (element.hasAttribute('aria-labelledby')) { + label = element.ownerDocument + .getElementById(element.getAttribute('aria-labelledby') as string) + ?.textContent?.trim() + } else if ( + isElementType(element, [ + 'button', + 'input', + 'meter', + 'output', + 'progress', + 'select', + 'textarea', + ]) && + element.labels?.length + ) { + label = Array.from(element.labels) + .map(el => el.textContent?.trim()) + .join('|') + } else if (isElementType(element, 'button')) { + label = element.textContent?.trim() + } + label = label?.replace(/\n/g, ' ') + if (Number(label?.length) > 30) { + label = `${label?.substring(0, 29)}…` + } + return label ? `(label=${label})` : '' +} + // With the eslint rule and prettier the bitwise operation isn't nice to read function hasBitFlag(conf: number, flag: number) { // eslint-disable-next-line no-bitwise diff --git a/tests/convenience/click.ts b/tests/convenience/click.ts index fc5c2ec4..fd1ae5ee 100644 --- a/tests/convenience/click.ts +++ b/tests/convenience/click.ts @@ -21,7 +21,7 @@ describe.each([ const {element, user} = setup(`
`) await expect(user[method](element)).rejects.toThrowError( - /has or inherits pointer-events/i, + /has `pointer-events: none`/i, ) }) diff --git a/tests/convenience/hover.ts b/tests/convenience/hover.ts index c4c7160f..7c884d44 100644 --- a/tests/convenience/hover.ts +++ b/tests/convenience/hover.ts @@ -33,7 +33,7 @@ describe.each([ clearEventCalls() await expect(user[method](element)).rejects.toThrowError( - /has or inherits pointer-events/i, + /has `pointer-events: none`/i, ) }) diff --git a/tests/utils/misc/hasPointerEvents.ts b/tests/utils/misc/hasPointerEvents.ts deleted file mode 100644 index 0ad0b233..00000000 --- a/tests/utils/misc/hasPointerEvents.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {hasPointerEvents} from '#src/utils' -import {setup} from '#testHelpers' - -test('get pointer-events from element or ancestor', async () => { - const {element} = setup(` -
- - - -
- `) - - expect(hasPointerEvents(element)).toBe(false) - expect(hasPointerEvents(element.children[0])).toBe(true) - expect(hasPointerEvents(element.children[1])).toBe(false) - expect(hasPointerEvents(element.children[2])).toBe(false) -}) diff --git a/tests/utils/pointer/cssPointerEvents.ts b/tests/utils/pointer/cssPointerEvents.ts new file mode 100644 index 00000000..98e202f8 --- /dev/null +++ b/tests/utils/pointer/cssPointerEvents.ts @@ -0,0 +1,86 @@ +import {createConfig} from '#src/setup/setup' +import {assertPointerEvents, hasPointerEvents} from '#src/utils' +import {setup} from '#testHelpers' + +test('get pointer-events from element or ancestor', async () => { + const {element} = setup(` +
+ + + +
+ `) + + expect(hasPointerEvents(element)).toBe(false) + expect(hasPointerEvents(element.children[0])).toBe(true) + expect(hasPointerEvents(element.children[1])).toBe(false) + expect(hasPointerEvents(element.children[2])).toBe(false) +}) + +test('report element that declared pointer-events', async () => { + const {element} = setup(` +
+ Some list + +
+ `) + + expect(() => assertPointerEvents(createConfig(), element)) + .toThrowErrorMatchingInlineSnapshot(` + Unable to perform pointer interaction as the element has \`pointer-events: none\`: + + DIV#foo + `) + + expect(() => + assertPointerEvents( + createConfig(), + element.querySelector('[data-testid="target"]') as Element, + ), + ).toThrowErrorMatchingInlineSnapshot(` + Unable to perform pointer interaction as the element inherits \`pointer-events: none\`: + + DIV#foo <-- This element declared \`pointer-events: none\` + UL(label=Some list) + LI(label=List entry) + SPAN(testId=target) <-- Asserted pointer events here + `) + + expect(() => + assertPointerEvents( + createConfig(), + element.querySelector('button') as Element, + ), + ).toThrowErrorMatchingInlineSnapshot(` + Unable to perform pointer interaction as the element inherits \`pointer-events: none\`: + + DIV#foo <-- This element declared \`pointer-events: none\` + UL(label=Some list) + LI(label=List entry) + BUTTON(label=foo) <-- Asserted pointer events here + `) + + expect(() => + assertPointerEvents( + createConfig(), + element.querySelector('input') as Element, + ), + ).toThrowErrorMatchingInlineSnapshot(` + Unable to perform pointer interaction as the element inherits \`pointer-events: none\`: + + DIV#foo <-- This element declared \`pointer-events: none\` + UL(label=Some list) + LI(label=List entry) + LABEL + INPUT(label=An input element with a reall…) <-- Asserted pointer events here + `) +})