chore: implement element screenshot (#10963)

This commit is contained in:
jrandolf 2023-09-21 21:21:12 +02:00 committed by GitHub
parent 3238b93a79
commit ecd6ac9dfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 740 additions and 614 deletions

View File

@ -156,7 +156,7 @@ module.exports = {
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'error', 'error',
{argsIgnorePattern: '^_'}, {argsIgnorePattern: '^_', varsIgnorePattern: '^_'},
], ],
'func-call-spacing': 'off', 'func-call-spacing': 'off',
'@typescript-eslint/func-call-spacing': 'error', '@typescript-eslint/func-call-spacing': 'error',

View File

@ -89,6 +89,7 @@ sidebar_label: API
| [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | Set of configurable options for CSS coverage. | | [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | Set of configurable options for CSS coverage. |
| [CustomQueryHandler](./puppeteer.customqueryhandler.md) | | | [CustomQueryHandler](./puppeteer.customqueryhandler.md) | |
| [Device](./puppeteer.device.md) | | | [Device](./puppeteer.device.md) | |
| [ElementScreenshotOptions](./puppeteer.elementscreenshotoptions.md) | |
| [FrameAddScriptTagOptions](./puppeteer.frameaddscripttagoptions.md) | | | [FrameAddScriptTagOptions](./puppeteer.frameaddscripttagoptions.md) | |
| [FrameAddStyleTagOptions](./puppeteer.frameaddstyletagoptions.md) | | | [FrameAddStyleTagOptions](./puppeteer.frameaddstyletagoptions.md) | |
| [FrameEvents](./puppeteer.frameevents.md) | | | [FrameEvents](./puppeteer.frameevents.md) | |

View File

@ -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. | | [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). | | [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). | | [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. | | [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 <code>change</code> and <code>input</code> event once all the provided options have been selected. If there's no <code>&lt;select&gt;</code> element matching <code>selector</code>, the method throws an error. | | [select(values)](./puppeteer.elementhandle.select.md) | | Triggers a <code>change</code> and <code>input</code> event once all the provided options have been selected. If there's no <code>&lt;select&gt;</code> element matching <code>selector</code>, 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. | | [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. |

View File

@ -4,7 +4,7 @@ sidebar_label: ElementHandle.screenshot
# ElementHandle.screenshot() method # 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: #### Signature:
@ -12,7 +12,7 @@ This method scrolls element into view if needed, and then uses [Page.screenshot(
class ElementHandle { class ElementHandle {
screenshot( screenshot(
this: ElementHandle<Element>, this: ElementHandle<Element>,
options?: ScreenshotOptions options?: Readonly<ElementScreenshotOptions>
): Promise<string | Buffer>; ): Promise<string | Buffer>;
} }
``` ```
@ -20,9 +20,9 @@ class ElementHandle {
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ------------ | | --------- | ----------------------------------------------------------------------------------- | ------------ |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | | | this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) | _(Optional)_ | | options | Readonly&lt;[ElementScreenshotOptions](./puppeteer.elementscreenshotoptions.md)&gt; | _(Optional)_ |
**Returns:** **Returns:**

View File

@ -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 | <code>optional</code> | boolean | | true |

View File

@ -127,9 +127,8 @@ page.off('request', logRequest);
| [reload(options)](./puppeteer.page.reload.md) | | Reloads the page. | | [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 <code>name</code> from the page's <code>window</code> object. | | [removeExposedFunction(name)](./puppeteer.page.removeexposedfunction.md) | | The method removes a previously added function via $[Page.exposeFunction()](./puppeteer.page.exposefunction.md) called <code>name</code> from the page's <code>window</code> object. |
| [removeScriptToEvaluateOnNewDocument(identifier)](./puppeteer.page.removescripttoevaluateonnewdocument.md) | | Removes script that injected into page by Page.evaluateOnNewDocument. | | [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_1.md) | | |
| [screenshot(options)](./puppeteer.page.screenshot_2.md) | | |
| [select(selector, values)](./puppeteer.page.select.md) | | Triggers a <code>change</code> and <code>input</code> event once all the provided options have been selected. If there's no <code>&lt;select&gt;</code> element matching <code>selector</code>, the method throws an error. | | [select(selector, values)](./puppeteer.page.select.md) | | Triggers a <code>change</code> and <code>input</code> event once all the provided options have been selected. If there's no <code>&lt;select&gt;</code> element matching <code>selector</code>, the method throws an error. |
| [setBypassCSP(enabled)](./puppeteer.page.setbypasscsp.md) | | Toggles bypassing page's Content-Security-Policy. | | [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. | | [setBypassServiceWorker(bypass)](./puppeteer.page.setbypassserviceworker.md) | | Toggles ignoring of service worker for each request. |

View File

@ -4,14 +4,14 @@ sidebar_label: Page.screenshot
# Page.screenshot() method # Page.screenshot() method
Captures screenshot of the current page. Captures a screenshot of this [page](./puppeteer.page.md).
#### Signature: #### Signature:
```typescript ```typescript
class Page { class Page {
abstract screenshot( screenshot(
options: ScreenshotOptions & { options: Readonly<ScreenshotOptions> & {
encoding: 'base64'; encoding: 'base64';
} }
): Promise<string>; ): Promise<string>;
@ -21,33 +21,9 @@ class Page {
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ----------------------------------------------------------------------------------- | ----------- | | --------- | --------------------------------------------------------------------------------------------------- | ------------------------------- |
| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) &amp; { encoding: 'base64'; } | | | options | Readonly&lt;[ScreenshotOptions](./puppeteer.screenshotoptions.md)&gt; &amp; { encoding: 'base64'; } | Configures screenshot behavior. |
**Returns:** **Returns:**
Promise&lt;string&gt; Promise&lt;string&gt;
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:<br/> - `x` : x-coordinate of top-left corner of clip area.<br/> - `y` : y-coordinate of top-left corner of clip area.<br/> - `width` : width of clipping area.<br/> - `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`.

View File

@ -8,19 +8,15 @@ sidebar_label: Page.screenshot_1
```typescript ```typescript
class Page { class Page {
abstract screenshot( screenshot(options?: Readonly<ScreenshotOptions>): Promise<Buffer>;
options?: ScreenshotOptions & {
encoding?: 'binary';
}
): Promise<Buffer>;
} }
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ------------------------------------------------------------------------------------ | ------------ | | --------- | --------------------------------------------------------------------- | ------------ |
| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) &amp; { encoding?: 'binary'; } | _(Optional)_ | | options | Readonly&lt;[ScreenshotOptions](./puppeteer.screenshotoptions.md)&gt; | _(Optional)_ |
**Returns:** **Returns:**

View File

@ -1,23 +0,0 @@
---
sidebar_label: Page.screenshot_2
---
# Page.screenshot() method
#### Signature:
```typescript
class Page {
abstract screenshot(options?: ScreenshotOptions): Promise<Buffer | string>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------- | ------------ |
| options | [ScreenshotOptions](./puppeteer.screenshotoptions.md) | _(Optional)_ |
**Returns:**
Promise&lt;Buffer \| string&gt;

View File

@ -13,14 +13,14 @@ export interface ScreenshotOptions
## Properties ## Properties
| Property | Modifiers | Type | Description | Default | | Property | Modifiers | Type | Description | Default |
| --------------------- | --------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | --------------------- | --------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| captureBeyondViewport | <code>optional</code> | boolean | Capture the screenshot beyond the viewport. | <code>true</code> | | captureBeyondViewport | <code>optional</code> | boolean | Capture the screenshot beyond the viewport. | <code>true</code> |
| clip | <code>optional</code> | [ScreenshotClip](./puppeteer.screenshotclip.md) | An object which specifies the clipping region of the page. | | | clip | <code>optional</code> | [ScreenshotClip](./puppeteer.screenshotclip.md) | Specifies the region of the page to clip. | |
| encoding | <code>optional</code> | 'base64' \| 'binary' | Encoding of the image. | <code>binary</code> | | encoding | <code>optional</code> | 'base64' \| 'binary' | Encoding of the image. | <code>'binary'</code> |
| fromSurface | <code>optional</code> | boolean | Capture the screenshot from the surface, rather than the view. | <code>true</code> | | fromSurface | <code>optional</code> | boolean | Capture the screenshot from the surface, rather than the view. | <code>false</code> |
| fullPage | <code>optional</code> | boolean | When <code>true</code>, takes a screenshot of the full page. | <code>false</code> | | fullPage | <code>optional</code> | boolean | When <code>true</code>, takes a screenshot of the full page. | <code>false</code> |
| omitBackground | <code>optional</code> | boolean | Hides default white background and allows capturing screenshots with transparency. | <code>false</code> | | omitBackground | <code>optional</code> | boolean | Hides default white background and allows capturing screenshots with transparency. | <code>false</code> |
| optimizeForSpeed | <code>optional</code> | boolean | | <code>false</code> | | optimizeForSpeed | <code>optional</code> | boolean | | <code>false</code> |
| path | <code>optional</code> | 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. | | | path | <code>optional</code> | 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 | <code>optional</code> | number | Quality of the image, between 0-100. Not applicable to <code>png</code> images. | | | quality | <code>optional</code> | number | Quality of the image, between 0-100. Not applicable to <code>png</code> images. | |
| type | <code>optional</code> | 'png' \| 'jpeg' \| 'webp' | | <code>png</code> | | type | <code>optional</code> | 'png' \| 'jpeg' \| 'webp' | | <code>'png'</code> |

View File

@ -103,6 +103,16 @@ export interface Point {
y: number; y: number;
} }
/**
* @public
*/
export interface ElementScreenshotOptions extends ScreenshotOptions {
/**
* @defaultValue true
*/
scrollIntoView?: boolean;
}
/** /**
* ElementHandle represents an in-page DOM element. * 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. * {@link Page.(screenshot:3) } to take a screenshot of the element.
* If the element is detached from DOM, the method throws an error. * If the element is detached from DOM, the method throws an error.
*/ */
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async screenshot( async screenshot(
this: ElementHandle<Element>, this: ElementHandle<Element>,
options?: ScreenshotOptions options: Readonly<ElementScreenshotOptions> = {}
): Promise<string | Buffer>; ): Promise<string | Buffer> {
async screenshot(this: ElementHandle<Element>): Promise<string | Buffer> { const {
throw new Error('Not implemented'); 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;
} }
/** /**

View File

@ -81,8 +81,13 @@ import {
} from '../common/util.js'; } from '../common/util.js';
import type {Viewport} from '../common/Viewport.js'; import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {guarded} from '../util/decorators.js';
import {type Deferred} from '../util/Deferred.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 {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js'; import type {BrowserContext} from './BrowserContext.js';
@ -227,9 +232,31 @@ export interface ScreenshotOptions {
*/ */
optimizeForSpeed?: boolean; optimizeForSpeed?: boolean;
/** /**
* @defaultValue `png` * @defaultValue `'png'`
*/ */
type?: 'png' | 'jpeg' | 'webp'; 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 * 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 * from file extension. If path is a relative path, then it is resolved
@ -238,38 +265,29 @@ export interface ScreenshotOptions {
*/ */
path?: string; path?: string;
/** /**
* When `true`, takes a screenshot of the full page. * Specifies the region of the page to clip.
* @defaultValue `false`
*/
fullPage?: boolean;
/**
* An object which specifies the clipping region of the page.
*/ */
clip?: ScreenshotClip; 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. * Encoding of the image.
* @defaultValue `binary` *
* @defaultValue `'binary'`
*/ */
encoding?: 'base64' | 'binary'; encoding?: 'base64' | 'binary';
/** /**
* Capture the screenshot beyond the viewport. * Capture the screenshot beyond the viewport.
*
* @defaultValue `true` * @defaultValue `true`
*/ */
captureBeyondViewport?: boolean; captureBeyondViewport?: boolean;
/** /**
* Capture the screenshot from the surface, rather than the view. * TODO(jrandolf): Investigate whether viewport expansion is a better
* @defaultValue `true` * alternative for cross-browser screenshots as opposed to
* `captureBeyondViewport`.
*
* @internal
*/ */
fromSurface?: boolean; allowViewportExpansion?: boolean;
} }
/** /**
@ -2243,61 +2261,195 @@ export abstract class Page extends EventEmitter<PageEvents> {
} }
/** /**
* Captures screenshot of the current page. * Captures a screenshot of this {@link Page | page}.
* *
* @remarks * @param options - Configures screenshot behavior.
* 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:<br/>
* - `x` : x-coordinate of top-left corner of clip area.<br/>
* - `y` : y-coordinate of top-left corner of clip area.<br/>
* - `width` : width of clipping area.<br/>
* - `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.
*/ */
abstract screenshot( async screenshot(
options: ScreenshotOptions & {encoding: 'base64'} options: Readonly<ScreenshotOptions> & {encoding: 'base64'}
): Promise<string>; ): Promise<string>;
abstract screenshot( async screenshot(options?: Readonly<ScreenshotOptions>): Promise<Buffer>;
options?: ScreenshotOptions & {encoding?: 'binary'} @guarded(function () {
): Promise<Buffer>; return this.browser();
abstract screenshot(options?: ScreenshotOptions): Promise<Buffer | string>; })
async screenshot(
userOptions: Readonly<ScreenshotOptions> = {}
): Promise<Buffer | string> {
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<ScreenshotOptions>): Promise<string>;
/**
* @internal
*/
async _createTemporaryViewportContainingBox(
clip: ScreenshotClip
): Promise<AsyncDisposable> {
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 * @internal
@ -2854,3 +3006,36 @@ function convertPrintParameterToInches(
} }
return pixels / unitToPixels[lengthUnit]; return pixels / unitToPixels[lengthUnit];
} }
/** @see https://w3c.github.io/webdriver-bidi/#normalize-rect */
function normalizeClip(clip: Readonly<ScreenshotClip>): 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>): 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};
}

View File

@ -618,35 +618,53 @@ export class BidiPage extends Page {
} }
} }
override screenshot(
options: ScreenshotOptions & {encoding: 'base64'}
): Promise<string>;
override screenshot(
options?: ScreenshotOptions & {encoding?: 'binary'}
): never;
override async screenshot( override async screenshot(
options: ScreenshotOptions = {} options: Readonly<ScreenshotOptions> & {encoding: 'base64'}
): Promise<string>;
override async screenshot(
options?: Readonly<ScreenshotOptions>
): Promise<Buffer>;
override async screenshot(
options: Readonly<ScreenshotOptions> = {}
): Promise<Buffer | string> { ): Promise<Buffer | string> {
const {path = undefined, encoding, ...args} = options; const {clip, type, captureBeyondViewport} = options;
if (Object.keys(args).length >= 1) { if (captureBeyondViewport) {
throw new Error('BiDi only supports "encoding" and "path" options'); throw new Error(`BiDi does not support 'captureBeyondViewport'.`);
}
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 {result} = await this.#connection.send( override async _screenshot(
'browsingContext.captureScreenshot', options: Readonly<ScreenshotOptions>
{ ): Promise<string> {
const {clip} = options;
const {
result: {data},
} = await this.#connection.send('browsingContext.captureScreenshot', {
context: this.mainFrame()._id, context: this.mainFrame()._id,
} clip: clip && {
); type: 'viewport',
...clip,
if (encoding === 'base64') { },
return result.data; });
} return data;
const buffer = Buffer.from(result.data, 'base64');
await this._maybeWriteBufferToFile(path, buffer);
return buffer;
} }
override async waitForRequest( override async waitForRequest(

View File

@ -20,19 +20,18 @@ import {type Protocol} from 'devtools-protocol';
import { import {
Browser as BrowserBase, Browser as BrowserBase,
BrowserEvent,
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
type BrowserCloseCallback, type BrowserCloseCallback,
type BrowserContextOptions, type BrowserContextOptions,
BrowserEvent,
type IsPageTargetCallback, type IsPageTargetCallback,
type Permission, type Permission,
type TargetFilterCallback, type TargetFilterCallback,
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
} from '../api/Browser.js'; } from '../api/Browser.js';
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.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 Page} from '../api/Page.js';
import {type Target} from '../api/Target.js'; import {type Target} from '../api/Target.js';
import {TaskQueue} from '../common/TaskQueue.js';
import {type Viewport} from '../common/Viewport.js'; import {type Viewport} from '../common/Viewport.js';
import {USE_TAB_TARGET} from '../environment.js'; import {USE_TAB_TARGET} from '../environment.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
@ -41,14 +40,14 @@ import {ChromeTargetManager} from './ChromeTargetManager.js';
import {type Connection} from './Connection.js'; import {type Connection} from './Connection.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js'; import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import { import {
type CdpTarget,
DevToolsTarget, DevToolsTarget,
InitializationStatus, InitializationStatus,
OtherTarget, OtherTarget,
PageTarget, PageTarget,
WorkerTarget, WorkerTarget,
type CdpTarget,
} from './Target.js'; } from './Target.js';
import {type TargetManager, TargetManagerEvent} from './TargetManager.js'; import {TargetManagerEvent, type TargetManager} from './TargetManager.js';
/** /**
* @internal * @internal
@ -92,7 +91,6 @@ export class CdpBrowser extends BrowserBase {
#isPageTargetCallback!: IsPageTargetCallback; #isPageTargetCallback!: IsPageTargetCallback;
#defaultContext: CdpBrowserContext; #defaultContext: CdpBrowserContext;
#contexts = new Map<string, CdpBrowserContext>(); #contexts = new Map<string, CdpBrowserContext>();
#screenshotTaskQueue: TaskQueue;
#targetManager: TargetManager; #targetManager: TargetManager;
override get _targets(): Map<string, CdpTarget> { override get _targets(): Map<string, CdpTarget> {
@ -117,7 +115,6 @@ export class CdpBrowser extends BrowserBase {
this.#ignoreHTTPSErrors = ignoreHTTPSErrors; this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#defaultViewport = defaultViewport; this.#defaultViewport = defaultViewport;
this.#process = process; this.#process = process;
this.#screenshotTaskQueue = new TaskQueue();
this.#connection = connection; this.#connection = connection;
this.#closeCallback = closeCallback || function (): void {}; this.#closeCallback = closeCallback || function (): void {};
this.#targetFilterCallback = this.#targetFilterCallback =
@ -290,8 +287,7 @@ export class CdpBrowser extends BrowserBase {
this.#targetManager, this.#targetManager,
createSession, createSession,
this.#ignoreHTTPSErrors, this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null, this.#defaultViewport ?? null
this.#screenshotTaskQueue
); );
} }
if (this.#isPageTargetCallback(otherTarget)) { if (this.#isPageTargetCallback(otherTarget)) {
@ -302,8 +298,7 @@ export class CdpBrowser extends BrowserBase {
this.#targetManager, this.#targetManager,
createSession, createSession,
this.#ignoreHTTPSErrors, this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null, this.#defaultViewport ?? null
this.#screenshotTaskQueue
); );
} }
if ( if (

View File

@ -17,8 +17,7 @@
import {type Protocol} from 'devtools-protocol'; import {type Protocol} from 'devtools-protocol';
import {type CDPSession} from '../api/CDPSession.js'; import {type CDPSession} from '../api/CDPSession.js';
import {type AutofillData, ElementHandle} from '../api/ElementHandle.js'; import {ElementHandle, type AutofillData} from '../api/ElementHandle.js';
import {type Page, type ScreenshotOptions} from '../api/Page.js';
import {debugError} from '../common/util.js'; import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {throwIfDisposed} from '../util/decorators.js'; import {throwIfDisposed} from '../util/decorators.js';
@ -63,10 +62,6 @@ export class CdpElementHandle<
return this.frame._frameManager; return this.frame._frameManager;
} }
get #page(): Page {
return this.frame.page();
}
override get frame(): CdpFrame { override get frame(): CdpFrame {
return this.realm.environment as CdpFrame; return this.realm.environment as CdpFrame;
} }
@ -162,66 +157,6 @@ export class CdpElementHandle<
} }
} }
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async screenshot(
this: CdpElementHandle<Element>,
options: ScreenshotOptions = {}
): Promise<string | Buffer> {
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() @throwIfDisposed()
override async autofill(data: AutofillData): Promise<void> { override async autofill(data: AutofillData): Promise<void> {
const nodeInfo = await this.client.send('DOM.describeNode', { const nodeInfo = await this.client.send('DOM.describeNode', {

View File

@ -26,13 +26,13 @@ import {
type HandleFor, type HandleFor,
} from '../common/types.js'; } from '../common/types.js';
import { import {
Mutex,
addPageBinding, addPageBinding,
debugError, debugError,
withSourcePuppeteerURLIfNone, withSourcePuppeteerURLIfNone,
} from '../common/util.js'; } from '../common/util.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js'; import {disposeSymbol} from '../util/disposable.js';
import {Mutex} from '../util/Mutex.js';
import {type Binding} from './Binding.js'; import {type Binding} from './Binding.js';
import {type ExecutionContext, createCdpHandle} from './ExecutionContext.js'; import {type ExecutionContext, createCdpHandle} from './ExecutionContext.js';

View File

@ -16,13 +16,13 @@
import type {Readable} from 'stream'; 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 {Browser} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js'; import type {BrowserContext} from '../api/BrowserContext.js';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js'; import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import {type ElementHandle} from '../api/ElementHandle.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 HTTPRequest} from '../api/HTTPRequest.js';
import {type HTTPResponse} from '../api/HTTPResponse.js'; import {type HTTPResponse} from '../api/HTTPResponse.js';
import {type JSHandle} from '../api/JSHandle.js'; import {type JSHandle} from '../api/JSHandle.js';
@ -33,7 +33,6 @@ import {
type MediaFeature, type MediaFeature,
type Metrics, type Metrics,
type NewDocumentScriptEvaluation, type NewDocumentScriptEvaluation,
type ScreenshotClip,
type ScreenshotOptions, type ScreenshotOptions,
type WaitTimeoutOptions, type WaitTimeoutOptions,
} from '../api/Page.js'; } from '../api/Page.js';
@ -44,7 +43,6 @@ import {
import {TargetCloseError} from '../common/Errors.js'; import {TargetCloseError} from '../common/Errors.js';
import {FileChooser} from '../common/FileChooser.js'; import {FileChooser} from '../common/FileChooser.js';
import {type PDFOptions} from '../common/PDFOptions.js'; import {type PDFOptions} from '../common/PDFOptions.js';
import {type TaskQueue} from '../common/TaskQueue.js';
import {TimeoutSettings} from '../common/TimeoutSettings.js'; import {TimeoutSettings} from '../common/TimeoutSettings.js';
import {type BindingPayload, type HandleFor} from '../common/types.js'; import {type BindingPayload, type HandleFor} from '../common/types.js';
import { import {
@ -61,6 +59,7 @@ import {
waitWithTimeout, waitWithTimeout,
} from '../common/util.js'; } from '../common/util.js';
import {type Viewport} from '../common/Viewport.js'; import {type Viewport} from '../common/Viewport.js';
import {AsyncDisposableStack} from '../puppeteer-core.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
@ -96,15 +95,9 @@ export class CdpPage extends Page {
client: CDPSession, client: CDPSession,
target: CdpTarget, target: CdpTarget,
ignoreHTTPSErrors: boolean, ignoreHTTPSErrors: boolean,
defaultViewport: Viewport | null, defaultViewport: Viewport | null
screenshotTaskQueue: TaskQueue
): Promise<CdpPage> { ): Promise<CdpPage> {
const page = new CdpPage( const page = new CdpPage(client, target, ignoreHTTPSErrors);
client,
target,
ignoreHTTPSErrors,
screenshotTaskQueue
);
await page.#initialize(); await page.#initialize();
if (defaultViewport) { if (defaultViewport) {
try { try {
@ -136,7 +129,6 @@ export class CdpPage extends Page {
#exposedFunctions = new Map<string, string>(); #exposedFunctions = new Map<string, string>();
#coverage: Coverage; #coverage: Coverage;
#viewport: Viewport | null; #viewport: Viewport | null;
#screenshotTaskQueue: TaskQueue;
#workers = new Map<string, WebWorker>(); #workers = new Map<string, WebWorker>();
#fileChooserDeferreds = new Set<Deferred<FileChooser>>(); #fileChooserDeferreds = new Set<Deferred<FileChooser>>();
#sessionCloseDeferred = Deferred.create<TargetCloseError>(); #sessionCloseDeferred = Deferred.create<TargetCloseError>();
@ -231,8 +223,7 @@ export class CdpPage extends Page {
constructor( constructor(
client: CDPSession, client: CDPSession,
target: CdpTarget, target: CdpTarget,
ignoreHTTPSErrors: boolean, ignoreHTTPSErrors: boolean
screenshotTaskQueue: TaskQueue
) { ) {
super(); super();
this.#client = client; this.#client = client;
@ -251,7 +242,6 @@ export class CdpPage extends Page {
this.#emulationManager = new EmulationManager(client); this.#emulationManager = new EmulationManager(client);
this.#tracing = new Tracing(client); this.#tracing = new Tracing(client);
this.#coverage = new Coverage(client); this.#coverage = new Coverage(client);
this.#screenshotTaskQueue = screenshotTaskQueue;
this.#viewport = null; this.#viewport = null;
this.#setupEventListeners(); this.#setupEventListeners();
@ -1048,188 +1038,37 @@ export class CdpPage extends Page {
await this.#frameManager.networkManager.setCacheEnabled(enabled); await this.#frameManager.networkManager.setCacheEnabled(enabled);
} }
override screenshot( async _screenshot(options: Readonly<ScreenshotOptions>): Promise<string> {
options: ScreenshotOptions & {encoding: 'base64'}
): Promise<string>;
override screenshot(
options?: ScreenshotOptions & {encoding?: 'binary'}
): Promise<Buffer>;
override async screenshot(
options: ScreenshotOptions = {}
): Promise<Buffer | string> {
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}\``
);
}
}
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<Buffer | string> {
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 { const {
isMobile = false, fromSurface,
deviceScaleFactor = 1, omitBackground,
isLandscape = false, optimizeForSpeed,
} = this.#viewport || {}; quality,
const screenOrientation: Protocol.Emulation.ScreenOrientation = clip,
isLandscape type,
? {angle: 90, type: 'landscapePrimary'} captureBeyondViewport,
: {angle: 0, type: 'portraitPrimary'}; } = options;
await this.#client.send('Emulation.setDeviceMetricsOverride', {
mobile: isMobile, await using stack = new AsyncDisposableStack();
width, if (omitBackground && (type === 'png' || type === 'webp')) {
height, await this.#emulationManager.setTransparentBackgroundColor();
deviceScaleFactor, stack.defer(async () => {
screenOrientation, await this.#emulationManager.resetDefaultBackgroundColor();
}); });
} }
} else if (!clip) {
captureBeyondViewport = false;
}
const shouldSetDefaultBackground = const {data} = await this.#client.send('Page.captureScreenshot', {
options.omitBackground && (format === 'png' || format === 'webp'); format: type,
if (shouldSetDefaultBackground) { optimizeForSpeed,
await this.#emulationManager.setTransparentBackgroundColor(); quality,
}
const result = await this.#client.send('Page.captureScreenshot', {
format,
optimizeForSpeed: options.optimizeForSpeed,
quality: options.quality,
clip: clip && { clip: clip && {
...clip, ...clip,
scale: clip.scale ?? 1, scale: clip.scale ?? 1,
}, },
captureBeyondViewport,
fromSurface, fromSurface,
captureBeyondViewport,
}); });
if (shouldSetDefaultBackground) { return data;
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};
}
} }
override async createPDFStream(options: PDFOptions = {}): Promise<Readable> { override async createPDFStream(options: PDFOptions = {}): Promise<Readable> {

View File

@ -19,9 +19,8 @@ import {type Protocol} from 'devtools-protocol';
import type {Browser} from '../api/Browser.js'; import type {Browser} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js'; import type {BrowserContext} from '../api/BrowserContext.js';
import {type CDPSession} from '../api/CDPSession.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 {Target, TargetType} from '../api/Target.js';
import {type TaskQueue} from '../common/TaskQueue.js';
import {debugError} from '../common/util.js'; import {debugError} from '../common/util.js';
import {type Viewport} from '../common/Viewport.js'; import {type Viewport} from '../common/Viewport.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
@ -189,7 +188,6 @@ export class CdpTarget extends Target {
export class PageTarget extends CdpTarget { export class PageTarget extends CdpTarget {
#defaultViewport?: Viewport; #defaultViewport?: Viewport;
protected pagePromise?: Promise<Page>; protected pagePromise?: Promise<Page>;
#screenshotTaskQueue: TaskQueue;
#ignoreHTTPSErrors: boolean; #ignoreHTTPSErrors: boolean;
constructor( constructor(
@ -199,13 +197,11 @@ export class PageTarget extends CdpTarget {
targetManager: TargetManager, targetManager: TargetManager,
sessionFactory: (isAutoAttachEmulated: boolean) => Promise<CDPSession>, sessionFactory: (isAutoAttachEmulated: boolean) => Promise<CDPSession>,
ignoreHTTPSErrors: boolean, ignoreHTTPSErrors: boolean,
defaultViewport: Viewport | null, defaultViewport: Viewport | null
screenshotTaskQueue: TaskQueue
) { ) {
super(targetInfo, session, browserContext, targetManager, sessionFactory); super(targetInfo, session, browserContext, targetManager, sessionFactory);
this.#ignoreHTTPSErrors = ignoreHTTPSErrors; this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#defaultViewport = defaultViewport ?? undefined; this.#defaultViewport = defaultViewport ?? undefined;
this.#screenshotTaskQueue = screenshotTaskQueue;
} }
override _initialize(): void { override _initialize(): void {
@ -246,8 +242,7 @@ export class PageTarget extends CdpTarget {
client, client,
this, this,
this.#ignoreHTTPSErrors, this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null, this.#defaultViewport ?? null
this.#screenshotTaskQueue
); );
}); });
} }

View File

@ -29,7 +29,6 @@ import {type Page} from '../api/Page.js';
import {isNode} from '../environment.js'; import {isNode} from '../environment.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {debug} from './Debug.js'; import {debug} from './Debug.js';
@ -606,45 +605,6 @@ export function validateDialogType(
return dialogType as 'alert' | 'confirm' | 'prompt' | 'beforeunload'; 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<InstanceType<typeof Mutex.Guard>> {
if (!this.#locked) {
this.#locked = true;
return new Mutex.Guard(this);
}
const deferred = Deferred.create<void>();
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 * @internal
*/ */

View File

@ -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<InstanceType<typeof Mutex.Guard>> {
if (!this.#locked) {
this.#locked = true;
return new Mutex.Guard(this);
}
const deferred = Deferred.create<void>();
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();
}
}

View File

@ -17,6 +17,7 @@
import {type Disposed, type Moveable} from '../common/types.js'; import {type Disposed, type Moveable} from '../common/types.js';
import {asyncDisposeSymbol, disposeSymbol} from './disposable.js'; import {asyncDisposeSymbol, disposeSymbol} from './disposable.js';
import {Mutex} from './Mutex.js';
const instances = new WeakSet<object>(); const instances = new WeakSet<object>();
@ -112,3 +113,26 @@ export function invokeAtMostOnceForArguments(
return target.call(this, ...args); return target.call(this, ...args);
}; };
} }
export function guarded<T extends object>(
getKey = function (this: T): object {
return this;
}
) {
return (
target: (this: T, ...args: any[]) => Promise<any>,
_: ClassMethodDecoratorContext<T>
): typeof target => {
const mutexes = new WeakMap<object, Mutex>();
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);
};
};
}

View File

@ -167,3 +167,119 @@ export class DisposableStack {
readonly [Symbol.toStringTag] = '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<void> {
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<T extends AsyncDisposable | null | undefined>(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<T>(value: T, onDispose: (value: T) => Promise<void>): 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>): 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';
}

View File

@ -317,6 +317,18 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "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 *", "testIdPattern": "[stacktrace.spec] Stack trace *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -791,12 +803,6 @@
"parameters": ["firefox"], "parameters": ["firefox"],
"expectations": ["SKIP"] "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", "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1260,9 +1266,15 @@
"expectations": ["FAIL", "SKIP"] "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"], "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"] "expectations": ["FAIL"]
}, },
{ {
@ -3626,8 +3638,8 @@
{ {
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should capture full element when larger than viewport", "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should capture full element when larger than viewport",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"] "expectations": ["FAIL"]
}, },
{ {
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset", "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset",
@ -3641,71 +3653,29 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "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", "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should clip rect",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"], "parameters": ["firefox", "webDriverBiDi"],
"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"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{ {
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["FAIL"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL", "PASS"]
}, },
{ {
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots", "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"] "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", "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["FAIL"]
},
{
"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"]
}, },
{ {
"testIdPattern": "[stacktrace.spec] Stack trace should work for none error objects", "testIdPattern": "[stacktrace.spec] Stack trace should work for none error objects",
@ -4068,28 +4038,46 @@
"expectations": ["FAIL", "PASS"] "expectations": ["FAIL", "PASS"]
}, },
{ {
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", "testIdPattern": "[screenshot.spec] Screenshots Cdp should work in \"fromSurface: false\" mode",
"platforms": ["linux"], "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"], "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", "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"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "chrome", "headless"], "parameters": ["cdp", "firefox", "headless"],
"expectations": ["SKIP"] "expectations": ["FAIL"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work in \"fromSurface: false\" mode",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "chrome", "new-headless"],
"expectations": ["SKIP"]
}, },
{ {
"testIdPattern": "[worker.spec] Workers Page.workers", "testIdPattern": "[worker.spec] Workers Page.workers",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 B

After

Width:  |  Height:  |  Size: 80 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

View File

@ -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 () => { it('should work with odd clip size on Retina displays', async () => {
const {page} = await getTestState(); const {page} = await getTestState();
@ -194,16 +164,6 @@ describe('Screenshots', function () {
'screenshot-sanity.png' '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 () { describe('ElementHandle.screenshot', function () {
@ -383,4 +343,47 @@ describe('Screenshots', function () {
expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); 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');
});
});
}); });