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:
parent
ae6f48c4b8
commit
2f11807552
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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="window.top._clicked=2">padding</div><div onclick="window.top._clicked=3">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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue