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])
|
||||
- `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.
|
||||
- `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.
|
||||
- `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:
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ import { BrowserContext } from './BrowserContext';
|
|||
import { Connection, ConnectionEvents, CDPSession } from './Connection';
|
||||
import { Page, Viewport } from './Page';
|
||||
import { Target } from './Target';
|
||||
import { TaskQueue } from './TaskQueue';
|
||||
import { Protocol } from './protocol';
|
||||
import { Chromium } from './features/chromium';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
|
||||
export class Browser extends EventEmitter {
|
||||
private _ignoreHTTPSErrors: boolean;
|
||||
private _defaultViewport: Viewport;
|
||||
private _process: childProcess.ChildProcess;
|
||||
private _screenshotTaskQueue = new TaskQueue();
|
||||
private _screenshotter = new Screenshotter();
|
||||
private _connection: Connection;
|
||||
_client: CDPSession;
|
||||
private _closeCallback: () => Promise<void>;
|
||||
|
|
@ -107,7 +107,7 @@ export class Browser extends EventEmitter {
|
|||
const {browserContextId} = targetInfo;
|
||||
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');
|
||||
this._targets.set(event.targetInfo.targetId, target);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assert, debugError } from '../helper';
|
||||
import { debugError } from '../helper';
|
||||
import * as dom from '../dom';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
|
|
@ -24,6 +24,7 @@ import { CDPSession } from './Connection';
|
|||
import { FrameManager } from './FrameManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { toRemoteObject, toHandle, ExecutionContextDelegate } from './ExecutionContext';
|
||||
import { ScreenshotOptions } from './Screenshotter';
|
||||
|
||||
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
readonly keyboard: input.Keyboard;
|
||||
|
|
@ -71,45 +72,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
|||
return {x, y, width, height};
|
||||
}
|
||||
|
||||
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
|
||||
let needsViewportReset = false;
|
||||
|
||||
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;
|
||||
screenshot(handle: dom.ElementHandle, options: ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||
const page = this._frameManager.page();
|
||||
return page._screenshotter.screenshotElement(page, handle, options);
|
||||
}
|
||||
|
||||
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: types.Point): Promise<types.Point> {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as mime from 'mime';
|
||||
import { assert, debugError, helper } from '../helper';
|
||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption, mediaTypes, mediaColorSchemes } from '../input';
|
||||
import { TimeoutSettings } from '../TimeoutSettings';
|
||||
|
|
@ -39,7 +37,6 @@ import { NetworkManagerEvents } from './NetworkManager';
|
|||
import { Protocol } from './protocol';
|
||||
import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper';
|
||||
import { Target } from './Target';
|
||||
import { TaskQueue } from './TaskQueue';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
import * as dom from '../dom';
|
||||
|
|
@ -48,8 +45,7 @@ import * as js from '../javascript';
|
|||
import * as network from '../network';
|
||||
import * as dialog from '../dialog';
|
||||
import { DOMWorldDelegate } from './JSHandle';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
import { Screenshotter, ScreenshotOptions } from './Screenshotter';
|
||||
|
||||
export type Viewport = {
|
||||
width: number;
|
||||
|
|
@ -78,20 +74,20 @@ export class Page extends EventEmitter {
|
|||
private _pageBindings = new Map<string, Function>();
|
||||
_javascriptEnabled = true;
|
||||
private _viewport: Viewport | null = null;
|
||||
private _screenshotTaskQueue: TaskQueue;
|
||||
_screenshotter: Screenshotter;
|
||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||
private _disconnectPromise: Promise<Error> | undefined;
|
||||
private _emulatedMediaType: string | undefined;
|
||||
|
||||
static async create(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise<Page> {
|
||||
const page = new Page(client, target, ignoreHTTPSErrors, screenshotTaskQueue);
|
||||
static async create(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
|
||||
const page = new Page(client, target, ignoreHTTPSErrors, screenshotter);
|
||||
await page._initialize();
|
||||
if (defaultViewport)
|
||||
await page.setViewport(defaultViewport);
|
||||
return page;
|
||||
}
|
||||
|
||||
constructor(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, screenshotTaskQueue: TaskQueue) {
|
||||
constructor(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, screenshotter: Screenshotter) {
|
||||
super();
|
||||
this._client = client;
|
||||
this._target = target;
|
||||
|
|
@ -107,7 +103,7 @@ export class Page extends EventEmitter {
|
|||
this.overrides = new Overrides(client);
|
||||
this.interception = new Interception(this._frameManager.networkManager());
|
||||
|
||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
||||
this._screenshotter = screenshotter;
|
||||
|
||||
client.on('Target.attachedToTarget', event => {
|
||||
if (event.targetInfo.type !== 'worker') {
|
||||
|
|
@ -521,84 +517,8 @@ export class Page extends EventEmitter {
|
|||
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
||||
}
|
||||
|
||||
async screenshot(options: ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
let screenshotType = 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);
|
||||
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};
|
||||
}
|
||||
screenshot(options: ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
return this._screenshotter.screenshotPage(this, options);
|
||||
}
|
||||
|
||||
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 = {
|
||||
name: 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 { Page, Viewport } from './Page';
|
||||
import { Protocol } from './protocol';
|
||||
import { TaskQueue } from './TaskQueue';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
|
||||
export class Target {
|
||||
private _targetInfo: Protocol.Target.TargetInfo;
|
||||
|
|
@ -31,7 +31,7 @@ export class Target {
|
|||
_sessionFactory: () => Promise<CDPSession>;
|
||||
private _ignoreHTTPSErrors: boolean;
|
||||
private _defaultViewport: Viewport;
|
||||
private _screenshotTaskQueue: TaskQueue;
|
||||
private _screenshotter: Screenshotter;
|
||||
private _pagePromise: Promise<Page> | null = null;
|
||||
private _workerPromise: Promise<Worker> | null = null;
|
||||
_initializedPromise: Promise<boolean>;
|
||||
|
|
@ -46,14 +46,14 @@ export class Target {
|
|||
sessionFactory: () => Promise<CDPSession>,
|
||||
ignoreHTTPSErrors: boolean,
|
||||
defaultViewport: Viewport | null,
|
||||
screenshotTaskQueue: TaskQueue) {
|
||||
screenshotter: Screenshotter) {
|
||||
this._targetInfo = targetInfo;
|
||||
this._browserContext = browserContext;
|
||||
this._targetId = targetInfo.targetId;
|
||||
this._sessionFactory = sessionFactory;
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._defaultViewport = defaultViewport;
|
||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
||||
this._screenshotter = screenshotter;
|
||||
this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => {
|
||||
if (!success)
|
||||
return false;
|
||||
|
|
@ -76,7 +76,7 @@ export class Target {
|
|||
async page(): Promise<Page | null> {
|
||||
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
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}) => {
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
|
||||
|
|
@ -168,6 +195,8 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
|||
}
|
||||
</style>
|
||||
<div class="to-screenshot"></div>
|
||||
<div class="to-screenshot"></div>
|
||||
<div class="to-screenshot"></div>
|
||||
`);
|
||||
const elementHandle = await page.$('div.to-screenshot');
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
|
|
|
|||
Loading…
Reference in a new issue