From 1cde2e49d059f63feafc0953218b70389a9cdbc2 Mon Sep 17 00:00:00 2001 From: DudaGod Date: Fri, 25 Sep 2026 09:31:00 +0300 Subject: [PATCH] fix: prevent clipped assertView screenshots in offset iframes --- .../screen-shooter/operations.ts | 14 +++++++---- .../screen-shooter/utils/clip-rect.ts | 8 ++++--- .../screen-shooter/utils/dom.ts | 4 ++++ .../screens/8486705/chrome/iframe-element.png | Bin 0 -> 23653 bytes test/e2e/static/offset-iframe-content.html | 20 ++++++++++++++++ test/e2e/static/offset-iframe.html | 22 ++++++++++++++++++ test/e2e/tests/assert-view.testplane.js | 8 +++++++ 7 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 test/e2e/screens/8486705/chrome/iframe-element.png create mode 100644 test/e2e/static/offset-iframe-content.html create mode 100644 test/e2e/static/offset-iframe.html diff --git a/src/browser/client-scripts/screen-shooter/operations.ts b/src/browser/client-scripts/screen-shooter/operations.ts index a25aa8b83..0d678e78b 100644 --- a/src/browser/client-scripts/screen-shooter/operations.ts +++ b/src/browser/client-scripts/screen-shooter/operations.ts @@ -20,6 +20,7 @@ import { findFixedPositionedParent, forEachRoot, getMainDocumentElem, + getMainWindow, getScreenshooterNamespaceData } from "./utils/dom"; import { @@ -49,7 +50,8 @@ export function computeScrollOffset(element: Element): Coord<"page", "css", "y"> } export function computeViewportSize(): Size<"css"> { - const visualViewport = window.visualViewport; + const mainWindow = getMainWindow(); + const visualViewport = mainWindow.visualViewport; // Visual viewport occasionally returns more correct values than innerWidth/Height, but may not be available in older browsers if (visualViewport && visualViewport.width > 0 && visualViewport.height > 0) { @@ -60,15 +62,17 @@ export function computeViewportSize(): Size<"css"> { } return { - width: window.innerWidth as Length<"css", "x">, - height: window.innerHeight as Length<"css", "y"> + width: mainWindow.innerWidth as Length<"css", "x">, + height: mainWindow.innerHeight as Length<"css", "y"> }; } export function computeViewportOffset(): Point<"page", "css"> { + const mainWindow = getMainWindow(); + return { - left: window.pageXOffset as Coord<"page", "css", "x">, - top: window.pageYOffset as Coord<"page", "css", "y"> + left: mainWindow.pageXOffset as Coord<"page", "css", "x">, + top: mainWindow.pageYOffset as Coord<"page", "css", "y"> }; } diff --git a/src/browser/client-scripts/screen-shooter/utils/clip-rect.ts b/src/browser/client-scripts/screen-shooter/utils/clip-rect.ts index 375f9a078..cf3a6b067 100644 --- a/src/browser/client-scripts/screen-shooter/utils/clip-rect.ts +++ b/src/browser/client-scripts/screen-shooter/utils/clip-rect.ts @@ -2,15 +2,17 @@ import { Rect, Coord, Length, getIntersection } from "@isomorphic"; import * as lib from "@lib"; import { getBoundingClientContentRect } from "./element-rect"; import { isRootLikeElement } from "./scroll"; -import { findContainingBlock } from "./dom"; +import { findContainingBlock, getMainWindow } from "./dom"; import { getReadableElementDescriptor } from "./descriptions"; function getViewportRect(): Rect<"viewport", "css"> { + const mainWindow = getMainWindow(); + return { top: 0 as Coord<"viewport", "css", "y">, left: 0 as Coord<"viewport", "css", "x">, - width: window.innerWidth as Length<"css", "x">, - height: window.innerHeight as Length<"css", "y"> + width: mainWindow.innerWidth as Length<"css", "x">, + height: mainWindow.innerHeight as Length<"css", "y"> }; } diff --git a/src/browser/client-scripts/screen-shooter/utils/dom.ts b/src/browser/client-scripts/screen-shooter/utils/dom.ts index 24777dc2e..473c65484 100644 --- a/src/browser/client-scripts/screen-shooter/utils/dom.ts +++ b/src/browser/client-scripts/screen-shooter/utils/dom.ts @@ -43,6 +43,10 @@ export function getMainDocumentElem(currDocumentElem?: HTMLElement): HTMLElement return getMainDocumentElem(currWindow.document.documentElement); } +export function getMainWindow(): Window { + return getMainDocumentElem().ownerDocument.defaultView ?? window; +} + export function forEachRoot(cb: (root: Element | ShadowRoot) => void): void { function traverseRoots(root: Element | ShadowRoot): void { cb(root); diff --git a/test/e2e/screens/8486705/chrome/iframe-element.png b/test/e2e/screens/8486705/chrome/iframe-element.png new file mode 100644 index 0000000000000000000000000000000000000000..1c9a64020cb49327ac2bde4a8f548ba6e035e554 GIT binary patch literal 23653 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 + + + + + + +
+ + diff --git a/test/e2e/static/offset-iframe.html b/test/e2e/static/offset-iframe.html new file mode 100644 index 000000000..420eb67f4 --- /dev/null +++ b/test/e2e/static/offset-iframe.html @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/test/e2e/tests/assert-view.testplane.js b/test/e2e/tests/assert-view.testplane.js index 566f060ee..03fc2b4d3 100644 --- a/test/e2e/tests/assert-view.testplane.js +++ b/test/e2e/tests/assert-view.testplane.js @@ -478,4 +478,12 @@ describe("assertView", () => { allowViewportOverflow: true, }); }); + + it("should capture the full width of an element inside an offset iframe", async ({ browser }) => { + await browser.setWindowSize(1500, 1000); + await browser.url("offset-iframe.html"); + + await browser.switchToFrame(await browser.$("#frame")); + await browser.assertView("iframe-element", ".target-element"); + }); });