From a566263ba28e58ff648bffbdb628606f75d5876f Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Mon, 17 Jan 2022 14:19:43 +0100 Subject: [PATCH] fix: apply OOPIF offsets to bounding box and box model calls (#7906) The doc for boundingBox says that it should return the boundingBox relative to the main frame, therefore, this fix would make the actual implementation correspond to the documentation. boxModel documentation does not have this note but I think it'd make sense to have it match the behaviour of the boundingBox API. --- src/common/JSHandle.ts | 83 ++++++++++++++++++++++++++++-------------- test/oopif.spec.ts | 17 ++++++++- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index 306a57df..6f081e61 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -88,6 +88,12 @@ export function createJSHandle( return new JSHandle(context, context._client, remoteObject); } +const applyOffsetsToQuad = ( + quad: Array<{ x: number; y: number }>, + offsetX: number, + offsetY: number +) => quad.map((part) => ({ x: part.x + offsetX, y: part.y + offsetY })); + /** * Represents an in-page JavaScript object. JSHandles can be created with the * {@link Page.evaluateHandle | page.evaluateHandle} method. @@ -469,27 +475,11 @@ export class ElementHandle< if (error) throw new Error(error); } - /** - * Returns the middle point within an element unless a specific offset is provided. - */ - async clickablePoint(offset?: Offset): Promise { - const [result, layoutMetrics] = await Promise.all([ - this._client - .send('DOM.getContentQuads', { - objectId: this._remoteObject.objectId, - }) - .catch(debugError), - this._page.client().send('Page.getLayoutMetrics'), - ]); - if (!result || !result.quads.length) - throw new Error('Node is either not clickable or not an HTMLElement'); - // Filter out quads that have too small area to click into. - // Fallback to `layoutViewport` in case of using Firefox. - const { clientWidth, clientHeight } = - layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport; + private async _getOOPIFOffsets( + frame: Frame + ): Promise<{ offsetX: number; offsetY: number }> { let offsetX = 0; let offsetY = 0; - let frame = this._frame; while (frame.parentFrame()) { const parent = frame.parentFrame(); if (!frame.isOOPFrame()) { @@ -511,11 +501,31 @@ export class ElementHandle< offsetY += topLeftCorner.y; frame = parent; } + return { offsetX, offsetY }; + } + + /** + * Returns the middle point within an element unless a specific offset is provided. + */ + async clickablePoint(offset?: Offset): Promise { + const [result, layoutMetrics] = await Promise.all([ + this._client + .send('DOM.getContentQuads', { + objectId: this._remoteObject.objectId, + }) + .catch(debugError), + this._page.client().send('Page.getLayoutMetrics'), + ]); + if (!result || !result.quads.length) + throw new Error('Node is either not clickable or not an HTMLElement'); + // Filter out quads that have too small area to click into. + // Fallback to `layoutViewport` in case of using Firefox. + const { clientWidth, clientHeight } = + layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport; + const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame); const quads = result.quads .map((quad) => this._fromProtocolQuad(quad)) - .map((quad) => - quad.map((part) => ({ x: part.x + offsetX, y: part.y + offsetY })) - ) + .map((quad) => applyOffsetsToQuad(quad, offsetX, offsetY)) .map((quad) => this._intersectQuadWithViewport(quad, clientWidth, clientHeight) ) @@ -860,13 +870,14 @@ export class ElementHandle< if (!result) return null; + const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame); const quad = result.model.border; const x = Math.min(quad[0], quad[2], quad[4], quad[6]); const y = Math.min(quad[1], quad[3], quad[5], quad[7]); const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; - return { x, y, width, height }; + return { x: x + offsetX, y: y + offsetY, width, height }; } /** @@ -882,12 +893,30 @@ export class ElementHandle< if (!result) return null; + const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame); + const { content, padding, border, margin, width, height } = result.model; return { - content: this._fromProtocolQuad(content), - padding: this._fromProtocolQuad(padding), - border: this._fromProtocolQuad(border), - margin: this._fromProtocolQuad(margin), + content: applyOffsetsToQuad( + this._fromProtocolQuad(content), + offsetX, + offsetY + ), + padding: applyOffsetsToQuad( + this._fromProtocolQuad(padding), + offsetX, + offsetY + ), + border: applyOffsetsToQuad( + this._fromProtocolQuad(border), + offsetX, + offsetY + ), + margin: applyOffsetsToQuad( + this._fromProtocolQuad(margin), + offsetX, + offsetY + ), width, height, }; diff --git a/test/oopif.spec.ts b/test/oopif.spec.ts index 2696ac6e..50727b61 100644 --- a/test/oopif.spec.ts +++ b/test/oopif.spec.ts @@ -282,7 +282,7 @@ describeChromeOnly('OOPIF', function () { expect(oopIframe.childFrames()).toHaveLength(0); }); - it('clickablePoint should work for elements inside OOPIFs', async () => { + it('clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs', async () => { const { server } = getTestState(); await page.goto(server.EMPTY_PAGE); const framePromise = page.waitForFrame((frame) => { @@ -311,6 +311,21 @@ describeChromeOnly('OOPIF', function () { const result = await button.clickablePoint(); expect(result.x).toBeGreaterThan(150); // padding + margin + border left expect(result.y).toBeGreaterThan(150); // padding + margin + border top + const resultBoxModel = await button.boxModel(); + for (const quad of [ + resultBoxModel.content, + resultBoxModel.border, + resultBoxModel.margin, + resultBoxModel.padding, + ]) { + for (const part of quad) { + expect(part.x).toBeGreaterThan(150); // padding + margin + border left + expect(part.y).toBeGreaterThan(150); // padding + margin + border top + } + } + const resultBoundingBox = await button.boundingBox(); + expect(resultBoundingBox.x).toBeGreaterThan(150); // padding + margin + border left + expect(resultBoundingBox.y).toBeGreaterThan(150); // padding + margin + border top }); it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => {