feat(screenshot): multiple element screenshots are now taken sequentially (#114)
This makes multiple element screenshots to not fight for the page viewport by putting viewport manipulation under screenshot task queue. Drive-by: encapsulated all screenshot logic in Screenshotter.
This commit is contained in:
parent
76ab83f581
commit
b6c892842b
|
|
@ -1560,7 +1560,7 @@ Page is guaranteed to have a main frame which persists during navigations.
|
||||||
#### page.screenshot([options])
|
#### page.screenshot([options])
|
||||||
- `options` <[Object]> Options object which might have the following properties:
|
- `options` <[Object]> Options object which might have the following properties:
|
||||||
- `path` <[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](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk.
|
- `path` <[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](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk.
|
||||||
- `type` <[string]> Specify screenshot type, can be either `jpeg` or `png`. Defaults to 'png'.
|
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to 'png'.
|
||||||
- `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images.
|
- `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images.
|
||||||
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page. Defaults to `false`.
|
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page. Defaults to `false`.
|
||||||
- `clip` <[Object]> An object which specifies clipping region of the page. Should have the following fields:
|
- `clip` <[Object]> An object which specifies clipping region of the page. Should have the following fields:
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,15 @@ import { BrowserContext } from './BrowserContext';
|
||||||
import { Connection, ConnectionEvents, CDPSession } from './Connection';
|
import { Connection, ConnectionEvents, CDPSession } from './Connection';
|
||||||
import { Page, Viewport } from './Page';
|
import { Page, Viewport } from './Page';
|
||||||
import { Target } from './Target';
|
import { Target } from './Target';
|
||||||
import { TaskQueue } from './TaskQueue';
|
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { Chromium } from './features/chromium';
|
import { Chromium } from './features/chromium';
|
||||||
|
import { Screenshotter } from './Screenshotter';
|
||||||
|
|
||||||
export class Browser extends EventEmitter {
|
export class Browser extends EventEmitter {
|
||||||
private _ignoreHTTPSErrors: boolean;
|
private _ignoreHTTPSErrors: boolean;
|
||||||
private _defaultViewport: Viewport;
|
private _defaultViewport: Viewport;
|
||||||
private _process: childProcess.ChildProcess;
|
private _process: childProcess.ChildProcess;
|
||||||
private _screenshotTaskQueue = new TaskQueue();
|
private _screenshotter = new Screenshotter();
|
||||||
private _connection: Connection;
|
private _connection: Connection;
|
||||||
_client: CDPSession;
|
_client: CDPSession;
|
||||||
private _closeCallback: () => Promise<void>;
|
private _closeCallback: () => Promise<void>;
|
||||||
|
|
@ -107,7 +107,7 @@ export class Browser extends EventEmitter {
|
||||||
const {browserContextId} = targetInfo;
|
const {browserContextId} = targetInfo;
|
||||||
const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext;
|
const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext;
|
||||||
|
|
||||||
const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue);
|
const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotter);
|
||||||
assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
|
assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
|
||||||
this._targets.set(event.targetInfo.targetId, target);
|
this._targets.set(event.targetInfo.targetId, target);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert, debugError } from '../helper';
|
import { debugError } from '../helper';
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
import * as input from '../input';
|
import * as input from '../input';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
|
@ -24,6 +24,7 @@ import { CDPSession } from './Connection';
|
||||||
import { FrameManager } from './FrameManager';
|
import { FrameManager } from './FrameManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { toRemoteObject, toHandle, ExecutionContextDelegate } from './ExecutionContext';
|
import { toRemoteObject, toHandle, ExecutionContextDelegate } from './ExecutionContext';
|
||||||
|
import { ScreenshotOptions } from './Screenshotter';
|
||||||
|
|
||||||
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
readonly keyboard: input.Keyboard;
|
readonly keyboard: input.Keyboard;
|
||||||
|
|
@ -71,45 +72,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
return {x, y, width, height};
|
return {x, y, width, height};
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
|
screenshot(handle: dom.ElementHandle, options: ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||||
let needsViewportReset = false;
|
const page = this._frameManager.page();
|
||||||
|
return page._screenshotter.screenshotElement(page, handle, options);
|
||||||
let boundingBox = await this.boundingBox(handle);
|
|
||||||
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
|
||||||
|
|
||||||
const viewport = this._frameManager.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._frameManager.page().setViewport(Object.assign({}, viewport, newViewport));
|
|
||||||
|
|
||||||
needsViewportReset = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handle._scrollIntoViewIfNeeded();
|
|
||||||
|
|
||||||
boundingBox = await this.boundingBox(handle);
|
|
||||||
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 { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
|
|
||||||
|
|
||||||
const clip = Object.assign({}, boundingBox);
|
|
||||||
clip.x += pageX;
|
|
||||||
clip.y += pageY;
|
|
||||||
|
|
||||||
const imageData = await this._frameManager.page().screenshot(Object.assign({}, {
|
|
||||||
clip
|
|
||||||
}, options));
|
|
||||||
|
|
||||||
if (needsViewportReset)
|
|
||||||
await this._frameManager.page().setViewport(viewport);
|
|
||||||
|
|
||||||
return imageData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: types.Point): Promise<types.Point> {
|
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: types.Point): Promise<types.Point> {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as mime from 'mime';
|
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, debugError, helper } from '../helper';
|
||||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption, mediaTypes, mediaColorSchemes } from '../input';
|
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption, mediaTypes, mediaColorSchemes } from '../input';
|
||||||
import { TimeoutSettings } from '../TimeoutSettings';
|
import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
|
|
@ -39,7 +37,6 @@ import { NetworkManagerEvents } from './NetworkManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper';
|
import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper';
|
||||||
import { Target } from './Target';
|
import { Target } from './Target';
|
||||||
import { TaskQueue } from './TaskQueue';
|
|
||||||
import * as input from '../input';
|
import * as input from '../input';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
|
|
@ -48,8 +45,7 @@ import * as js from '../javascript';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as dialog from '../dialog';
|
import * as dialog from '../dialog';
|
||||||
import { DOMWorldDelegate } from './JSHandle';
|
import { DOMWorldDelegate } from './JSHandle';
|
||||||
|
import { Screenshotter, ScreenshotOptions } from './Screenshotter';
|
||||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
|
||||||
|
|
||||||
export type Viewport = {
|
export type Viewport = {
|
||||||
width: number;
|
width: number;
|
||||||
|
|
@ -78,20 +74,20 @@ export class Page extends EventEmitter {
|
||||||
private _pageBindings = new Map<string, Function>();
|
private _pageBindings = new Map<string, Function>();
|
||||||
_javascriptEnabled = true;
|
_javascriptEnabled = true;
|
||||||
private _viewport: Viewport | null = null;
|
private _viewport: Viewport | null = null;
|
||||||
private _screenshotTaskQueue: TaskQueue;
|
_screenshotter: Screenshotter;
|
||||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||||
private _disconnectPromise: Promise<Error> | undefined;
|
private _disconnectPromise: Promise<Error> | undefined;
|
||||||
private _emulatedMediaType: string | undefined;
|
private _emulatedMediaType: string | undefined;
|
||||||
|
|
||||||
static async create(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise<Page> {
|
static async create(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
|
||||||
const page = new Page(client, target, ignoreHTTPSErrors, screenshotTaskQueue);
|
const page = new Page(client, target, ignoreHTTPSErrors, screenshotter);
|
||||||
await page._initialize();
|
await page._initialize();
|
||||||
if (defaultViewport)
|
if (defaultViewport)
|
||||||
await page.setViewport(defaultViewport);
|
await page.setViewport(defaultViewport);
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, screenshotTaskQueue: TaskQueue) {
|
constructor(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, screenshotter: Screenshotter) {
|
||||||
super();
|
super();
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._target = target;
|
this._target = target;
|
||||||
|
|
@ -107,7 +103,7 @@ export class Page extends EventEmitter {
|
||||||
this.overrides = new Overrides(client);
|
this.overrides = new Overrides(client);
|
||||||
this.interception = new Interception(this._frameManager.networkManager());
|
this.interception = new Interception(this._frameManager.networkManager());
|
||||||
|
|
||||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
this._screenshotter = screenshotter;
|
||||||
|
|
||||||
client.on('Target.attachedToTarget', event => {
|
client.on('Target.attachedToTarget', event => {
|
||||||
if (event.targetInfo.type !== 'worker') {
|
if (event.targetInfo.type !== 'worker') {
|
||||||
|
|
@ -521,84 +517,8 @@ export class Page extends EventEmitter {
|
||||||
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(options: ScreenshotOptions = {}): Promise<Buffer | string> {
|
screenshot(options: ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||||
let screenshotType = null;
|
return this._screenshotter.screenshotPage(this, options);
|
||||||
// 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) {
|
|
||||||
assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
|
|
||||||
screenshotType = options.type;
|
|
||||||
} else if (options.path) {
|
|
||||||
const mimeType = mime.getType(options.path);
|
|
||||||
if (mimeType === 'image/png')
|
|
||||||
screenshotType = 'png';
|
|
||||||
else if (mimeType === 'image/jpeg')
|
|
||||||
screenshotType = 'jpeg';
|
|
||||||
assert(screenshotType, 'Unsupported screenshot mime type: ' + mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!screenshotType)
|
|
||||||
screenshotType = 'png';
|
|
||||||
|
|
||||||
if (options.quality) {
|
|
||||||
assert(screenshotType === 'jpeg', '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 this._screenshotTaskQueue.postTask(this._screenshotTask.bind(this, screenshotType, options));
|
|
||||||
}
|
|
||||||
|
|
||||||
async _screenshotTask(format: 'png' | 'jpeg', options?: ScreenshotOptions): Promise<Buffer | string> {
|
|
||||||
await this._client.send('Target.activateTarget', {targetId: this._target._targetId});
|
|
||||||
let clip = options.clip ? processClip(options.clip) : undefined;
|
|
||||||
|
|
||||||
if (options.fullPage) {
|
|
||||||
const metrics = await this._client.send('Page.getLayoutMetrics');
|
|
||||||
const width = Math.ceil(metrics.contentSize.width);
|
|
||||||
const height = Math.ceil(metrics.contentSize.height);
|
|
||||||
|
|
||||||
// Overwrite clip for full page at all times.
|
|
||||||
clip = { x: 0, y: 0, width, height, scale: 1 };
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
|
|
||||||
if (shouldSetDefaultBackground)
|
|
||||||
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
|
|
||||||
const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
|
|
||||||
if (shouldSetDefaultBackground)
|
|
||||||
await this._client.send('Emulation.setDefaultBackgroundColorOverride');
|
|
||||||
|
|
||||||
if (options.fullPage && this._viewport)
|
|
||||||
await this.setViewport(this._viewport);
|
|
||||||
|
|
||||||
const buffer = options.encoding === 'base64' ? result.data : Buffer.from(result.data, 'base64');
|
|
||||||
if (options.path)
|
|
||||||
await writeFileAsync(options.path, buffer);
|
|
||||||
return buffer;
|
|
||||||
|
|
||||||
function processClip(clip) {
|
|
||||||
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: 1};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
|
|
@ -676,16 +596,6 @@ export class Page extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScreenshotOptions = {
|
|
||||||
type?: string,
|
|
||||||
path?: string,
|
|
||||||
fullPage?: boolean,
|
|
||||||
clip?: {x: number, y: number, width: number, height: number},
|
|
||||||
quality?: number,
|
|
||||||
omitBackground?: boolean,
|
|
||||||
encoding?: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type MediaFeature = {
|
type MediaFeature = {
|
||||||
name: string,
|
name: string,
|
||||||
value: string
|
value: string
|
||||||
|
|
|
||||||
181
src/chromium/Screenshotter.ts
Normal file
181
src/chromium/Screenshotter.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2019 Google Inc. All rights reserved.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { Page } from './Page';
|
||||||
|
import { assert, helper } from '../helper';
|
||||||
|
import * as mime from 'mime';
|
||||||
|
import { Protocol } from './protocol';
|
||||||
|
import * as dom from '../dom';
|
||||||
|
|
||||||
|
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||||
|
|
||||||
|
export type ScreenshotOptions = {
|
||||||
|
type?: 'png' | 'jpeg',
|
||||||
|
path?: string,
|
||||||
|
fullPage?: boolean,
|
||||||
|
clip?: {x: number, y: number, width: number, height: number},
|
||||||
|
quality?: number,
|
||||||
|
omitBackground?: boolean,
|
||||||
|
encoding?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Screenshotter {
|
||||||
|
private _queue = new TaskQueue();
|
||||||
|
|
||||||
|
async screenshotPage(page: Page, options: ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||||
|
const format = this._format(options);
|
||||||
|
return this._queue.postTask(() => this._screenshot(page, format, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
async screenshotElement(page: Page, handle: dom.ElementHandle, options: ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||||
|
const format = this._format(options);
|
||||||
|
return this._queue.postTask(async () => {
|
||||||
|
let needsViewportReset = false;
|
||||||
|
|
||||||
|
let boundingBox = await handle.boundingBox();
|
||||||
|
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
||||||
|
|
||||||
|
const viewport = 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 page.setViewport(Object.assign({}, viewport, newViewport));
|
||||||
|
|
||||||
|
needsViewportReset = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handle._scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
|
boundingBox = await handle.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 { layoutViewport: { pageX, pageY } } = await page._client.send('Page.getLayoutMetrics');
|
||||||
|
|
||||||
|
const clip = Object.assign({}, boundingBox);
|
||||||
|
clip.x += pageX;
|
||||||
|
clip.y += pageY;
|
||||||
|
|
||||||
|
const imageData = await this._screenshot(page, format, {...options, clip});
|
||||||
|
|
||||||
|
if (needsViewportReset)
|
||||||
|
await page.setViewport(viewport);
|
||||||
|
|
||||||
|
return imageData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _screenshot(page: Page, format: 'png' | 'jpeg', options: ScreenshotOptions): Promise<Buffer | string> {
|
||||||
|
await page._client.send('Target.activateTarget', {targetId: page.target()._targetId});
|
||||||
|
let clip = options.clip ? processClip(options.clip) : undefined;
|
||||||
|
const viewport = page.viewport();
|
||||||
|
|
||||||
|
if (options.fullPage) {
|
||||||
|
const metrics = await page._client.send('Page.getLayoutMetrics');
|
||||||
|
const width = Math.ceil(metrics.contentSize.width);
|
||||||
|
const height = Math.ceil(metrics.contentSize.height);
|
||||||
|
|
||||||
|
// Overwrite clip for full page at all times.
|
||||||
|
clip = { x: 0, y: 0, width, height, scale: 1 };
|
||||||
|
const {
|
||||||
|
isMobile = false,
|
||||||
|
deviceScaleFactor = 1,
|
||||||
|
isLandscape = false
|
||||||
|
} = viewport || {};
|
||||||
|
const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
|
||||||
|
await page._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation });
|
||||||
|
}
|
||||||
|
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
|
||||||
|
if (shouldSetDefaultBackground)
|
||||||
|
await page._client.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
|
||||||
|
const result = await page._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
|
||||||
|
if (shouldSetDefaultBackground)
|
||||||
|
await page._client.send('Emulation.setDefaultBackgroundColorOverride');
|
||||||
|
|
||||||
|
if (options.fullPage && viewport)
|
||||||
|
await page.setViewport(viewport);
|
||||||
|
|
||||||
|
const buffer = options.encoding === 'base64' ? result.data : Buffer.from(result.data, 'base64');
|
||||||
|
if (options.path)
|
||||||
|
await writeFileAsync(options.path, buffer);
|
||||||
|
return buffer;
|
||||||
|
|
||||||
|
function processClip(clip) {
|
||||||
|
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: 1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _format(options: ScreenshotOptions): 'png' | 'jpeg' {
|
||||||
|
let format: 'png' | 'jpeg' | null = null;
|
||||||
|
// 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) {
|
||||||
|
assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
|
||||||
|
format = options.type;
|
||||||
|
} else if (options.path) {
|
||||||
|
const mimeType = mime.getType(options.path);
|
||||||
|
if (mimeType === 'image/png')
|
||||||
|
format = 'png';
|
||||||
|
else if (mimeType === 'image/jpeg')
|
||||||
|
format = 'jpeg';
|
||||||
|
assert(format, 'Unsupported screenshot mime type: ' + mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!format)
|
||||||
|
format = 'png';
|
||||||
|
|
||||||
|
if (options.quality) {
|
||||||
|
assert(format === 'jpeg', 'options.quality is unsupported for the ' + format + ' 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 format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskQueue {
|
||||||
|
private _chain: Promise<any>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._chain = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
postTask(task: () => any): Promise<any> {
|
||||||
|
const result = this._chain.then(task);
|
||||||
|
this._chain = result.catch(() => {});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ import { Events } from './events';
|
||||||
import { Worker } from './features/workers';
|
import { Worker } from './features/workers';
|
||||||
import { Page, Viewport } from './Page';
|
import { Page, Viewport } from './Page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { TaskQueue } from './TaskQueue';
|
import { Screenshotter } from './Screenshotter';
|
||||||
|
|
||||||
export class Target {
|
export class Target {
|
||||||
private _targetInfo: Protocol.Target.TargetInfo;
|
private _targetInfo: Protocol.Target.TargetInfo;
|
||||||
|
|
@ -31,7 +31,7 @@ export class Target {
|
||||||
_sessionFactory: () => Promise<CDPSession>;
|
_sessionFactory: () => Promise<CDPSession>;
|
||||||
private _ignoreHTTPSErrors: boolean;
|
private _ignoreHTTPSErrors: boolean;
|
||||||
private _defaultViewport: Viewport;
|
private _defaultViewport: Viewport;
|
||||||
private _screenshotTaskQueue: TaskQueue;
|
private _screenshotter: Screenshotter;
|
||||||
private _pagePromise: Promise<Page> | null = null;
|
private _pagePromise: Promise<Page> | null = null;
|
||||||
private _workerPromise: Promise<Worker> | null = null;
|
private _workerPromise: Promise<Worker> | null = null;
|
||||||
_initializedPromise: Promise<boolean>;
|
_initializedPromise: Promise<boolean>;
|
||||||
|
|
@ -46,14 +46,14 @@ export class Target {
|
||||||
sessionFactory: () => Promise<CDPSession>,
|
sessionFactory: () => Promise<CDPSession>,
|
||||||
ignoreHTTPSErrors: boolean,
|
ignoreHTTPSErrors: boolean,
|
||||||
defaultViewport: Viewport | null,
|
defaultViewport: Viewport | null,
|
||||||
screenshotTaskQueue: TaskQueue) {
|
screenshotter: Screenshotter) {
|
||||||
this._targetInfo = targetInfo;
|
this._targetInfo = targetInfo;
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
this._targetId = targetInfo.targetId;
|
this._targetId = targetInfo.targetId;
|
||||||
this._sessionFactory = sessionFactory;
|
this._sessionFactory = sessionFactory;
|
||||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||||
this._defaultViewport = defaultViewport;
|
this._defaultViewport = defaultViewport;
|
||||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
this._screenshotter = screenshotter;
|
||||||
this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => {
|
this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => {
|
||||||
if (!success)
|
if (!success)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -76,7 +76,7 @@ export class Target {
|
||||||
async page(): Promise<Page | null> {
|
async page(): Promise<Page | null> {
|
||||||
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
|
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
|
||||||
this._pagePromise = this._sessionFactory()
|
this._pagePromise = this._sessionFactory()
|
||||||
.then(client => Page.create(client, this, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue));
|
.then(client => Page.create(client, this, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotter));
|
||||||
}
|
}
|
||||||
return this._pagePromise;
|
return this._pagePromise;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2019 Google Inc. All rights reserved.
|
|
||||||
* Modifications copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class TaskQueue {
|
|
||||||
private _chain: Promise<any>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._chain = Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
postTask(task: () => any): Promise<any> {
|
|
||||||
const result = this._chain.then(task);
|
|
||||||
this._chain = result.catch(() => {});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -151,6 +151,33 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
const screenshot = await elementHandle.screenshot();
|
const screenshot = await elementHandle.screenshot();
|
||||||
expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
|
expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
|
||||||
});
|
});
|
||||||
|
it('should capture full element when larger than viewport in parallel', async({page, server}) => {
|
||||||
|
await page.setViewport({width: 500, height: 500});
|
||||||
|
|
||||||
|
await page.setContent(`
|
||||||
|
something above
|
||||||
|
<style>
|
||||||
|
div.to-screenshot {
|
||||||
|
border: 1px solid blue;
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="to-screenshot"></div>
|
||||||
|
<div class="to-screenshot"></div>
|
||||||
|
<div class="to-screenshot"></div>
|
||||||
|
`);
|
||||||
|
const elementHandles = await page.$$('div.to-screenshot');
|
||||||
|
const promises = elementHandles.map(handle => handle.screenshot());
|
||||||
|
const screenshots = await Promise.all(promises);
|
||||||
|
expect(screenshots[2]).toBeGolden('screenshot-element-larger-than-viewport.png');
|
||||||
|
|
||||||
|
expect(await page.evaluate(() => ({ w: window.innerWidth, h: window.innerHeight }))).toEqual({ w: 500, h: 500 });
|
||||||
|
});
|
||||||
it('should capture full element when larger than viewport', async({page, server}) => {
|
it('should capture full element when larger than viewport', async({page, server}) => {
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
|
|
||||||
|
|
@ -168,6 +195,8 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="to-screenshot"></div>
|
<div class="to-screenshot"></div>
|
||||||
|
<div class="to-screenshot"></div>
|
||||||
|
<div class="to-screenshot"></div>
|
||||||
`);
|
`);
|
||||||
const elementHandle = await page.$('div.to-screenshot');
|
const elementHandle = await page.$('div.to-screenshot');
|
||||||
const screenshot = await elementHandle.screenshot();
|
const screenshot = await elementHandle.screenshot();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue