Skip to content

Commit

Permalink
feat: add flag to skip pointer-events check (#731)
Browse files Browse the repository at this point in the history
* feat: add flag to skip pointer-events check

Add a flag to skip the check for pointer-events set to none.

The check is costly to check and this flag makes it possible to skip it.

* skip redundant check

Co-authored-by: Philipp Fritsche <ph.fritsche@gmail.com>
  • Loading branch information
jesperorb and ph-fritsche committed Oct 10, 2021
1 parent a4943d5 commit 26b7d0b
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 22 deletions.
6 changes: 6 additions & 0 deletions src/__tests__/click.js
Expand Up @@ -478,3 +478,9 @@ test('throws when clicking element with pointer-events set to none', () => {
const {element} = setup(`<div style="pointer-events: none"></div>`)
expect(() => userEvent.click(element)).toThrowError(/unable to click/i)
})

test('does not throws when clicking element with pointer-events set to none and skipPointerEventsCheck is set', () => {
const {element, getEvents} = setup(`<div style="pointer-events: none"></div>`)
userEvent.click(element, undefined, {skipPointerEventsCheck: true})
expect(getEvents('click')).toHaveLength(1)
})
6 changes: 6 additions & 0 deletions src/__tests__/dblclick.js
Expand Up @@ -287,3 +287,9 @@ test('throws an error when dblClick element with pointer-events set to none', ()
/unable to double-click/i,
)
})

test('does not throws when clicking element with pointer-events set to none and skipPointerEventsCheck is set', () => {
const {element, getEvents} = setup(`<div style="pointer-events: none"></div>`)
userEvent.dblClick(element, undefined, {skipPointerEventsCheck: true})
expect(getEvents('click')).toHaveLength(2)
})
17 changes: 17 additions & 0 deletions src/__tests__/hover.js
Expand Up @@ -127,3 +127,20 @@ test('throws when hovering element with pointer-events set to none', () => {
const {element} = setup(`<div style="pointer-events: none"></div>`)
expect(() => userEvent.hover(element)).toThrowError(/unable to hover/i)
})

test('does not throws when hover element with pointer-events set to none and skipPointerEventsCheck is set', () => {
const {element, getEventSnapshot} = setup(
`<div style="pointer-events: none"></div>`,
)
userEvent.hover(element, undefined, {skipPointerEventsCheck: true})
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: div
div - pointerover
div - pointerenter
div - mouseover: Left (0)
div - mouseenter: Left (0)
div - pointermove
div - mousemove: Left (0)
`)
})
85 changes: 85 additions & 0 deletions src/__tests__/select-options.js
Expand Up @@ -259,3 +259,88 @@ test('fire no pointer events when multiple select has disabled pointer events',
expect(o2.selected).toBe(true)
expect(o3.selected).toBe(true)
})

test('fires correct events when pointer events set to none but skipPointerEvents is set', () => {
const {select, options, getEventSnapshot} = setupSelect({
pointerEvents: 'none',
})
userEvent.selectOptions(select, '2', undefined, {
skipPointerEventsCheck: true,
})
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: select[name="select"][value="2"]
select[name="select"][value="1"] - pointerover
select[name="select"][value="1"] - pointerenter
select[name="select"][value="1"] - mouseover: Left (0)
select[name="select"][value="1"] - mouseenter: Left (0)
select[name="select"][value="1"] - pointermove
select[name="select"][value="1"] - mousemove: Left (0)
select[name="select"][value="1"] - pointerdown
select[name="select"][value="1"] - mousedown: Left (0)
select[name="select"][value="1"] - focus
select[name="select"][value="1"] - focusin
select[name="select"][value="1"] - pointerup
select[name="select"][value="1"] - mouseup: Left (0)
select[name="select"][value="1"] - click: Left (0)
select[name="select"][value="2"] - input
select[name="select"][value="2"] - change
select[name="select"][value="2"] - pointerover
select[name="select"][value="2"] - pointerenter
select[name="select"][value="2"] - mouseover: Left (0)
select[name="select"][value="2"] - mouseenter: Left (0)
select[name="select"][value="2"] - pointerup
select[name="select"][value="2"] - mouseup: Left (0)
select[name="select"][value="2"] - click: Left (0)
`)
const [o1, o2, o3] = options
expect(o1.selected).toBe(false)
expect(o2.selected).toBe(true)
expect(o3.selected).toBe(false)
})

test('fires correct events on multi-selects when pointer events is set and skipPointerEventsCheck is set', () => {
const {select, options, getEventSnapshot} = setupSelect({
multiple: true,
pointerEvents: 'none',
})
userEvent.selectOptions(select, ['1', '3'], undefined, {
skipPointerEventsCheck: true,
})
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: select[name="select"][value=["1","3"]]
option[value="1"][selected=false] - pointerover
select[name="select"][value=[]] - pointerenter
option[value="1"][selected=false] - mouseover: Left (0)
select[name="select"][value=[]] - mouseenter: Left (0)
option[value="1"][selected=false] - pointermove
option[value="1"][selected=false] - mousemove: Left (0)
option[value="1"][selected=false] - pointerdown
option[value="1"][selected=false] - mousedown: Left (0)
select[name="select"][value=[]] - focus
select[name="select"][value=[]] - focusin
option[value="1"][selected=false] - pointerup
option[value="1"][selected=false] - mouseup: Left (0)
select[name="select"][value=["1"]] - input
select[name="select"][value=["1"]] - change
option[value="1"][selected=true] - click: Left (0)
option[value="3"][selected=false] - pointerover
select[name="select"][value=["1"]] - pointerenter
option[value="3"][selected=false] - mouseover: Left (0)
select[name="select"][value=["1"]] - mouseenter: Left (0)
option[value="3"][selected=false] - pointermove
option[value="3"][selected=false] - mousemove: Left (0)
option[value="3"][selected=false] - pointerdown
option[value="3"][selected=false] - mousedown: Left (0)
option[value="3"][selected=false] - pointerup
option[value="3"][selected=false] - mouseup: Left (0)
select[name="select"][value=["1","3"]] - input
select[name="select"][value=["1","3"]] - change
option[value="3"][selected=true] - click: Left (0)
`)
const [o1, o2, o3] = options
expect(o1.selected).toBe(true)
expect(o2.selected).toBe(false)
expect(o3.selected).toBe(true)
})
17 changes: 17 additions & 0 deletions src/__tests__/unhover.js
Expand Up @@ -43,3 +43,20 @@ test('throws when unhover element with pointer-events set to none', () => {
const {element} = setup(`<div style="pointer-events: none"></div>`)
expect(() => userEvent.unhover(element)).toThrowError(/unable to unhover/i)
})

test('does not throws when hover element with pointer-events set to none and skipPointerEventsCheck is set', () => {
const {element, getEventSnapshot} = setup(
`<div style="pointer-events: none"></div>`,
)
userEvent.unhover(element, undefined, {skipPointerEventsCheck: true})
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: div
div - pointermove
div - mousemove: Left (0)
div - pointerout
div - pointerleave
div - mouseout: Left (0)
div - mouseleave: Left (0)
`)
})
32 changes: 21 additions & 11 deletions src/click.ts
Expand Up @@ -6,6 +6,7 @@ import {
isDisabled,
isElementType,
hasPointerEvents,
PointerOptions,
} from './utils'
import {hover} from './hover'
import {blur} from './blur'
Expand All @@ -28,7 +29,7 @@ export declare interface clickOptions {
function clickLabel(
label: HTMLLabelElement,
init: MouseEventInit | undefined,
{clickCount}: clickOptions,
{clickCount}: clickOptions & PointerOptions,
) {
if (isLabelWithInternallyDisabledControl(label)) return

Expand All @@ -49,7 +50,7 @@ function clickLabel(
function clickBooleanElement(
element: HTMLInputElement,
init: MouseEventInit | undefined,
{clickCount}: clickOptions,
{clickCount}: clickOptions & PointerOptions,
) {
fireEvent.pointerDown(element, init)
if (!element.disabled) {
Expand All @@ -72,7 +73,7 @@ function clickBooleanElement(
function clickElement(
element: Element,
init: MouseEventInit | undefined,
{clickCount}: clickOptions,
{clickCount}: clickOptions & PointerOptions,
) {
const previousElement = getPreviouslyFocusedElement(element)
fireEvent.pointerDown(element, init)
Expand Down Expand Up @@ -116,14 +117,19 @@ function findClosest(element: Element, callback: (e: Element) => boolean) {
function click(
element: Element,
init?: MouseEventInit,
{skipHover = false, clickCount = 0}: clickOptions = {},
{
skipHover = false,
clickCount = 0,
skipPointerEventsCheck = false,
}: clickOptions & PointerOptions = {},
) {
if (!hasPointerEvents(element)) {
if (!skipPointerEventsCheck && !hasPointerEvents(element)) {
throw new Error(
'unable to click element as it has or inherits pointer-events set to "none".',
)
}
if (!skipHover) hover(element, init)
// We just checked for `pointerEvents`. We can always skip this one in `hover`.
if (!skipHover) hover(element, init, {skipPointerEventsCheck: true})

if (isElementType(element, 'label')) {
clickLabel(element, init, {clickCount})
Expand All @@ -146,15 +152,19 @@ function fireClick(element: Element, mouseEventOptions: MouseEventInit) {
}
}

function dblClick(element: Element, init?: MouseEventInit) {
if (!hasPointerEvents(element)) {
function dblClick(
element: Element,
init?: MouseEventInit,
{skipPointerEventsCheck = false}: clickOptions & PointerOptions = {},
) {
if (!skipPointerEventsCheck && !hasPointerEvents(element)) {
throw new Error(
'unable to double-click element as it has or inherits pointer-events set to "none".',
)
}
hover(element, init)
click(element, init, {skipHover: true, clickCount: 0})
click(element, init, {skipHover: true, clickCount: 1})
hover(element, init, {skipPointerEventsCheck})
click(element, init, {skipHover: true, clickCount: 0, skipPointerEventsCheck})
click(element, init, {skipHover: true, clickCount: 1, skipPointerEventsCheck})
fireEvent.dblClick(element, getMouseEventOptions('dblclick', init, 2))
}

Expand Down
17 changes: 13 additions & 4 deletions src/hover.ts
Expand Up @@ -4,6 +4,7 @@ import {
getMouseEventOptions,
isDisabled,
hasPointerEvents,
PointerOptions,
} from './utils'

// includes `element`
Expand All @@ -16,8 +17,12 @@ function getParentElements(element: Element) {
return parentElements
}

function hover(element: Element, init?: MouseEventInit) {
if (!hasPointerEvents(element)) {
function hover(
element: Element,
init?: MouseEventInit,
{skipPointerEventsCheck = false}: PointerOptions = {},
) {
if (!skipPointerEventsCheck && !hasPointerEvents(element)) {
throw new Error(
'unable to hover element as it has or inherits pointer-events set to "none".',
)
Expand All @@ -42,8 +47,12 @@ function hover(element: Element, init?: MouseEventInit) {
}
}

function unhover(element: Element, init?: MouseEventInit) {
if (!hasPointerEvents(element)) {
function unhover(
element: Element,
init?: MouseEventInit,
{skipPointerEventsCheck = false}: PointerOptions = {},
) {
if (!skipPointerEventsCheck && !hasPointerEvents(element)) {
throw new Error(
'unable to unhover element as it has or inherits pointer-events set to "none".',
)
Expand Down
24 changes: 17 additions & 7 deletions src/select-options.ts
@@ -1,5 +1,10 @@
import {createEvent, getConfig, fireEvent} from '@testing-library/dom'
import {hasPointerEvents, isDisabled, isElementType} from './utils'
import {
hasPointerEvents,
isDisabled,
isElementType,
PointerOptions,
} from './utils'
import {click} from './click'
import {focus} from './focus'
import {hover, unhover} from './hover'
Expand All @@ -9,6 +14,7 @@ function selectOptionsBase(
select: Element,
values: HTMLElement | HTMLElement[] | string[] | string,
init?: MouseEventInit,
{skipPointerEventsCheck = false}: PointerOptions = {},
) {
if (!newValue && !(select as HTMLSelectElement).multiple) {
throw getConfig().getElementError(
Expand Down Expand Up @@ -47,7 +53,9 @@ function selectOptionsBase(
if (isElementType(select, 'select')) {
if (select.multiple) {
for (const option of selectedOptions) {
const withPointerEvents = hasPointerEvents(option)
const withPointerEvents = skipPointerEventsCheck
? true
: hasPointerEvents(option)

// events fired for multiple select are weird. Can't use hover...
if (withPointerEvents) {
Expand Down Expand Up @@ -75,10 +83,12 @@ function selectOptionsBase(
}
}
} else if (selectedOptions.length === 1) {
const withPointerEvents = hasPointerEvents(select)
const withPointerEvents = skipPointerEventsCheck
? true
: hasPointerEvents(select)
// the click to open the select options
if (withPointerEvents) {
click(select, init)
click(select, init, {skipPointerEventsCheck})
} else {
focus(select)
}
Expand All @@ -104,9 +114,9 @@ function selectOptionsBase(
}
} else if (select.getAttribute('role') === 'listbox') {
selectedOptions.forEach(option => {
hover(option, init)
click(option, init)
unhover(option, init)
hover(option, init, {skipPointerEventsCheck})
click(option, init, {skipPointerEventsCheck})
unhover(option, init, {skipPointerEventsCheck})
})
} else {
throw getConfig().getElementError(
Expand Down
15 changes: 15 additions & 0 deletions src/utils/misc/hasPointerEvents.ts
@@ -1,5 +1,20 @@
import {getWindowFromNode} from '@testing-library/dom/dist/helpers'

/**
* Options that can be passed to any event that relies
* on pointer-events property
*/
export declare interface PointerOptions {
/**
* When set to `true` the event skips checking if any element
* in the DOM-tree has `'pointer-events: none'` set. This check is
* costly in general and very costly when rendering large DOM-trees.
* Can be used to speed up tests.
* Default: `false`
* */
skipPointerEventsCheck?: boolean
}

export function hasPointerEvents(element: Element): boolean {
const window = getWindowFromNode(element)

Expand Down

0 comments on commit 26b7d0b

Please sign in to comment.