fix: better hittarget testing for clicking (#2217)

While checking for hittarget, we first bubble from a target element
up to find the first element without `pointer-events: none` style.

This bubbling does not make much sense: we risk desperately clicking
"body" element, when we were actually asked to click some deeply-nested
"span".

Additionally, in many cases the original intent is to click a button. In this
case, we should use the enclosing "button" as a hit target directly.

Fixes #2175
This commit is contained in:
Andrey Lushnikov 2020-05-19 16:27:56 -07:00 committed by GitHub
parent b8410bd19e
commit 545c43d28d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 20 additions and 4 deletions

View File

@ -375,10 +375,9 @@ export default class InjectedScript {
checkHitTargetAt(node: Node, point: types.Point): types.InjectedScriptResult<boolean> {
let element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
while (element && window.getComputedStyle(element).pointerEvents === 'none')
element = element.parentElement;
if (!element || !element.isConnected)
return { status: 'notconnected' };
element = element.closest('button, [role=button]') || element;
let hitElement = this._deepElementFromPoint(document, point.x, point.y);
while (hitElement && hitElement !== element)
hitElement = this._parentElementOrShadowHost(hitElement);

View File

@ -503,9 +503,26 @@ describe('Page.click', function() {
expect(await page.evaluate(() => window.result)).toBe('Was not clicked');
});
it('should climb dom for pointer-events:none targets', async({page, server}) => {
await page.setContent('<button><label style="pointer-events:none">Click target</label></button>')
it('should climb dom for inner label with pointer-events:none', async({page, server}) => {
await page.setContent('<button onclick="javascript:window.__CLICKED=true;"><label style="pointer-events:none">Click target</label></button>');
await page.click('text=Click target');
expect(await page.evaluate(() => window.__CLICKED)).toBe(true);
});
it('should climb up to [role=button]', async({page, server}) => {
await page.setContent('<div role=button onclick="javascript:window.__CLICKED=true;"><div style="pointer-events:none"><span><div>Click target</div></span></div>');
await page.click('text=Click target');
expect(await page.evaluate(() => window.__CLICKED)).toBe(true);
});
it('should wait for BUTTON to be clickable when it has pointer-events:none', async({page, server}) => {
await page.setContent('<button onclick="javascript:window.__CLICKED=true;" style="pointer-events:none"><span>Click target</span></button>');
const clickPromise = page.click('text=Click target');
// Do a few roundtrips to the page.
for (let i = 0; i < 5; ++i)
expect(await page.evaluate(() => window.__CLICKED)).toBe(undefined);
// remove `pointer-events: none` css from button.
await page.evaluate(() => document.querySelector('button').style.removeProperty('pointer-events'));
await clickPromise;
expect(await page.evaluate(() => window.__CLICKED)).toBe(true);
});
it('should update modifiers correctly', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');