Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: report element with declaration in pointerEventsCheck #950

Merged
merged 5 commits into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 81 additions & 7 deletions src/utils/pointer/cssPointerEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/convenience/click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe.each([
const {element, user} = setup(`<div style="pointer-events: none"></div>`)

await expect(user[method](element)).rejects.toThrowError(
/has or inherits pointer-events/i,
/has `pointer-events: none`/i,
)
})

Expand Down
2 changes: 1 addition & 1 deletion tests/convenience/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
})

Expand Down
17 changes: 0 additions & 17 deletions tests/utils/misc/hasPointerEvents.ts

This file was deleted.

86 changes: 86 additions & 0 deletions tests/utils/pointer/cssPointerEvents.ts
Original file line number Diff line number Diff line change
@@ -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(`
<div style="pointer-events: none">
<input style="pointer-events: initial"/>
<input style="pointer-events: inherit"/>
<input/>
</div>
`)

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(`
<div id="foo" style="pointer-events: none">
<span id="listlabel">Some list</span>
<ul aria-labelledby="listlabel">
<li aria-label="List entry">
<span data-testid="target"></span>
<button>foo</button>
<label>
An input element with a really long label text
<input/>
</label>
</li>
</ul>
</div>
`)

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
`)
})