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