From ecd6ac9dfa4c04ecd806e970857a5618d56a1bd4 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:21:12 +0200 Subject: [PATCH] chore: implement element screenshot (#10963) --- .eslintrc.js | 2 +- docs/api/index.md | 1 + docs/api/puppeteer.elementhandle.md | 2 +- .../api/puppeteer.elementhandle.screenshot.md | 12 +- .../api/puppeteer.elementscreenshotoptions.md | 19 + docs/api/puppeteer.page.md | 3 +- docs/api/puppeteer.page.screenshot.md | 36 +- docs/api/puppeteer.page.screenshot_1.md | 12 +- docs/api/puppeteer.page.screenshot_2.md | 23 -- docs/api/puppeteer.screenshotoptions.md | 24 +- .../puppeteer-core/src/api/ElementHandle.ts | 67 +++- packages/puppeteer-core/src/api/Page.ts | 331 ++++++++++++++---- packages/puppeteer-core/src/bidi/Page.ts | 66 ++-- packages/puppeteer-core/src/cdp/Browser.ts | 19 +- .../puppeteer-core/src/cdp/ElementHandle.ts | 67 +--- .../puppeteer-core/src/cdp/IsolatedWorld.ts | 2 +- packages/puppeteer-core/src/cdp/Page.ts | 215 ++---------- packages/puppeteer-core/src/cdp/Target.ts | 11 +- packages/puppeteer-core/src/common/util.ts | 40 --- packages/puppeteer-core/src/util/Mutex.ts | 41 +++ .../puppeteer-core/src/util/decorators.ts | 24 ++ .../puppeteer-core/src/util/disposable.ts | 116 ++++++ test/TestExpectations.json | 138 ++++---- .../screenshot-element-rotate.png | Bin 2342 -> 2355 bytes test/golden-firefox/grid-cell-0.png | Bin 331 -> 550 bytes test/golden-firefox/grid-cell-1.png | Bin 201 -> 340 bytes .../screenshot-clip-odd-size.png | Bin 75 -> 80 bytes .../screenshot-clip-rect-scale2.png | Bin 8472 -> 10361 bytes test/golden-firefox/screenshot-clip-rect.png | Bin 1371 -> 2501 bytes .../screenshot-element-bounding-box.png | Bin 311 -> 514 bytes .../screenshot-element-fractional.png | Bin 109 -> 151 bytes ...creenshot-element-larger-than-viewport.png | Bin 2797 -> 7703 bytes .../screenshot-element-padding-border.png | Bin 153 -> 234 bytes .../screenshot-element-scrolled-into-view.png | Bin 153 -> 234 bytes .../screenshot-offscreen-clip.png | Bin 279 -> 873 bytes test/golden-firefox/screenshot-sanity.png | Bin 26146 -> 46034 bytes test/golden-firefox/transparent.png | Bin 0 -> 119 bytes test/golden-firefox/white.jpg | Bin 0 -> 823 bytes test/src/screenshot.spec.ts | 83 ++--- 39 files changed, 740 insertions(+), 614 deletions(-) create mode 100644 docs/api/puppeteer.elementscreenshotoptions.md delete mode 100644 docs/api/puppeteer.page.screenshot_2.md create mode 100644 packages/puppeteer-core/src/util/Mutex.ts create mode 100644 test/golden-firefox/transparent.png create mode 100644 test/golden-firefox/white.jpg diff --git a/.eslintrc.js b/.eslintrc.js index 7524d4fb27f..82f7d2c0612 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -156,7 +156,7 @@ module.exports = { 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ 'error', - {argsIgnorePattern: '^_'}, + {argsIgnorePattern: '^_', varsIgnorePattern: '^_'}, ], 'func-call-spacing': 'off', '@typescript-eslint/func-call-spacing': 'error', diff --git a/docs/api/index.md b/docs/api/index.md index 6215e543c3d..cc834402edc 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -89,6 +89,7 @@ sidebar_label: API | [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | Set of configurable options for CSS coverage. | | [CustomQueryHandler](./puppeteer.customqueryhandler.md) | | | [Device](./puppeteer.device.md) | | +| [ElementScreenshotOptions](./puppeteer.elementscreenshotoptions.md) | | | [FrameAddScriptTagOptions](./puppeteer.frameaddscripttagoptions.md) | | | [FrameAddStyleTagOptions](./puppeteer.frameaddstyletagoptions.md) | | | [FrameEvents](./puppeteer.frameevents.md) | | diff --git a/docs/api/puppeteer.elementhandle.md b/docs/api/puppeteer.elementhandle.md index a247ff59479..769d6e4fba0 100644 --- a/docs/api/puppeteer.elementhandle.md +++ b/docs/api/puppeteer.elementhandle.md @@ -73,7 +73,7 @@ The constructor for this class is marked as internal. Third-party code should no | [isIntersectingViewport(this, options)](./puppeteer.elementhandle.isintersectingviewport.md) | | Resolves to true if the element is visible in the current viewport. If an element is an SVG, we check if the svg owner element is in the viewport instead. See https://crbug.com/963246. | | [isVisible()](./puppeteer.elementhandle.isvisible.md) | | Checks if an element is visible using the same mechanism as [ElementHandle.waitForSelector()](./puppeteer.elementhandle.waitforselector.md). | | [press(key, options)](./puppeteer.elementhandle.press.md) | | Focuses the element, and then uses [Keyboard.down()](./puppeteer.keyboard.down.md) and [Keyboard.up()](./puppeteer.keyboard.up.md). | -| [screenshot(this, options)](./puppeteer.elementhandle.screenshot.md) | | This method scrolls element into view if needed, and then uses [Page.screenshot()](./puppeteer.page.screenshot_2.md) to take a screenshot of the element. If the element is detached from DOM, the method throws an error. | +| [screenshot(this, options)](./puppeteer.elementhandle.screenshot.md) | | This method scrolls element into view if needed, and then uses to take a screenshot of the element. If the element is detached from DOM, the method throws an error. | | [scrollIntoView(this)](./puppeteer.elementhandle.scrollintoview.md) | | Scrolls the element into view using either the automation protocol client or by calling element.scrollIntoView. | | [select(values)](./puppeteer.elementhandle.select.md) | | Triggers a change and input event once all the provided options have been selected. If there's no <select> element matching selector, the method throws an error. | | [tap(this)](./puppeteer.elementhandle.tap.md) | | This method scrolls element into view if needed, and then uses [Touchscreen.tap()](./puppeteer.touchscreen.tap.md) to tap in the center of the element. If the element is detached from DOM, the method throws an error. | diff --git a/docs/api/puppeteer.elementhandle.screenshot.md b/docs/api/puppeteer.elementhandle.screenshot.md index 56ae127a516..3f31b828eff 100644 --- a/docs/api/puppeteer.elementhandle.screenshot.md +++ b/docs/api/puppeteer.elementhandle.screenshot.md @@ -4,7 +4,7 @@ sidebar_label: ElementHandle.screenshot # ElementHandle.screenshot() method -This method scrolls element into view if needed, and then uses [Page.screenshot()](./puppeteer.page.screenshot_2.md) to take a screenshot of the element. If the element is detached from DOM, the method throws an error. +This method scrolls element into view if needed, and then uses to take a screenshot of the element. If the element is detached from DOM, the method throws an error. #### Signature: @@ -12,17 +12,17 @@ This method scrolls element into view if needed, and then uses [Page.screenshot( class ElementHandle { screenshot( this: ElementHandle, - options?: ScreenshotOptions + options?: Readonly ): Promise; } ``` ## Parameters -| Parameter | Type | Description | -| --------- | ------------------------------------------------------------ | ------------ | -| this | [ElementHandle](./puppeteer.elementhandle.md)<Element> | | -| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) | _(Optional)_ | +| Parameter | Type | Description | +| --------- | ----------------------------------------------------------------------------------- | ------------ | +| this | [ElementHandle](./puppeteer.elementhandle.md)<Element> | | +| options | Readonly<[ElementScreenshotOptions](./puppeteer.elementscreenshotoptions.md)> | _(Optional)_ | **Returns:** diff --git a/docs/api/puppeteer.elementscreenshotoptions.md b/docs/api/puppeteer.elementscreenshotoptions.md new file mode 100644 index 00000000000..120b2a00013 --- /dev/null +++ b/docs/api/puppeteer.elementscreenshotoptions.md @@ -0,0 +1,19 @@ +--- +sidebar_label: ElementScreenshotOptions +--- + +# ElementScreenshotOptions interface + +#### Signature: + +```typescript +export interface ElementScreenshotOptions extends ScreenshotOptions +``` + +**Extends:** [ScreenshotOptions](./puppeteer.screenshotoptions.md) + +## Properties + +| Property | Modifiers | Type | Description | Default | +| -------------- | --------------------- | ------- | ----------- | ------- | +| scrollIntoView | optional | boolean | | true | diff --git a/docs/api/puppeteer.page.md b/docs/api/puppeteer.page.md index 7ba3c73d187..0a75028d400 100644 --- a/docs/api/puppeteer.page.md +++ b/docs/api/puppeteer.page.md @@ -127,9 +127,8 @@ page.off('request', logRequest); | [reload(options)](./puppeteer.page.reload.md) | | Reloads the page. | | [removeExposedFunction(name)](./puppeteer.page.removeexposedfunction.md) | | The method removes a previously added function via $[Page.exposeFunction()](./puppeteer.page.exposefunction.md) called name from the page's window object. | | [removeScriptToEvaluateOnNewDocument(identifier)](./puppeteer.page.removescripttoevaluateonnewdocument.md) | | Removes script that injected into page by Page.evaluateOnNewDocument. | -| [screenshot(options)](./puppeteer.page.screenshot.md) | | Captures screenshot of the current page. | +| [screenshot(options)](./puppeteer.page.screenshot.md) | | Captures a screenshot of this [page](./puppeteer.page.md). | | [screenshot(options)](./puppeteer.page.screenshot_1.md) | | | -| [screenshot(options)](./puppeteer.page.screenshot_2.md) | | | | [select(selector, values)](./puppeteer.page.select.md) | | Triggers a change and input event once all the provided options have been selected. If there's no <select> element matching selector, the method throws an error. | | [setBypassCSP(enabled)](./puppeteer.page.setbypasscsp.md) | | Toggles bypassing page's Content-Security-Policy. | | [setBypassServiceWorker(bypass)](./puppeteer.page.setbypassserviceworker.md) | | Toggles ignoring of service worker for each request. | diff --git a/docs/api/puppeteer.page.screenshot.md b/docs/api/puppeteer.page.screenshot.md index 30c780eb31e..d31d530450c 100644 --- a/docs/api/puppeteer.page.screenshot.md +++ b/docs/api/puppeteer.page.screenshot.md @@ -4,14 +4,14 @@ sidebar_label: Page.screenshot # Page.screenshot() method -Captures screenshot of the current page. +Captures a screenshot of this [page](./puppeteer.page.md). #### Signature: ```typescript class Page { - abstract screenshot( - options: ScreenshotOptions & { + screenshot( + options: Readonly & { encoding: 'base64'; } ): Promise; @@ -20,34 +20,10 @@ class Page { ## Parameters -| Parameter | Type | Description | -| --------- | ----------------------------------------------------------------------------------- | ----------- | -| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) & { encoding: 'base64'; } | | +| Parameter | Type | Description | +| --------- | --------------------------------------------------------------------------------------------------- | ------------------------------- | +| options | Readonly<[ScreenshotOptions](./puppeteer.screenshotoptions.md)> & { encoding: 'base64'; } | Configures screenshot behavior. | **Returns:** Promise<string> - -Promise which resolves to buffer or a base64 string (depending on the value of `encoding`) with captured screenshot. - -## Remarks - -Options object which might have the following properties: - -- `path` : The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk. - -- `type` : Specify screenshot type, can be `jpeg`, `png` or `webp`. Defaults to 'png'. - -- `quality` : The quality of the image, between 0-100. Not applicable to `png` images. - -- `fullPage` : When true, takes a screenshot of the full scrollable page. Defaults to `false`. - -- `clip` : An object which specifies clipping region of the page. Should have the following fields:
- `x` : x-coordinate of top-left corner of clip area.
- `y` : y-coordinate of top-left corner of clip area.
- `width` : width of clipping area.
- `height` : height of clipping area. - -- `omitBackground` : Hides default white background and allows capturing screenshots with transparency. Defaults to `false`. - -- `encoding` : The encoding of the image, can be either base64 or binary. Defaults to `binary`. - -- `captureBeyondViewport` : When true, captures screenshot [beyond the viewport](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot). When false, falls back to old behaviour, and cuts the screenshot by the viewport size. Defaults to `true`. - -- `fromSurface` : When true, captures screenshot [from the surface rather than the view](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot). When false, works only in headful mode and ignores page viewport (but not browser window's bounds). Defaults to `true`. diff --git a/docs/api/puppeteer.page.screenshot_1.md b/docs/api/puppeteer.page.screenshot_1.md index 71b55355012..5a1c5c0d743 100644 --- a/docs/api/puppeteer.page.screenshot_1.md +++ b/docs/api/puppeteer.page.screenshot_1.md @@ -8,19 +8,15 @@ sidebar_label: Page.screenshot_1 ```typescript class Page { - abstract screenshot( - options?: ScreenshotOptions & { - encoding?: 'binary'; - } - ): Promise; + screenshot(options?: Readonly): Promise; } ``` ## Parameters -| Parameter | Type | Description | -| --------- | ------------------------------------------------------------------------------------ | ------------ | -| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) & { encoding?: 'binary'; } | _(Optional)_ | +| Parameter | Type | Description | +| --------- | --------------------------------------------------------------------- | ------------ | +| options | Readonly<[ScreenshotOptions](./puppeteer.screenshotoptions.md)> | _(Optional)_ | **Returns:** diff --git a/docs/api/puppeteer.page.screenshot_2.md b/docs/api/puppeteer.page.screenshot_2.md deleted file mode 100644 index 6d1bb2b25eb..00000000000 --- a/docs/api/puppeteer.page.screenshot_2.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -sidebar_label: Page.screenshot_2 ---- - -# Page.screenshot() method - -#### Signature: - -```typescript -class Page { - abstract screenshot(options?: ScreenshotOptions): Promise; -} -``` - -## Parameters - -| Parameter | Type | Description | -| --------- | ----------------------------------------------------- | ------------ | -| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) | _(Optional)_ | - -**Returns:** - -Promise<Buffer \| string> diff --git a/docs/api/puppeteer.screenshotoptions.md b/docs/api/puppeteer.screenshotoptions.md index 47470e8499d..568a9bf3b1c 100644 --- a/docs/api/puppeteer.screenshotoptions.md +++ b/docs/api/puppeteer.screenshotoptions.md @@ -12,15 +12,15 @@ export interface ScreenshotOptions ## Properties -| Property | Modifiers | Type | Description | Default | -| --------------------- | --------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| captureBeyondViewport | optional | boolean | Capture the screenshot beyond the viewport. | true | -| clip | optional | [ScreenshotClip](./puppeteer.screenshotclip.md) | An object which specifies the clipping region of the page. | | -| encoding | optional | 'base64' \| 'binary' | Encoding of the image. | binary | -| fromSurface | optional | boolean | Capture the screenshot from the surface, rather than the view. | true | -| fullPage | optional | boolean | When true, takes a screenshot of the full page. | false | -| omitBackground | optional | boolean | Hides default white background and allows capturing screenshots with transparency. | false | -| optimizeForSpeed | optional | boolean | | false | -| path | optional | string | The file path to save the image to. The screenshot type will be inferred from file extension. If path is a relative path, then it is resolved relative to current working directory. If no path is provided, the image won't be saved to the disk. | | -| quality | optional | number | Quality of the image, between 0-100. Not applicable to png images. | | -| type | optional | 'png' \| 'jpeg' \| 'webp' | | png | +| Property | Modifiers | Type | Description | Default | +| --------------------- | --------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| captureBeyondViewport | optional | boolean | Capture the screenshot beyond the viewport. | true | +| clip | optional | [ScreenshotClip](./puppeteer.screenshotclip.md) | Specifies the region of the page to clip. | | +| encoding | optional | 'base64' \| 'binary' | Encoding of the image. | 'binary' | +| fromSurface | optional | boolean | Capture the screenshot from the surface, rather than the view. | false | +| fullPage | optional | boolean | When true, takes a screenshot of the full page. | false | +| omitBackground | optional | boolean | Hides default white background and allows capturing screenshots with transparency. | false | +| optimizeForSpeed | optional | boolean | | false | +| path | optional | string | The file path to save the image to. The screenshot type will be inferred from file extension. If path is a relative path, then it is resolved relative to current working directory. If no path is provided, the image won't be saved to the disk. | | +| quality | optional | number | Quality of the image, between 0-100. Not applicable to png images. | | +| type | optional | 'png' \| 'jpeg' \| 'webp' | | 'png' | diff --git a/packages/puppeteer-core/src/api/ElementHandle.ts b/packages/puppeteer-core/src/api/ElementHandle.ts index 4b5e6dda278..6a303b952c9 100644 --- a/packages/puppeteer-core/src/api/ElementHandle.ts +++ b/packages/puppeteer-core/src/api/ElementHandle.ts @@ -103,6 +103,16 @@ export interface Point { y: number; } +/** + * @public + */ +export interface ElementScreenshotOptions extends ScreenshotOptions { + /** + * @defaultValue true + */ + scrollIntoView?: boolean; +} + /** * ElementHandle represents an in-page DOM element. * @@ -1319,12 +1329,61 @@ export abstract class ElementHandle< * {@link Page.(screenshot:3) } to take a screenshot of the element. * If the element is detached from DOM, the method throws an error. */ + @throwIfDisposed() + @ElementHandle.bindIsolatedHandle async screenshot( this: ElementHandle, - options?: ScreenshotOptions - ): Promise; - async screenshot(this: ElementHandle): Promise { - throw new Error('Not implemented'); + options: Readonly = {} + ): Promise { + const { + scrollIntoView = true, + captureBeyondViewport = true, + allowViewportExpansion = true, + } = options; + + let clip = await this.#nonEmptyVisibleBoundingBox(); + + const page = this.frame.page(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + await using _ = + (captureBeyondViewport || allowViewportExpansion) && clip + ? await page._createTemporaryViewportContainingBox(clip) + : null; + + if (scrollIntoView) { + await this.scrollIntoViewIfNeeded(); + + // We measure again just in case. + clip = await this.#nonEmptyVisibleBoundingBox(); + } + + const [pageLeft, pageTop] = await this.evaluate(() => { + if (!window.visualViewport) { + throw new Error('window.visualViewport is not supported.'); + } + return [ + window.visualViewport.pageLeft, + window.visualViewport.pageTop, + ] as const; + }); + clip.x += pageLeft; + clip.y += pageTop; + + return await page.screenshot({ + ...options, + captureBeyondViewport: false, + allowViewportExpansion: false, + clip, + }); + } + + async #nonEmptyVisibleBoundingBox() { + const box = await this.boundingBox(); + assert(box, 'Node is either not visible or not an HTMLElement'); + assert(box.width !== 0, 'Node has 0 width.'); + assert(box.height !== 0, 'Node has 0 height.'); + return box; } /** diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts index 066d6cc59f9..23f6035a353 100644 --- a/packages/puppeteer-core/src/api/Page.ts +++ b/packages/puppeteer-core/src/api/Page.ts @@ -81,8 +81,13 @@ import { } from '../common/util.js'; import type {Viewport} from '../common/Viewport.js'; import {assert} from '../util/assert.js'; +import {guarded} from '../util/decorators.js'; import {type Deferred} from '../util/Deferred.js'; -import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js'; +import { + AsyncDisposableStack, + asyncDisposeSymbol, + disposeSymbol, +} from '../util/disposable.js'; import type {Browser} from './Browser.js'; import type {BrowserContext} from './BrowserContext.js'; @@ -227,9 +232,31 @@ export interface ScreenshotOptions { */ optimizeForSpeed?: boolean; /** - * @defaultValue `png` + * @defaultValue `'png'` */ type?: 'png' | 'jpeg' | 'webp'; + /** + * Quality of the image, between 0-100. Not applicable to `png` images. + */ + quality?: number; + /** + * Capture the screenshot from the surface, rather than the view. + * + * @defaultValue `false` + */ + fromSurface?: boolean; + /** + * When `true`, takes a screenshot of the full page. + * + * @defaultValue `false` + */ + fullPage?: boolean; + /** + * Hides default white background and allows capturing screenshots with transparency. + * + * @defaultValue `false` + */ + omitBackground?: boolean; /** * The file path to save the image to. The screenshot type will be inferred * from file extension. If path is a relative path, then it is resolved @@ -238,38 +265,29 @@ export interface ScreenshotOptions { */ path?: string; /** - * When `true`, takes a screenshot of the full page. - * @defaultValue `false` - */ - fullPage?: boolean; - /** - * An object which specifies the clipping region of the page. + * Specifies the region of the page to clip. */ clip?: ScreenshotClip; - /** - * Quality of the image, between 0-100. Not applicable to `png` images. - */ - quality?: number; - /** - * Hides default white background and allows capturing screenshots with transparency. - * @defaultValue `false` - */ - omitBackground?: boolean; /** * Encoding of the image. - * @defaultValue `binary` + * + * @defaultValue `'binary'` */ encoding?: 'base64' | 'binary'; /** * Capture the screenshot beyond the viewport. + * * @defaultValue `true` */ captureBeyondViewport?: boolean; /** - * Capture the screenshot from the surface, rather than the view. - * @defaultValue `true` + * TODO(jrandolf): Investigate whether viewport expansion is a better + * alternative for cross-browser screenshots as opposed to + * `captureBeyondViewport`. + * + * @internal */ - fromSurface?: boolean; + allowViewportExpansion?: boolean; } /** @@ -2243,61 +2261,195 @@ export abstract class Page extends EventEmitter { } /** - * Captures screenshot of the current page. + * Captures a screenshot of this {@link Page | page}. * - * @remarks - * Options object which might have the following properties: - * - * - `path` : The file path to save the image to. The screenshot type - * will be inferred from file extension. If `path` is a relative path, then - * it is resolved relative to - * {@link https://nodejs.org/api/process.html#process_process_cwd - * | current working directory}. - * If no path is provided, the image won't be saved to the disk. - * - * - `type` : Specify screenshot type, can be `jpeg`, `png` or `webp`. - * Defaults to 'png'. - * - * - `quality` : The quality of the image, between 0-100. Not - * applicable to `png` images. - * - * - `fullPage` : When true, takes a screenshot of the full - * scrollable page. Defaults to `false`. - * - * - `clip` : An object which specifies clipping region of the page. - * Should have the following fields:
- * - `x` : x-coordinate of top-left corner of clip area.
- * - `y` : y-coordinate of top-left corner of clip area.
- * - `width` : width of clipping area.
- * - `height` : height of clipping area. - * - * - `omitBackground` : Hides default white background and allows - * capturing screenshots with transparency. Defaults to `false`. - * - * - `encoding` : The encoding of the image, can be either base64 or - * binary. Defaults to `binary`. - * - * - `captureBeyondViewport` : When true, captures screenshot - * {@link https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot - * | beyond the viewport}. When false, falls back to old behaviour, - * and cuts the screenshot by the viewport size. Defaults to `true`. - * - * - `fromSurface` : When true, captures screenshot - * {@link https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot - * | from the surface rather than the view}. When false, works only in - * headful mode and ignores page viewport (but not browser window's - * bounds). Defaults to `true`. - * - * @returns Promise which resolves to buffer or a base64 string (depending on - * the value of `encoding`) with captured screenshot. + * @param options - Configures screenshot behavior. */ - abstract screenshot( - options: ScreenshotOptions & {encoding: 'base64'} + async screenshot( + options: Readonly & {encoding: 'base64'} ): Promise; - abstract screenshot( - options?: ScreenshotOptions & {encoding?: 'binary'} - ): Promise; - abstract screenshot(options?: ScreenshotOptions): Promise; + async screenshot(options?: Readonly): Promise; + @guarded(function () { + return this.browser(); + }) + async screenshot( + userOptions: Readonly = {} + ): Promise { + await this.bringToFront(); + + const options = structuredClone(userOptions) as ScreenshotOptions; + if (options.type === undefined && options.path !== undefined) { + const filePath = options.path; + // Note we cannot use Node.js here due to browser compatability. + const extension = filePath + .slice(filePath.lastIndexOf('.') + 1) + .toLowerCase(); + switch (extension) { + case 'png': + options.type = 'png'; + break; + case 'jpeg': + case 'jpg': + options.type = 'jpeg'; + break; + case 'webp': + options.type = 'webp'; + break; + } + } + if (options.quality) { + assert( + options.type === 'jpeg' || options.type === 'webp', + `options.quality is unsupported for the ${options.type} screenshots` + ); + assert( + typeof options.quality === 'number', + `Expected options.quality to be a number but found ${typeof options.quality}` + ); + assert( + Number.isInteger(options.quality), + 'Expected options.quality to be an integer' + ); + assert( + options.quality >= 0 && options.quality <= 100, + `Expected options.quality to be between 0 and 100 (inclusive), got ${options.quality}` + ); + } + assert( + !options.clip || !options.fullPage, + 'options.clip and options.fullPage are exclusive' + ); + if (options.clip) { + assert( + typeof options.clip.x === 'number', + `Expected options.clip.x to be a number but found ${typeof options.clip + .x}` + ); + assert( + typeof options.clip.y === 'number', + `Expected options.clip.y to be a number but found ${typeof options.clip + .y}` + ); + assert( + typeof options.clip.width === 'number', + `Expected options.clip.width to be a number but found ${typeof options + .clip.width}` + ); + assert( + typeof options.clip.height === 'number', + `Expected options.clip.height to be a number but found ${typeof options + .clip.height}` + ); + assert( + options.clip.width !== 0, + 'Expected options.clip.width not to be 0.' + ); + assert( + options.clip.height !== 0, + 'Expected options.clip.height not to be 0.' + ); + } + + options.captureBeyondViewport ??= true; + options.allowViewportExpansion ??= true; + options.clip = options.clip && roundClip(normalizeClip(options.clip)); + + await using stack = new AsyncDisposableStack(); + if (options.allowViewportExpansion || options.captureBeyondViewport) { + if (options.fullPage) { + const dimensions = await this.mainFrame() + .isolatedRealm() + .evaluate(() => { + const {scrollHeight, scrollWidth} = document.documentElement; + const {height: viewportHeight, width: viewportWidth} = + window.visualViewport!; + return { + height: Math.max(scrollHeight, viewportHeight), + width: Math.max(scrollWidth, viewportWidth), + }; + }); + options.clip = {...dimensions, x: 0, y: 0}; + stack.use( + await this._createTemporaryViewportContainingBox(options.clip) + ); + } else if (options.clip && !options.captureBeyondViewport) { + stack.use( + options.clip && + (await this._createTemporaryViewportContainingBox(options.clip)) + ); + } else if (!options.clip) { + options.captureBeyondViewport = false; + } + } + + const data = await this._screenshot(options); + if (options.encoding === 'base64') { + return data; + } + const buffer = Buffer.from(data, 'base64'); + await this._maybeWriteBufferToFile(options.path, buffer); + return buffer; + } + + /** + * @internal + */ + abstract _screenshot(options: Readonly): Promise; + + /** + * @internal + */ + async _createTemporaryViewportContainingBox( + clip: ScreenshotClip + ): Promise { + const viewport = await this.mainFrame() + .isolatedRealm() + .evaluate(() => { + return { + pageLeft: window.visualViewport!.pageLeft, + pageTop: window.visualViewport!.pageTop, + width: window.visualViewport!.width, + height: window.visualViewport!.height, + }; + }); + await using stack = new AsyncDisposableStack(); + if (clip.x < viewport.pageLeft || clip.y < viewport.pageTop) { + await this.evaluate( + (left, top) => { + window.scroll({left, top, behavior: 'instant'}); + }, + Math.floor(clip.x), + Math.floor(clip.y) + ); + stack.defer(async () => { + await this.evaluate( + (left, top) => { + window.scroll({left, top, behavior: 'instant'}); + }, + viewport.pageLeft, + viewport.pageTop + ).catch(debugError); + }); + } + if ( + clip.width + clip.x > viewport.width || + clip.height + clip.y > viewport.height + ) { + const originalViewport = this.viewport() ?? { + width: 0, + height: 0, + }; + // We add 1 for fractional x and y. + await this.setViewport({ + width: Math.max(viewport.width, Math.ceil(clip.width + clip.x)), + height: Math.max(viewport.height, Math.ceil(clip.height + clip.y)), + }); + stack.defer(async () => { + await this.setViewport(originalViewport).catch(debugError); + }); + } + return stack.move(); + } /** * @internal @@ -2854,3 +3006,36 @@ function convertPrintParameterToInches( } return pixels / unitToPixels[lengthUnit]; } + +/** @see https://w3c.github.io/webdriver-bidi/#normalize-rect */ +function normalizeClip(clip: Readonly): ScreenshotClip { + return { + ...(clip.width < 0 + ? { + x: clip.x + clip.width, + width: -clip.width, + } + : { + x: clip.x, + width: clip.width, + }), + ...(clip.height < 0 + ? { + y: clip.y + clip.height, + height: -clip.height, + } + : { + y: clip.y, + height: clip.height, + }), + scale: clip.scale, + }; +} + +function roundClip(clip: Readonly): ScreenshotClip { + const x = Math.round(clip.x); + const y = Math.round(clip.y); + const width = Math.round(clip.width + clip.x - x); + const height = Math.round(clip.height + clip.y - y); + return {x, y, width, height, scale: clip.scale}; +} diff --git a/packages/puppeteer-core/src/bidi/Page.ts b/packages/puppeteer-core/src/bidi/Page.ts index 6afd36e7af4..3a4aeafe298 100644 --- a/packages/puppeteer-core/src/bidi/Page.ts +++ b/packages/puppeteer-core/src/bidi/Page.ts @@ -618,35 +618,53 @@ export class BidiPage extends Page { } } - override screenshot( - options: ScreenshotOptions & {encoding: 'base64'} - ): Promise; - override screenshot( - options?: ScreenshotOptions & {encoding?: 'binary'} - ): never; override async screenshot( - options: ScreenshotOptions = {} + options: Readonly & {encoding: 'base64'} + ): Promise; + override async screenshot( + options?: Readonly + ): Promise; + override async screenshot( + options: Readonly = {} ): Promise { - const {path = undefined, encoding, ...args} = options; - if (Object.keys(args).length >= 1) { - throw new Error('BiDi only supports "encoding" and "path" options'); + const {clip, type, captureBeyondViewport} = options; + if (captureBeyondViewport) { + throw new Error(`BiDi does not support 'captureBeyondViewport'.`); } - - const {result} = await this.#connection.send( - 'browsingContext.captureScreenshot', - { - context: this.mainFrame()._id, - } - ); - - if (encoding === 'base64') { - return result.data; + const invalidOption = Object.keys(options).find(option => { + return [ + 'fromSurface', + 'omitBackground', + 'optimizeForSpeed', + 'quality', + ].includes(option); + }); + if (invalidOption !== undefined) { + throw new Error(`BiDi does not support ${invalidOption}.`); } + if ((type ?? 'png') !== 'png') { + throw new Error(`BiDi only supports 'png' type.`); + } + if (clip?.scale !== undefined) { + throw new Error(`BiDi does not support 'scale' in 'clip'.`); + } + return await super.screenshot({...options, captureBeyondViewport: false}); + } - const buffer = Buffer.from(result.data, 'base64'); - await this._maybeWriteBufferToFile(path, buffer); - - return buffer; + override async _screenshot( + options: Readonly + ): Promise { + const {clip} = options; + const { + result: {data}, + } = await this.#connection.send('browsingContext.captureScreenshot', { + context: this.mainFrame()._id, + clip: clip && { + type: 'viewport', + ...clip, + }, + }); + return data; } override async waitForRequest( diff --git a/packages/puppeteer-core/src/cdp/Browser.ts b/packages/puppeteer-core/src/cdp/Browser.ts index 60a953c86f4..f3284dffe41 100644 --- a/packages/puppeteer-core/src/cdp/Browser.ts +++ b/packages/puppeteer-core/src/cdp/Browser.ts @@ -20,19 +20,18 @@ import {type Protocol} from 'devtools-protocol'; import { Browser as BrowserBase, + BrowserEvent, + WEB_PERMISSION_TO_PROTOCOL_PERMISSION, type BrowserCloseCallback, type BrowserContextOptions, - BrowserEvent, type IsPageTargetCallback, type Permission, type TargetFilterCallback, - WEB_PERMISSION_TO_PROTOCOL_PERMISSION, } from '../api/Browser.js'; import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js'; -import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js'; +import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js'; import {type Page} from '../api/Page.js'; import {type Target} from '../api/Target.js'; -import {TaskQueue} from '../common/TaskQueue.js'; import {type Viewport} from '../common/Viewport.js'; import {USE_TAB_TARGET} from '../environment.js'; import {assert} from '../util/assert.js'; @@ -41,14 +40,14 @@ import {ChromeTargetManager} from './ChromeTargetManager.js'; import {type Connection} from './Connection.js'; import {FirefoxTargetManager} from './FirefoxTargetManager.js'; import { - type CdpTarget, DevToolsTarget, InitializationStatus, OtherTarget, PageTarget, WorkerTarget, + type CdpTarget, } from './Target.js'; -import {type TargetManager, TargetManagerEvent} from './TargetManager.js'; +import {TargetManagerEvent, type TargetManager} from './TargetManager.js'; /** * @internal @@ -92,7 +91,6 @@ export class CdpBrowser extends BrowserBase { #isPageTargetCallback!: IsPageTargetCallback; #defaultContext: CdpBrowserContext; #contexts = new Map(); - #screenshotTaskQueue: TaskQueue; #targetManager: TargetManager; override get _targets(): Map { @@ -117,7 +115,6 @@ export class CdpBrowser extends BrowserBase { this.#ignoreHTTPSErrors = ignoreHTTPSErrors; this.#defaultViewport = defaultViewport; this.#process = process; - this.#screenshotTaskQueue = new TaskQueue(); this.#connection = connection; this.#closeCallback = closeCallback || function (): void {}; this.#targetFilterCallback = @@ -290,8 +287,7 @@ export class CdpBrowser extends BrowserBase { this.#targetManager, createSession, this.#ignoreHTTPSErrors, - this.#defaultViewport ?? null, - this.#screenshotTaskQueue + this.#defaultViewport ?? null ); } if (this.#isPageTargetCallback(otherTarget)) { @@ -302,8 +298,7 @@ export class CdpBrowser extends BrowserBase { this.#targetManager, createSession, this.#ignoreHTTPSErrors, - this.#defaultViewport ?? null, - this.#screenshotTaskQueue + this.#defaultViewport ?? null ); } if ( diff --git a/packages/puppeteer-core/src/cdp/ElementHandle.ts b/packages/puppeteer-core/src/cdp/ElementHandle.ts index a0fe4e4014f..b34ac143fcb 100644 --- a/packages/puppeteer-core/src/cdp/ElementHandle.ts +++ b/packages/puppeteer-core/src/cdp/ElementHandle.ts @@ -17,8 +17,7 @@ import {type Protocol} from 'devtools-protocol'; import {type CDPSession} from '../api/CDPSession.js'; -import {type AutofillData, ElementHandle} from '../api/ElementHandle.js'; -import {type Page, type ScreenshotOptions} from '../api/Page.js'; +import {ElementHandle, type AutofillData} from '../api/ElementHandle.js'; import {debugError} from '../common/util.js'; import {assert} from '../util/assert.js'; import {throwIfDisposed} from '../util/decorators.js'; @@ -63,10 +62,6 @@ export class CdpElementHandle< return this.frame._frameManager; } - get #page(): Page { - return this.frame.page(); - } - override get frame(): CdpFrame { return this.realm.environment as CdpFrame; } @@ -162,66 +157,6 @@ export class CdpElementHandle< } } - @throwIfDisposed() - @ElementHandle.bindIsolatedHandle - override async screenshot( - this: CdpElementHandle, - options: ScreenshotOptions = {} - ): Promise { - let needsViewportReset = false; - - let boundingBox = await this.boundingBox(); - assert(boundingBox, 'Node is either not visible or not an HTMLElement'); - - const viewport = this.#page.viewport(); - - if ( - viewport && - (boundingBox.width > viewport.width || - boundingBox.height > viewport.height) - ) { - const newViewport = { - width: Math.max(viewport.width, Math.ceil(boundingBox.width)), - height: Math.max(viewport.height, Math.ceil(boundingBox.height)), - }; - await this.#page.setViewport(Object.assign({}, viewport, newViewport)); - - needsViewportReset = true; - } - - await this.scrollIntoViewIfNeeded(); - - boundingBox = await this.boundingBox(); - assert(boundingBox, 'Node is either not visible or not an HTMLElement'); - assert(boundingBox.width !== 0, 'Node has 0 width.'); - assert(boundingBox.height !== 0, 'Node has 0 height.'); - - const layoutMetrics = await this.client.send('Page.getLayoutMetrics'); - // Fallback to `layoutViewport` in case of using Firefox. - const {pageX, pageY} = - layoutMetrics.cssVisualViewport || layoutMetrics.layoutViewport; - - const clip = Object.assign({}, boundingBox); - clip.x += pageX; - clip.y += pageY; - - const imageData = await this.#page.screenshot( - Object.assign( - {}, - { - clip, - }, - options - ) - ); - - if (needsViewportReset && viewport) { - await this.#page.setViewport(viewport); - } - - return imageData; - } - @throwIfDisposed() override async autofill(data: AutofillData): Promise { const nodeInfo = await this.client.send('DOM.describeNode', { diff --git a/packages/puppeteer-core/src/cdp/IsolatedWorld.ts b/packages/puppeteer-core/src/cdp/IsolatedWorld.ts index 013928f1ab9..bf15ccf4ed4 100644 --- a/packages/puppeteer-core/src/cdp/IsolatedWorld.ts +++ b/packages/puppeteer-core/src/cdp/IsolatedWorld.ts @@ -26,13 +26,13 @@ import { type HandleFor, } from '../common/types.js'; import { - Mutex, addPageBinding, debugError, withSourcePuppeteerURLIfNone, } from '../common/util.js'; import {Deferred} from '../util/Deferred.js'; import {disposeSymbol} from '../util/disposable.js'; +import {Mutex} from '../util/Mutex.js'; import {type Binding} from './Binding.js'; import {type ExecutionContext, createCdpHandle} from './ExecutionContext.js'; diff --git a/packages/puppeteer-core/src/cdp/Page.ts b/packages/puppeteer-core/src/cdp/Page.ts index df89ae6327e..02502809258 100644 --- a/packages/puppeteer-core/src/cdp/Page.ts +++ b/packages/puppeteer-core/src/cdp/Page.ts @@ -16,13 +16,13 @@ import type {Readable} from 'stream'; -import {Protocol} from 'devtools-protocol'; +import {type Protocol} from 'devtools-protocol'; import type {Browser} from '../api/Browser.js'; import type {BrowserContext} from '../api/BrowserContext.js'; import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js'; import {type ElementHandle} from '../api/ElementHandle.js'; -import {type WaitForOptions, type Frame} from '../api/Frame.js'; +import {type Frame, type WaitForOptions} from '../api/Frame.js'; import {type HTTPRequest} from '../api/HTTPRequest.js'; import {type HTTPResponse} from '../api/HTTPResponse.js'; import {type JSHandle} from '../api/JSHandle.js'; @@ -33,7 +33,6 @@ import { type MediaFeature, type Metrics, type NewDocumentScriptEvaluation, - type ScreenshotClip, type ScreenshotOptions, type WaitTimeoutOptions, } from '../api/Page.js'; @@ -44,7 +43,6 @@ import { import {TargetCloseError} from '../common/Errors.js'; import {FileChooser} from '../common/FileChooser.js'; import {type PDFOptions} from '../common/PDFOptions.js'; -import {type TaskQueue} from '../common/TaskQueue.js'; import {TimeoutSettings} from '../common/TimeoutSettings.js'; import {type BindingPayload, type HandleFor} from '../common/types.js'; import { @@ -61,6 +59,7 @@ import { waitWithTimeout, } from '../common/util.js'; import {type Viewport} from '../common/Viewport.js'; +import {AsyncDisposableStack} from '../puppeteer-core.js'; import {assert} from '../util/assert.js'; import {Deferred} from '../util/Deferred.js'; import {isErrorLike} from '../util/ErrorLike.js'; @@ -96,15 +95,9 @@ export class CdpPage extends Page { client: CDPSession, target: CdpTarget, ignoreHTTPSErrors: boolean, - defaultViewport: Viewport | null, - screenshotTaskQueue: TaskQueue + defaultViewport: Viewport | null ): Promise { - const page = new CdpPage( - client, - target, - ignoreHTTPSErrors, - screenshotTaskQueue - ); + const page = new CdpPage(client, target, ignoreHTTPSErrors); await page.#initialize(); if (defaultViewport) { try { @@ -136,7 +129,6 @@ export class CdpPage extends Page { #exposedFunctions = new Map(); #coverage: Coverage; #viewport: Viewport | null; - #screenshotTaskQueue: TaskQueue; #workers = new Map(); #fileChooserDeferreds = new Set>(); #sessionCloseDeferred = Deferred.create(); @@ -231,8 +223,7 @@ export class CdpPage extends Page { constructor( client: CDPSession, target: CdpTarget, - ignoreHTTPSErrors: boolean, - screenshotTaskQueue: TaskQueue + ignoreHTTPSErrors: boolean ) { super(); this.#client = client; @@ -251,7 +242,6 @@ export class CdpPage extends Page { this.#emulationManager = new EmulationManager(client); this.#tracing = new Tracing(client); this.#coverage = new Coverage(client); - this.#screenshotTaskQueue = screenshotTaskQueue; this.#viewport = null; this.#setupEventListeners(); @@ -1048,188 +1038,37 @@ export class CdpPage extends Page { await this.#frameManager.networkManager.setCacheEnabled(enabled); } - override screenshot( - options: ScreenshotOptions & {encoding: 'base64'} - ): Promise; - override screenshot( - options?: ScreenshotOptions & {encoding?: 'binary'} - ): Promise; - override async screenshot( - options: ScreenshotOptions = {} - ): Promise { - let screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Png; - // options.type takes precedence over inferring the type from options.path - // because it may be a 0-length file with no extension created beforehand - // (i.e. as a temp file). - if (options.type) { - screenshotType = - options.type as Protocol.Page.CaptureScreenshotRequestFormat; - } else if (options.path) { - const filePath = options.path; - const extension = filePath - .slice(filePath.lastIndexOf('.') + 1) - .toLowerCase(); - switch (extension) { - case 'png': - screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Png; - break; - case 'jpeg': - case 'jpg': - screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Jpeg; - break; - case 'webp': - screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Webp; - break; - default: - throw new Error( - `Unsupported screenshot type for extension \`.${extension}\`` - ); - } - } + async _screenshot(options: Readonly): Promise { + const { + fromSurface, + omitBackground, + optimizeForSpeed, + quality, + clip, + type, + captureBeyondViewport, + } = options; - if (options.quality) { - assert( - screenshotType === Protocol.Page.CaptureScreenshotRequestFormat.Jpeg || - screenshotType === Protocol.Page.CaptureScreenshotRequestFormat.Webp, - 'options.quality is unsupported for the ' + - screenshotType + - ' screenshots' - ); - assert( - typeof options.quality === 'number', - 'Expected options.quality to be a number but found ' + - typeof options.quality - ); - assert( - Number.isInteger(options.quality), - 'Expected options.quality to be an integer' - ); - assert( - options.quality >= 0 && options.quality <= 100, - 'Expected options.quality to be between 0 and 100 (inclusive), got ' + - options.quality - ); - } - assert( - !options.clip || !options.fullPage, - 'options.clip and options.fullPage are exclusive' - ); - if (options.clip) { - assert( - typeof options.clip.x === 'number', - 'Expected options.clip.x to be a number but found ' + - typeof options.clip.x - ); - assert( - typeof options.clip.y === 'number', - 'Expected options.clip.y to be a number but found ' + - typeof options.clip.y - ); - assert( - typeof options.clip.width === 'number', - 'Expected options.clip.width to be a number but found ' + - typeof options.clip.width - ); - assert( - typeof options.clip.height === 'number', - 'Expected options.clip.height to be a number but found ' + - typeof options.clip.height - ); - assert( - options.clip.width !== 0, - 'Expected options.clip.width not to be 0.' - ); - assert( - options.clip.height !== 0, - 'Expected options.clip.height not to be 0.' - ); - } - return await this.#screenshotTaskQueue.postTask(() => { - return this.#screenshotTask(screenshotType, options); - }); - } - - async #screenshotTask( - format: Protocol.Page.CaptureScreenshotRequestFormat, - options: ScreenshotOptions = {} - ): Promise { - await this.#client.send('Target.activateTarget', { - targetId: this.#target._targetId, - }); - let clip = options.clip ? processClip(options.clip) : undefined; - let captureBeyondViewport = options.captureBeyondViewport ?? true; - const fromSurface = options.fromSurface; - - if (options.fullPage) { - // Overwrite clip for full page. - clip = undefined; - - if (!captureBeyondViewport) { - const metrics = await this.#client.send('Page.getLayoutMetrics'); - // Fallback to `contentSize` in case of using Firefox. - const {width, height} = metrics.cssContentSize || metrics.contentSize; - const { - isMobile = false, - deviceScaleFactor = 1, - isLandscape = false, - } = this.#viewport || {}; - const screenOrientation: Protocol.Emulation.ScreenOrientation = - isLandscape - ? {angle: 90, type: 'landscapePrimary'} - : {angle: 0, type: 'portraitPrimary'}; - await this.#client.send('Emulation.setDeviceMetricsOverride', { - mobile: isMobile, - width, - height, - deviceScaleFactor, - screenOrientation, - }); - } - } else if (!clip) { - captureBeyondViewport = false; - } - - const shouldSetDefaultBackground = - options.omitBackground && (format === 'png' || format === 'webp'); - if (shouldSetDefaultBackground) { + await using stack = new AsyncDisposableStack(); + if (omitBackground && (type === 'png' || type === 'webp')) { await this.#emulationManager.setTransparentBackgroundColor(); + stack.defer(async () => { + await this.#emulationManager.resetDefaultBackgroundColor(); + }); } - const result = await this.#client.send('Page.captureScreenshot', { - format, - optimizeForSpeed: options.optimizeForSpeed, - quality: options.quality, + const {data} = await this.#client.send('Page.captureScreenshot', { + format: type, + optimizeForSpeed, + quality, clip: clip && { ...clip, scale: clip.scale ?? 1, }, - captureBeyondViewport, fromSurface, + captureBeyondViewport, }); - if (shouldSetDefaultBackground) { - await this.#emulationManager.resetDefaultBackgroundColor(); - } - - if (options.fullPage && this.#viewport) { - await this.setViewport(this.#viewport); - } - - if (options.encoding === 'base64') { - return result.data; - } - - const buffer = Buffer.from(result.data, 'base64'); - await this._maybeWriteBufferToFile(options.path, buffer); - - return buffer; - - function processClip(clip: ScreenshotClip): ScreenshotClip { - const x = Math.round(clip.x); - const y = Math.round(clip.y); - const width = Math.round(clip.width + clip.x - x); - const height = Math.round(clip.height + clip.y - y); - return {x, y, width, height, scale: clip.scale}; - } + return data; } override async createPDFStream(options: PDFOptions = {}): Promise { diff --git a/packages/puppeteer-core/src/cdp/Target.ts b/packages/puppeteer-core/src/cdp/Target.ts index 48ed6e33f59..4d226d7b462 100644 --- a/packages/puppeteer-core/src/cdp/Target.ts +++ b/packages/puppeteer-core/src/cdp/Target.ts @@ -19,9 +19,8 @@ import {type Protocol} from 'devtools-protocol'; import type {Browser} from '../api/Browser.js'; import type {BrowserContext} from '../api/BrowserContext.js'; import {type CDPSession} from '../api/CDPSession.js'; -import {type Page, PageEvent} from '../api/Page.js'; +import {PageEvent, type Page} from '../api/Page.js'; import {Target, TargetType} from '../api/Target.js'; -import {type TaskQueue} from '../common/TaskQueue.js'; import {debugError} from '../common/util.js'; import {type Viewport} from '../common/Viewport.js'; import {Deferred} from '../util/Deferred.js'; @@ -189,7 +188,6 @@ export class CdpTarget extends Target { export class PageTarget extends CdpTarget { #defaultViewport?: Viewport; protected pagePromise?: Promise; - #screenshotTaskQueue: TaskQueue; #ignoreHTTPSErrors: boolean; constructor( @@ -199,13 +197,11 @@ export class PageTarget extends CdpTarget { targetManager: TargetManager, sessionFactory: (isAutoAttachEmulated: boolean) => Promise, ignoreHTTPSErrors: boolean, - defaultViewport: Viewport | null, - screenshotTaskQueue: TaskQueue + defaultViewport: Viewport | null ) { super(targetInfo, session, browserContext, targetManager, sessionFactory); this.#ignoreHTTPSErrors = ignoreHTTPSErrors; this.#defaultViewport = defaultViewport ?? undefined; - this.#screenshotTaskQueue = screenshotTaskQueue; } override _initialize(): void { @@ -246,8 +242,7 @@ export class PageTarget extends CdpTarget { client, this, this.#ignoreHTTPSErrors, - this.#defaultViewport ?? null, - this.#screenshotTaskQueue + this.#defaultViewport ?? null ); }); } diff --git a/packages/puppeteer-core/src/common/util.ts b/packages/puppeteer-core/src/common/util.ts index 0b16a3dbd81..860677b6be9 100644 --- a/packages/puppeteer-core/src/common/util.ts +++ b/packages/puppeteer-core/src/common/util.ts @@ -29,7 +29,6 @@ import {type Page} from '../api/Page.js'; import {isNode} from '../environment.js'; import {assert} from '../util/assert.js'; import {Deferred} from '../util/Deferred.js'; -import {disposeSymbol} from '../util/disposable.js'; import {isErrorLike} from '../util/ErrorLike.js'; import {debug} from './Debug.js'; @@ -606,45 +605,6 @@ export function validateDialogType( return dialogType as 'alert' | 'confirm' | 'prompt' | 'beforeunload'; } -/** - * @internal - */ -export class Mutex { - static Guard = class Guard { - #mutex: Mutex; - constructor(mutex: Mutex) { - this.#mutex = mutex; - } - [disposeSymbol](): void { - return this.#mutex.release(); - } - }; - - #locked = false; - #acquirers: Array<() => void> = []; - - // This is FIFO. - async acquire(): Promise> { - if (!this.#locked) { - this.#locked = true; - return new Mutex.Guard(this); - } - const deferred = Deferred.create(); - this.#acquirers.push(deferred.resolve.bind(deferred)); - await deferred.valueOrThrow(); - return new Mutex.Guard(this); - } - - release(): void { - const resolve = this.#acquirers.shift(); - if (!resolve) { - this.#locked = false; - return; - } - resolve(); - } -} - /** * @internal */ diff --git a/packages/puppeteer-core/src/util/Mutex.ts b/packages/puppeteer-core/src/util/Mutex.ts new file mode 100644 index 00000000000..9498bac3060 --- /dev/null +++ b/packages/puppeteer-core/src/util/Mutex.ts @@ -0,0 +1,41 @@ +import {Deferred} from './Deferred.js'; +import {disposeSymbol} from './disposable.js'; + +/** + * @internal + */ +export class Mutex { + static Guard = class Guard { + #mutex: Mutex; + constructor(mutex: Mutex) { + this.#mutex = mutex; + } + [disposeSymbol](): void { + return this.#mutex.release(); + } + }; + + #locked = false; + #acquirers: Array<() => void> = []; + + // This is FIFO. + async acquire(): Promise> { + if (!this.#locked) { + this.#locked = true; + return new Mutex.Guard(this); + } + const deferred = Deferred.create(); + this.#acquirers.push(deferred.resolve.bind(deferred)); + await deferred.valueOrThrow(); + return new Mutex.Guard(this); + } + + release(): void { + const resolve = this.#acquirers.shift(); + if (!resolve) { + this.#locked = false; + return; + } + resolve(); + } +} diff --git a/packages/puppeteer-core/src/util/decorators.ts b/packages/puppeteer-core/src/util/decorators.ts index f2c3a88ac5d..eb2a62a7f5b 100644 --- a/packages/puppeteer-core/src/util/decorators.ts +++ b/packages/puppeteer-core/src/util/decorators.ts @@ -17,6 +17,7 @@ import {type Disposed, type Moveable} from '../common/types.js'; import {asyncDisposeSymbol, disposeSymbol} from './disposable.js'; +import {Mutex} from './Mutex.js'; const instances = new WeakSet(); @@ -112,3 +113,26 @@ export function invokeAtMostOnceForArguments( return target.call(this, ...args); }; } + +export function guarded( + getKey = function (this: T): object { + return this; + } +) { + return ( + target: (this: T, ...args: any[]) => Promise, + _: ClassMethodDecoratorContext + ): typeof target => { + const mutexes = new WeakMap(); + return async function (...args) { + const key = getKey.call(this); + let mutex = mutexes.get(key); + if (!mutex) { + mutex = new Mutex(); + mutexes.set(key, mutex); + } + await using _ = await mutex.acquire(); + return await target.call(this, ...args); + }; + }; +} diff --git a/packages/puppeteer-core/src/util/disposable.ts b/packages/puppeteer-core/src/util/disposable.ts index 94825900f5f..8c323b2ede3 100644 --- a/packages/puppeteer-core/src/util/disposable.ts +++ b/packages/puppeteer-core/src/util/disposable.ts @@ -167,3 +167,119 @@ export class DisposableStack { readonly [Symbol.toStringTag] = 'DisposableStack'; } + +/** + * @internal + */ +export class AsyncDisposableStack { + #disposed = false; + #stack: AsyncDisposable[] = []; + + /** + * Returns a value indicating whether this stack has been disposed. + */ + get disposed(): boolean { + return this.#disposed; + } + + /** + * Disposes each resource in the stack in the reverse order that they were added. + */ + async dispose(): Promise { + if (this.#disposed) { + return; + } + this.#disposed = true; + for (const resource of this.#stack.reverse()) { + await resource[asyncDisposeSymbol](); + } + } + + /** + * Adds a disposable resource to the stack, returning the resource. + * + * @param value - The resource to add. `null` and `undefined` will not be added, + * but will be returned. + * @returns The provided {@link value}. + */ + use(value: T): T { + if (value) { + this.#stack.push(value); + } + return value; + } + + /** + * Adds a value and associated disposal callback as a resource to the stack. + * + * @param value - The value to add. + * @param onDispose - The callback to use in place of a `[disposeSymbol]()` + * method. Will be invoked with `value` as the first parameter. + * @returns The provided {@link value}. + */ + adopt(value: T, onDispose: (value: T) => Promise): T { + this.#stack.push({ + [asyncDisposeSymbol]() { + return onDispose(value); + }, + }); + return value; + } + + /** + * Adds a callback to be invoked when the stack is disposed. + */ + defer(onDispose: () => Promise): void { + this.#stack.push({ + [asyncDisposeSymbol]() { + return onDispose(); + }, + }); + } + + /** + * Move all resources out of this stack and into a new `DisposableStack`, and + * marks this stack as disposed. + * + * @example + * + * ```ts + * class C { + * #res1: Disposable; + * #res2: Disposable; + * #disposables: DisposableStack; + * constructor() { + * // stack will be disposed when exiting constructor for any reason + * using stack = new DisposableStack(); + * + * // get first resource + * this.#res1 = stack.use(getResource1()); + * + * // get second resource. If this fails, both `stack` and `#res1` will be disposed. + * this.#res2 = stack.use(getResource2()); + * + * // all operations succeeded, move resources out of `stack` so that + * // they aren't disposed when constructor exits + * this.#disposables = stack.move(); + * } + * + * [disposeSymbol]() { + * this.#disposables.dispose(); + * } + * } + * ``` + */ + move(): AsyncDisposableStack { + if (this.#disposed) { + throw new ReferenceError('a disposed stack can not use anything new'); // step 3 + } + const stack = new AsyncDisposableStack(); // step 4-5 + stack.#stack = this.#stack; + this.#disposed = true; + return stack; + } + + [asyncDisposeSymbol] = this.dispose; + + readonly [Symbol.toStringTag] = 'AsyncDisposableStack'; +} diff --git a/test/TestExpectations.json b/test/TestExpectations.json index c4c61af2927..802affc68b4 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -317,6 +317,18 @@ "parameters": ["webDriverBiDi"], "expectations": ["PASS"] }, + { + "testIdPattern": "[screenshot.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Cdp *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, { "testIdPattern": "[stacktrace.spec] Stack trace *", "platforms": ["darwin", "linux", "win32"], @@ -791,12 +803,6 @@ "parameters": ["firefox"], "expectations": ["SKIP"] }, - { - "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should take fullPage screenshots when defaultViewport is null", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["webDriverBiDi"], - "expectations": ["FAIL"] - }, { "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments", "platforms": ["darwin", "linux", "win32"], @@ -1260,9 +1266,15 @@ "expectations": ["FAIL", "SKIP"] }, { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot *", + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should get screenshot bigger than the viewport", "platforms": ["darwin", "linux", "win32"], - "parameters": ["chrome", "webDriverBiDi"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should use scale for clip", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], "expectations": ["FAIL"] }, { @@ -3626,8 +3638,8 @@ { "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should capture full element when larger than viewport", "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["SKIP"] + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] }, { "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset", @@ -3641,71 +3653,29 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL"] }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should allow transparency", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["FAIL"] - }, { "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should clip rect", "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["FAIL", "PASS"] - }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should get screenshot bigger than the viewport", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["SKIP"] - }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should render white background on jpeg file", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], + "parameters": ["firefox", "webDriverBiDi"], "expectations": ["FAIL"] }, { "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", "platforms": ["darwin", "linux", "win32"], - "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["PASS"] - }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["FAIL", "PASS"] + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] }, { "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots", "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], + "parameters": ["firefox", "webDriverBiDi"], "expectations": ["FAIL"] }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should use scale for clip", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["FAIL", "PASS"] - }, { "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work", "platforms": ["darwin", "linux", "win32"], - "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["PASS"] - }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["FAIL", "PASS"] - }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work with webp", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "firefox"], - "expectations": ["SKIP"] + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] }, { "testIdPattern": "[stacktrace.spec] Stack trace should work for none error objects", @@ -4068,28 +4038,46 @@ "expectations": ["FAIL", "PASS"] }, { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", - "platforms": ["linux"], + "testIdPattern": "[screenshot.spec] Screenshots Cdp should work in \"fromSurface: false\" mode", + "platforms": ["darwin", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Cdp should work in \"fromSurface: false\" mode", + "platforms": ["darwin"], "parameters": ["cdp", "chrome", "new-headless"], - "expectations": ["FAIL", "PASS"] + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should clip rect", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should use scale for clip", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] }, { "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work", - "platforms": ["linux"], - "parameters": ["cdp", "chrome", "new-headless"], - "expectations": ["FAIL", "PASS"] - }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work in \"fromSurface: false\" mode", "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "chrome", "headless"], - "expectations": ["SKIP"] - }, - { - "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work in \"fromSurface: false\" mode", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "chrome", "new-headless"], - "expectations": ["SKIP"] + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] }, { "testIdPattern": "[worker.spec] Workers Page.workers", diff --git a/test/golden-chrome/screenshot-element-rotate.png b/test/golden-chrome/screenshot-element-rotate.png index 52e2a0f6d3c66bd455961250051d416897790ffd..d0c05ba795c1fc4b1b8bd128860f5c19fb92c20f 100644 GIT binary patch delta 22 ecmZ1`v{`6^GB*Qbage(c!@6@aFE=W(asmKPC0j10DbX diff --git a/test/golden-firefox/grid-cell-0.png b/test/golden-firefox/grid-cell-0.png index 4677bdbc4f84999748dc38c3c2038b729acfae17..2e671db41c464f2717fe405eca03be40a99eab92 100644 GIT binary patch delta 525 zcmX@jw2WnfO8r|;7srqc=eJWgW*>5pI68kpw_cx~pOY+?R#@irM*-XUxSSjV+Wll* z#1Fb&@O%-_u2=Sve}nVXYpxrVnl!G~A9P%`TXfD^bBPN_Rm$Cce;TRX-@E5~U3urq zxb^uT`1ORji=G!rTAg}b!o^kO#nE{}^I}3=2Z!LqD-5~1o=59nO;c6cwR-ZF2^}6< zVcT^CFUHPY((2;kyYYQD>!gz^VXH-LCc^86qY$KY>ndm z9=AT})yB(?J7bRdZ(n`$Mg2zBpynL4$$P)Q-*)@q-(YRunEzbn}{U{;ANz0*SwM^4G&w7d9I_f72tAw@_B_aL(G;32uuU zS6p5xP$}6z@%;1N(i`U=74EqEZm+-YZ{EDGpB5htY30D zQ$>hVq}z4N?c6O#sFr}>#@bJ-1qF#^Yq%Ua^{@Aq{;WyU#b&L(Fqr`eJYCuRT-G@yGywqV)%OAb delta 304 zcmZ3+a++y^O1+S$i(^Pe`0>@Z;i8;^IQA_otbYtveVC7eRc zZC&qP_EhO7radv(S-bO*`Eir@OJe6N>zoSCO*B-S>$GI4K&GZk6xYOQ2(%A97x(di)+@Rxq?z2y}F5`z?>$WIV*3a9#YSrzuwW*g)G94KTW-YsYtyH$x z@TKFv{PnM=@jS8MV@lw7xK01G6GP8Rhi|p&4D+7PG;8_zef8I-KVKvJov)Sd&3$aK z?E8()I=??&eOBq0of|bFQG%l(_qI{XfnDo38*Xn~%9_~o=Jz~?d%cf;oUrV7{%73k s;jMTnsAG$Wrjjn44Pw6L{n@gQp>Mmfw3^9*bqqk@>FVdQ&MBb@0ND_W*Z=?k diff --git a/test/golden-firefox/grid-cell-1.png b/test/golden-firefox/grid-cell-1.png index 532dc8db65b0424f59369dc874c6b1a100e23bb8..a2a61af3d314be220a1a4b109dbfcc4aaafbba17 100644 GIT binary patch delta 314 zcmV-A0mc5w0n`GJBYy!KNklgcK=qE87&Y zun;Q?!OBLk4=5;#AXr!lHcCYM%))-MVN%cT&%C$0UQrZpX~E}JMaKP}(^aPBURb0_ z3TdQCHmZN25gM^XfJe&`SmSMJ#2B5Wg*0Lbtns!qVvNqxLVp@Tz;2!|3Bynx>lLyL z%BmRg21D_E-`ygU?M|{R`;v9KNM$05A_;;(@;rCf$ntiVQMV(D^Ht(FcDKm?w$2Vm zNz;@?>O5a6uNIBeQSr;r2mni3bF}8CizG?n{;P%9GtH{p5!0VT*0BgeOCw7o8kCV4 zG=k95$kK=gWiw<3jUco%vNWPW8JR&N2rZ2)jc8CtX0*=;(!QzL4~|1^7Ot@}N&o-= M07*qoM6N<$f~0AUV*mgE delta 174 zcmcb@bdqs`ay`QYPZ!6KjK;U;H}W1)5MVj5w&BqoR@1vP1R8i`lGD=gRy;E<)K$^ RvhFbefv2mV%Q~loCIEKsP3Zst diff --git a/test/golden-firefox/screenshot-clip-odd-size.png b/test/golden-firefox/screenshot-clip-odd-size.png index 8e86dc90178ac70ddba9e087de3b3c80f0bd1b2d..a6f69dd20a701ab060af7f7c1370a2f1447caa60 100644 GIT binary patch delta 49 zcmeYcn4qE~?&;zfA`zbaho6PVh3Qz}Q4d8^5oczGJ4V_Mv=1(6WB>wBS3j3^P6 diff --git a/test/golden-firefox/screenshot-clip-rect-scale2.png b/test/golden-firefox/screenshot-clip-rect-scale2.png index d713d279439265b0abf6460c8c4a5726e2b8ffed..5cce794edb2ebc8804116b47b6b1f7a0c954a961 100644 GIT binary patch literal 10361 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 z zzA?s%m2^^ty!UxaUd9ke-}g90Tx~dybIiBH9rtO>l>7HUZ&LPlcIE$5S(n{;PyxIc z(DD0Sfd4H|+5ZUge*<4RdyD;p1o_i8;PrrxD-yLg@2T^c)0==hB2t#ukSmrkfBzpG CZOnrJ diff --git a/test/golden-firefox/screenshot-clip-rect.png b/test/golden-firefox/screenshot-clip-rect.png index 7a744578693d3594b2a278978fb3807a6856cdef..0a96e67f9a6b5aa9f791f36733c12cefc3b33a43 100644 GIT binary patch literal 2501 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 literal 1371 zcmeAS@N?(olHy`uVBq!ia0vp^(||aIgAGJRpZvhUz^dfw;uumf=k476UfQuDN6ROd z%~M$G%f+o-y>Yfz4YTLM*0l$nZm*nTVzx|}Nzgt&ybl+@P7%FD}Vf6mX(pQXY%CnD80?Br3Q)o*Hich>FiSG{)Y)~v1$Ia%44 z877xhJVRW*{QI}>lcf@qdRT1#`zJ}Aq2+ew*I$1HN<4Yua{k=8uKk=X5#{COT1r*L z#hVqPXD#FYdHVF}N1{zC({3N0uHk!mY4OG*MdzQNp0+IWguIQogMGqym|5OpWT#*Ylr7Y_oo7NZTkGP*m}OTG+#TnrQpSj86xp( zv?i%&ie{V57B!ch6cHNHAFy+Ng|^P&bK%o(-ntbeco4`*PEK}qb(ueJo|f_T>(@Ud z?X-0Qy8G$8^>04Def##&W6$;14~t|M7YDn#oIig)yZ8k{h2HmHzgF$uy&LG6s;Vy4 zu{gvty>FGz zv{dfR2NMLIXtygQsd_G&>zBUrxk6W}t<%$_&eZ3tPxl;ueCd4b-Z=ip$4+fpQ&B1- zCzrMVzP^Q>U0!d5ukEz6z?gVh^Kb37mn9`Oa<4ysF8+8lt3$dtbt4}iUzd_yYP`9` z<(DE}YnI8KewNgEwZzUo-)Lsa<(DO?E1UL;DjleIN`}WdRT2)@Mh;Hiqe}l-X8Nf8 zTKPFFIn@rBCf6L_eJ+}pk^~8JoU6gcKfZH)D{pV;Qk!=Fv9{&ygf5k7_mi*2h-_SC zR(j3$x%SNYVyjoR&tDCcQ3wm|fB$Sz$J4x5U(ze*=|B0)yIKPneSae7JwJNpZo(^2 zA}iT=NA91_`Oiy>1M}BBEQqL5?sZ#yY3J@lQP(y7_O^d!e%&xXw|V9K&AiJt9^nDCNc;4r)|p>FJb%Y_d4f{A0EGL) zPp>QD-7>%OyxCT_m4LZvZZv;+nzWDe{XWGbMb$fsUvJFOd(|7dedT7|q?@`SR}b&K z`pRs^F|pTs7alJ!x_IQ*l~2BNOF?0)a?Rb9|NF;fAm8i_{}Q1p?z`s3hxr%q6dAA> jBB9*rcG7Wj`YbKZ>AzxsJFf|_&}8s*^>bP0l+XkK?WceC diff --git a/test/golden-firefox/screenshot-element-bounding-box.png b/test/golden-firefox/screenshot-element-bounding-box.png index f4e059c300ccd27816427fb0dc2c466edc640418..63956b2a7c3a9c4a33b883526e8d08657e33dea0 100644 GIT binary patch delta 489 zcmdna)WkADxt{T)r;B4qhV$Dg8@-tdMULmja zQGB>4SGQHytJ~C)&27Pjiz?=$><`s*j2f4(-*H84O!L_k`)P^r}Ruv<#hU8mJooy=ytYKSjd?lKAm0p^dx!@D5AMO`o6svh<#?>myfa z=*P0<{|&Yjm+>7o*dzR;e!o?_^63t@#rOD(cF%R%e6wZU>j){?qT13oEIQ$@zk7=Z z{mw9v(h%#m=+M1YsbrhZFBIXq=x*NP5UoUWBY&fpE=Qz}EI$(|m$g;u+A_5{KNQbz zU^TRVlefM5dwcaJOLM1b3d^^bJ}I(1?QS)98&j?9{`>up&z73Y$)4TBax-bSM)dt7 z@#=E@)xW>LUU5yXEu!PggLa*%3Qu0mXmM&;8SyDf(P>?ur-sXnF4gs-N?3(uZIqs9 c{qp$1d24N@nETrJCI%qzboFyt=akR{0GaXH>;M1& delta 285 zcmZo-+0HaUxt`&lr;B4qM&sM7itAboL|PvvZzyMt&|x~Nbc6Mv!Uln`hQkYQIEbn| zP*{_2CFh0BR{>+mn@^_2e4IV&S@!HWi|0RD9?Vg`zkY#n$2Oity3HA<6>b?Jn4h>_ z?&@RUXvm!P$>#hA1$$n;>}`-cwo+j5l|^iF?j)$US~IZ-GhGI9Oa zO*)60{&=|@YEOxDW7xA!JH}()bCv^zGJD@XlodK|$XGB-ZGHPw(NFfy*Gj*3ec%!l zbPIeU`#{u)=WwmG@8u1vR_)&R{&3@!(pqOb?ti~|KV?cDQDD0`-Em8*K(3@qG~2~# gV5Tkqrxvk)4CmvMbN;#fp3VRSp00i_>zopr02bYZKL7v# diff --git a/test/golden-firefox/screenshot-element-fractional.png b/test/golden-firefox/screenshot-element-fractional.png index d1431bd91dea17468f279240d5eb2ef56c606e33..5f58502b49ed59f7807240ab01be3d8db68117c0 100644 GIT binary patch delta 121 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 literal 26146 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 siv2j|fBXqN|ICKzNYvN4 fzW~T20yHdVoU??1aarlqK9FipS3j3^P6^lOiET&UP@Y7ModgWM?qOlT~kX_QeM|USHnP6LsJ7}2qQZ?I~NC+Fc+7w zhLo6;2Fc+60R}-1h7^VrW=16jCP7AKLB{__803NOWMu>c1}I=;Vg|aGor9B$8>nEb z00R>vGcywlGb<|#3s7|}P@aKBkX1<0(2-3zFp*uUP{gQl;zAB(r;P_igD!qhF-|IK z;^Yz&myncFRa4i{)G{$OGqmaka3YSZQ|TeofBv2)j3M&~ka)>xhT)6Qdr?PR-2hpUWi(FzVCJ$9Vg1iRy o8F3zKBFkrRk0JbZi-Cuk5g2*Qf(-Tyzm|1rEMOYqnE(Gx09#cG^8f$< literal 0 HcmV?d00001 diff --git a/test/src/screenshot.spec.ts b/test/src/screenshot.spec.ts index d5941bc509a..4cdf419b3b6 100644 --- a/test/src/screenshot.spec.ts +++ b/test/src/screenshot.spec.ts @@ -139,36 +139,6 @@ describe('Screenshots', function () { }) ); }); - it('should allow transparency', async () => { - const {page, server} = await getTestState(); - - await page.setViewport({width: 100, height: 100}); - await page.goto(server.EMPTY_PAGE); - const screenshot = await page.screenshot({omitBackground: true}); - expect(screenshot).toBeGolden('transparent.png'); - }); - it('should render white background on jpeg file', async () => { - const {page, server} = await getTestState(); - - await page.setViewport({width: 100, height: 100}); - await page.goto(server.EMPTY_PAGE); - const screenshot = await page.screenshot({ - omitBackground: true, - type: 'jpeg', - }); - expect(screenshot).toBeGolden('white.jpg'); - }); - it('should work with webp', async () => { - const {page, server} = await getTestState(); - - await page.setViewport({width: 100, height: 100}); - await page.goto(server.PREFIX + '/grid.html'); - const screenshot = await page.screenshot({ - type: 'webp', - }); - - expect(screenshot).toBeInstanceOf(Buffer); - }); it('should work with odd clip size on Retina displays', async () => { const {page} = await getTestState(); @@ -194,16 +164,6 @@ describe('Screenshots', function () { 'screenshot-sanity.png' ); }); - it('should work in "fromSurface: false" mode', async () => { - const {page, server} = await getTestState(); - - await page.setViewport({width: 500, height: 500}); - await page.goto(server.PREFIX + '/grid.html'); - const screenshot = await page.screenshot({ - fromSurface: false, - }); - expect(screenshot).toBeDefined(); // toBeGolden('screenshot-fromsurface-false.png'); - }); }); describe('ElementHandle.screenshot', function () { @@ -383,4 +343,47 @@ describe('Screenshots', function () { expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); }); }); + + describe('Cdp', () => { + it('should allow transparency', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 100, height: 100}); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({omitBackground: true}); + expect(screenshot).toBeGolden('transparent.png'); + }); + it('should render white background on jpeg file', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 100, height: 100}); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({ + omitBackground: true, + type: 'jpeg', + }); + expect(screenshot).toBeGolden('white.jpg'); + }); + it('should work with webp', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 100, height: 100}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + type: 'webp', + }); + + expect(screenshot).toBeInstanceOf(Buffer); + }); + it('should work in "fromSurface: false" mode', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fromSurface: false, + }); + expect(screenshot).toBeDefined(); // toBeGolden('screenshot-fromsurface-false.png'); + }); + }); });