diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
index 4d575ef0e9..4730c3e5dd 100644
--- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
+++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
@@ -438,13 +438,13 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
expectValue(value);
attrs[kSelectedAttribute] = value;
}
- if (nodeName === 'CANVAS') {
- const boundingRect = (element as HTMLCanvasElement).getBoundingClientRect();
+ if (nodeName === 'CANVAS' || nodeName === 'IFRAME' || nodeName === 'FRAME') {
+ const boundingRect = (element as HTMLElement).getBoundingClientRect();
const value = JSON.stringify({
- left: boundingRect.left / window.innerWidth,
- top: boundingRect.top / window.innerHeight,
- right: boundingRect.right / window.innerWidth,
- bottom: boundingRect.bottom / window.innerHeight
+ left: boundingRect.left,
+ top: boundingRect.top,
+ right: boundingRect.right,
+ bottom: boundingRect.bottom
});
expectValue(kBoundingRectAttribute);
expectValue(value);
diff --git a/packages/trace-viewer/src/sw/snapshotRenderer.ts b/packages/trace-viewer/src/sw/snapshotRenderer.ts
index 93363878ab..41c23ffa9c 100644
--- a/packages/trace-viewer/src/sw/snapshotRenderer.ts
+++ b/packages/trace-viewer/src/sw/snapshotRenderer.ts
@@ -152,7 +152,7 @@ export class SnapshotRenderer {
const html = prefix + [
// Hide the document in order to prevent flickering. We will unhide once script has processed shadow.
'',
- ``
+ ``
].join('') + result.join('');
return { value: html, size: html.length };
});
@@ -236,10 +236,39 @@ function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
return (snapshot as any)._nodes;
}
-function snapshotScript(...targetIds: (string | undefined)[]) {
- function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, ...targetIds: (string | undefined)[]) {
+type ViewportSize = { width: number, height: number };
+type BoundingRect = { left: number, top: number, right: number, bottom: number };
+type FrameBoundingRectsInfo = {
+ viewport: ViewportSize;
+ frames: WeakMap;
+};
+
+declare global {
+ interface Window {
+ __playwright_frame_bounding_rects__: FrameBoundingRectsInfo;
+ }
+}
+
+function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
+ function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
const isUnderTest = new URLSearchParams(location.search).has('isUnderTest');
+ // info to recursively compute canvas position relative to the top snapshot frame.
+ // Before rendering each iframe, its parent extracts the '__playwright_canvas_render_info__' attribute
+ // value and keeps in this variable. It can then remove the attribute and render the element,
+ // which will eventually trigger the same process inside the iframe recursively.
+ // When there's a canvas to render, we iterate over its ancestor frames to compute
+ // its position relative to the top snapshot frame.
+ const frameBoundingRectsInfo = {
+ viewport,
+ frames: new WeakMap(),
+ };
+ window['__playwright_frame_bounding_rects__'] = frameBoundingRectsInfo;
+
const kPointerWarningTitle = 'Recorded click position in absolute coordinates did not' +
' match the center of the clicked element. This is likely due to a difference between' +
' the test runner and the trace viewer operating systems.';
@@ -249,6 +278,10 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
const targetElements: Element[] = [];
const canvasElements: HTMLCanvasElement[] = [];
+ let topSnapshotWindow: Window = window;
+ while (topSnapshotWindow !== topSnapshotWindow.parent && !topSnapshotWindow.location.pathname.match(/\/page@[a-z0-9]+$/))
+ topSnapshotWindow = topSnapshotWindow.parent;
+
const visit = (root: Document | ShadowRoot) => {
// Collect all scrolled elements for later use.
for (const e of root.querySelectorAll(`[__playwright_scroll_top_]`))
@@ -288,6 +321,11 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
}
for (const iframe of root.querySelectorAll('iframe, frame')) {
+ const boundingRectJson = iframe.getAttribute('__playwright_bounding_rect__');
+ iframe.removeAttribute('__playwright_bounding_rect__');
+ const boundingRect = boundingRectJson ? JSON.parse(boundingRectJson) : undefined;
+ if (boundingRect)
+ frameBoundingRectsInfo.frames.set(iframe, { boundingRect, scrollLeft: 0, scrollTop: 0 });
const src = iframe.getAttribute('__playwright_src__');
if (!src) {
iframe.setAttribute('src', 'data:text/html,');
@@ -339,16 +377,20 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
for (const element of scrollTops) {
element.scrollTop = +element.getAttribute('__playwright_scroll_top_')!;
element.removeAttribute('__playwright_scroll_top_');
+ if (frameBoundingRectsInfo.frames.has(element))
+ frameBoundingRectsInfo.frames.get(element)!.scrollTop = element.scrollTop;
}
for (const element of scrollLefts) {
element.scrollLeft = +element.getAttribute('__playwright_scroll_left_')!;
element.removeAttribute('__playwright_scroll_left_');
+ if (frameBoundingRectsInfo.frames.has(element))
+ frameBoundingRectsInfo.frames.get(element)!.scrollLeft = element.scrollTop;
}
document.styleSheets[0].disabled = true;
const search = new URL(window.location.href).searchParams;
- const isTopFrame = window.location.pathname.match(/\/page@[a-z0-9]+$/);
+ const isTopFrame = window === topSnapshotWindow;
if (search.get('pointX') && search.get('pointY')) {
const pointX = +search.get('pointX')!;
@@ -419,16 +461,6 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
context.fillRect(0, 0, canvas.width, canvas.height);
}
-
- if (!isTopFrame) {
- for (const canvas of canvasElements) {
- const context = canvas.getContext('2d')!;
- drawCheckerboard(context, canvas);
- canvas.title = `Playwright displays canvas contents on a best-effort basis. It doesn't support canvas elements inside an iframe yet. If this impacts your workflow, please open an issue so we can prioritize.`;
- }
- return;
- }
-
const img = new Image();
img.onload = () => {
for (const canvas of canvasElements) {
@@ -446,6 +478,31 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
continue;
}
+ let currWindow: Window = window;
+ while (currWindow !== topSnapshotWindow) {
+ const iframe = currWindow.frameElement!;
+ currWindow = currWindow.parent;
+
+ const iframeInfo = currWindow['__playwright_frame_bounding_rects__']?.frames.get(iframe);
+ if (!iframeInfo?.boundingRect)
+ break;
+
+ const leftOffset = iframeInfo.boundingRect.left - iframeInfo.scrollLeft;
+ const topOffset = iframeInfo.boundingRect.top - iframeInfo.scrollTop;
+
+ boundingRect.left += leftOffset;
+ boundingRect.top += topOffset;
+ boundingRect.right += leftOffset;
+ boundingRect.bottom += topOffset;
+ }
+
+ const { width, height } = topSnapshotWindow['__playwright_frame_bounding_rects__'].viewport;
+
+ boundingRect.left = boundingRect.left / width;
+ boundingRect.top = boundingRect.top / height;
+ boundingRect.right = boundingRect.right / width;
+ boundingRect.bottom = boundingRect.bottom / height;
+
const partiallyUncaptured = boundingRect.right > 1 || boundingRect.bottom > 1;
const fullyUncaptured = boundingRect.left > 1 || boundingRect.top > 1;
if (fullyUncaptured) {
@@ -483,7 +540,7 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
window.addEventListener('DOMContentLoaded', onDOMContentLoaded);
}
- return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()}${targetIds.map(id => `, "${id}"`).join('')})`;
+ return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()}, ${JSON.stringify(viewport)}${targetIds.map(id => `, "${id}"`).join('')})`;
}
diff --git a/tests/library/snapshotter.spec.ts b/tests/library/snapshotter.spec.ts
index 7ada643dcc..3e9cfce46f 100644
--- a/tests/library/snapshotter.spec.ts
+++ b/tests/library/snapshotter.spec.ts
@@ -268,10 +268,12 @@ it.describe('snapshots', () => {
});
});
-function distillSnapshot(snapshot, distillTarget = true) {
+function distillSnapshot(snapshot, options: { distillTarget: boolean, distillBoundingRect: boolean } = { distillTarget: true, distillBoundingRect: true }) {
let { html } = snapshot.render();
- if (distillTarget)
+ if (options.distillTarget)
html = html.replace(/\s__playwright_target__="[^"]+"/g, '');
+ if (options.distillBoundingRect)
+ html = html.replace(/\s__playwright_bounding_rect__="[^"]+"/g, '');
return html
.replace(/