feat(screenshots): make them work everywhere (#164)

This commit is contained in:
Pavel Feldman 2019-12-06 11:33:24 -08:00 committed by Dmitry Gozman
parent 57313e3f73
commit bb1888c86e
55 changed files with 453 additions and 458 deletions

View file

@ -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.

View file

@ -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",

View file

@ -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);

View file

@ -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;

View file

@ -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> {

View file

@ -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,
}; };

View file

@ -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

View file

@ -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;
} }
} }

View file

@ -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;
}); });

View file

@ -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),
]); ]);

View file

@ -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;

View file

@ -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> {

View file

@ -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

View file

@ -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');
} }
} }

View file

@ -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
View 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
};
}

View file

@ -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;
}

View file

@ -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> {

View file

@ -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> {

View file

@ -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,
}; };

View file

@ -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

View file

@ -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;
} }
} }

View file

@ -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;
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 B

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 B

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 B

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

View file

@ -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);