Skip to content

Commit c76f5b0

Browse files
committed
fix(webview): page.getContentQuads should work for elements inside <iframe>
Walk up the ancestor `<iframe>` chain, adding each frame's content-box offset to the element's own quads. Each offset is computed in the parent realm from `getBoundingClientRect()` plus the border/padding reported by `describeIFrameStyle()`. This also works for cross-origin iframes. The offsets are read from the parent side, so no same-origin `contentDocument` access is needed; and `getContentQuads` now propagates `error:notconnected`. A cross-origin (out-of-process) frame's context is torn down and recreated on navigations and process swaps, so the element handle can go stale mid-measurement. Returning `error:notconnected` instead of `null` (which would read as merely "not visible") lets the caller re-resolve the element in the fresh context and retry instead of waiting out the timeout.
1 parent b5a45d9 commit c76f5b0

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

packages/playwright-core/src/server/webkit/webview/wvPage.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -824,7 +824,7 @@ export class WVPage implements PageDelegate {
824824

825825
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
826826
const quads = await this.getContentQuads(handle);
827-
if (!quads || !quads.length)
827+
if (quads === 'error:notconnected' || !quads || !quads.length)
828828
return null;
829829
let minX = Infinity;
830830
let maxX = -Infinity;
@@ -869,7 +869,7 @@ export class WVPage implements PageDelegate {
869869
return 1;
870870
}
871871

872-
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
872+
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null | 'error:notconnected'> {
873873
const result = await handle.evaluateInUtility(([, node]) => {
874874
let element: Element | null = node as Element;
875875
while (element && element.nodeType !== 1 /* Node.ELEMENT_NODE */)
@@ -886,9 +886,32 @@ export class WVPage implements PageDelegate {
886886
{ x: r.left, y: r.bottom },
887887
]);
888888
}, {});
889+
if (result === 'error:notconnected')
890+
return result;
889891
if (!result || typeof result === 'string')
890892
return null;
891-
return result as types.Quad[];
893+
let quads = result as types.Quad[];
894+
let frame: frames.Frame | null = handle._frame;
895+
while (frame?.parentFrame()) {
896+
const frameElement = await this.getFrameElement(frame).catch(() => null);
897+
if (!frameElement)
898+
return null;
899+
const offset = await frameElement.evaluateInUtility(([injected, iframe]) => {
900+
const element = iframe as Element;
901+
const style = injected.describeIFrameStyle(element);
902+
if (style === 'error:notconnected' || style === 'transformed')
903+
return style;
904+
const rect = element.getBoundingClientRect();
905+
return { x: rect.left + style.left, y: rect.top + style.top };
906+
}, {}).finally(() => frameElement.dispose());
907+
if (offset === 'error:notconnected')
908+
return offset;
909+
if (!offset || typeof offset === 'string')
910+
return null;
911+
quads = quads.map(quad => quad.map(point => ({ x: point.x + offset.x, y: point.y + offset.y })) as types.Quad);
912+
frame = frame.parentFrame();
913+
}
914+
return quads;
892915
}
893916

894917
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {

tests/page/elementhandle-bounding-box.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,36 @@ it('should return null for invisible elements', async ({ page, server }) => {
8484
expect(await element.boundingBox()).toBe(null);
8585
});
8686

87+
it('should get bounding box of element inside an iframe', async ({ page, server }) => {
88+
await page.goto(server.EMPTY_PAGE);
89+
await page.setContent(`
90+
<div style="height:120px"></div>
91+
<iframe style="border:0;margin-left:30px;width:300px;height:200px" src="${server.PREFIX}/input/button.html"></iframe>
92+
`);
93+
const frame = page.frames()[1];
94+
const button = await frame.waitForSelector('button');
95+
const iframeBox = (await (await page.$('iframe')).boundingBox())!;
96+
const inner = await button.evaluate(b => { const r = b.getBoundingClientRect(); return { x: r.left, y: r.top }; });
97+
const box = (await button.boundingBox())!;
98+
expect(Math.round(box.x)).toBe(Math.round(iframeBox.x + inner.x));
99+
expect(Math.round(box.y)).toBe(Math.round(iframeBox.y + inner.y));
100+
});
101+
102+
it('should get bounding box of element inside a cross-origin iframe', async ({ page, server }) => {
103+
await page.goto(server.EMPTY_PAGE);
104+
await page.setContent(`
105+
<div style="height:120px"></div>
106+
<iframe style="border:0;margin-left:30px;width:300px;height:200px" src="${server.CROSS_PROCESS_PREFIX}/input/button.html"></iframe>
107+
`);
108+
const frame = page.frames()[1];
109+
const button = await frame.waitForSelector('button');
110+
const iframeBox = (await (await page.$('iframe')).boundingBox())!;
111+
const inner = await button.evaluate(b => { const r = b.getBoundingClientRect(); return { x: r.left, y: r.top }; });
112+
const box = (await button.boundingBox())!;
113+
expect(Math.round(box.x)).toBe(Math.round(iframeBox.x + inner.x));
114+
expect(Math.round(box.y)).toBe(Math.round(iframeBox.y + inner.y));
115+
});
116+
87117
it('should force a layout', async ({ page, server }) => {
88118
await page.setViewportSize({ width: 500, height: 500 });
89119
await page.setContent('<div style="width: 100px; height: 100px">hello</div>');

0 commit comments

Comments
 (0)