diff --git a/packages/puppeteer-core/src/api/ElementHandle.ts b/packages/puppeteer-core/src/api/ElementHandle.ts index 6a303b95..77c39736 100644 --- a/packages/puppeteer-core/src/api/ElementHandle.ts +++ b/packages/puppeteer-core/src/api/ElementHandle.ts @@ -1338,7 +1338,7 @@ export abstract class ElementHandle< const { scrollIntoView = true, captureBeyondViewport = true, - allowViewportExpansion = true, + allowViewportExpansion = captureBeyondViewport, } = options; let clip = await this.#nonEmptyVisibleBoundingBox(); @@ -1347,7 +1347,7 @@ export abstract class ElementHandle< // eslint-disable-next-line @typescript-eslint/no-unused-vars await using _ = - (captureBeyondViewport || allowViewportExpansion) && clip + allowViewportExpansion && clip ? await page._createTemporaryViewportContainingBox(clip) : null; @@ -1373,7 +1373,6 @@ export abstract class ElementHandle< return await page.screenshot({ ...options, captureBeyondViewport: false, - allowViewportExpansion: false, clip, }); } diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts index 23f6035a..d8f62dc5 100644 --- a/packages/puppeteer-core/src/api/Page.ts +++ b/packages/puppeteer-core/src/api/Page.ts @@ -2351,7 +2351,7 @@ export abstract class Page extends EventEmitter { } options.captureBeyondViewport ??= true; - options.allowViewportExpansion ??= true; + options.allowViewportExpansion ??= options.captureBeyondViewport; options.clip = options.clip && roundClip(normalizeClip(options.clip)); await using stack = new AsyncDisposableStack(); diff --git a/packages/puppeteer-core/src/bidi/Page.ts b/packages/puppeteer-core/src/bidi/Page.ts index 3a4aeafe..0ca50ee4 100644 --- a/packages/puppeteer-core/src/bidi/Page.ts +++ b/packages/puppeteer-core/src/bidi/Page.ts @@ -627,7 +627,12 @@ export class BidiPage extends Page { override async screenshot( options: Readonly = {} ): Promise { - const {clip, type, captureBeyondViewport} = options; + const { + clip, + type, + captureBeyondViewport, + allowViewportExpansion = true, + } = options; if (captureBeyondViewport) { throw new Error(`BiDi does not support 'captureBeyondViewport'.`); } @@ -648,7 +653,11 @@ export class BidiPage extends Page { if (clip?.scale !== undefined) { throw new Error(`BiDi does not support 'scale' in 'clip'.`); } - return await super.screenshot({...options, captureBeyondViewport: false}); + return await super.screenshot({ + ...options, + captureBeyondViewport, + allowViewportExpansion: captureBeyondViewport ?? allowViewportExpansion, + }); } override async _screenshot( diff --git a/packages/puppeteer-core/src/cdp/Page.ts b/packages/puppeteer-core/src/cdp/Page.ts index 02502809..f998c0c7 100644 --- a/packages/puppeteer-core/src/cdp/Page.ts +++ b/packages/puppeteer-core/src/cdp/Page.ts @@ -35,6 +35,7 @@ import { type NewDocumentScriptEvaluation, type ScreenshotOptions, type WaitTimeoutOptions, + type ScreenshotClip, } from '../api/Page.js'; import { ConsoleMessage, @@ -1044,7 +1045,7 @@ export class CdpPage extends Page { omitBackground, optimizeForSpeed, quality, - clip, + clip: userClip, type, captureBeyondViewport, } = options; @@ -1057,6 +1058,22 @@ export class CdpPage extends Page { }); } + let clip = userClip; + if (clip && !captureBeyondViewport) { + const viewport = await this.mainFrame() + .isolatedRealm() + .evaluate(() => { + const { + height, + pageLeft: x, + pageTop: y, + width, + } = window.visualViewport!; + return {x, y, height, width}; + }); + clip = getIntersectionRect(clip, viewport); + } + const {data} = await this.#client.send('Page.captureScreenshot', { format: type, optimizeForSpeed, @@ -1204,3 +1221,25 @@ const supportedMetrics = new Set([ 'JSHeapUsedSize', 'JSHeapTotalSize', ]); + +/** @see https://w3c.github.io/webdriver-bidi/#rectangle-intersection */ +function getIntersectionRect( + clip: Readonly, + viewport: Readonly +): ScreenshotClip { + // Note these will already be normalized. + const x = Math.max(clip.x, viewport.x); + const y = Math.max(clip.y, viewport.y); + return { + x, + y, + width: Math.max( + Math.min(clip.x + clip.width, viewport.x + viewport.width) - x, + 0 + ), + height: Math.max( + Math.min(clip.y + clip.height, viewport.y + viewport.height) - y, + 0 + ), + }; +} diff --git a/test/TestExpectations.json b/test/TestExpectations.json index fff22101..77d2ef40 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -1265,6 +1265,12 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL", "SKIP"] }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should clip clip bigger than the viewport without \"captureBeyondViewport\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL", "PASS"] + }, { "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should get screenshot bigger than the viewport", "platforms": ["darwin", "linux", "win32"], diff --git a/test/golden-chrome/screenshot-offscreen-clip-2.png b/test/golden-chrome/screenshot-offscreen-clip-2.png new file mode 100644 index 00000000..7ec69d30 Binary files /dev/null and b/test/golden-chrome/screenshot-offscreen-clip-2.png differ diff --git a/test/golden-firefox/screenshot-offscreen-clip-2.png b/test/golden-firefox/screenshot-offscreen-clip-2.png new file mode 100644 index 00000000..f7c0830b Binary files /dev/null and b/test/golden-firefox/screenshot-offscreen-clip-2.png differ diff --git a/test/src/screenshot.spec.ts b/test/src/screenshot.spec.ts index 4cdf419b..6abcbeb4 100644 --- a/test/src/screenshot.spec.ts +++ b/test/src/screenshot.spec.ts @@ -77,6 +77,21 @@ describe('Screenshots', function () { }); expect(screenshot).toBeGolden('screenshot-offscreen-clip.png'); }); + it('should clip clip bigger than the viewport without "captureBeyondViewport"', async () => { + const {page, server} = await getTestState(); + await page.setViewport({width: 50, height: 50}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + captureBeyondViewport: false, + clip: { + x: 25, + y: 25, + width: 100, + height: 100, + }, + }); + expect(screenshot).toBeGolden('screenshot-offscreen-clip-2.png'); + }); it('should run in parallel', async () => { const {page, server} = await getTestState();