fix(click): no element should intercept events over the target frame (#15043)

When target element is inside a non-main frame, there could be an
overlay in some of the parent frames that intercepts pointer events.
However, we never detected this case.
This commit is contained in:
Dmitry Gozman 2022-06-24 13:17:25 -07:00 committed by GitHub
parent ae6f48c4b8
commit 2f11807552
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 50 additions and 15 deletions

View File

@ -436,9 +436,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if ((options as any).__testHookBeforeHitTarget)
await (options as any).__testHookBeforeHitTarget();
const hitPoint = await this._viewportPointToDocument(point);
if (hitPoint === 'error:notconnected')
return hitPoint;
const frameCheckResult = await this._checkFrameIsHitTarget(point);
if (frameCheckResult === 'error:notconnected' || ('hitTargetDescription' in frameCheckResult))
return frameCheckResult;
const hitPoint = frameCheckResult.framePoint;
const actionType = actionName === 'move and up' ? 'drag' : ((actionName === 'hover' || actionName === 'tap') ? actionName : 'mouse');
const handle = await this.evaluateHandleInUtility(([injected, node, { actionType, hitPoint, trial }]) => injected.setupHitTargetInterceptor(node, actionType, hitPoint, trial), { actionType, hitPoint, trial: !!options.trial } as const);
if (handle === 'error:notconnected')
@ -855,19 +856,34 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return result;
}
async _viewportPointToDocument(point: types.Point): Promise<types.Point | 'error:notconnected'> {
if (!this._frame.parentFrame())
return point;
const frame = await this.ownerFrame();
if (frame && frame.parentFrame()) {
const element = await frame.frameElement();
const box = await element.boundingBox();
async _checkFrameIsHitTarget(point: types.Point): Promise<{ framePoint: types.Point } | 'error:notconnected' | { hitTargetDescription: string }> {
let frame = this._frame;
const data: { frame: frames.Frame, frameElement: ElementHandle<Element> | null, pointInFrame: types.Point }[] = [];
while (frame.parentFrame()) {
const frameElement = await frame.frameElement() as ElementHandle<Element>;
const box = await frameElement.boundingBox();
if (!box)
return 'error:notconnected';
// Translate from viewport coordinates to frame coordinates.
point = { x: point.x - box.x, y: point.y - box.y };
const pointInFrame = { x: point.x - box.x, y: point.y - box.y };
data.push({ frame, frameElement, pointInFrame });
frame = frame.parentFrame()!;
}
return point;
// Add main frame.
data.push({ frame, frameElement: null, pointInFrame: point });
for (let i = data.length - 1; i > 0; i--) {
const element = data[i - 1].frameElement!;
const point = data[i].pointInFrame;
// Hit target in the parent frame should hit the child frame element.
const hitTargetResult = await element.evaluateInUtility(([injected, element, hitPoint]) => {
const hitElement = injected.deepElementFromPoint(document, hitPoint.x, hitPoint.y);
return injected.expectHitTargetParent(hitElement, element);
}, point);
if (hitTargetResult !== 'done')
return hitTargetResult;
}
return { framePoint: data[0].pointInFrame };
}
}

View File

@ -715,7 +715,7 @@ export class InjectedScript {
input.dispatchEvent(new Event('change', { 'bubbles': true }));
}
private _expectHitTargetParent(hitElement: Element | undefined, targetElement: Element) {
expectHitTargetParent(hitElement: Element | undefined, targetElement: Element) {
targetElement = targetElement.closest('button, [role=button], a, [role=link]') || targetElement;
const hitParents: Element[] = [];
while (hitElement && hitElement !== targetElement) {
@ -782,7 +782,7 @@ export class InjectedScript {
// First do a preliminary check, to reduce the possibility of some iframe
// intercepting the action.
const preliminaryHitElement = this.deepElementFromPoint(document, hitPoint.x, hitPoint.y);
const preliminaryResult = this._expectHitTargetParent(preliminaryHitElement, element);
const preliminaryResult = this.expectHitTargetParent(preliminaryHitElement, element);
if (preliminaryResult !== 'done')
return preliminaryResult.hitTargetDescription;
@ -817,7 +817,7 @@ export class InjectedScript {
// subsequent events will be fine.
if (result === undefined && point) {
const hitElement = this.deepElementFromPoint(document, point.clientX, point.clientY);
result = this._expectHitTargetParent(hitElement, element);
result = this.expectHitTargetParent(hitElement, element);
}
if (blockAllEvents || (result !== 'done' && result !== undefined)) {

View File

@ -254,3 +254,22 @@ it('should not click iframe overlaying the target', async ({ page, server }) =>
expect(await page.evaluate('window._clicked')).toBe(undefined);
expect(error.message).toContain(`<iframe srcdoc="<body onclick='window.top._clicked=2' st…></iframe> from <div>…</div> subtree intercepts pointer events`);
});
it('should not click an element overlaying iframe with the target', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await page.setContent(`
<div onclick='window.top._clicked=1'>padding</div>
<iframe width=600 height=600 srcdoc="<iframe srcdoc='<div onclick=&quot;window.top._clicked=2&quot;>padding</div><div onclick=&quot;window.top._clicked=3&quot;>inner</div>'></iframe><div onclick='window.top._clicked=4'>outer</div>"></iframe>
<div onclick='window.top._clicked=5' style="position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: rgba(255, 0, 0, 0.1); padding: 200px;">PINK OVERLAY</div>
`);
const target = page.frameLocator('iframe').frameLocator('iframe').locator('text=inner');
const error = await target.click({ timeout: 500 }).catch(e => e);
expect(await page.evaluate('window._clicked')).toBe(undefined);
expect(error.message).toContain(`<div onclick="window.top._clicked=5">PINK OVERLAY</div> intercepts pointer events`);
await page.locator('text=overlay').evaluate(e => e.style.display = 'none');
await target.click();
expect(await page.evaluate('window._clicked')).toBe(3);
});