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
|
||||
- `height` <[number]> height of clipping area
|
||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
||||
- `encoding` <[string]> The encoding of the image, can be either `base64` or `binary`. Defaults to `binary`.
|
||||
- returns: <[Promise]<[string]|[Buffer]>> Promise which resolves to buffer or a base64 string (depending on the value of `encoding`) with captured screenshot.
|
||||
- returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with the captured screenshot.
|
||||
|
||||
> **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.
|
||||
|
||||
#### 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.
|
||||
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to 'png'.
|
||||
- `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images.
|
||||
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page. Defaults to `false`.
|
||||
- `clip` <[Object]> Passed clip value is ignored and instead set to the element's bounding box.
|
||||
- `x` <[number]>
|
||||
- `y` <[number]>
|
||||
- `width` <[number]>
|
||||
- `height` <[number]>
|
||||
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
|
||||
- `encoding` <[string]> The encoding of the image, can be either `base64` or `binary`. Defaults to `binary`.
|
||||
- returns: <[Promise]<[string]|[Buffer]>> Promise which resolves to buffer or a base64 string (depending on the value of `options.encoding`) with captured screenshot.
|
||||
- returns: <[Promise]<|[Buffer]>> Promise which resolves to buffer with the captured screenshot.
|
||||
|
||||
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.
|
||||
|
@ -10,7 +10,7 @@
|
||||
"playwright": {
|
||||
"chromium_revision": "719491",
|
||||
"firefox_revision": "1004",
|
||||
"webkit_revision": "1011"
|
||||
"webkit_revision": "1015"
|
||||
},
|
||||
"scripts": {
|
||||
"unit": "node test/test.js",
|
||||
|
@ -21,17 +21,16 @@ import { Events } from './events';
|
||||
import { assert, helper } from '../helper';
|
||||
import { BrowserContext } from './BrowserContext';
|
||||
import { Connection, ConnectionEvents, CDPSession } from './Connection';
|
||||
import { Page, Viewport } from './Page';
|
||||
import { Page } from './Page';
|
||||
import { Target } from './Target';
|
||||
import { Protocol } from './protocol';
|
||||
import { Chromium } from './features/chromium';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
import * as types from '../types';
|
||||
|
||||
export class Browser extends EventEmitter {
|
||||
private _ignoreHTTPSErrors: boolean;
|
||||
private _defaultViewport: Viewport;
|
||||
private _defaultViewport: types.Viewport;
|
||||
private _process: childProcess.ChildProcess;
|
||||
private _screenshotter = new Screenshotter();
|
||||
_connection: Connection;
|
||||
_client: CDPSession;
|
||||
private _closeCallback: () => Promise<void>;
|
||||
@ -44,7 +43,7 @@ export class Browser extends EventEmitter {
|
||||
connection: Connection,
|
||||
contextIds: string[],
|
||||
ignoreHTTPSErrors: boolean,
|
||||
defaultViewport: Viewport | null,
|
||||
defaultViewport: types.Viewport | null,
|
||||
process: childProcess.ChildProcess | null,
|
||||
closeCallback?: (() => Promise<void>)) {
|
||||
const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
|
||||
@ -56,7 +55,7 @@ export class Browser extends EventEmitter {
|
||||
connection: Connection,
|
||||
contextIds: string[],
|
||||
ignoreHTTPSErrors: boolean,
|
||||
defaultViewport: Viewport | null,
|
||||
defaultViewport: types.Viewport | null,
|
||||
process: childProcess.ChildProcess | null,
|
||||
closeCallback?: (() => Promise<void>)) {
|
||||
super();
|
||||
@ -107,7 +106,7 @@ export class Browser extends EventEmitter {
|
||||
const {browserContextId} = targetInfo;
|
||||
const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext;
|
||||
|
||||
const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._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');
|
||||
this._targets.set(event.targetInfo.targetId, target);
|
||||
|
||||
|
@ -16,8 +16,8 @@
|
||||
*/
|
||||
|
||||
import { CDPSession } from './Connection';
|
||||
import { Viewport } from './Page';
|
||||
import { Protocol } from './protocol';
|
||||
import * as types from '../types';
|
||||
|
||||
export class EmulationManager {
|
||||
private _client: CDPSession;
|
||||
@ -28,7 +28,7 @@ export class EmulationManager {
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
async emulateViewport(viewport: Viewport): Promise<boolean> {
|
||||
async emulateViewport(viewport: types.Viewport): Promise<boolean> {
|
||||
const mobile = viewport.isMobile || false;
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
|
@ -90,9 +90,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
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();
|
||||
return page._screenshotter.screenshotElement(page, handle, options);
|
||||
return page._screenshotter.screenshotElement(handle, options);
|
||||
}
|
||||
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
|
@ -28,7 +28,7 @@ import { BrowserFetcher } from './BrowserFetcher';
|
||||
import { Connection } from './Connection';
|
||||
import { TimeoutError } from '../Errors';
|
||||
import { assert, debugError, helper } from '../helper';
|
||||
import { Viewport } from './Page';
|
||||
import * as types from '../types';
|
||||
import { PipeTransport } from './PipeTransport';
|
||||
import { WebSocketTransport } from './WebSocketTransport';
|
||||
import { ConnectionTransport } from '../ConnectionTransport';
|
||||
@ -392,6 +392,6 @@ export type LauncherLaunchOptions = {
|
||||
|
||||
export type LauncherBrowserOptions = {
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
defaultViewport?: Viewport | null,
|
||||
defaultViewport?: types.Viewport | null,
|
||||
slowMo?: number,
|
||||
};
|
||||
|
@ -16,9 +16,18 @@
|
||||
*/
|
||||
|
||||
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 { 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 * as types from '../types';
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserContext } from './BrowserContext';
|
||||
import { CDPSession, CDPSessionEvents } from './Connection';
|
||||
@ -26,34 +35,17 @@ import { EmulationManager } from './EmulationManager';
|
||||
import { Events } from './events';
|
||||
import { Accessibility } from './features/accessibility';
|
||||
import { Coverage } from './features/coverage';
|
||||
import { Overrides } from './features/overrides';
|
||||
import { Interception } from './features/interception';
|
||||
import { Overrides } from './features/overrides';
|
||||
import { PDF } from './features/pdf';
|
||||
import { Workers } from './features/workers';
|
||||
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||
import { RawMouseImpl, RawKeyboardImpl } from './Input';
|
||||
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||
import { DOMWorldDelegate } from './JSHandle';
|
||||
import { NetworkManagerEvents } from './NetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { getExceptionMessage, releaseObject } from './protocolHelper';
|
||||
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';
|
||||
import { DOMWorldDelegate } from './JSHandle';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
|
||||
export type Viewport = {
|
||||
width: number;
|
||||
height: number;
|
||||
deviceScaleFactor?: number;
|
||||
isMobile?: boolean;
|
||||
isLandscape?: boolean;
|
||||
hasTouch?: boolean;
|
||||
}
|
||||
import { CRScreenshotDelegate } from './Screenshotter';
|
||||
|
||||
export class Page extends EventEmitter {
|
||||
private _closed = false;
|
||||
@ -74,21 +66,21 @@ export class Page extends EventEmitter {
|
||||
readonly workers: Workers;
|
||||
private _pageBindings = new Map<string, Function>();
|
||||
_javascriptEnabled = true;
|
||||
private _viewport: Viewport | null = null;
|
||||
private _viewport: types.Viewport | null = null;
|
||||
_screenshotter: Screenshotter;
|
||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||
private _disconnectPromise: Promise<Error> | undefined;
|
||||
private _emulatedMediaType: string | undefined;
|
||||
|
||||
static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
|
||||
const page = new Page(client, browserContext, ignoreHTTPSErrors, screenshotter);
|
||||
static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: types.Viewport | null): Promise<Page> {
|
||||
const page = new Page(client, browserContext, ignoreHTTPSErrors);
|
||||
await page._initialize();
|
||||
if (defaultViewport)
|
||||
await page.setViewport(defaultViewport);
|
||||
return page;
|
||||
}
|
||||
|
||||
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, screenshotter: Screenshotter) {
|
||||
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
|
||||
super();
|
||||
this._client = client;
|
||||
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.overrides = new Overrides(client);
|
||||
this.interception = new Interception(this._frameManager.networkManager());
|
||||
|
||||
this._screenshotter = screenshotter;
|
||||
this._screenshotter = new Screenshotter(this, new CRScreenshotDelegate(this._client), browserContext.browser());
|
||||
|
||||
client.on('Target.attachedToTarget', event => {
|
||||
if (event.targetInfo.type !== 'worker') {
|
||||
@ -456,7 +447,7 @@ export class Page extends EventEmitter {
|
||||
return response;
|
||||
}
|
||||
|
||||
async emulate(options: { viewport: Viewport; userAgent: string; }) {
|
||||
async emulate(options: { viewport: types.Viewport; userAgent: string; }) {
|
||||
await Promise.all([
|
||||
this.setViewport(options.viewport),
|
||||
this.setUserAgent(options.userAgent)
|
||||
@ -485,14 +476,14 @@ export class Page extends EventEmitter {
|
||||
this._emulatedMediaType = options.type;
|
||||
}
|
||||
|
||||
async setViewport(viewport: Viewport) {
|
||||
async setViewport(viewport: types.Viewport) {
|
||||
const needsReload = await this._emulationManager.emulateViewport(viewport);
|
||||
this._viewport = viewport;
|
||||
if (needsReload)
|
||||
await this.reload();
|
||||
}
|
||||
|
||||
viewport(): Viewport | null {
|
||||
viewport(): types.Viewport | null {
|
||||
return this._viewport;
|
||||
}
|
||||
|
||||
@ -509,8 +500,8 @@ export class Page extends EventEmitter {
|
||||
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
||||
}
|
||||
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer | string> {
|
||||
return this._screenshotter.screenshotPage(this, options);
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
|
||||
return this._screenshotter.screenshotPage(options);
|
||||
}
|
||||
|
||||
async title(): Promise<string> {
|
||||
@ -585,11 +576,6 @@ export class Page extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
type MediaFeature = {
|
||||
name: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
type FileChooser = {
|
||||
element: dom.ElementHandle,
|
||||
multiple: boolean
|
||||
|
@ -1,134 +1,40 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT 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 { ScreenshotterDelegate } from '../screenshotter';
|
||||
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 {
|
||||
private _queue = new TaskQueue();
|
||||
|
||||
async screenshotPage(page: Page, options: types.ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
return this._queue.postTask(() => this._screenshot(page, format, options));
|
||||
constructor(session: CDPSession) {
|
||||
this._session = session;
|
||||
}
|
||||
|
||||
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();
|
||||
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
||||
|
||||
const viewport = page.viewport();
|
||||
|
||||
if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) {
|
||||
const newViewport = {
|
||||
width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
||||
height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
|
||||
};
|
||||
await page.setViewport(Object.assign({}, viewport, newViewport));
|
||||
|
||||
needsViewportReset = true;
|
||||
}
|
||||
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
|
||||
boundingBox = await handle.boundingBox();
|
||||
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
||||
assert(boundingBox.width !== 0, 'Node has 0 width.');
|
||||
assert(boundingBox.height !== 0, 'Node has 0 height.');
|
||||
|
||||
const { layoutViewport: { pageX, pageY } } = await page._client.send('Page.getLayoutMetrics');
|
||||
|
||||
const clip = Object.assign({}, boundingBox);
|
||||
clip.x += pageX;
|
||||
clip.y += pageY;
|
||||
|
||||
const imageData = await this._screenshot(page, format, {...options, clip});
|
||||
|
||||
if (needsViewportReset)
|
||||
await page.setViewport(viewport);
|
||||
|
||||
return imageData;
|
||||
});
|
||||
async getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | undefined> {
|
||||
const rect = await handle.boundingBox();
|
||||
if (!rect)
|
||||
return rect;
|
||||
const { layoutViewport: { pageX, pageY } } = await this._session.send('Page.getLayoutMetrics');
|
||||
rect.x += pageX;
|
||||
rect.y += pageY;
|
||||
return rect;
|
||||
}
|
||||
|
||||
private async _screenshot(page: Page, format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer | string> {
|
||||
await page.browser()._activatePage(page);
|
||||
let clip = options.clip ? processClip(options.clip) : undefined;
|
||||
const viewport = page.viewport();
|
||||
canCaptureOutsideViewport(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.fullPage) {
|
||||
const metrics = await page._client.send('Page.getLayoutMetrics');
|
||||
const width = Math.ceil(metrics.contentSize.width);
|
||||
const height = Math.ceil(metrics.contentSize.height);
|
||||
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||
await this._session.send('Emulation.setDefaultBackgroundColorOverride', { color });
|
||||
}
|
||||
|
||||
// Overwrite clip for full page at all times.
|
||||
clip = { x: 0, y: 0, width, height, scale: 1 };
|
||||
const {
|
||||
isMobile = false,
|
||||
deviceScaleFactor = 1,
|
||||
isLandscape = false
|
||||
} = viewport || {};
|
||||
const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
|
||||
await page._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation });
|
||||
}
|
||||
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
|
||||
if (shouldSetDefaultBackground)
|
||||
await page._client.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
|
||||
const result = await page._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
|
||||
if (shouldSetDefaultBackground)
|
||||
await page._client.send('Emulation.setDefaultBackgroundColorOverride');
|
||||
|
||||
if (options.fullPage && viewport)
|
||||
await page.setViewport(viewport);
|
||||
|
||||
const buffer = options.encoding === 'base64' ? result.data : Buffer.from(result.data, 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
|
||||
function processClip(clip) {
|
||||
const x = Math.round(clip.x);
|
||||
const y = Math.round(clip.y);
|
||||
const width = Math.round(clip.width + clip.x - x);
|
||||
const height = Math.round(clip.height + clip.y - y);
|
||||
return {x, y, width, height, scale: 1};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
async screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
|
||||
const clip = options.clip ? { ...options.clip, scale: 1 } : undefined;
|
||||
const result = await this._session.send('Page.captureScreenshot', { format, quality: options.quality, clip });
|
||||
return Buffer.from(result.data, 'base64');
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as types from '../types';
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserContext } from './BrowserContext';
|
||||
import { CDPSession } from './Connection';
|
||||
import { Events } from './events';
|
||||
import { Worker } from './features/workers';
|
||||
import { Page, Viewport } from './Page';
|
||||
import { Page } from './Page';
|
||||
import { Protocol } from './protocol';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
|
||||
const targetSymbol = Symbol('target');
|
||||
|
||||
@ -32,8 +32,7 @@ export class Target {
|
||||
_targetId: string;
|
||||
private _sessionFactory: () => Promise<CDPSession>;
|
||||
private _ignoreHTTPSErrors: boolean;
|
||||
private _defaultViewport: Viewport;
|
||||
private _screenshotter: Screenshotter;
|
||||
private _defaultViewport: types.Viewport;
|
||||
private _pagePromise: Promise<Page> | null = null;
|
||||
private _workerPromise: Promise<Worker> | null = null;
|
||||
_initializedPromise: Promise<boolean>;
|
||||
@ -49,15 +48,13 @@ export class Target {
|
||||
browserContext: BrowserContext,
|
||||
sessionFactory: () => Promise<CDPSession>,
|
||||
ignoreHTTPSErrors: boolean,
|
||||
defaultViewport: Viewport | null,
|
||||
screenshotter: Screenshotter) {
|
||||
defaultViewport: types.Viewport | null) {
|
||||
this._targetInfo = targetInfo;
|
||||
this._browserContext = browserContext;
|
||||
this._targetId = targetInfo.targetId;
|
||||
this._sessionFactory = sessionFactory;
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._defaultViewport = defaultViewport;
|
||||
this._screenshotter = screenshotter;
|
||||
this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => {
|
||||
if (!success)
|
||||
return false;
|
||||
@ -84,7 +81,7 @@ export class Target {
|
||||
async page(): Promise<Page | null> {
|
||||
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
|
||||
this._pagePromise = this._sessionFactory().then(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;
|
||||
return page;
|
||||
});
|
||||
|
@ -10,7 +10,6 @@ import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
|
||||
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
||||
import { assert, helper, debugError } from './helper';
|
||||
import Injected from './injected/injected';
|
||||
import { SelectorRoot } from './injected/selectorEngine';
|
||||
|
||||
export interface DOMWorldDelegate {
|
||||
keyboard: input.Keyboard;
|
||||
@ -146,7 +145,7 @@ export class DOMWorld {
|
||||
}
|
||||
|
||||
export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
private readonly _world: DOMWorld;
|
||||
readonly _world: DOMWorld;
|
||||
|
||||
constructor(context: js.ExecutionContext, remoteObject: any) {
|
||||
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}> {
|
||||
const [box, border] = await Promise.all([
|
||||
this.boundingBox(),
|
||||
this.evaluate((e: Element) => {
|
||||
const style = e.ownerDocument.defaultView.getComputedStyle(e);
|
||||
this.evaluate((node: Node) => {
|
||||
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) };
|
||||
}).catch(debugError),
|
||||
]);
|
||||
|
@ -21,11 +21,12 @@ import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } f
|
||||
import { Connection, ConnectionEvents } from './Connection';
|
||||
import { Events } from './events';
|
||||
import { Permissions } from './features/permissions';
|
||||
import { Page, Viewport } from './Page';
|
||||
import { Page } from './Page';
|
||||
import * as types from '../types';
|
||||
|
||||
export class Browser extends EventEmitter {
|
||||
private _connection: Connection;
|
||||
_defaultViewport: Viewport;
|
||||
_defaultViewport: types.Viewport;
|
||||
private _process: import('child_process').ChildProcess;
|
||||
private _closeCallback: () => void;
|
||||
_targets: Map<string, Target>;
|
||||
@ -33,14 +34,14 @@ export class Browser extends EventEmitter {
|
||||
private _contexts: Map<string, BrowserContext>;
|
||||
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 browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback);
|
||||
await connection.send('Target.enable');
|
||||
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();
|
||||
this._connection = connection;
|
||||
this._defaultViewport = defaultViewport;
|
||||
|
@ -93,9 +93,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
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;
|
||||
return page._screenshotter.screenshotElement(page, handle, options);
|
||||
return page._screenshotter.screenshotElement(handle, options);
|
||||
}
|
||||
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
|
@ -16,27 +16,28 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as console from '../console';
|
||||
import * as dialog from '../dialog';
|
||||
import * as dom from '../dom';
|
||||
import { TimeoutError } from '../Errors';
|
||||
import * as frames from '../frames';
|
||||
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 * as types from '../types';
|
||||
import { BrowserContext } from './Browser';
|
||||
import { JugglerSession, JugglerSessionEvents } from './Connection';
|
||||
import { Events } from './events';
|
||||
import { Accessibility } from './features/accessibility';
|
||||
import { Interception } from './features/interception';
|
||||
import { FrameManager, FrameManagerEvents, normalizeWaitUntil } from './FrameManager';
|
||||
import { RawMouseImpl, RawKeyboardImpl } from './Input';
|
||||
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||
import { NavigationWatchdog } from './NavigationWatchdog';
|
||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
||||
import * as input from '../input';
|
||||
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';
|
||||
import { FFScreenshotDelegate } from './Screenshotter';
|
||||
|
||||
export class Page extends EventEmitter {
|
||||
private _timeoutSettings: TimeoutSettings;
|
||||
@ -54,12 +55,12 @@ export class Page extends EventEmitter {
|
||||
_frameManager: FrameManager;
|
||||
_javascriptEnabled = true;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
private _viewport: Viewport;
|
||||
private _viewport: types.Viewport;
|
||||
private _disconnectPromise: Promise<Error>;
|
||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||
_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);
|
||||
await Promise.all([
|
||||
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)),
|
||||
];
|
||||
this._viewport = null;
|
||||
this._screenshotter = new Screenshotter(session);
|
||||
this._screenshotter = new Screenshotter(this, new FFScreenshotDelegate(session, this._frameManager), browserContext.browser());
|
||||
}
|
||||
|
||||
_didClose() {
|
||||
@ -247,7 +248,7 @@ export class Page extends EventEmitter {
|
||||
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([
|
||||
this.setViewport(options.viewport),
|
||||
this.setUserAgent(options.userAgent),
|
||||
@ -268,7 +269,7 @@ export class Page extends EventEmitter {
|
||||
return this._viewport;
|
||||
}
|
||||
|
||||
async setViewport(viewport: Viewport) {
|
||||
async setViewport(viewport: types.Viewport) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
@ -280,8 +281,8 @@ export class Page extends EventEmitter {
|
||||
await this._session.send('Page.setViewport', {
|
||||
viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape },
|
||||
});
|
||||
const oldIsMobile = this._viewport ? this._viewport.isMobile : false;
|
||||
const oldHasTouch = this._viewport ? this._viewport.hasTouch : false;
|
||||
const oldIsMobile = this._viewport ? !!this._viewport.isMobile : false;
|
||||
const oldHasTouch = this._viewport ? !!this._viewport.hasTouch : false;
|
||||
this._viewport = viewport;
|
||||
if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch)
|
||||
await this.reload();
|
||||
@ -424,8 +425,8 @@ export class Page extends EventEmitter {
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<string | Buffer> {
|
||||
return this._screenshotter.screenshotPage(this, options);
|
||||
screenshot(options: types.ScreenshotOptions = {}): Promise<Buffer> {
|
||||
return this._screenshotter.screenshotPage(options);
|
||||
}
|
||||
|
||||
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 = {
|
||||
element: dom.ElementHandle,
|
||||
multiple: boolean
|
||||
|
@ -1,64 +1,42 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { Page } from './Page';
|
||||
import { assert, helper } from '../helper';
|
||||
import * as dom from '../dom';
|
||||
import { ScreenshotterDelegate } from '../screenshotter';
|
||||
import * as types from '../types';
|
||||
import * as dom from '../dom';
|
||||
import { JugglerSession } from './Connection';
|
||||
import { FrameManager } from './FrameManager';
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
export class Screenshotter {
|
||||
export class FFScreenshotDelegate implements ScreenshotterDelegate {
|
||||
private _session: JugglerSession;
|
||||
private _frameManager: FrameManager;
|
||||
|
||||
constructor(session: JugglerSession) {
|
||||
constructor(session: JugglerSession, frameManager: FrameManager) {
|
||||
this._session = session;
|
||||
this._frameManager = frameManager;
|
||||
}
|
||||
|
||||
async screenshotPage(page: Page, options: types.ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
const {data} = await this._session.send('Page.screenshot', {
|
||||
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
|
||||
fullPage: options.fullPage,
|
||||
clip: processClip(options.clip),
|
||||
});
|
||||
const buffer = options.encoding === 'base64' ? data : Buffer.from(data, 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
|
||||
function processClip(clip) {
|
||||
if (!clip)
|
||||
return undefined;
|
||||
const x = Math.round(clip.x);
|
||||
const y = Math.round(clip.y);
|
||||
const width = Math.round(clip.width + clip.x - x);
|
||||
const height = Math.round(clip.height + clip.y - y);
|
||||
return {x, y, width, height};
|
||||
}
|
||||
}
|
||||
|
||||
async screenshotElement(page: Page, handle: dom.ElementHandle, options: types.ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||
const frameId = page._frameManager._frameData(handle.executionContext().frame()).frameId;
|
||||
const clip = await this._session.send('Page.getBoundingBox', {
|
||||
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | undefined> {
|
||||
const frameId = this._frameManager._frameData(handle.executionContext().frame()).frameId;
|
||||
return this._session.send('Page.getBoundingBox', {
|
||||
frameId,
|
||||
objectId: handle._remoteObject.objectId,
|
||||
});
|
||||
if (!clip)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
assert(clip.width, 'Node has 0 width.');
|
||||
assert(clip.height, 'Node has 0 height.');
|
||||
await handle._scrollIntoViewIfNeeded();
|
||||
return this.screenshotPage(page, {
|
||||
...options,
|
||||
clip: {
|
||||
x: clip.x,
|
||||
y: clip.y,
|
||||
width: clip.width,
|
||||
height: clip.height,
|
||||
},
|
||||
}
|
||||
|
||||
canCaptureOutsideViewport(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||
}
|
||||
|
||||
async screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
|
||||
const { data } = await this._session.send('Page.screenshot', {
|
||||
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 mime from 'mime';
|
||||
import { TimeoutError } from './Errors';
|
||||
import * as types from './types';
|
||||
|
||||
export const debugError = debug(`playwright:error`);
|
||||
|
||||
@ -155,43 +153,6 @@ class Helper {
|
||||
clearTimeout(timeoutTimer);
|
||||
}
|
||||
}
|
||||
|
||||
static validateScreeshotOptions(options: types.ScreenshotOptions): 'png' | 'jpeg' {
|
||||
let format: 'png' | 'jpeg' | null = null;
|
||||
// options.type takes precedence over inferring the type from options.path
|
||||
// because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).
|
||||
if (options.type) {
|
||||
assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
|
||||
format = options.type;
|
||||
} else if (options.path) {
|
||||
const mimeType = mime.getType(options.path);
|
||||
if (mimeType === 'image/png')
|
||||
format = 'png';
|
||||
else if (mimeType === 'image/jpeg')
|
||||
format = 'jpeg';
|
||||
assert(format, 'Unsupported screenshot mime type: ' + mimeType);
|
||||
}
|
||||
|
||||
if (!format)
|
||||
format = 'png';
|
||||
|
||||
if (options.quality) {
|
||||
assert(format === 'jpeg', 'options.quality is unsupported for the ' + format + ' screenshots');
|
||||
assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality));
|
||||
assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
|
||||
assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);
|
||||
}
|
||||
assert(!options.clip || !options.fullPage, 'options.clip and options.fullPage are exclusive');
|
||||
if (options.clip) {
|
||||
assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + (typeof options.clip.x));
|
||||
assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + (typeof options.clip.y));
|
||||
assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width));
|
||||
assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + (typeof options.clip.height));
|
||||
assert(options.clip.width !== 0, 'Expected options.clip.width not to be 0.');
|
||||
assert(options.clip.height !== 0, 'Expected options.clip.height not to be 0.');
|
||||
}
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
export function assert(value: any, message?: string) {
|
||||
|
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 };
|
||||
}
|
||||
|
||||
export type ScreenshotOptions = {
|
||||
export type ElementScreenshotOptions = {
|
||||
type?: 'png' | 'jpeg',
|
||||
path?: string,
|
||||
fullPage?: boolean,
|
||||
clip?: Rect,
|
||||
quality?: number,
|
||||
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 { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network';
|
||||
import { Connection } from './Connection';
|
||||
import { Page, Viewport } from './Page';
|
||||
import { Page } from './Page';
|
||||
import { Target } from './Target';
|
||||
import { Screenshotter } from './Screenshotter';
|
||||
import { Protocol } from './protocol';
|
||||
import * as types from '../types';
|
||||
|
||||
export class Browser extends EventEmitter {
|
||||
_defaultViewport: Viewport;
|
||||
_defaultViewport: types.Viewport;
|
||||
private _process: childProcess.ChildProcess;
|
||||
_screenshotter = new Screenshotter();
|
||||
_connection: Connection;
|
||||
private _closeCallback: () => Promise<void>;
|
||||
private _defaultContext: BrowserContext;
|
||||
@ -39,7 +38,7 @@ export class Browser extends EventEmitter {
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
defaultViewport: Viewport | null,
|
||||
defaultViewport: types.Viewport | null,
|
||||
process: childProcess.ChildProcess | null,
|
||||
closeCallback?: (() => Promise<void>)) {
|
||||
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.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> {
|
||||
|
@ -88,9 +88,9 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate {
|
||||
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;
|
||||
return page._screenshotter.screenshotElement(page, handle, options);
|
||||
return page._screenshotter.screenshotElement(handle, options);
|
||||
}
|
||||
|
||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
||||
|
@ -19,7 +19,7 @@ import { debugError, helper } from '../helper';
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserFetcher } from './BrowserFetcher';
|
||||
import { Connection } from './Connection';
|
||||
import { Viewport } from './Page';
|
||||
import * as types from '../types';
|
||||
import { PipeTransport } from './PipeTransport';
|
||||
|
||||
const DEFAULT_ARGS = [
|
||||
@ -186,6 +186,6 @@ export type LauncherLaunchOptions = {
|
||||
headless?: boolean,
|
||||
dumpio?: boolean,
|
||||
env?: {[key: string]: string} | undefined,
|
||||
defaultViewport?: Viewport | null,
|
||||
defaultViewport?: types.Viewport | null,
|
||||
slowMo?: number,
|
||||
};
|
||||
|
@ -16,9 +16,18 @@
|
||||
*/
|
||||
|
||||
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 * as input 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 * as types from '../types';
|
||||
import { Browser, BrowserContext } from './Browser';
|
||||
import { TargetSession, TargetSessionEvents } from './Connection';
|
||||
import { Events } from './events';
|
||||
@ -26,20 +35,7 @@ import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||
import { NetworkManagerEvents } from './NetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { Screenshotter } 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;
|
||||
}
|
||||
import { WKScreenshotDelegate } from './Screenshotter';
|
||||
|
||||
export class Page extends EventEmitter {
|
||||
private _closed = false;
|
||||
@ -53,7 +49,7 @@ export class Page extends EventEmitter {
|
||||
private _frameManager: FrameManager;
|
||||
private _bootstrapScripts: string[] = [];
|
||||
_javascriptEnabled = true;
|
||||
private _viewport: Viewport | null = null;
|
||||
private _viewport: types.Viewport | null = null;
|
||||
_screenshotter: Screenshotter;
|
||||
private _workers = new Map<string, Worker>();
|
||||
private _disconnectPromise: Promise<Error> | undefined;
|
||||
@ -61,15 +57,15 @@ export class Page extends EventEmitter {
|
||||
private _emulatedMediaType: string | undefined;
|
||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||
|
||||
static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
|
||||
const page = new Page(session, browserContext, screenshotter);
|
||||
static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: types.Viewport | null): Promise<Page> {
|
||||
const page = new Page(session, browserContext);
|
||||
await page._initialize();
|
||||
if (defaultViewport)
|
||||
await page.setViewport(defaultViewport);
|
||||
return page;
|
||||
}
|
||||
|
||||
constructor(session: TargetSession, browserContext: BrowserContext, screenshotter: Screenshotter) {
|
||||
constructor(session: TargetSession, browserContext: BrowserContext) {
|
||||
super();
|
||||
this._closedPromise = new Promise(f => this._closedCallback = f);
|
||||
this._keyboard = new input.Keyboard(new RawKeyboardImpl(session));
|
||||
@ -77,7 +73,7 @@ export class Page extends EventEmitter {
|
||||
this._timeoutSettings = new 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._browserContext = browserContext;
|
||||
@ -315,7 +311,7 @@ export class Page extends EventEmitter {
|
||||
}, timeout, this._sessionClosePromise());
|
||||
}
|
||||
|
||||
async emulate(options: { viewport: Viewport; userAgent: string; }) {
|
||||
async emulate(options: { viewport: types.Viewport; userAgent: string; }) {
|
||||
await Promise.all([
|
||||
this.setViewport(options.viewport),
|
||||
this.setUserAgent(options.userAgent)
|
||||
@ -333,14 +329,14 @@ export class Page extends EventEmitter {
|
||||
this._emulatedMediaType = options.type;
|
||||
}
|
||||
|
||||
async setViewport(viewport: Viewport) {
|
||||
async setViewport(viewport: types.Viewport) {
|
||||
this._viewport = viewport;
|
||||
const width = viewport.width;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -367,8 +363,8 @@ export class Page extends EventEmitter {
|
||||
await this._frameManager.networkManager().setCacheEnabled(enabled);
|
||||
}
|
||||
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer | string> {
|
||||
return this._screenshotter.screenshotPage(this, options);
|
||||
screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
|
||||
return this._screenshotter.screenshotPage(options);
|
||||
}
|
||||
|
||||
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 = {
|
||||
element: dom.ElementHandle,
|
||||
multiple: boolean
|
||||
|
@ -1,83 +1,40 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { Page } from './Page';
|
||||
import { assert, helper, debugError } from '../helper';
|
||||
import { Protocol } from './protocol';
|
||||
import * as jpeg from 'jpeg-js';
|
||||
import { PNG } from 'pngjs';
|
||||
import * as dom from '../dom';
|
||||
import { ScreenshotterDelegate } from '../screenshotter';
|
||||
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 {
|
||||
private _queue = new TaskQueue();
|
||||
|
||||
async screenshotPage(page: Page, options: types.ScreenshotOptions = {}): Promise<Buffer | string> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
assert(format === 'png', 'Only png format is supported');
|
||||
return this._queue.postTask(async () => {
|
||||
const params: Protocol.Page.snapshotRectParameters = { x: 0, y: 0, width: 800, height: 600, coordinateSystem: 'Page' };
|
||||
if (options.fullPage) {
|
||||
const pageSize = await page.evaluate(() =>
|
||||
({
|
||||
width: document.body.scrollWidth,
|
||||
height: document.body.scrollHeight
|
||||
}));
|
||||
Object.assign(params, pageSize);
|
||||
} else if (options.clip) {
|
||||
Object.assign(params, options.clip);
|
||||
} else if (page.viewport()) {
|
||||
Object.assign(params, page.viewport());
|
||||
}
|
||||
const [, result] = await Promise.all([
|
||||
page.browser()._activatePage(page),
|
||||
page._session.send('Page.snapshotRect', params),
|
||||
]).catch(e => {
|
||||
debugError('Failed to take screenshot: ' + e);
|
||||
throw e;
|
||||
});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
});
|
||||
constructor(session: TargetSession) {
|
||||
this._session = session;
|
||||
}
|
||||
|
||||
async screenshotElement(page: Page, handle: dom.ElementHandle, options: types.ScreenshotOptions = {}): Promise<string | Buffer> {
|
||||
const format = helper.validateScreeshotOptions(options);
|
||||
assert(format === 'png', 'Only png format is supported');
|
||||
return this._queue.postTask(async () => {
|
||||
const objectId = (handle._remoteObject as Protocol.Runtime.RemoteObject).objectId;
|
||||
page._session.send('DOM.getDocument');
|
||||
const {nodeId} = await page._session.send('DOM.requestNode', {objectId});
|
||||
const [, result] = await Promise.all([
|
||||
page.browser()._activatePage(page),
|
||||
page._session.send('Page.snapshotNode', {nodeId})
|
||||
]).catch(e => {
|
||||
debugError('Failed to take screenshot: ' + e);
|
||||
throw e;
|
||||
});
|
||||
const prefix = 'data:image/png;base64,';
|
||||
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TaskQueue {
|
||||
private _chain: Promise<any>;
|
||||
|
||||
constructor() {
|
||||
this._chain = Promise.resolve();
|
||||
}
|
||||
|
||||
postTask(task: () => any): Promise<any> {
|
||||
const result = this._chain.then(task);
|
||||
this._chain = result.catch(() => {});
|
||||
return result;
|
||||
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | undefined> {
|
||||
return handle.boundingBox();
|
||||
}
|
||||
|
||||
canCaptureOutsideViewport(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||
// TODO: line below crashes, sort it out.
|
||||
this._session.send('Page.setDefaultBackgroundColorOverride', { color });
|
||||
}
|
||||
|
||||
async screenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport ): Promise<Buffer> {
|
||||
const rect = options.clip || { x: 0, y: 0, width: viewport.width, height: viewport.height };
|
||||
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: options.fullPage ? 'Page' : 'Viewport' });
|
||||
const prefix = 'data:image/png;base64,';
|
||||
let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (format === 'jpeg')
|
||||
buffer = jpeg.encode(PNG.sync.read(buffer)).data;
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export class Target {
|
||||
async page(): Promise<Page | null> {
|
||||
if (this._type === 'page' && !this._pagePromise) {
|
||||
const session = this.browser()._connection.session(this._targetId);
|
||||
this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport, this.browser()._screenshotter).then(page => {
|
||||
this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport).then(page => {
|
||||
this._adoptPage(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');
|
||||
});
|
||||
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.goto(server.PREFIX + '/grid.html');
|
||||
const screenshot = await page.screenshot({
|
||||
clip: {
|
||||
x: 50,
|
||||
y: 600,
|
||||
width: 100,
|
||||
y: 450,
|
||||
width: 1000,
|
||||
height: 100
|
||||
}
|
||||
});
|
||||
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.goto(server.PREFIX + '/grid.html');
|
||||
const promises = [];
|
||||
@ -92,13 +105,21 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||
expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
|
||||
await Promise.all(pages.map(page => page.close()));
|
||||
});
|
||||
it.skip(FFOX)('should allow transparency', async({page, server}) => {
|
||||
await page.setViewport({ width: 100, height: 100 });
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
it.skip(FFOX || WEBKIT)('should allow transparency', async({page, server}) => {
|
||||
await page.setViewport({ width: 50, height: 150 });
|
||||
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});
|
||||
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.goto(server.EMPTY_PAGE);
|
||||
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}) => {
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
await page.setContent(`
|
||||
something above
|
||||
<div style="height: 14px">oooo</div>
|
||||
<style>div {
|
||||
border: 2px solid blue;
|
||||
background: green;
|
||||
@ -145,9 +166,9 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
<div></div>
|
||||
<div id="d"></div>
|
||||
`);
|
||||
const elementHandle = await page.$('div');
|
||||
const elementHandle = await page.$('div#d');
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
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.setContent(`
|
||||
something above
|
||||
<div style="height: 14px">oooo</div>
|
||||
<style>
|
||||
div.to-screenshot {
|
||||
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.setContent(`
|
||||
something above
|
||||
<div style="height: 14px">oooo</div>
|
||||
<style>
|
||||
div.to-screenshot {
|
||||
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}) => {
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
await page.setContent(`
|
||||
something above
|
||||
<div style="height: 14px">oooo</div>
|
||||
<style>div.above {
|
||||
border: 2px solid blue;
|
||||
background: red;
|
||||
@ -227,7 +248,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
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.setContent(`<div style="position:absolute;
|
||||
top: 100px;
|
||||
@ -240,14 +261,14 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
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>');
|
||||
const elementHandle = await page.$('h1');
|
||||
await page.evaluate(element => element.remove(), elementHandle);
|
||||
const screenshotError = await elementHandle.screenshot().catch(error => error);
|
||||
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>');
|
||||
const div = await page.$('div');
|
||||
const error = await div.screenshot().catch(e => e);
|
||||
|