diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index e1942d13fd..b6ca9df065 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -436,9 +436,10 @@ export class ElementHandle extends js.JSHandle { 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 extends js.JSHandle { return result; } - async _viewportPointToDocument(point: types.Point): Promise { - 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 | null, pointInFrame: types.Point }[] = []; + while (frame.parentFrame()) { + const frameElement = await frame.frameElement() as ElementHandle; + 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 }; } } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index f2a8af0440..013ce97239 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -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)) { diff --git a/tests/library/hit-target.spec.ts b/tests/library/hit-target.spec.ts index 07bdf475c4..f53493da9f 100644 --- a/tests/library/hit-target.spec.ts +++ b/tests/library/hit-target.spec.ts @@ -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(`
outer
"> +
PINK OVERLAY
+ `); + + 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(`
PINK OVERLAY
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); +});