chore: implement document screenshots in BiDi (#11398)

This commit is contained in:
jrandolf 2023-11-16 13:46:28 +01:00 committed by GitHub
parent 923434bd56
commit 2bf28ea1e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 78 additions and 149 deletions

View File

@ -14,6 +14,6 @@ export interface ElementScreenshotOptions extends ScreenshotOptions
## Properties
| Property | Modifiers | Type | Description | Default |
| -------------- | --------------------- | ------- | ----------- | ------- |
| scrollIntoView | <code>optional</code> | boolean | | true |
| Property | Modifiers | Type | Description | Default |
| -------------- | --------------------- | ------- | ----------- | ----------------- |
| scrollIntoView | <code>optional</code> | boolean | | <code>true</code> |

View File

@ -12,15 +12,15 @@ export interface ScreenshotOptions
## Properties
| Property | Modifiers | Type | Description | Default |
| --------------------- | --------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| captureBeyondViewport | <code>optional</code> | boolean | Capture the screenshot beyond the viewport. | <code>true</code> |
| 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> |
| fromSurface | <code>optional</code> | boolean | Capture the screenshot from the surface, rather than the view. | <code>true</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> |
| 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. | |
| 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> |
| Property | Modifiers | Type | Description | Default |
| --------------------- | --------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| captureBeyondViewport | <code>optional</code> | boolean | Capture the screenshot beyond the viewport. | <code>false</code> if there is no <code>clip</code>. <code>true</code> otherwise. |
| 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> |
| fromSurface | <code>optional</code> | boolean | Capture the screenshot from the surface, rather than the view. | <code>true</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> |
| 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. | |
| 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> |

View File

@ -109,7 +109,7 @@ export interface Point {
*/
export interface ElementScreenshotOptions extends ScreenshotOptions {
/**
* @defaultValue true
* @defaultValue `true`
*/
scrollIntoView?: boolean;
}
@ -1348,21 +1348,12 @@ export abstract class ElementHandle<
this: ElementHandle<Element>,
options: Readonly<ElementScreenshotOptions> = {}
): Promise<string | Buffer> {
const {
scrollIntoView = true,
captureBeyondViewport = true,
allowViewportExpansion = captureBeyondViewport,
} = options;
const {scrollIntoView = true} = options;
let clip = await this.#nonEmptyVisibleBoundingBox();
const page = this.frame.page();
await using _ =
allowViewportExpansion && clip
? await page._createTemporaryViewportContainingBox(clip)
: null;
if (scrollIntoView) {
await this.scrollIntoViewIfNeeded();
@ -1382,11 +1373,7 @@ export abstract class ElementHandle<
clip.x += pageLeft;
clip.y += pageTop;
return await page.screenshot({
...options,
captureBeyondViewport: false,
clip,
});
return await page.screenshot({...options, clip});
}
async #nonEmptyVisibleBoundingBox() {

View File

@ -85,7 +85,6 @@ import type {ScreenRecorder} from '../node/ScreenRecorder.js';
import {assert} from '../util/assert.js';
import {guarded} from '../util/decorators.js';
import {
AsyncDisposableStack,
asyncDisposeSymbol,
DisposableStack,
disposeSymbol,
@ -279,17 +278,9 @@ export interface ScreenshotOptions {
/**
* Capture the screenshot beyond the viewport.
*
* @defaultValue `true`
* @defaultValue `false` if there is no `clip`. `true` otherwise.
*/
captureBeyondViewport?: boolean;
/**
* TODO(jrandolf): Investigate whether viewport expansion is a better
* alternative for cross-browser screenshots as opposed to
* `captureBeyondViewport`.
*
* @internal
*/
allowViewportExpansion?: boolean;
}
/**
@ -555,7 +546,6 @@ export function setDefaultScreenshotOptions(options: ScreenshotOptions): void {
options.omitBackground ??= false;
options.encoding ??= 'binary';
options.captureBeyondViewport ??= true;
options.allowViewportExpansion ??= options.captureBeyondViewport;
}
/**
@ -2439,7 +2429,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
): Promise<Buffer | string> {
await this.bringToFront();
// TODO: use structuredClone after Node 16 support is dropped.«
// TODO: use structuredClone after Node 16 support is dropped.
const options = {
...userOptions,
clip: userOptions.clip
@ -2482,10 +2472,6 @@ export abstract class Page extends EventEmitter<PageEvents> {
);
}
}
assert(
!options.clip || !options.fullPage,
"'clip' and 'fullPage' are exclusive"
);
if (options.clip) {
if (options.clip.width <= 0) {
throw new Error("'width' in 'clip' must be positive.");
@ -2500,32 +2486,15 @@ export abstract class Page extends EventEmitter<PageEvents> {
options.clip =
options.clip && roundRectangle(normalizeRectangle(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;
if (options.fullPage) {
if (options.clip) {
throw new Error("'clip' and 'fullPage' are exclusive");
}
} else if (
!options.clip &&
userOptions.captureBeyondViewport === undefined
) {
options.captureBeyondViewport = false;
}
const data = await this._screenshot(options);
@ -2542,61 +2511,6 @@ export abstract class Page extends EventEmitter<PageEvents> {
*/
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
*/

View File

@ -29,6 +29,7 @@ import {
raceWith,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {BoundingBox} from '../api/ElementHandle.js';
import type {WaitForOptions} from '../api/Frame.js';
import {
Page,
@ -647,13 +648,7 @@ export class BidiPage extends Page {
override async _screenshot(
options: Readonly<ScreenshotOptions>
): Promise<string> {
const {clip, type, captureBeyondViewport, allowViewportExpansion, quality} =
options;
if (captureBeyondViewport && !allowViewportExpansion) {
throw new UnsupportedOperation(
`BiDi does not support 'captureBeyondViewport'. Use 'allowViewportExpansion'.`
);
}
const {clip, type, captureBeyondViewport, quality} = options;
if (options.omitBackground !== undefined && options.omitBackground) {
throw new UnsupportedOperation(`BiDi does not support 'omitBackground'.`);
}
@ -665,6 +660,29 @@ export class BidiPage extends Page {
if (options.fromSurface !== undefined && !options.fromSurface) {
throw new UnsupportedOperation(`BiDi does not support 'fromSurface'.`);
}
let box: BoundingBox | undefined;
if (clip) {
if (captureBeyondViewport) {
box = clip;
} else {
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;
});
box = {
...clip,
x: clip.x - pageLeft,
y: clip.y - pageTop,
};
}
}
if (clip !== undefined && clip.scale !== undefined && clip.scale !== 1) {
throw new UnsupportedOperation(
`BiDi does not support 'scale' in 'clip'.`
@ -675,14 +693,12 @@ export class BidiPage extends Page {
result: {data},
} = await this.#connection.send('browsingContext.captureScreenshot', {
context: this.mainFrame()._id,
origin: captureBeyondViewport ? 'document' : 'viewport',
format: {
type: `image/${type}`,
quality: quality ? quality / 100 : undefined,
},
clip: clip && {
type: 'box',
...clip,
...(quality !== undefined ? {quality: quality / 100} : {}),
},
...(box ? {clip: {type: 'box', ...box}} : {}),
});
return data;
}

View File

@ -1166,7 +1166,7 @@
{
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should get screenshot bigger than the viewport",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"parameters": ["cdp"],
"expectations": ["FAIL"]
},
{
@ -3348,13 +3348,19 @@
"expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should capture full element when larger than viewport",
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset",
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
@ -3362,7 +3368,7 @@
{
"testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
@ -3383,12 +3389,24 @@
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work with odd clip size on Retina displays",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[stacktrace.spec] Stack trace should work for none error objects",
"platforms": ["darwin", "linux", "win32"],
@ -3713,12 +3731,6 @@
"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"],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 B

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 873 B

After

Width:  |  Height:  |  Size: 459 B