From c776bc3e14377080066c73d2619b7497464e43aa Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Wed, 11 May 2022 08:57:02 +0000 Subject: [PATCH 1/4] refactor: move test file --- .../{misc/hasPointerEvents.ts => pointer/cssPointerEvents.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/utils/{misc/hasPointerEvents.ts => pointer/cssPointerEvents.ts} (100%) diff --git a/tests/utils/misc/hasPointerEvents.ts b/tests/utils/pointer/cssPointerEvents.ts similarity index 100% rename from tests/utils/misc/hasPointerEvents.ts rename to tests/utils/pointer/cssPointerEvents.ts From a13b152d5e8fbe066f16172c4a0ef71471bcc5f6 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Wed, 11 May 2022 10:03:36 +0000 Subject: [PATCH 2/4] feat: report element with declaration in `pointerEventsCheck` --- src/utils/pointer/cssPointerEvents.ts | 61 ++++++++++++++++++++++--- tests/convenience/click.ts | 2 +- tests/convenience/hover.ts | 2 +- tests/utils/pointer/cssPointerEvents.ts | 37 ++++++++++++++- 4 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/utils/pointer/cssPointerEvents.ts b/src/utils/pointer/cssPointerEvents.ts index ae1d8772..048a8c06 100644 --- a/src/utils/pointer/cssPointerEvents.ts +++ b/src/utils/pointer/cssPointerEvents.ts @@ -4,20 +4,30 @@ import {ApiLevel, getLevelRef} from '..' import {getWindow} from '../misc/getWindow' 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 +62,58 @@ 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')})`, + el.hasAttribute('aria-label') && + `(label=${el.getAttribute('aria-label')})`, + el.hasAttribute('aria-labelledby') && + `(label=${ + el.ownerDocument.getElementById( + el.getAttribute('aria-labelledby') ?? '', + )?.textContent + })`, + 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') +} + // 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/pointer/cssPointerEvents.ts b/tests/utils/pointer/cssPointerEvents.ts index 0ad0b233..2b8871f6 100644 --- a/tests/utils/pointer/cssPointerEvents.ts +++ b/tests/utils/pointer/cssPointerEvents.ts @@ -1,4 +1,5 @@ -import {hasPointerEvents} from '#src/utils' +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 () => { @@ -15,3 +16,37 @@ test('get pointer-events from element or ancestor', async () => { 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 + `) +}) From c13c2c2aa6df5c13642cb65c30e873df4c1e42fc Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Wed, 11 May 2022 10:16:25 +0000 Subject: [PATCH 3/4] remove unnecessary null coalescing --- src/utils/pointer/cssPointerEvents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/pointer/cssPointerEvents.ts b/src/utils/pointer/cssPointerEvents.ts index 048a8c06..a677f9bf 100644 --- a/src/utils/pointer/cssPointerEvents.ts +++ b/src/utils/pointer/cssPointerEvents.ts @@ -98,7 +98,7 @@ function printTree(tree: Element[]) { el.hasAttribute('aria-labelledby') && `(label=${ el.ownerDocument.getElementById( - el.getAttribute('aria-labelledby') ?? '', + el.getAttribute('aria-labelledby') as string, )?.textContent })`, tree.length > 1 && From e8e6c6740667b7fe20cc983f1db4a54f8a4c7366 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Wed, 11 May 2022 10:46:45 +0000 Subject: [PATCH 4/4] labels and buttons --- src/utils/pointer/cssPointerEvents.ts | 43 ++++++++++++++++++++----- tests/utils/pointer/cssPointerEvents.ts | 34 +++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/utils/pointer/cssPointerEvents.ts b/src/utils/pointer/cssPointerEvents.ts index a677f9bf..88a31c80 100644 --- a/src/utils/pointer/cssPointerEvents.ts +++ b/src/utils/pointer/cssPointerEvents.ts @@ -2,6 +2,7 @@ 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' @@ -93,14 +94,7 @@ function printTree(tree: Element[]) { el.id && `#${el.id}`, el.hasAttribute('data-testid') && `(testId=${el.getAttribute('data-testid')})`, - el.hasAttribute('aria-label') && - `(label=${el.getAttribute('aria-label')})`, - el.hasAttribute('aria-labelledby') && - `(label=${ - el.ownerDocument.getElementById( - el.getAttribute('aria-labelledby') as string, - )?.textContent - })`, + getLabelDescr(el), tree.length > 1 && i === 0 && ' <-- This element declared `pointer-events: none`', @@ -114,6 +108,39 @@ function printTree(tree: Element[]) { .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/utils/pointer/cssPointerEvents.ts b/tests/utils/pointer/cssPointerEvents.ts index 2b8871f6..98e202f8 100644 --- a/tests/utils/pointer/cssPointerEvents.ts +++ b/tests/utils/pointer/cssPointerEvents.ts @@ -24,6 +24,11 @@ test('report element that declared pointer-events', async () => {
  • + +
@@ -49,4 +54,33 @@ test('report element that declared pointer-events', async () => { 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 + `) })