Skip to content

Commit

Permalink
feat(click): provide preview of the element intercepting pointer even…
Browse files Browse the repository at this point in the history
…ts (#3449)
  • Loading branch information
dgozman authored Aug 14, 2020
1 parent 85c93e9 commit 69e1e71
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 11 deletions.
10 changes: 5 additions & 5 deletions src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,18 +306,18 @@ 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;
}
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();
Expand Down Expand Up @@ -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();
Expand Down
29 changes: 25 additions & 4 deletions src/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
35 changes: 33 additions & 2 deletions test/click-timeout-3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand All @@ -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';
Expand All @@ -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');
});

0 comments on commit 69e1e71

Please sign in to comment.