feat(click): provide preview of the element intercepting pointer events (#3449)

This commit is contained in:
Dmitry Gozman 2020-08-14 14:48:36 -07:00 committed by GitHub
parent 85c93e91a7
commit 69e1e713ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 63 additions and 11 deletions

View File

@ -306,10 +306,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
progress.logger.info(' element is outside of the viewport');
continue;
}
if (result === 'error:nothittarget') {
if (typeof result === 'object' && 'hitTargetDescription' in result) {
if (options.force)
throw new Error('Element does not receive pointer events');
progress.logger.info(' element does not receive pointer events');
throw new Error(`Element does not receive pointer events, ${result.hitTargetDescription} intercepts them`);
progress.logger.info(` ${result.hitTargetDescription} intercepts pointer events`);
continue;
}
return result;
@ -317,7 +317,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return 'done';
}
async _performPointerAction(progress: Progress, actionName: string, waitForEnabled: boolean, action: (point: types.Point) => Promise<void>, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:nothittarget' | 'done'> {
async _performPointerAction(progress: Progress, actionName: string, waitForEnabled: boolean, action: (point: types.Point) => Promise<void>, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | { hitTargetDescription: string } | 'done'> {
const { force = false, position } = options;
if ((options as any).__testHookBeforeStable)
await (options as any).__testHookBeforeStable();
@ -685,7 +685,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return result;
}
async _checkHitTargetAt(point: types.Point): Promise<'error:notconnected' | 'error:nothittarget' | 'done'> {
async _checkHitTargetAt(point: types.Point): Promise<'error:notconnected' | { hitTargetDescription: string } | 'done'> {
const frame = await this.ownerFrame();
if (frame && frame.parentFrame()) {
const element = await frame.frameElement();

View File

@ -479,15 +479,36 @@ export default class InjectedScript {
});
}
checkHitTargetAt(node: Node, point: types.Point): 'error:notconnected' | 'error:nothittarget' | 'done' {
let element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
checkHitTargetAt(node: Node, point: types.Point): 'error:notconnected' | 'done' | { hitTargetDescription: string } {
let element: Element | null | undefined = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
if (!element || !element.isConnected)
return 'error:notconnected';
element = element.closest('button, [role=button]') || element;
let hitElement = this.deepElementFromPoint(document, point.x, point.y);
while (hitElement && hitElement !== element)
const hitParents: Element[] = [];
while (hitElement && hitElement !== element) {
hitParents.push(hitElement);
hitElement = this._parentElementOrShadowHost(hitElement);
return hitElement === element ? 'done' : 'error:nothittarget';
}
if (hitElement === element)
return 'done';
const hitTargetDescription = this.previewNode(hitParents[0]);
// Root is the topmost element in the hitTarget's chain that is not in the
// element's chain. For example, it might be a dialog element that overlays
// the target.
let rootHitTargetDescription: string | undefined;
while (element) {
const index = hitParents.indexOf(element);
if (index !== -1) {
if (index > 1)
rootHitTargetDescription = this.previewNode(hitParents[index - 1]);
break;
}
element = this._parentElementOrShadowHost(element);
}
if (rootHitTargetDescription)
return { hitTargetDescription: `${hitTargetDescription} from ${rootHitTargetDescription} subtree` };
return { hitTargetDescription };
}
dispatchEvent(node: Node, type: string, eventInit: Object) {

View File

@ -31,7 +31,7 @@ it.skip(WIRE)('should fail when element jumps during hit testing', async({page,
expect(clicked).toBe(false);
expect(await page.evaluate('window.clicked')).toBe(undefined);
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
expect(error.message).toContain('element does not receive pointer events');
expect(error.message).toContain('<body>…</body> intercepts pointer events');
expect(error.message).toContain('retrying click action');
});
@ -41,6 +41,7 @@ it('should timeout waiting for hit target', async({page, server}) => {
await page.evaluate(() => {
document.body.style.position = 'relative';
const blocker = document.createElement('div');
blocker.id = 'blocker';
blocker.style.position = 'absolute';
blocker.style.width = '400px';
blocker.style.height = '20px';
@ -50,6 +51,36 @@ it('should timeout waiting for hit target', async({page, server}) => {
});
const error = await button.click({ timeout: 5000 }).catch(e => e);
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
expect(error.message).toContain('element does not receive pointer events');
expect(error.message).toContain('<div id="blocker"></div> intercepts pointer events');
expect(error.message).toContain('retrying click action');
});
it('should report wrong hit target subtree', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(() => {
document.body.style.position = 'relative';
const blocker = document.createElement('div');
blocker.id = 'blocker';
blocker.style.position = 'absolute';
blocker.style.width = '400px';
blocker.style.height = '20px';
blocker.style.left = '0';
blocker.style.top = '0';
document.body.appendChild(blocker);
const inner = document.createElement('div');
inner.id = 'inner';
inner.style.position = 'absolute';
inner.style.left = '0';
inner.style.top = '0';
inner.style.right = '0';
inner.style.bottom = '0';
blocker.appendChild(inner);
});
const error = await button.click({ timeout: 5000 }).catch(e => e);
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
expect(error.message).toContain('<div id="inner"></div> from <div id="blocker">…</div> subtree intercepts pointer events');
expect(error.message).toContain('retrying click action');
});