chore: unify screenshot handling between browsers, introduce Screenshotter everywhere (#156)
This commit is contained in:
parent
69d2d81c05
commit
e992c7fa7d
11
docs/api.md
11
docs/api.md
|
|
@ -3457,6 +3457,17 @@ If `key` is a single character and no modifier keys besides `Shift` are being he
|
|||
|
||||
#### elementHandle.screenshot([options])
|
||||
- `options` <[Object]> Same options as in [page.screenshot](#pagescreenshotoptions).
|
||||
- `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` <"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]> Passed clip value is ignored and instead set to the element's bounding box.
|
||||
- `x` <[number]>
|
||||
- `y` <[number]>
|
||||
- `width` <[number]>
|
||||
- `height` <[number]>
|
||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
||||
- `encoding` <[string]> The encoding of the image, can be either `base64` or `binary`. Defaults to `binary`.
|
||||
- returns: <[Promise]<[string]|[Buffer]>> Promise which resolves to buffer or a base64 string (depending on the value of `options.encoding`) with captured screenshot.
|
||||
|
||||
This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import * as frames from '../frames';
|
|||
import { CDPSession } from './Connection';
|
||||
import { FrameManager } from './FrameManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { ScreenshotOptions } from './Screenshotter';
|
||||
import { ExecutionContextDelegate } from './ExecutionContext';
|
||||
|
||||
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
|
|
@ -91,7 +90,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
|||
return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight };
|
||||
}
|
||||
|
||||
screenshot(handle: dom.ElementHandle, options: ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||
screenshot(handle: dom.ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
||||
const page = this._frameManager.page();
|
||||
return page._screenshotter.screenshotElement(page, handle, options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import * as network from '../network';
|
|||
import * as dialog from '../dialog';
|
||||
import * as console from '../console';
|
||||
import { DOMWorldDelegate } from './JSHandle';
|
||||
import { Screenshotter, ScreenshotOptions } from './Screenshotter';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
|
||||
export type Viewport = {
|
||||
width: number;
|
||||
|
|
@ -509,7 +509,7 @@ export class Page extends EventEmitter {
|
|||
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
||||
}
|
||||
|
||||
screenshot(options: ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer | string> {
|
||||
return this._screenshotter.screenshotPage(this, options);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,32 +18,22 @@
|
|||
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';
|
||||
import * as types from '../types';
|
||||
|
||||
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);
|
||||
async screenshotPage(page: Page, options: types.ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
const format = helper.validateScreeshotOptions(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);
|
||||
async screenshotElement(page: Page, handle: dom.ElementHandle, options: types.ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
return this._queue.postTask(async () => {
|
||||
let needsViewportReset = false;
|
||||
|
||||
|
|
@ -84,7 +74,7 @@ export class Screenshotter {
|
|||
});
|
||||
}
|
||||
|
||||
private async _screenshot(page: Page, format: 'png' | 'jpeg', options: ScreenshotOptions): Promise<Buffer | string> {
|
||||
private async _screenshot(page: Page, format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer | string> {
|
||||
await page.browser()._activatePage(page);
|
||||
let clip = options.clip ? processClip(options.clip) : undefined;
|
||||
const viewport = page.viewport();
|
||||
|
|
@ -127,43 +117,6 @@ export class Screenshotter {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface DOMWorldDelegate {
|
|||
contentQuads(handle: ElementHandle): Promise<types.Quad[] | null>;
|
||||
layoutViewport(): Promise<{ width: number, height: number }>;
|
||||
boundingBox(handle: ElementHandle): Promise<types.Rect | null>;
|
||||
screenshot(handle: ElementHandle, options?: any): Promise<string | Buffer>;
|
||||
screenshot(handle: ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer>;
|
||||
setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise<void>;
|
||||
adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise<ElementHandle>;
|
||||
}
|
||||
|
|
@ -359,7 +359,7 @@ export class ElementHandle extends js.JSHandle {
|
|||
return this._world.delegate.boundingBox(this);
|
||||
}
|
||||
|
||||
async screenshot(options: any = {}): Promise<string | Buffer> {
|
||||
async screenshot(options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
||||
return this._world.delegate.screenshot(this, options);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,25 +92,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
|||
return this._frameManager._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||
}
|
||||
|
||||
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
|
||||
const clip = await this._session.send('Page.getBoundingBox', {
|
||||
frameId: this._frameId,
|
||||
objectId: toRemoteObject(handle).objectId,
|
||||
});
|
||||
if (!clip)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
assert(clip.width, 'Node has 0 width.');
|
||||
assert(clip.height, 'Node has 0 height.');
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
|
||||
return await this._frameManager._page.screenshot(Object.assign({}, options, {
|
||||
clip: {
|
||||
x: clip.x,
|
||||
y: clip.y,
|
||||
width: clip.width,
|
||||
height: clip.height,
|
||||
},
|
||||
}));
|
||||
async screenshot(handle: dom.ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
||||
const page = this._frameManager._page;
|
||||
return page._screenshotter.screenshotElement(page, handle, options);
|
||||
}
|
||||
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as mime from 'mime';
|
||||
import { TimeoutError } from '../Errors';
|
||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||
import { TimeoutSettings } from '../TimeoutSettings';
|
||||
|
|
@ -38,8 +36,7 @@ import * as network from '../network';
|
|||
import * as frames from '../frames';
|
||||
import * as dialog from '../dialog';
|
||||
import * as console from '../console';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
|
||||
export class Page extends EventEmitter {
|
||||
private _timeoutSettings: TimeoutSettings;
|
||||
|
|
@ -60,6 +57,7 @@ export class Page extends EventEmitter {
|
|||
private _viewport: Viewport;
|
||||
private _disconnectPromise: Promise<Error>;
|
||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||
_screenshotter: Screenshotter;
|
||||
|
||||
static async create(session: JugglerSession, browserContext: BrowserContext, defaultViewport: Viewport | null) {
|
||||
const page = new Page(session, browserContext);
|
||||
|
|
@ -107,6 +105,7 @@ export class Page extends EventEmitter {
|
|||
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)),
|
||||
];
|
||||
this._viewport = null;
|
||||
this._screenshotter = new Screenshotter(session);
|
||||
}
|
||||
|
||||
_didClose() {
|
||||
|
|
@ -425,26 +424,8 @@ export class Page extends EventEmitter {
|
|||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
async screenshot(options: { fullPage?: boolean; clip?: { width: number; height: number; x: number; y: number; }; encoding?: string; path?: string; } = {}): Promise<string | Buffer> {
|
||||
const {data} = await this._session.send('Page.screenshot', {
|
||||
mimeType: getScreenshotMimeType(options),
|
||||
fullPage: options.fullPage,
|
||||
clip: processClip(options.clip),
|
||||
});
|
||||
const buffer = options.encoding === 'base64' ? data : Buffer.from(data, 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
|
||||
function processClip(clip) {
|
||||
if (!clip)
|
||||
return undefined;
|
||||
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};
|
||||
}
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
||||
return this._screenshotter.screenshotPage(this, options);
|
||||
}
|
||||
|
||||
evaluate: types.Evaluate = (pageFunction, ...args) => {
|
||||
|
|
@ -589,25 +570,6 @@ export class Page extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
function getScreenshotMimeType(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) {
|
||||
if (options.type === 'png')
|
||||
return 'image/png';
|
||||
if (options.type === 'jpeg')
|
||||
return 'image/jpeg';
|
||||
throw new Error('Unknown options.type value: ' + options.type);
|
||||
}
|
||||
if (options.path) {
|
||||
const fileType = mime.getType(options.path);
|
||||
if (fileType === 'image/png' || fileType === 'image/jpeg')
|
||||
return fileType;
|
||||
throw new Error('Unsupported screenshot mime type: ' + fileType);
|
||||
}
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
export type Viewport = {
|
||||
width: number;
|
||||
height: number;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { Page } from './Page';
|
||||
import { assert, helper } from '../helper';
|
||||
import * as dom from '../dom';
|
||||
import * as types from '../types';
|
||||
import { JugglerSession } from './Connection';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
export class Screenshotter {
|
||||
private _session: JugglerSession;
|
||||
|
||||
constructor(session: JugglerSession) {
|
||||
this._session = session;
|
||||
}
|
||||
|
||||
async screenshotPage(page: Page, options: types.ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
const {data} = await this._session.send('Page.screenshot', {
|
||||
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
|
||||
fullPage: options.fullPage,
|
||||
clip: processClip(options.clip),
|
||||
});
|
||||
const buffer = options.encoding === 'base64' ? data : Buffer.from(data, 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
|
||||
function processClip(clip) {
|
||||
if (!clip)
|
||||
return undefined;
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
||||
async screenshotElement(page: Page, handle: dom.ElementHandle, options: types.ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||
const frameId = page._frameManager._frameData(handle.executionContext().frame()).frameId;
|
||||
const clip = await this._session.send('Page.getBoundingBox', {
|
||||
frameId,
|
||||
objectId: handle._remoteObject.objectId,
|
||||
});
|
||||
if (!clip)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
assert(clip.width, 'Node has 0 width.');
|
||||
assert(clip.height, 'Node has 0 height.');
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
return this.screenshotPage(page, {
|
||||
...options,
|
||||
clip: {
|
||||
x: clip.x,
|
||||
y: clip.y,
|
||||
width: clip.width,
|
||||
height: clip.height,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,11 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as debug from 'debug';
|
||||
import * as mime from 'mime';
|
||||
import { TimeoutError } from './Errors';
|
||||
import * as types from './types';
|
||||
|
||||
export const debugError = debug(`playwright:error`);
|
||||
|
||||
|
|
@ -152,6 +155,43 @@ class Helper {
|
|||
clearTimeout(timeoutTimer);
|
||||
}
|
||||
}
|
||||
|
||||
static validateScreeshotOptions(options: types.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;
|
||||
}
|
||||
}
|
||||
|
||||
export function assert(value: any, message?: string) {
|
||||
|
|
|
|||
10
src/types.ts
10
src/types.ts
|
|
@ -38,3 +38,13 @@ export function clearSelector(selector: string | Selector): string | Selector {
|
|||
return selector;
|
||||
return { selector: selector.selector, visible: selector.visible };
|
||||
}
|
||||
|
||||
export type ScreenshotOptions = {
|
||||
type?: 'png' | 'jpeg',
|
||||
path?: string,
|
||||
fullPage?: boolean,
|
||||
clip?: Rect,
|
||||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
encoding?: string,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } f
|
|||
import { Connection } from './Connection';
|
||||
import { Page, Viewport } from './Page';
|
||||
import { Target } from './Target';
|
||||
import { TaskQueue } from './TaskQueue';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
export class Browser extends EventEmitter {
|
||||
_defaultViewport: Viewport;
|
||||
private _process: childProcess.ChildProcess;
|
||||
_screenshotTaskQueue = new TaskQueue();
|
||||
_screenshotter = new Screenshotter();
|
||||
_connection: Connection;
|
||||
private _closeCallback: () => Promise<void>;
|
||||
private _defaultContext: BrowserContext;
|
||||
|
|
@ -62,7 +62,7 @@ export class Browser extends EventEmitter {
|
|||
];
|
||||
|
||||
// Taking multiple screenshots in parallel doesn't work well, so we serialize them.
|
||||
this._screenshotTaskQueue = new TaskQueue();
|
||||
this._screenshotter = new Screenshotter();
|
||||
}
|
||||
|
||||
async userAgent(): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { debugError, helper, assert } from '../helper';
|
||||
import { debugError, assert } from '../helper';
|
||||
import * as input from '../input';
|
||||
import * as dom from '../dom';
|
||||
import * as frames from '../frames';
|
||||
|
|
@ -25,8 +24,6 @@ import { TargetSession } from './Connection';
|
|||
import { FrameManager } from './FrameManager';
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
readonly keyboard: input.Keyboard;
|
||||
readonly mouse: input.Mouse;
|
||||
|
|
@ -91,16 +88,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
|||
return this._frameManager._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||
}
|
||||
|
||||
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
|
||||
const objectId = toRemoteObject(handle).objectId;
|
||||
this._client.send('DOM.getDocument');
|
||||
const {nodeId} = await this._client.send('DOM.requestNode', {objectId});
|
||||
const result = await this._client.send('Page.snapshotNode', {nodeId});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
screenshot(handle: dom.ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
||||
const page = this._frameManager._page;
|
||||
return page._screenshotter.screenshotElement(page, handle, options);
|
||||
}
|
||||
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as mime from 'mime';
|
||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||
import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions } from '../input';
|
||||
import { TimeoutSettings } from '../TimeoutSettings';
|
||||
|
|
@ -28,7 +26,7 @@ import { FrameManager, FrameManagerEvents } from './FrameManager';
|
|||
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||
import { NetworkManagerEvents } from './NetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { TaskQueue } from './TaskQueue';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
import * as input from '../input';
|
||||
import * as types from '../types';
|
||||
import * as frames from '../frames';
|
||||
|
|
@ -38,8 +36,6 @@ import * as network from '../network';
|
|||
import * as dialog from '../dialog';
|
||||
import * as console from '../console';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
export type Viewport = {
|
||||
width: number;
|
||||
height: number;
|
||||
|
|
@ -58,22 +54,22 @@ export class Page extends EventEmitter {
|
|||
private _bootstrapScripts: string[] = [];
|
||||
_javascriptEnabled = true;
|
||||
private _viewport: Viewport | null = null;
|
||||
private _screenshotTaskQueue: TaskQueue;
|
||||
_screenshotter: Screenshotter;
|
||||
private _workers = new Map<string, Worker>();
|
||||
private _disconnectPromise: Promise<Error> | undefined;
|
||||
private _sessionListeners: RegisteredListener[] = [];
|
||||
private _emulatedMediaType: string | undefined;
|
||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||
|
||||
static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise<Page> {
|
||||
const page = new Page(session, browserContext, screenshotTaskQueue);
|
||||
static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
|
||||
const page = new Page(session, browserContext, screenshotter);
|
||||
await page._initialize();
|
||||
if (defaultViewport)
|
||||
await page.setViewport(defaultViewport);
|
||||
return page;
|
||||
}
|
||||
|
||||
constructor(session: TargetSession, browserContext: BrowserContext, screenshotTaskQueue: TaskQueue) {
|
||||
constructor(session: TargetSession, browserContext: BrowserContext, screenshotter: Screenshotter) {
|
||||
super();
|
||||
this._closedPromise = new Promise(f => this._closedCallback = f);
|
||||
this._keyboard = new input.Keyboard(new RawKeyboardImpl(session));
|
||||
|
|
@ -81,7 +77,7 @@ export class Page extends EventEmitter {
|
|||
this._timeoutSettings = new TimeoutSettings();
|
||||
this._frameManager = new FrameManager(session, this, this._timeoutSettings);
|
||||
|
||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
||||
this._screenshotter = screenshotter;
|
||||
|
||||
this._setSession(session);
|
||||
this._browserContext = browserContext;
|
||||
|
|
@ -371,63 +367,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', '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';
|
||||
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(!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, options));
|
||||
}
|
||||
|
||||
async _screenshotTask(options?: ScreenshotOptions): Promise<Buffer | string> {
|
||||
const params: Protocol.Page.snapshotRectParameters = { x: 0, y: 0, width: 800, height: 600, coordinateSystem: 'Page' };
|
||||
if (options.fullPage) {
|
||||
const pageSize = await this.evaluate(() =>
|
||||
({
|
||||
width: document.body.scrollWidth,
|
||||
height: document.body.scrollHeight
|
||||
}));
|
||||
Object.assign(params, pageSize);
|
||||
} else if (options.clip) {
|
||||
Object.assign(params, options.clip);
|
||||
} else if (this._viewport) {
|
||||
Object.assign(params, this._viewport);
|
||||
}
|
||||
const [, result] = await Promise.all([
|
||||
this.browser()._activatePage(this),
|
||||
this._session.send('Page.snapshotRect', params),
|
||||
]).catch(e => {
|
||||
debugError('Failed to take screenshot: ' + e);
|
||||
throw e;
|
||||
});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer | string> {
|
||||
return this._screenshotter.screenshotPage(this, options);
|
||||
}
|
||||
|
||||
async title(): Promise<string> {
|
||||
|
|
@ -540,16 +481,6 @@ type Metrics = {
|
|||
JSHeapTotalSize?: number,
|
||||
}
|
||||
|
||||
type ScreenshotOptions = {
|
||||
type?: string,
|
||||
path?: string,
|
||||
fullPage?: boolean,
|
||||
clip?: {x: number, y: number, width: number, height: number},
|
||||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
encoding?: string,
|
||||
}
|
||||
|
||||
type FileChooser = {
|
||||
element: dom.ElementHandle,
|
||||
multiple: boolean
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { Page } from './Page';
|
||||
import { assert, helper, debugError } from '../helper';
|
||||
import { Protocol } from './protocol';
|
||||
import * as dom from '../dom';
|
||||
import * as types from '../types';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
export class Screenshotter {
|
||||
private _queue = new TaskQueue();
|
||||
|
||||
async screenshotPage(page: Page, options: types.ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
assert(format === 'png', 'Only png format is supported');
|
||||
return this._queue.postTask(async () => {
|
||||
const params: Protocol.Page.snapshotRectParameters = { x: 0, y: 0, width: 800, height: 600, coordinateSystem: 'Page' };
|
||||
if (options.fullPage) {
|
||||
const pageSize = await page.evaluate(() =>
|
||||
({
|
||||
width: document.body.scrollWidth,
|
||||
height: document.body.scrollHeight
|
||||
}));
|
||||
Object.assign(params, pageSize);
|
||||
} else if (options.clip) {
|
||||
Object.assign(params, options.clip);
|
||||
} else if (page.viewport()) {
|
||||
Object.assign(params, page.viewport());
|
||||
}
|
||||
const [, result] = await Promise.all([
|
||||
page.browser()._activatePage(page),
|
||||
page._session.send('Page.snapshotRect', params),
|
||||
]).catch(e => {
|
||||
debugError('Failed to take screenshot: ' + e);
|
||||
throw e;
|
||||
});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
async screenshotElement(page: Page, handle: dom.ElementHandle, options: types.ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
assert(format === 'png', 'Only png format is supported');
|
||||
return this._queue.postTask(async () => {
|
||||
const objectId = (handle._remoteObject as Protocol.Runtime.RemoteObject).objectId;
|
||||
page._session.send('DOM.getDocument');
|
||||
const {nodeId} = await page._session.send('DOM.requestNode', {objectId});
|
||||
const [, result] = await Promise.all([
|
||||
page.browser()._activatePage(page),
|
||||
page._session.send('Page.snapshotNode', {nodeId})
|
||||
]).catch(e => {
|
||||
debugError('Failed to take screenshot: ' + e);
|
||||
throw e;
|
||||
});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ export class Target {
|
|||
async page(): Promise<Page | null> {
|
||||
if (this._type === 'page' && !this._pagePromise) {
|
||||
const session = this.browser()._connection.session(this._targetId);
|
||||
this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport, this.browser()._screenshotTaskQueue).then(page => {
|
||||
this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport, this.browser()._screenshotter).then(page => {
|
||||
this._adoptPage(page);
|
||||
return page;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue