feat(screenshots): make them work everywhere (#164)
|
|
@ -1506,8 +1506,7 @@ Page is guaranteed to have a main frame which persists during navigations.
|
||||||
- `width` <[number]> width of clipping area
|
- `width` <[number]> width of clipping area
|
||||||
- `height` <[number]> height of clipping area
|
- `height` <[number]> height of clipping area
|
||||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
- `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]<[Buffer]>> Promise which resolves to buffer with the captured screenshot.
|
||||||
- returns: <[Promise]<[string]|[Buffer]>> Promise which resolves to buffer or a base64 string (depending on the value of `encoding`) with captured screenshot.
|
|
||||||
|
|
||||||
> **NOTE** Screenshots take at least 1/6 second on OS X. See https://crbug.com/741689 for discussion.
|
> **NOTE** Screenshots take at least 1/6 second on OS X. See https://crbug.com/741689 for discussion.
|
||||||
|
|
||||||
|
|
@ -3456,19 +3455,17 @@ If `key` is a single character and no modifier keys besides `Shift` are being he
|
||||||
> **NOTE** Modifier keys DO effect `elementHandle.press`. Holding down `Shift` will type the text in upper case.
|
> **NOTE** Modifier keys DO effect `elementHandle.press`. Holding down `Shift` will type the text in upper case.
|
||||||
|
|
||||||
#### elementHandle.screenshot([options])
|
#### elementHandle.screenshot([options])
|
||||||
- `options` <[Object]> Same options as in [page.screenshot](#pagescreenshotoptions).
|
- `options` <[Object]> Screenshot options.
|
||||||
- `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` <"png"|"jpeg"> Specify screenshot type, 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`.
|
|
||||||
- `clip` <[Object]> Passed clip value is ignored and instead set to the element's bounding box.
|
- `clip` <[Object]> Passed clip value is ignored and instead set to the element's bounding box.
|
||||||
- `x` <[number]>
|
- `x` <[number]>
|
||||||
- `y` <[number]>
|
- `y` <[number]>
|
||||||
- `width` <[number]>
|
- `width` <[number]>
|
||||||
- `height` <[number]>
|
- `height` <[number]>
|
||||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
- `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]<|[Buffer]>> Promise which resolves to buffer with the captured screenshot.
|
||||||
- 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.
|
This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element.
|
||||||
If the element is detached from DOM, the method throws an error.
|
If the element is detached from DOM, the method throws an error.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"chromium_revision": "719491",
|
"chromium_revision": "719491",
|
||||||
"firefox_revision": "1004",
|
"firefox_revision": "1004",
|
||||||
"webkit_revision": "1011"
|
"webkit_revision": "1015"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"unit": "node test/test.js",
|
"unit": "node test/test.js",
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,16 @@ import { Events } from './events';
|
||||||
import { assert, helper } from '../helper';
|
import { assert, helper } from '../helper';
|
||||||
import { BrowserContext } from './BrowserContext';
|
import { BrowserContext } from './BrowserContext';
|
||||||
import { Connection, ConnectionEvents, CDPSession } from './Connection';
|
import { Connection, ConnectionEvents, CDPSession } from './Connection';
|
||||||
import { Page, Viewport } from './Page';
|
import { Page } from './Page';
|
||||||
import { Target } from './Target';
|
import { Target } from './Target';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { Chromium } from './features/chromium';
|
import { Chromium } from './features/chromium';
|
||||||
import { Screenshotter } from './Screenshotter';
|
import * as types from '../types';
|
||||||
|
|
||||||
export class Browser extends EventEmitter {
|
export class Browser extends EventEmitter {
|
||||||
private _ignoreHTTPSErrors: boolean;
|
private _ignoreHTTPSErrors: boolean;
|
||||||
private _defaultViewport: Viewport;
|
private _defaultViewport: types.Viewport;
|
||||||
private _process: childProcess.ChildProcess;
|
private _process: childProcess.ChildProcess;
|
||||||
private _screenshotter = new Screenshotter();
|
|
||||||
_connection: Connection;
|
_connection: Connection;
|
||||||
_client: CDPSession;
|
_client: CDPSession;
|
||||||
private _closeCallback: () => Promise<void>;
|
private _closeCallback: () => Promise<void>;
|
||||||
|
|
@ -44,7 +43,7 @@ export class Browser extends EventEmitter {
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
contextIds: string[],
|
contextIds: string[],
|
||||||
ignoreHTTPSErrors: boolean,
|
ignoreHTTPSErrors: boolean,
|
||||||
defaultViewport: Viewport | null,
|
defaultViewport: types.Viewport | null,
|
||||||
process: childProcess.ChildProcess | null,
|
process: childProcess.ChildProcess | null,
|
||||||
closeCallback?: (() => Promise<void>)) {
|
closeCallback?: (() => Promise<void>)) {
|
||||||
const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
|
const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
|
||||||
|
|
@ -56,7 +55,7 @@ export class Browser extends EventEmitter {
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
contextIds: string[],
|
contextIds: string[],
|
||||||
ignoreHTTPSErrors: boolean,
|
ignoreHTTPSErrors: boolean,
|
||||||
defaultViewport: Viewport | null,
|
defaultViewport: types.Viewport | null,
|
||||||
process: childProcess.ChildProcess | null,
|
process: childProcess.ChildProcess | null,
|
||||||
closeCallback?: (() => Promise<void>)) {
|
closeCallback?: (() => Promise<void>)) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -107,7 +106,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._screenshotter);
|
const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport);
|
||||||
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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CDPSession } from './Connection';
|
import { CDPSession } from './Connection';
|
||||||
import { Viewport } from './Page';
|
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import * as types from '../types';
|
||||||
|
|
||||||
export class EmulationManager {
|
export class EmulationManager {
|
||||||
private _client: CDPSession;
|
private _client: CDPSession;
|
||||||
|
|
@ -28,7 +28,7 @@ export class EmulationManager {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
async emulateViewport(viewport: Viewport): Promise<boolean> {
|
async emulateViewport(viewport: types.Viewport): Promise<boolean> {
|
||||||
const mobile = viewport.isMobile || false;
|
const mobile = viewport.isMobile || false;
|
||||||
const width = viewport.width;
|
const width = viewport.width;
|
||||||
const height = viewport.height;
|
const height = viewport.height;
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight };
|
return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshot(handle: dom.ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
screenshot(handle: dom.ElementHandle, options?: types.ElementScreenshotOptions): Promise<Buffer> {
|
||||||
const page = this._frameManager.page();
|
const page = this._frameManager.page();
|
||||||
return page._screenshotter.screenshotElement(page, handle, options);
|
return page._screenshotter.screenshotElement(handle, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { BrowserFetcher } from './BrowserFetcher';
|
||||||
import { Connection } from './Connection';
|
import { Connection } from './Connection';
|
||||||
import { TimeoutError } from '../Errors';
|
import { TimeoutError } from '../Errors';
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, debugError, helper } from '../helper';
|
||||||
import { Viewport } from './Page';
|
import * as types from '../types';
|
||||||
import { PipeTransport } from './PipeTransport';
|
import { PipeTransport } from './PipeTransport';
|
||||||
import { WebSocketTransport } from './WebSocketTransport';
|
import { WebSocketTransport } from './WebSocketTransport';
|
||||||
import { ConnectionTransport } from '../ConnectionTransport';
|
import { ConnectionTransport } from '../ConnectionTransport';
|
||||||
|
|
@ -392,6 +392,6 @@ export type LauncherLaunchOptions = {
|
||||||
|
|
||||||
export type LauncherBrowserOptions = {
|
export type LauncherBrowserOptions = {
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
defaultViewport?: Viewport | null,
|
defaultViewport?: types.Viewport | null,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import * as console from '../console';
|
||||||
|
import * as dialog from '../dialog';
|
||||||
|
import * as dom from '../dom';
|
||||||
|
import * as frames from '../frames';
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, debugError, helper } from '../helper';
|
||||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption, mediaTypes, mediaColorSchemes } from '../input';
|
import * as input from '../input';
|
||||||
|
import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions, PointerActionOptions, SelectOption } from '../input';
|
||||||
|
import * as js from '../javascript';
|
||||||
|
import * as network from '../network';
|
||||||
|
import { Screenshotter } from '../screenshotter';
|
||||||
import { TimeoutSettings } from '../TimeoutSettings';
|
import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
|
import * as types from '../types';
|
||||||
import { Browser } from './Browser';
|
import { Browser } from './Browser';
|
||||||
import { BrowserContext } from './BrowserContext';
|
import { BrowserContext } from './BrowserContext';
|
||||||
import { CDPSession, CDPSessionEvents } from './Connection';
|
import { CDPSession, CDPSessionEvents } from './Connection';
|
||||||
|
|
@ -26,34 +35,17 @@ import { EmulationManager } from './EmulationManager';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Accessibility } from './features/accessibility';
|
import { Accessibility } from './features/accessibility';
|
||||||
import { Coverage } from './features/coverage';
|
import { Coverage } from './features/coverage';
|
||||||
import { Overrides } from './features/overrides';
|
|
||||||
import { Interception } from './features/interception';
|
import { Interception } from './features/interception';
|
||||||
|
import { Overrides } from './features/overrides';
|
||||||
import { PDF } from './features/pdf';
|
import { PDF } from './features/pdf';
|
||||||
import { Workers } from './features/workers';
|
import { Workers } from './features/workers';
|
||||||
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||||
import { RawMouseImpl, RawKeyboardImpl } from './Input';
|
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||||
|
import { DOMWorldDelegate } from './JSHandle';
|
||||||
import { NetworkManagerEvents } from './NetworkManager';
|
import { NetworkManagerEvents } from './NetworkManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { getExceptionMessage, releaseObject } from './protocolHelper';
|
import { getExceptionMessage, releaseObject } from './protocolHelper';
|
||||||
import * as input from '../input';
|
import { CRScreenshotDelegate } from './Screenshotter';
|
||||||
import * as types from '../types';
|
|
||||||
import * as frames from '../frames';
|
|
||||||
import * as js from '../javascript';
|
|
||||||
import * as dom from '../dom';
|
|
||||||
import * as network from '../network';
|
|
||||||
import * as dialog from '../dialog';
|
|
||||||
import * as console from '../console';
|
|
||||||
import { DOMWorldDelegate } from './JSHandle';
|
|
||||||
import { Screenshotter } from './Screenshotter';
|
|
||||||
|
|
||||||
export type Viewport = {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
deviceScaleFactor?: number;
|
|
||||||
isMobile?: boolean;
|
|
||||||
isLandscape?: boolean;
|
|
||||||
hasTouch?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Page extends EventEmitter {
|
export class Page extends EventEmitter {
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
|
|
@ -74,21 +66,21 @@ export class Page extends EventEmitter {
|
||||||
readonly workers: Workers;
|
readonly workers: Workers;
|
||||||
private _pageBindings = new Map<string, Function>();
|
private _pageBindings = new Map<string, Function>();
|
||||||
_javascriptEnabled = true;
|
_javascriptEnabled = true;
|
||||||
private _viewport: Viewport | null = null;
|
private _viewport: types.Viewport | null = null;
|
||||||
_screenshotter: Screenshotter;
|
_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, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
|
static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: types.Viewport | null): Promise<Page> {
|
||||||
const page = new Page(client, browserContext, ignoreHTTPSErrors, screenshotter);
|
const page = new Page(client, browserContext, ignoreHTTPSErrors);
|
||||||
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, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, screenshotter: Screenshotter) {
|
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
|
||||||
super();
|
super();
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._closedPromise = new Promise(f => this._closedCallback = f);
|
this._closedPromise = new Promise(f => this._closedCallback = f);
|
||||||
|
|
@ -104,8 +96,7 @@ export class Page extends EventEmitter {
|
||||||
this.workers = new Workers(client, this._addConsoleMessage.bind(this), this._handleException.bind(this));
|
this.workers = new Workers(client, this._addConsoleMessage.bind(this), this._handleException.bind(this));
|
||||||
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._screenshotter = new Screenshotter(this, new CRScreenshotDelegate(this._client), browserContext.browser());
|
||||||
this._screenshotter = screenshotter;
|
|
||||||
|
|
||||||
client.on('Target.attachedToTarget', event => {
|
client.on('Target.attachedToTarget', event => {
|
||||||
if (event.targetInfo.type !== 'worker') {
|
if (event.targetInfo.type !== 'worker') {
|
||||||
|
|
@ -456,7 +447,7 @@ export class Page extends EventEmitter {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async emulate(options: { viewport: Viewport; userAgent: string; }) {
|
async emulate(options: { viewport: types.Viewport; userAgent: string; }) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.setViewport(options.viewport),
|
this.setViewport(options.viewport),
|
||||||
this.setUserAgent(options.userAgent)
|
this.setUserAgent(options.userAgent)
|
||||||
|
|
@ -485,14 +476,14 @@ export class Page extends EventEmitter {
|
||||||
this._emulatedMediaType = options.type;
|
this._emulatedMediaType = options.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewport(viewport: Viewport) {
|
async setViewport(viewport: types.Viewport) {
|
||||||
const needsReload = await this._emulationManager.emulateViewport(viewport);
|
const needsReload = await this._emulationManager.emulateViewport(viewport);
|
||||||
this._viewport = viewport;
|
this._viewport = viewport;
|
||||||
if (needsReload)
|
if (needsReload)
|
||||||
await this.reload();
|
await this.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport(): Viewport | null {
|
viewport(): types.Viewport | null {
|
||||||
return this._viewport;
|
return this._viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -509,8 +500,8 @@ export class Page extends EventEmitter {
|
||||||
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer | string> {
|
screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
|
||||||
return this._screenshotter.screenshotPage(this, options);
|
return this._screenshotter.screenshotPage(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
|
|
@ -585,11 +576,6 @@ export class Page extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaFeature = {
|
|
||||||
name: string,
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileChooser = {
|
type FileChooser = {
|
||||||
element: dom.ElementHandle,
|
element: dom.ElementHandle,
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
|
|
||||||
|
|
@ -1,134 +1,40 @@
|
||||||
/**
|
// Copyright (c) Microsoft Corporation.
|
||||||
* Copyright 2019 Google Inc. All rights reserved.
|
// Licensed under the MIT license.
|
||||||
* 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 { Protocol } from './protocol';
|
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
|
import { ScreenshotterDelegate } from '../screenshotter';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
import { CDPSession } from './api';
|
||||||
|
|
||||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
export class CRScreenshotDelegate implements ScreenshotterDelegate {
|
||||||
|
private _session: CDPSession;
|
||||||
|
|
||||||
export class Screenshotter {
|
constructor(session: CDPSession) {
|
||||||
private _queue = new TaskQueue();
|
this._session = session;
|
||||||
|
|
||||||
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: types.ScreenshotOptions = {}): Promise<string | Buffer> {
|
|
||||||
const format = helper.validateScreeshotOptions(options);
|
|
||||||
return this._queue.postTask(async () => {
|
|
||||||
let needsViewportReset = false;
|
|
||||||
|
|
||||||
let boundingBox = await handle.boundingBox();
|
async getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | undefined> {
|
||||||
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
const rect = await handle.boundingBox();
|
||||||
|
if (!rect)
|
||||||
const viewport = page.viewport();
|
return rect;
|
||||||
|
const { layoutViewport: { pageX, pageY } } = await this._session.send('Page.getLayoutMetrics');
|
||||||
if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) {
|
rect.x += pageX;
|
||||||
const newViewport = {
|
rect.y += pageY;
|
||||||
width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
return rect;
|
||||||
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: types.ScreenshotOptions): Promise<Buffer | string> {
|
canCaptureOutsideViewport(): boolean {
|
||||||
await page.browser()._activatePage(page);
|
return false;
|
||||||
let clip = options.clip ? processClip(options.clip) : undefined;
|
}
|
||||||
const viewport = page.viewport();
|
|
||||||
|
|
||||||
if (options.fullPage) {
|
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||||
const metrics = await page._client.send('Page.getLayoutMetrics');
|
await this._session.send('Emulation.setDefaultBackgroundColorOverride', { color });
|
||||||
const width = Math.ceil(metrics.contentSize.width);
|
}
|
||||||
const height = Math.ceil(metrics.contentSize.height);
|
|
||||||
|
|
||||||
// Overwrite clip for full page at all times.
|
async screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
|
||||||
clip = { x: 0, y: 0, width, height, scale: 1 };
|
const clip = options.clip ? { ...options.clip, scale: 1 } : undefined;
|
||||||
const {
|
const result = await this._session.send('Page.captureScreenshot', { format, quality: options.quality, clip });
|
||||||
isMobile = false,
|
return Buffer.from(result.data, 'base64');
|
||||||
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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as types from '../types';
|
||||||
import { Browser } from './Browser';
|
import { Browser } from './Browser';
|
||||||
import { BrowserContext } from './BrowserContext';
|
import { BrowserContext } from './BrowserContext';
|
||||||
import { CDPSession } from './Connection';
|
import { CDPSession } from './Connection';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Worker } from './features/workers';
|
import { Worker } from './features/workers';
|
||||||
import { Page, Viewport } from './Page';
|
import { Page } from './Page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { Screenshotter } from './Screenshotter';
|
|
||||||
|
|
||||||
const targetSymbol = Symbol('target');
|
const targetSymbol = Symbol('target');
|
||||||
|
|
||||||
|
|
@ -32,8 +32,7 @@ export class Target {
|
||||||
_targetId: string;
|
_targetId: string;
|
||||||
private _sessionFactory: () => Promise<CDPSession>;
|
private _sessionFactory: () => Promise<CDPSession>;
|
||||||
private _ignoreHTTPSErrors: boolean;
|
private _ignoreHTTPSErrors: boolean;
|
||||||
private _defaultViewport: Viewport;
|
private _defaultViewport: types.Viewport;
|
||||||
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>;
|
||||||
|
|
@ -49,15 +48,13 @@ export class Target {
|
||||||
browserContext: BrowserContext,
|
browserContext: BrowserContext,
|
||||||
sessionFactory: () => Promise<CDPSession>,
|
sessionFactory: () => Promise<CDPSession>,
|
||||||
ignoreHTTPSErrors: boolean,
|
ignoreHTTPSErrors: boolean,
|
||||||
defaultViewport: Viewport | null,
|
defaultViewport: types.Viewport | null) {
|
||||||
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._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;
|
||||||
|
|
@ -84,7 +81,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().then(async client => {
|
this._pagePromise = this._sessionFactory().then(async client => {
|
||||||
const page = await Page.create(client, this._browserContext, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotter);
|
const page = await Page.create(client, this._browserContext, this._ignoreHTTPSErrors, this._defaultViewport);
|
||||||
page[targetSymbol] = this;
|
page[targetSymbol] = this;
|
||||||
return page;
|
return page;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
|
||||||
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
||||||
import { assert, helper, debugError } from './helper';
|
import { assert, helper, debugError } from './helper';
|
||||||
import Injected from './injected/injected';
|
import Injected from './injected/injected';
|
||||||
import { SelectorRoot } from './injected/selectorEngine';
|
|
||||||
|
|
||||||
export interface DOMWorldDelegate {
|
export interface DOMWorldDelegate {
|
||||||
keyboard: input.Keyboard;
|
keyboard: input.Keyboard;
|
||||||
|
|
@ -146,7 +145,7 @@ export class DOMWorld {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
private readonly _world: DOMWorld;
|
readonly _world: DOMWorld;
|
||||||
|
|
||||||
constructor(context: js.ExecutionContext, remoteObject: any) {
|
constructor(context: js.ExecutionContext, remoteObject: any) {
|
||||||
super(context, remoteObject);
|
super(context, remoteObject);
|
||||||
|
|
@ -258,8 +257,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
private async _viewportPointAndScroll(relativePoint: types.Point): Promise<{point: types.Point, scrollX: number, scrollY: number}> {
|
private async _viewportPointAndScroll(relativePoint: types.Point): Promise<{point: types.Point, scrollX: number, scrollY: number}> {
|
||||||
const [box, border] = await Promise.all([
|
const [box, border] = await Promise.all([
|
||||||
this.boundingBox(),
|
this.boundingBox(),
|
||||||
this.evaluate((e: Element) => {
|
this.evaluate((node: Node) => {
|
||||||
const style = e.ownerDocument.defaultView.getComputedStyle(e);
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
const style = node.ownerDocument.defaultView.getComputedStyle(node as Element);
|
||||||
return { x: parseInt(style.borderLeftWidth, 10), y: parseInt(style.borderTopWidth, 10) };
|
return { x: parseInt(style.borderLeftWidth, 10), y: parseInt(style.borderTopWidth, 10) };
|
||||||
}).catch(debugError),
|
}).catch(debugError),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,12 @@ import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } f
|
||||||
import { Connection, ConnectionEvents } from './Connection';
|
import { Connection, ConnectionEvents } from './Connection';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Permissions } from './features/permissions';
|
import { Permissions } from './features/permissions';
|
||||||
import { Page, Viewport } from './Page';
|
import { Page } from './Page';
|
||||||
|
import * as types from '../types';
|
||||||
|
|
||||||
export class Browser extends EventEmitter {
|
export class Browser extends EventEmitter {
|
||||||
private _connection: Connection;
|
private _connection: Connection;
|
||||||
_defaultViewport: Viewport;
|
_defaultViewport: types.Viewport;
|
||||||
private _process: import('child_process').ChildProcess;
|
private _process: import('child_process').ChildProcess;
|
||||||
private _closeCallback: () => void;
|
private _closeCallback: () => void;
|
||||||
_targets: Map<string, Target>;
|
_targets: Map<string, Target>;
|
||||||
|
|
@ -33,14 +34,14 @@ export class Browser extends EventEmitter {
|
||||||
private _contexts: Map<string, BrowserContext>;
|
private _contexts: Map<string, BrowserContext>;
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
|
|
||||||
static async create(connection: Connection, defaultViewport: Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) {
|
static async create(connection: Connection, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) {
|
||||||
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
|
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
|
||||||
const browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback);
|
const browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback);
|
||||||
await connection.send('Target.enable');
|
await connection.send('Target.enable');
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(connection: Connection, browserContextIds: Array<string>, defaultViewport: Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) {
|
constructor(connection: Connection, browserContextIds: Array<string>, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) {
|
||||||
super();
|
super();
|
||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
this._defaultViewport = defaultViewport;
|
this._defaultViewport = defaultViewport;
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
return this._frameManager._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
return this._frameManager._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(handle: dom.ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
async screenshot(handle: dom.ElementHandle, options?: types.ElementScreenshotOptions): Promise<Buffer> {
|
||||||
const page = this._frameManager._page;
|
const page = this._frameManager._page;
|
||||||
return page._screenshotter.screenshotElement(page, handle, options);
|
return page._screenshotter.screenshotElement(handle, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -16,27 +16,28 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import * as console from '../console';
|
||||||
|
import * as dialog from '../dialog';
|
||||||
|
import * as dom from '../dom';
|
||||||
import { TimeoutError } from '../Errors';
|
import { TimeoutError } from '../Errors';
|
||||||
|
import * as frames from '../frames';
|
||||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||||
|
import * as input from '../input';
|
||||||
|
import * as js from '../javascript';
|
||||||
|
import * as network from '../network';
|
||||||
|
import { Screenshotter } from '../screenshotter';
|
||||||
import { TimeoutSettings } from '../TimeoutSettings';
|
import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
|
import * as types from '../types';
|
||||||
import { BrowserContext } from './Browser';
|
import { BrowserContext } from './Browser';
|
||||||
import { JugglerSession, JugglerSessionEvents } from './Connection';
|
import { JugglerSession, JugglerSessionEvents } from './Connection';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Accessibility } from './features/accessibility';
|
import { Accessibility } from './features/accessibility';
|
||||||
import { Interception } from './features/interception';
|
import { Interception } from './features/interception';
|
||||||
import { FrameManager, FrameManagerEvents, normalizeWaitUntil } from './FrameManager';
|
import { FrameManager, FrameManagerEvents, normalizeWaitUntil } from './FrameManager';
|
||||||
import { RawMouseImpl, RawKeyboardImpl } from './Input';
|
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||||
import { NavigationWatchdog } from './NavigationWatchdog';
|
import { NavigationWatchdog } from './NavigationWatchdog';
|
||||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
||||||
import * as input from '../input';
|
import { FFScreenshotDelegate } from './Screenshotter';
|
||||||
import * as types from '../types';
|
|
||||||
import * as js from '../javascript';
|
|
||||||
import * as dom from '../dom';
|
|
||||||
import * as network from '../network';
|
|
||||||
import * as frames from '../frames';
|
|
||||||
import * as dialog from '../dialog';
|
|
||||||
import * as console from '../console';
|
|
||||||
import { Screenshotter } from './Screenshotter';
|
|
||||||
|
|
||||||
export class Page extends EventEmitter {
|
export class Page extends EventEmitter {
|
||||||
private _timeoutSettings: TimeoutSettings;
|
private _timeoutSettings: TimeoutSettings;
|
||||||
|
|
@ -54,12 +55,12 @@ export class Page extends EventEmitter {
|
||||||
_frameManager: FrameManager;
|
_frameManager: FrameManager;
|
||||||
_javascriptEnabled = true;
|
_javascriptEnabled = true;
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
private _viewport: Viewport;
|
private _viewport: types.Viewport;
|
||||||
private _disconnectPromise: Promise<Error>;
|
private _disconnectPromise: Promise<Error>;
|
||||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||||
_screenshotter: Screenshotter;
|
_screenshotter: Screenshotter;
|
||||||
|
|
||||||
static async create(session: JugglerSession, browserContext: BrowserContext, defaultViewport: Viewport | null) {
|
static async create(session: JugglerSession, browserContext: BrowserContext, defaultViewport: types.Viewport | null) {
|
||||||
const page = new Page(session, browserContext);
|
const page = new Page(session, browserContext);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
session.send('Runtime.enable'),
|
session.send('Runtime.enable'),
|
||||||
|
|
@ -105,7 +106,7 @@ export class Page extends EventEmitter {
|
||||||
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)),
|
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)),
|
||||||
];
|
];
|
||||||
this._viewport = null;
|
this._viewport = null;
|
||||||
this._screenshotter = new Screenshotter(session);
|
this._screenshotter = new Screenshotter(this, new FFScreenshotDelegate(session, this._frameManager), browserContext.browser());
|
||||||
}
|
}
|
||||||
|
|
||||||
_didClose() {
|
_didClose() {
|
||||||
|
|
@ -247,7 +248,7 @@ export class Page extends EventEmitter {
|
||||||
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
|
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
|
||||||
}
|
}
|
||||||
|
|
||||||
async emulate(options: { viewport: Viewport; userAgent: string; }) {
|
async emulate(options: { viewport: types.Viewport; userAgent: string; }) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.setViewport(options.viewport),
|
this.setViewport(options.viewport),
|
||||||
this.setUserAgent(options.userAgent),
|
this.setUserAgent(options.userAgent),
|
||||||
|
|
@ -268,7 +269,7 @@ export class Page extends EventEmitter {
|
||||||
return this._viewport;
|
return this._viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewport(viewport: Viewport) {
|
async setViewport(viewport: types.Viewport) {
|
||||||
const {
|
const {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|
@ -280,8 +281,8 @@ export class Page extends EventEmitter {
|
||||||
await this._session.send('Page.setViewport', {
|
await this._session.send('Page.setViewport', {
|
||||||
viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape },
|
viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape },
|
||||||
});
|
});
|
||||||
const oldIsMobile = this._viewport ? this._viewport.isMobile : false;
|
const oldIsMobile = this._viewport ? !!this._viewport.isMobile : false;
|
||||||
const oldHasTouch = this._viewport ? this._viewport.hasTouch : false;
|
const oldHasTouch = this._viewport ? !!this._viewport.hasTouch : false;
|
||||||
this._viewport = viewport;
|
this._viewport = viewport;
|
||||||
if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch)
|
if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch)
|
||||||
await this.reload();
|
await this.reload();
|
||||||
|
|
@ -424,8 +425,8 @@ export class Page extends EventEmitter {
|
||||||
return watchDog.navigationResponse();
|
return watchDog.navigationResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshot(options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
screenshot(options: types.ScreenshotOptions = {}): Promise<Buffer> {
|
||||||
return this._screenshotter.screenshotPage(this, options);
|
return this._screenshotter.screenshotPage(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate: types.Evaluate = (pageFunction, ...args) => {
|
evaluate: types.Evaluate = (pageFunction, ...args) => {
|
||||||
|
|
@ -570,15 +571,6 @@ export class Page extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Viewport = {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
deviceScaleFactor?: number;
|
|
||||||
isMobile?: boolean;
|
|
||||||
isLandscape?: boolean;
|
|
||||||
hasTouch?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileChooser = {
|
type FileChooser = {
|
||||||
element: dom.ElementHandle,
|
element: dom.ElementHandle,
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,42 @@
|
||||||
// Copyright (c) Microsoft Corporation.
|
// Copyright (c) Microsoft Corporation.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import { ScreenshotterDelegate } from '../screenshotter';
|
||||||
import { Page } from './Page';
|
|
||||||
import { assert, helper } from '../helper';
|
|
||||||
import * as dom from '../dom';
|
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
import * as dom from '../dom';
|
||||||
import { JugglerSession } from './Connection';
|
import { JugglerSession } from './Connection';
|
||||||
|
import { FrameManager } from './FrameManager';
|
||||||
|
|
||||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
export class FFScreenshotDelegate implements ScreenshotterDelegate {
|
||||||
|
|
||||||
export class Screenshotter {
|
|
||||||
private _session: JugglerSession;
|
private _session: JugglerSession;
|
||||||
|
private _frameManager: FrameManager;
|
||||||
|
|
||||||
constructor(session: JugglerSession) {
|
constructor(session: JugglerSession, frameManager: FrameManager) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
this._frameManager = frameManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshotPage(page: Page, options: types.ScreenshotOptions = {}): Promise<Buffer | string> {
|
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | undefined> {
|
||||||
const format = helper.validateScreeshotOptions(options);
|
const frameId = this._frameManager._frameData(handle.executionContext().frame()).frameId;
|
||||||
const {data} = await this._session.send('Page.screenshot', {
|
return this._session.send('Page.getBoundingBox', {
|
||||||
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,
|
frameId,
|
||||||
objectId: handle._remoteObject.objectId,
|
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.');
|
canCaptureOutsideViewport(): boolean {
|
||||||
assert(clip.height, 'Node has 0 height.');
|
return true;
|
||||||
await handle._scrollIntoViewIfNeeded();
|
}
|
||||||
return this.screenshotPage(page, {
|
|
||||||
...options,
|
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||||
clip: {
|
}
|
||||||
x: clip.x,
|
|
||||||
y: clip.y,
|
async screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
|
||||||
width: clip.width,
|
const { data } = await this._session.send('Page.screenshot', {
|
||||||
height: clip.height,
|
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
|
||||||
},
|
fullPage: options.fullPage,
|
||||||
|
clip: options.clip,
|
||||||
});
|
});
|
||||||
|
return Buffer.from(data, 'base64');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
import * as mime from 'mime';
|
|
||||||
import { TimeoutError } from './Errors';
|
import { TimeoutError } from './Errors';
|
||||||
import * as types from './types';
|
|
||||||
|
|
||||||
export const debugError = debug(`playwright:error`);
|
export const debugError = debug(`playwright:error`);
|
||||||
|
|
||||||
|
|
@ -155,43 +153,6 @@ class Helper {
|
||||||
clearTimeout(timeoutTimer);
|
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) {
|
export function assert(value: any, message?: string) {
|
||||||
|
|
|
||||||
213
src/screenshotter.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
/**
|
||||||
|
* 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 * as mime from 'mime';
|
||||||
|
import * as dom from './dom';
|
||||||
|
import { assert, helper } from './helper';
|
||||||
|
import * as types from './types';
|
||||||
|
|
||||||
|
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||||
|
|
||||||
|
export interface Page {
|
||||||
|
viewport(): types.Viewport;
|
||||||
|
setViewport(v: types.Viewport): Promise<void>;
|
||||||
|
evaluate(f: () => any): Promise<types.Rect>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScreenshotterDelegate {
|
||||||
|
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | undefined>;
|
||||||
|
canCaptureOutsideViewport(): boolean;
|
||||||
|
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
|
||||||
|
screenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise<Buffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Screenshotter {
|
||||||
|
private _queue = new TaskQueue();
|
||||||
|
private _delegate: ScreenshotterDelegate;
|
||||||
|
private _page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page, delegate: ScreenshotterDelegate, browserObject: any) {
|
||||||
|
this._delegate = delegate;
|
||||||
|
this._page = page;
|
||||||
|
|
||||||
|
this._queue = browserObject[taskQueueSymbol];
|
||||||
|
if (!this._queue) {
|
||||||
|
this._queue = new TaskQueue();
|
||||||
|
browserObject[taskQueueSymbol] = this._queue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async screenshotPage(options: types.ScreenshotOptions = {}): Promise<Buffer> {
|
||||||
|
const format = validateScreeshotOptions(options);
|
||||||
|
return this._queue.postTask(async () => {
|
||||||
|
let overridenViewport: types.Viewport | undefined;
|
||||||
|
const viewport = this._page.viewport();
|
||||||
|
if (options.fullPage && !this._delegate.canCaptureOutsideViewport()) {
|
||||||
|
const fullPage = await this._page.evaluate(() => ({
|
||||||
|
width: Math.max(
|
||||||
|
document.body.scrollWidth, document.documentElement.scrollWidth,
|
||||||
|
document.body.offsetWidth, document.documentElement.offsetWidth,
|
||||||
|
document.body.clientWidth, document.documentElement.clientWidth
|
||||||
|
),
|
||||||
|
height: Math.max(
|
||||||
|
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||||||
|
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||||
|
document.body.clientHeight, document.documentElement.clientHeight
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
overridenViewport = { ...viewport, ...fullPage };
|
||||||
|
await this._page.setViewport(overridenViewport);
|
||||||
|
} else if (options.clip) {
|
||||||
|
options.clip = trimClipToViewport(viewport, options.clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this._screenshot(format, options, overridenViewport || viewport);
|
||||||
|
|
||||||
|
if (overridenViewport)
|
||||||
|
await this._page.setViewport(viewport);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async screenshotElement(handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise<Buffer> {
|
||||||
|
const format = validateScreeshotOptions(options);
|
||||||
|
const rewrittenOptions: types.ScreenshotOptions = { ...options };
|
||||||
|
return this._queue.postTask(async () => {
|
||||||
|
let overridenViewport: types.Viewport | undefined;
|
||||||
|
|
||||||
|
let boundingBox = await this._delegate.getBoundingBox(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.');
|
||||||
|
boundingBox = enclosingIntRect(boundingBox);
|
||||||
|
const viewport = this._page.viewport();
|
||||||
|
|
||||||
|
if (!this._delegate.canCaptureOutsideViewport()) {
|
||||||
|
if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) {
|
||||||
|
overridenViewport = {
|
||||||
|
...viewport,
|
||||||
|
width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
||||||
|
height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
|
||||||
|
};
|
||||||
|
await this._page.setViewport(overridenViewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
await handle._scrollIntoViewIfNeeded();
|
||||||
|
boundingBox = enclosingIntRect(await this._delegate.getBoundingBox(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overridenViewport)
|
||||||
|
rewrittenOptions.clip = boundingBox;
|
||||||
|
|
||||||
|
const result = await this._screenshot(format, rewrittenOptions, overridenViewport || viewport);
|
||||||
|
|
||||||
|
if (overridenViewport)
|
||||||
|
await this._page.setViewport(viewport);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewport: types.Viewport): Promise<Buffer> {
|
||||||
|
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
|
||||||
|
if (shouldSetDefaultBackground)
|
||||||
|
await this._delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0});
|
||||||
|
const buffer = await this._delegate.screenshot(format, options, viewport);
|
||||||
|
if (shouldSetDefaultBackground)
|
||||||
|
await this._delegate.setBackgroundColor();
|
||||||
|
if (options.path)
|
||||||
|
await writeFileAsync(options.path, buffer);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskQueueSymbol = Symbol('TaskQueue');
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimClipToViewport(viewport: types.Viewport, clip: types.Rect | undefined): types.Rect | undefined {
|
||||||
|
if (!clip)
|
||||||
|
return;
|
||||||
|
const p1 = { x: Math.min(clip.x, viewport.width), y: Math.min(clip.y, viewport.height) };
|
||||||
|
const p2 = { x: Math.min(clip.x + clip.width, viewport.width), y: Math.min(clip.y + clip.height, viewport.height) };
|
||||||
|
const result = { x: p1.x, y: p1.y, width: p2.x - p1.x, height: p2.y - p1.y };
|
||||||
|
assert(result.width && result.height, 'Clipped area is either empty or outside the viewport');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enclosingIntRect(rect: types.Rect): types.Rect {
|
||||||
|
const x = rect.x | 0;
|
||||||
|
const y = rect.y | 0;
|
||||||
|
const x2 = Math.ceil(((rect.x + rect.width) * 100 | 0) / 100);
|
||||||
|
const y2 = Math.ceil(((rect.y + rect.height) * 100 | 0) / 100);
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: x2 - x,
|
||||||
|
height: y2 - y
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/types.ts
|
|
@ -43,12 +43,23 @@ export function clearSelector(selector: string | Selector): string | Selector {
|
||||||
return { selector: selector.selector, visible: selector.visible };
|
return { selector: selector.selector, visible: selector.visible };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScreenshotOptions = {
|
export type ElementScreenshotOptions = {
|
||||||
type?: 'png' | 'jpeg',
|
type?: 'png' | 'jpeg',
|
||||||
path?: string,
|
path?: string,
|
||||||
fullPage?: boolean,
|
|
||||||
clip?: Rect,
|
|
||||||
quality?: number,
|
quality?: number,
|
||||||
omitBackground?: boolean,
|
omitBackground?: boolean,
|
||||||
encoding?: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScreenshotOptions = ElementScreenshotOptions & {
|
||||||
|
fullPage?: boolean,
|
||||||
|
clip?: Rect,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Viewport = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
deviceScaleFactor?: number;
|
||||||
|
isMobile?: boolean;
|
||||||
|
isLandscape?: boolean;
|
||||||
|
hasTouch?: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,14 @@ import { EventEmitter } from 'events';
|
||||||
import { assert, helper, RegisteredListener, debugError } from '../helper';
|
import { assert, helper, RegisteredListener, debugError } from '../helper';
|
||||||
import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network';
|
import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network';
|
||||||
import { Connection } from './Connection';
|
import { Connection } from './Connection';
|
||||||
import { Page, Viewport } from './Page';
|
import { Page } from './Page';
|
||||||
import { Target } from './Target';
|
import { Target } from './Target';
|
||||||
import { Screenshotter } from './Screenshotter';
|
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import * as types from '../types';
|
||||||
|
|
||||||
export class Browser extends EventEmitter {
|
export class Browser extends EventEmitter {
|
||||||
_defaultViewport: Viewport;
|
_defaultViewport: types.Viewport;
|
||||||
private _process: childProcess.ChildProcess;
|
private _process: childProcess.ChildProcess;
|
||||||
_screenshotter = new Screenshotter();
|
|
||||||
_connection: Connection;
|
_connection: Connection;
|
||||||
private _closeCallback: () => Promise<void>;
|
private _closeCallback: () => Promise<void>;
|
||||||
private _defaultContext: BrowserContext;
|
private _defaultContext: BrowserContext;
|
||||||
|
|
@ -39,7 +38,7 @@ export class Browser extends EventEmitter {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
defaultViewport: Viewport | null,
|
defaultViewport: types.Viewport | null,
|
||||||
process: childProcess.ChildProcess | null,
|
process: childProcess.ChildProcess | null,
|
||||||
closeCallback?: (() => Promise<void>)) {
|
closeCallback?: (() => Promise<void>)) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -60,9 +59,6 @@ export class Browser extends EventEmitter {
|
||||||
helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
|
helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
|
||||||
helper.addEventListener(this._connection, 'Target.didCommitProvisionalTarget', this._onProvisionalTargetCommitted.bind(this)),
|
helper.addEventListener(this._connection, 'Target.didCommitProvisionalTarget', this._onProvisionalTargetCommitted.bind(this)),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Taking multiple screenshots in parallel doesn't work well, so we serialize them.
|
|
||||||
this._screenshotter = new Screenshotter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async userAgent(): Promise<string> {
|
async userAgent(): Promise<string> {
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||||
return this._frameManager._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
return this._frameManager._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshot(handle: dom.ElementHandle, options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
screenshot(handle: dom.ElementHandle, options?: types.ElementScreenshotOptions): Promise<string | Buffer> {
|
||||||
const page = this._frameManager._page;
|
const page = this._frameManager._page;
|
||||||
return page._screenshotter.screenshotElement(page, handle, options);
|
return page._screenshotter.screenshotElement(handle, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { debugError, helper } from '../helper';
|
||||||
import { Browser } from './Browser';
|
import { Browser } from './Browser';
|
||||||
import { BrowserFetcher } from './BrowserFetcher';
|
import { BrowserFetcher } from './BrowserFetcher';
|
||||||
import { Connection } from './Connection';
|
import { Connection } from './Connection';
|
||||||
import { Viewport } from './Page';
|
import * as types from '../types';
|
||||||
import { PipeTransport } from './PipeTransport';
|
import { PipeTransport } from './PipeTransport';
|
||||||
|
|
||||||
const DEFAULT_ARGS = [
|
const DEFAULT_ARGS = [
|
||||||
|
|
@ -186,6 +186,6 @@ export type LauncherLaunchOptions = {
|
||||||
headless?: boolean,
|
headless?: boolean,
|
||||||
dumpio?: boolean,
|
dumpio?: boolean,
|
||||||
env?: {[key: string]: string} | undefined,
|
env?: {[key: string]: string} | undefined,
|
||||||
defaultViewport?: Viewport | null,
|
defaultViewport?: types.Viewport | null,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import * as console from '../console';
|
||||||
|
import * as dialog from '../dialog';
|
||||||
|
import * as dom from '../dom';
|
||||||
|
import * as frames from '../frames';
|
||||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||||
|
import * as input from '../input';
|
||||||
import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions } from '../input';
|
import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions } from '../input';
|
||||||
|
import * as js from '../javascript';
|
||||||
|
import * as network from '../network';
|
||||||
|
import { Screenshotter } from '../screenshotter';
|
||||||
import { TimeoutSettings } from '../TimeoutSettings';
|
import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
|
import * as types from '../types';
|
||||||
import { Browser, BrowserContext } from './Browser';
|
import { Browser, BrowserContext } from './Browser';
|
||||||
import { TargetSession, TargetSessionEvents } from './Connection';
|
import { TargetSession, TargetSessionEvents } from './Connection';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
|
@ -26,20 +35,7 @@ import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||||
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||||
import { NetworkManagerEvents } from './NetworkManager';
|
import { NetworkManagerEvents } from './NetworkManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { Screenshotter } from './Screenshotter';
|
import { WKScreenshotDelegate } from './Screenshotter';
|
||||||
import * as input from '../input';
|
|
||||||
import * as types from '../types';
|
|
||||||
import * as frames from '../frames';
|
|
||||||
import * as js from '../javascript';
|
|
||||||
import * as dom from '../dom';
|
|
||||||
import * as network from '../network';
|
|
||||||
import * as dialog from '../dialog';
|
|
||||||
import * as console from '../console';
|
|
||||||
|
|
||||||
export type Viewport = {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Page extends EventEmitter {
|
export class Page extends EventEmitter {
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
|
|
@ -53,7 +49,7 @@ export class Page extends EventEmitter {
|
||||||
private _frameManager: FrameManager;
|
private _frameManager: FrameManager;
|
||||||
private _bootstrapScripts: string[] = [];
|
private _bootstrapScripts: string[] = [];
|
||||||
_javascriptEnabled = true;
|
_javascriptEnabled = true;
|
||||||
private _viewport: Viewport | null = null;
|
private _viewport: types.Viewport | null = null;
|
||||||
_screenshotter: Screenshotter;
|
_screenshotter: Screenshotter;
|
||||||
private _workers = new Map<string, Worker>();
|
private _workers = new Map<string, Worker>();
|
||||||
private _disconnectPromise: Promise<Error> | undefined;
|
private _disconnectPromise: Promise<Error> | undefined;
|
||||||
|
|
@ -61,15 +57,15 @@ export class Page extends EventEmitter {
|
||||||
private _emulatedMediaType: string | undefined;
|
private _emulatedMediaType: string | undefined;
|
||||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||||
|
|
||||||
static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
|
static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: types.Viewport | null): Promise<Page> {
|
||||||
const page = new Page(session, browserContext, screenshotter);
|
const page = new Page(session, browserContext);
|
||||||
await page._initialize();
|
await page._initialize();
|
||||||
if (defaultViewport)
|
if (defaultViewport)
|
||||||
await page.setViewport(defaultViewport);
|
await page.setViewport(defaultViewport);
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(session: TargetSession, browserContext: BrowserContext, screenshotter: Screenshotter) {
|
constructor(session: TargetSession, browserContext: BrowserContext) {
|
||||||
super();
|
super();
|
||||||
this._closedPromise = new Promise(f => this._closedCallback = f);
|
this._closedPromise = new Promise(f => this._closedCallback = f);
|
||||||
this._keyboard = new input.Keyboard(new RawKeyboardImpl(session));
|
this._keyboard = new input.Keyboard(new RawKeyboardImpl(session));
|
||||||
|
|
@ -77,7 +73,7 @@ export class Page extends EventEmitter {
|
||||||
this._timeoutSettings = new TimeoutSettings();
|
this._timeoutSettings = new TimeoutSettings();
|
||||||
this._frameManager = new FrameManager(session, this, this._timeoutSettings);
|
this._frameManager = new FrameManager(session, this, this._timeoutSettings);
|
||||||
|
|
||||||
this._screenshotter = screenshotter;
|
this._screenshotter = new Screenshotter(this, new WKScreenshotDelegate(session), browserContext.browser());
|
||||||
|
|
||||||
this._setSession(session);
|
this._setSession(session);
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
|
|
@ -315,7 +311,7 @@ export class Page extends EventEmitter {
|
||||||
}, timeout, this._sessionClosePromise());
|
}, timeout, this._sessionClosePromise());
|
||||||
}
|
}
|
||||||
|
|
||||||
async emulate(options: { viewport: Viewport; userAgent: string; }) {
|
async emulate(options: { viewport: types.Viewport; userAgent: string; }) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.setViewport(options.viewport),
|
this.setViewport(options.viewport),
|
||||||
this.setUserAgent(options.userAgent)
|
this.setUserAgent(options.userAgent)
|
||||||
|
|
@ -333,14 +329,14 @@ export class Page extends EventEmitter {
|
||||||
this._emulatedMediaType = options.type;
|
this._emulatedMediaType = options.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewport(viewport: Viewport) {
|
async setViewport(viewport: types.Viewport) {
|
||||||
this._viewport = viewport;
|
this._viewport = viewport;
|
||||||
const width = viewport.width;
|
const width = viewport.width;
|
||||||
const height = viewport.height;
|
const height = viewport.height;
|
||||||
await this._session.send('Emulation.setDeviceMetricsOverride', { width, height });
|
await this._session.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: viewport.deviceScaleFactor || 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport(): Viewport | null {
|
viewport(): types.Viewport | null {
|
||||||
return this._viewport;
|
return this._viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,8 +363,8 @@ export class Page extends EventEmitter {
|
||||||
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer | string> {
|
screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
|
||||||
return this._screenshotter.screenshotPage(this, options);
|
return this._screenshotter.screenshotPage(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
|
|
@ -464,23 +460,6 @@ export class Page extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Metrics = {
|
|
||||||
Timestamp?: number,
|
|
||||||
Documents?: number,
|
|
||||||
Frames?: number,
|
|
||||||
JSEventListeners?: number,
|
|
||||||
Nodes?: number,
|
|
||||||
LayoutCount?: number,
|
|
||||||
RecalcStyleCount?: number,
|
|
||||||
LayoutDuration?: number,
|
|
||||||
RecalcStyleDuration?: number,
|
|
||||||
ScriptDuration?: number,
|
|
||||||
TaskDuration?: number,
|
|
||||||
JSHeapUsedSize?: number,
|
|
||||||
JSHeapTotalSize?: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileChooser = {
|
type FileChooser = {
|
||||||
element: dom.ElementHandle,
|
element: dom.ElementHandle,
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,40 @@
|
||||||
// Copyright (c) Microsoft Corporation.
|
// Copyright (c) Microsoft Corporation.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as jpeg from 'jpeg-js';
|
||||||
import { Page } from './Page';
|
import { PNG } from 'pngjs';
|
||||||
import { assert, helper, debugError } from '../helper';
|
|
||||||
import { Protocol } from './protocol';
|
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
|
import { ScreenshotterDelegate } from '../screenshotter';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
import { TargetSession } from './Connection';
|
||||||
|
|
||||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
export class WKScreenshotDelegate implements ScreenshotterDelegate {
|
||||||
|
private _session: TargetSession;
|
||||||
|
|
||||||
export class Screenshotter {
|
constructor(session: TargetSession) {
|
||||||
private _queue = new TaskQueue();
|
this._session = session;
|
||||||
|
|
||||||
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> {
|
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | undefined> {
|
||||||
const format = helper.validateScreeshotOptions(options);
|
return handle.boundingBox();
|
||||||
assert(format === 'png', 'Only png format is supported');
|
}
|
||||||
return this._queue.postTask(async () => {
|
|
||||||
const objectId = (handle._remoteObject as Protocol.Runtime.RemoteObject).objectId;
|
canCaptureOutsideViewport(): boolean {
|
||||||
page._session.send('DOM.getDocument');
|
return false;
|
||||||
const {nodeId} = await page._session.send('DOM.requestNode', {objectId});
|
}
|
||||||
const [, result] = await Promise.all([
|
|
||||||
page.browser()._activatePage(page),
|
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||||
page._session.send('Page.snapshotNode', {nodeId})
|
// TODO: line below crashes, sort it out.
|
||||||
]).catch(e => {
|
this._session.send('Page.setDefaultBackgroundColorOverride', { color });
|
||||||
debugError('Failed to take screenshot: ' + e);
|
}
|
||||||
throw e;
|
|
||||||
});
|
async screenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport ): Promise<Buffer> {
|
||||||
const prefix = 'data:image/png;base64,';
|
const rect = options.clip || { x: 0, y: 0, width: viewport.width, height: viewport.height };
|
||||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: options.fullPage ? 'Page' : 'Viewport' });
|
||||||
if (options.path)
|
const prefix = 'data:image/png;base64,';
|
||||||
await writeFileAsync(options.path, buffer);
|
let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||||
return buffer;
|
if (format === 'jpeg')
|
||||||
});
|
buffer = jpeg.encode(PNG.sync.read(buffer)).data;
|
||||||
}
|
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> {
|
async page(): Promise<Page | null> {
|
||||||
if (this._type === 'page' && !this._pagePromise) {
|
if (this._type === 'page' && !this._pagePromise) {
|
||||||
const session = this.browser()._connection.session(this._targetId);
|
const session = this.browser()._connection.session(this._targetId);
|
||||||
this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport, this.browser()._screenshotter).then(page => {
|
this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport).then(page => {
|
||||||
this._adoptPage(page);
|
this._adoptPage(page);
|
||||||
return page;
|
return page;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 138 B After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 119 B After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 75 B After Width: | Height: | Size: 81 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 461 B |
|
Before Width: | Height: | Size: 109 B After Width: | Height: | Size: 138 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 153 B After Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 153 B After Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 35 KiB |
BIN
test/golden-firefox/white.jpg
Normal file
|
After Width: | Height: | Size: 357 B |
|
Before Width: | Height: | Size: 97 B After Width: | Height: | Size: 81 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 461 B |
|
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 136 B After Width: | Height: | Size: 138 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 168 B |
BIN
test/golden-webkit/screenshot-element-rotate.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 329 B |
BIN
test/golden-webkit/white.jpg
Normal file
|
After Width: | Height: | Size: 357 B |
BIN
test/golden-webkit/white.png
Normal file
|
After Width: | Height: | Size: 433 B |
|
|
@ -39,20 +39,33 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
});
|
});
|
||||||
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
|
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
|
||||||
});
|
});
|
||||||
it.skip(FFOX)('should clip elements to the viewport', async({page, server}) => {
|
it('should clip elements to the viewport', async({page, server}) => {
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
await page.goto(server.PREFIX + '/grid.html');
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
const screenshot = await page.screenshot({
|
const screenshot = await page.screenshot({
|
||||||
clip: {
|
clip: {
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 600,
|
y: 450,
|
||||||
width: 100,
|
width: 1000,
|
||||||
height: 100
|
height: 100
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
|
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
|
||||||
});
|
});
|
||||||
it.skip(WEBKIT)('should run in parallel', async({page, server}) => {
|
it('should throw on clip outside the viewport', async({page, server}) => {
|
||||||
|
await page.setViewport({width: 500, height: 500});
|
||||||
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
|
const screenshotError = await page.screenshot({
|
||||||
|
clip: {
|
||||||
|
x: 50,
|
||||||
|
y: 650,
|
||||||
|
width: 100,
|
||||||
|
height: 100
|
||||||
|
}
|
||||||
|
}).catch(error => error);
|
||||||
|
expect(screenshotError.message).toBe('Clipped area is either empty or outside the viewport');
|
||||||
|
});
|
||||||
|
it('should run in parallel', async({page, server}) => {
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
await page.goto(server.PREFIX + '/grid.html');
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
@ -92,13 +105,21 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
|
expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
|
||||||
await Promise.all(pages.map(page => page.close()));
|
await Promise.all(pages.map(page => page.close()));
|
||||||
});
|
});
|
||||||
it.skip(FFOX)('should allow transparency', async({page, server}) => {
|
it.skip(FFOX || WEBKIT)('should allow transparency', async({page, server}) => {
|
||||||
await page.setViewport({ width: 100, height: 100 });
|
await page.setViewport({ width: 50, height: 150 });
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.setContent(`
|
||||||
|
<style>
|
||||||
|
body { margin: 0 }
|
||||||
|
div { width: 50px; height: 50px; }
|
||||||
|
</style>
|
||||||
|
<div style="background:black"></div>
|
||||||
|
<div style="background:white"></div>
|
||||||
|
<div style="background:transparent"></div>
|
||||||
|
`);
|
||||||
const screenshot = await page.screenshot({omitBackground: true});
|
const screenshot = await page.screenshot({omitBackground: true});
|
||||||
expect(screenshot).toBeGolden('transparent.png');
|
expect(screenshot).toBeGolden('transparent.png');
|
||||||
});
|
});
|
||||||
it.skip(FFOX || WEBKIT)('should render white background on jpeg file', async({page, server}) => {
|
it('should render white background on jpeg file', async({page, server}) => {
|
||||||
await page.setViewport({ width: 100, height: 100 });
|
await page.setViewport({ width: 100, height: 100 });
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const screenshot = await page.screenshot({omitBackground: true, type: 'jpeg'});
|
const screenshot = await page.screenshot({omitBackground: true, type: 'jpeg'});
|
||||||
|
|
@ -137,7 +158,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
it('should take into account padding and border', async({page, server}) => {
|
it('should take into account padding and border', async({page, server}) => {
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
something above
|
<div style="height: 14px">oooo</div>
|
||||||
<style>div {
|
<style>div {
|
||||||
border: 2px solid blue;
|
border: 2px solid blue;
|
||||||
background: green;
|
background: green;
|
||||||
|
|
@ -145,9 +166,9 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div></div>
|
<div id="d"></div>
|
||||||
`);
|
`);
|
||||||
const elementHandle = await page.$('div');
|
const elementHandle = await page.$('div#d');
|
||||||
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');
|
||||||
});
|
});
|
||||||
|
|
@ -155,7 +176,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
|
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
something above
|
<div style="height: 14px">oooo</div>
|
||||||
<style>
|
<style>
|
||||||
div.to-screenshot {
|
div.to-screenshot {
|
||||||
border: 1px solid blue;
|
border: 1px solid blue;
|
||||||
|
|
@ -182,7 +203,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
|
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
something above
|
<div style="height: 14px">oooo</div>
|
||||||
<style>
|
<style>
|
||||||
div.to-screenshot {
|
div.to-screenshot {
|
||||||
border: 1px solid blue;
|
border: 1px solid blue;
|
||||||
|
|
@ -207,7 +228,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||||
it('should scroll element into view', async({page, server}) => {
|
it('should scroll element into view', async({page, server}) => {
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
something above
|
<div style="height: 14px">oooo</div>
|
||||||
<style>div.above {
|
<style>div.above {
|
||||||
border: 2px solid blue;
|
border: 2px solid blue;
|
||||||
background: red;
|
background: red;
|
||||||
|
|
@ -227,7 +248,7 @@ 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-scrolled-into-view.png');
|
expect(screenshot).toBeGolden('screenshot-element-scrolled-into-view.png');
|
||||||
});
|
});
|
||||||
it.skip(WEBKIT)('should work with a rotated element', async({page, server}) => {
|
it('should work with a rotated element', async({page, server}) => {
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
await page.setContent(`<div style="position:absolute;
|
await page.setContent(`<div style="position:absolute;
|
||||||
top: 100px;
|
top: 100px;
|
||||||
|
|
@ -240,14 +261,14 @@ 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-rotate.png');
|
expect(screenshot).toBeGolden('screenshot-element-rotate.png');
|
||||||
});
|
});
|
||||||
it.skip(WEBKIT)('should fail to screenshot a detached element', async({page, server}) => {
|
it('should fail to screenshot a detached element', async({page, server}) => {
|
||||||
await page.setContent('<h1>remove this</h1>');
|
await page.setContent('<h1>remove this</h1>');
|
||||||
const elementHandle = await page.$('h1');
|
const elementHandle = await page.$('h1');
|
||||||
await page.evaluate(element => element.remove(), elementHandle);
|
await page.evaluate(element => element.remove(), elementHandle);
|
||||||
const screenshotError = await elementHandle.screenshot().catch(error => error);
|
const screenshotError = await elementHandle.screenshot().catch(error => error);
|
||||||
expect(screenshotError.message).toBe('Node is either not visible or not an HTMLElement');
|
expect(screenshotError.message).toBe('Node is either not visible or not an HTMLElement');
|
||||||
});
|
});
|
||||||
it.skip(WEBKIT)('should not hang with zero width/height element', async({page, server}) => {
|
it('should not hang with zero width/height element', async({page, server}) => {
|
||||||
await page.setContent('<div style="width: 50px; height: 0"></div>');
|
await page.setContent('<div style="width: 50px; height: 0"></div>');
|
||||||
const div = await page.$('div');
|
const div = await page.$('div');
|
||||||
const error = await div.screenshot().catch(e => e);
|
const error = await div.screenshot().catch(e => e);
|
||||||
|
|
|
||||||