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.
This commit is contained in:
Alex Rudenko 2022-01-17 14:19:43 +01:00 committed by GitHub
parent d7937b806d
commit a566263ba2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 72 additions and 28 deletions

View File

@ -88,6 +88,12 @@ export function createJSHandle(
return new JSHandle(context, context._client, remoteObject); 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 * Represents an in-page JavaScript object. JSHandles can be created with the
* {@link Page.evaluateHandle | page.evaluateHandle} method. * {@link Page.evaluateHandle | page.evaluateHandle} method.
@ -469,27 +475,11 @@ export class ElementHandle<
if (error) throw new Error(error); if (error) throw new Error(error);
} }
/** private async _getOOPIFOffsets(
* Returns the middle point within an element unless a specific offset is provided. frame: Frame
*/ ): Promise<{ offsetX: number; offsetY: number }> {
async clickablePoint(offset?: Offset): Promise<Point> {
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;
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
let frame = this._frame;
while (frame.parentFrame()) { while (frame.parentFrame()) {
const parent = frame.parentFrame(); const parent = frame.parentFrame();
if (!frame.isOOPFrame()) { if (!frame.isOOPFrame()) {
@ -511,11 +501,31 @@ export class ElementHandle<
offsetY += topLeftCorner.y; offsetY += topLeftCorner.y;
frame = parent; frame = parent;
} }
return { offsetX, offsetY };
}
/**
* Returns the middle point within an element unless a specific offset is provided.
*/
async clickablePoint(offset?: Offset): Promise<Point> {
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 const quads = result.quads
.map((quad) => this._fromProtocolQuad(quad)) .map((quad) => this._fromProtocolQuad(quad))
.map((quad) => .map((quad) => applyOffsetsToQuad(quad, offsetX, offsetY))
quad.map((part) => ({ x: part.x + offsetX, y: part.y + offsetY }))
)
.map((quad) => .map((quad) =>
this._intersectQuadWithViewport(quad, clientWidth, clientHeight) this._intersectQuadWithViewport(quad, clientWidth, clientHeight)
) )
@ -860,13 +870,14 @@ export class ElementHandle<
if (!result) return null; if (!result) return null;
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
const quad = result.model.border; const quad = result.model.border;
const x = Math.min(quad[0], quad[2], quad[4], quad[6]); 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 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 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; 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; if (!result) return null;
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
const { content, padding, border, margin, width, height } = result.model; const { content, padding, border, margin, width, height } = result.model;
return { return {
content: this._fromProtocolQuad(content), content: applyOffsetsToQuad(
padding: this._fromProtocolQuad(padding), this._fromProtocolQuad(content),
border: this._fromProtocolQuad(border), offsetX,
margin: this._fromProtocolQuad(margin), offsetY
),
padding: applyOffsetsToQuad(
this._fromProtocolQuad(padding),
offsetX,
offsetY
),
border: applyOffsetsToQuad(
this._fromProtocolQuad(border),
offsetX,
offsetY
),
margin: applyOffsetsToQuad(
this._fromProtocolQuad(margin),
offsetX,
offsetY
),
width, width,
height, height,
}; };

View File

@ -282,7 +282,7 @@ describeChromeOnly('OOPIF', function () {
expect(oopIframe.childFrames()).toHaveLength(0); 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(); const { server } = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) => { const framePromise = page.waitForFrame((frame) => {
@ -311,6 +311,21 @@ describeChromeOnly('OOPIF', function () {
const result = await button.clickablePoint(); const result = await button.clickablePoint();
expect(result.x).toBeGreaterThan(150); // padding + margin + border left expect(result.x).toBeGreaterThan(150); // padding + margin + border left
expect(result.y).toBeGreaterThan(150); // padding + margin + border top 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 () => { it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => {