chore: merge PageDelegate, FrameDelegate and ScreenshotterDelegate (#213)

This commit is contained in:
Dmitry Gozman 2019-12-11 12:36:42 -08:00 committed by Yury Semikhatsky
parent ce21019c7d
commit b70eebc4b2
9 changed files with 128 additions and 205 deletions

View File

@ -32,7 +32,6 @@ import { toConsoleMessageLocation, exceptionToError, releaseObject } from './pro
import * as dialog from '../dialog';
import { PageDelegate } from '../page';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { CRScreenshotDelegate } from './Screenshotter';
import { Accessibility } from './features/accessibility';
import { Coverage } from './features/coverage';
import { PDF } from './features/pdf';
@ -60,7 +59,7 @@ type FrameData = {
id: string,
};
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
export class FrameManager extends EventEmitter implements PageDelegate {
_client: CDPSession;
private _page: Page;
private _networkManager: NetworkManager;
@ -70,14 +69,12 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
private _mainFrame: frames.Frame;
rawMouse: RawMouseImpl;
rawKeyboard: RawKeyboardImpl;
screenshotterDelegate: CRScreenshotDelegate;
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
super();
this._client = client;
this.rawKeyboard = new RawKeyboardImpl(client);
this.rawMouse = new RawMouseImpl(client);
this.screenshotterDelegate = new CRScreenshotDelegate(client);
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this._page = new Page(this, browserContext);
(this._page as any).accessibility = new Accessibility(client);
@ -272,7 +269,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
return;
assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId);
const frame = new frames.Frame(this, this._page, parentFrame);
const frame = new frames.Frame(this._page, parentFrame);
const data: FrameData = {
id: frameId,
};
@ -302,7 +299,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
data.id = framePayload.id;
} else {
// Initial main frame navigation.
frame = new frames.Frame(this, this._page, null);
frame = new frames.Frame(this._page, null);
const data: FrameData = {
id: framePayload.id,
};
@ -542,6 +539,34 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
else
await (this._page.browser() as Browser)._closePage(this._page);
}
async getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
const rect = await handle.boundingBox();
if (!rect)
return rect;
const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
rect.x += pageX;
rect.y += pageY;
return rect;
}
canScreenshotOutsideViewport(): boolean {
return false;
}
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
}
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
const clip = options.clip ? { ...options.clip, scale: 1 } : undefined;
const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
return Buffer.from(result.data, 'base64');
}
async resetViewport(): Promise<void> {
await this._client.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 });
}
}
function assertNoLegacyNavigationOptions(options: frames.NavigateOptions) {

View File

@ -1,44 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as dom from '../dom';
import { ScreenshotterDelegate } from '../screenshotter';
import * as types from '../types';
import { CDPSession } from './api';
export class CRScreenshotDelegate implements ScreenshotterDelegate {
private _session: CDPSession;
constructor(session: CDPSession) {
this._session = session;
}
async getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
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;
}
canCaptureOutsideViewport(): boolean {
return false;
}
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
await this._session.send('Emulation.setDefaultBackgroundColorOverride', { color });
}
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');
}
async resetViewport(): Promise<void> {
await this._session.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 });
}
}

View File

@ -30,8 +30,6 @@ import * as dialog from '../dialog';
import { Protocol } from './protocol';
import * as input from '../input';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { FFScreenshotDelegate } from './Screenshotter';
import { Browser } from './Browser';
import { BrowserContext } from '../browserContext';
import { Interception } from './features/interception';
import { Accessibility } from './features/accessibility';
@ -51,10 +49,9 @@ type FrameData = {
frameId: string,
};
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
export class FrameManager extends EventEmitter implements PageDelegate {
readonly rawMouse: RawMouseImpl;
readonly rawKeyboard: RawKeyboardImpl;
readonly screenshotterDelegate: FFScreenshotDelegate;
readonly _session: JugglerSession;
readonly _page: Page;
private readonly _networkManager: NetworkManager;
@ -68,7 +65,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
this._session = session;
this.rawKeyboard = new RawKeyboardImpl(session);
this.rawMouse = new RawMouseImpl(session);
this.screenshotterDelegate = new FFScreenshotDelegate(session, this);
this._networkManager = new NetworkManager(session, this);
this._mainFrame = null;
this._frames = new Map();
@ -182,7 +178,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
_onFrameAttached(params) {
const parentFrame = this._frames.get(params.parentFrameId) || null;
const frame = new frames.Frame(this, this._page, parentFrame);
const frame = new frames.Frame(this._page, parentFrame);
const data: FrameData = {
frameId: params.frameId,
};
@ -399,6 +395,36 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
async closePage(runBeforeUnload: boolean): Promise<void> {
await this._session.send('Page.close', { runBeforeUnload });
}
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
const frameId = this._frameData(handle.executionContext().frame()).frameId;
return this._session.send('Page.getBoundingBox', {
frameId,
objectId: handle._remoteObject.objectId,
});
}
canScreenshotOutsideViewport(): boolean {
return true;
}
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
if (color)
throw new Error('Not implemented');
}
async takeScreenshot(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');
}
async resetViewport(): Promise<void> {
await this._session.send('Page.setViewport', { viewport: null });
}
}
export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] {

View File

@ -1,46 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { ScreenshotterDelegate } from '../screenshotter';
import * as types from '../types';
import * as dom from '../dom';
import { JugglerSession } from './Connection';
import { FrameManager } from './FrameManager';
export class FFScreenshotDelegate implements ScreenshotterDelegate {
private _session: JugglerSession;
private _frameManager: FrameManager;
constructor(session: JugglerSession, frameManager: FrameManager) {
this._session = session;
this._frameManager = frameManager;
}
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
const frameId = this._frameManager._frameData(handle.executionContext().frame()).frameId;
return this._session.send('Page.getBoundingBox', {
frameId,
objectId: handle._remoteObject.objectId,
});
}
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');
}
async resetViewport(): Promise<void> {
await this._session.send('Page.setViewport', { viewport: null });
}
}

View File

@ -22,10 +22,9 @@ import * as dom from './dom';
import * as network from './network';
import { helper, assert, RegisteredListener } from './helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
import { TimeoutSettings } from './TimeoutSettings';
import { TimeoutError } from './Errors';
import { Events } from './events';
import { EventEmitter } from 'events';
import { Page } from './page';
const readFileAsync = helper.promisify(fs.readFile);
@ -46,22 +45,9 @@ export type GotoOptions = NavigateOptions & {
referer?: string,
};
export interface FrameDelegate {
navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise<network.Response | null>;
waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise<network.Response | null>;
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
}
interface Page extends EventEmitter {
_lifecycleWatchers: Set<LifecycleWatcher>;
_timeoutSettings: TimeoutSettings;
_disconnectedPromise: Promise<Error>;
}
export type LifecycleEvent = 'load' | 'domcontentloaded';
export class Frame {
readonly _delegate: FrameDelegate;
readonly _firedLifecycleEvents: Set<LifecycleEvent>;
_lastDocumentId: string;
readonly _page: Page;
@ -72,8 +58,7 @@ export class Frame {
private _childFrames = new Set<Frame>();
private _name: string;
constructor(delegate: FrameDelegate, page: Page, parentFrame: Frame | null) {
this._delegate = delegate;
constructor(page: Page, parentFrame: Frame | null) {
this._firedLifecycleEvents = new Set();
this._lastDocumentId = '';
this._page = page;
@ -89,11 +74,11 @@ export class Frame {
}
async goto(url: string, options?: GotoOptions): Promise<network.Response | null> {
return this._delegate.navigateFrame(this, url, options);
return this._page._delegate.navigateFrame(this, url, options);
}
async waitForNavigation(options?: NavigateOptions): Promise<network.Response | null> {
return this._delegate.waitForFrameNavigation(this, options);
return this._page._delegate.waitForFrameNavigation(this, options);
}
_mainContext(): Promise<js.ExecutionContext> {
@ -174,7 +159,7 @@ export class Frame {
}
async setContent(html: string, options?: NavigateOptions): Promise<void> {
return this._delegate.setFrameContent(this, html, options);
return this._page._delegate.setFrameContent(this, html, options);
}
name(): string {

View File

@ -22,7 +22,7 @@ import { assert, debugError, helper } from './helper';
import * as input from './input';
import * as js from './javascript';
import * as network from './network';
import { Screenshotter, ScreenshotterDelegate } from './screenshotter';
import { Screenshotter } from './screenshotter';
import { TimeoutSettings } from './TimeoutSettings';
import * as types from './types';
import { Events } from './events';
@ -32,9 +32,7 @@ import { ConsoleMessage, ConsoleMessageLocation } from './console';
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
readonly rawKeyboard: input.RawKeyboard;
readonly screenshotterDelegate: ScreenshotterDelegate;
mainFrame(): frames.Frame;
frames(): frames.Frame[];
reload(options?: frames.NavigateOptions): Promise<network.Response | null>;
goBack(options?: frames.NavigateOptions): Promise<network.Response | null>;
goForward(options?: frames.NavigateOptions): Promise<network.Response | null>;
@ -44,6 +42,12 @@ export interface PageDelegate {
// TODO: reverse didClose call sequence.
didClose(): void;
mainFrame(): frames.Frame;
frames(): frames.Frame[];
navigateFrame(frame: frames.Frame, url: string, options?: frames.GotoOptions): Promise<network.Response | null>;
waitForFrameNavigation(frame: frames.Frame, options?: frames.NavigateOptions): Promise<network.Response | null>;
setFrameContent(frame: frames.Frame, html: string, options?: frames.NavigateOptions): Promise<void>;
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
setUserAgent(userAgent: string): Promise<void>;
setJavaScriptEnabled(enabled: boolean): Promise<void>;
@ -51,6 +55,12 @@ export interface PageDelegate {
setViewport(viewport: types.Viewport): Promise<void>;
setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise<void>;
setCacheEnabled(enabled: boolean): Promise<void>;
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null>;
canScreenshotOutsideViewport(): boolean;
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
takeScreenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise<Buffer>;
resetViewport(oldSize: types.Size): Promise<void>;
}
type PageState = {
@ -106,7 +116,7 @@ export class Page extends EventEmitter {
this.keyboard = new input.Keyboard(delegate.rawKeyboard);
this.mouse = new input.Mouse(delegate.rawMouse, this.keyboard);
this._timeoutSettings = new TimeoutSettings();
this._screenshotter = new Screenshotter(this, delegate.screenshotterDelegate, browserContext.browser());
this._screenshotter = new Screenshotter(this);
}
_didClose() {

View File

@ -20,36 +20,22 @@ import * as mime from 'mime';
import * as dom from './dom';
import { assert, helper } from './helper';
import * as types from './types';
import { Page } from './page';
const writeFileAsync = helper.promisify(fs.writeFile);
export interface Page {
viewport(): types.Viewport | null;
setViewport(v: types.Viewport): Promise<void>;
evaluate(f: () => any): Promise<types.Rect>;
}
export interface ScreenshotterDelegate {
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | null>;
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>;
resetViewport(oldSize: types.Size): Promise<void>;
}
export class Screenshotter {
private _queue = new TaskQueue();
private _delegate: ScreenshotterDelegate;
private _page: Page;
constructor(page: Page, delegate: ScreenshotterDelegate, browserObject: any) {
this._delegate = delegate;
constructor(page: Page) {
this._page = page;
this._queue = browserObject[taskQueueSymbol];
const browser = page.browser();
this._queue = browser[taskQueueSymbol];
if (!this._queue) {
this._queue = new TaskQueue();
browserObject[taskQueueSymbol] = this._queue;
browser[taskQueueSymbol] = this._queue;
}
}
@ -65,7 +51,7 @@ export class Screenshotter {
height: Math.max(document.body.offsetHeight, document.documentElement.offsetHeight)
}));
}
if (options.fullPage && !this._delegate.canCaptureOutsideViewport()) {
if (options.fullPage && !this._page._delegate.canScreenshotOutsideViewport()) {
const fullPageRect = await this._page.evaluate(() => ({
width: Math.max(
document.body.scrollWidth, document.documentElement.scrollWidth,
@ -90,7 +76,7 @@ export class Screenshotter {
if (viewport)
await this._page.setViewport(viewport);
else
await this._delegate.resetViewport(viewportSize);
await this._page._delegate.resetViewport(viewportSize);
}
return result;
});
@ -102,14 +88,14 @@ export class Screenshotter {
return this._queue.postTask(async () => {
let overridenViewport: types.Viewport | undefined;
let boundingBox = await this._delegate.getBoundingBox(handle);
let boundingBox = await this._page._delegate.getBoundingBoxForScreenshot(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 (!this._page._delegate.canScreenshotOutsideViewport()) {
if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) {
overridenViewport = {
...viewport,
@ -120,7 +106,7 @@ export class Screenshotter {
}
await handle._scrollIntoViewIfNeeded();
boundingBox = enclosingIntRect(await this._delegate.getBoundingBox(handle));
boundingBox = enclosingIntRect(await this._page._delegate.getBoundingBoxForScreenshot(handle));
}
if (!overridenViewport)
@ -138,10 +124,10 @@ export class Screenshotter {
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);
await this._page._delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0});
const buffer = await this._page._delegate.takeScreenshot(format, options, viewport);
if (shouldSetDefaultBackground)
await this._delegate.setBackgroundColor();
await this._page._delegate.setBackgroundColor();
if (options.path)
await writeFileAsync(options.path, buffer);
return buffer;

View File

@ -32,9 +32,10 @@ import * as dialog from '../dialog';
import { Browser } from './Browser';
import { BrowserContext } from '../browserContext';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { WKScreenshotDelegate } from './Screenshotter';
import * as input from '../input';
import * as types from '../types';
import * as jpeg from 'jpeg-js';
import { PNG } from 'pngjs';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -54,10 +55,9 @@ type FrameData = {
let lastDocumentId = 0;
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
export class FrameManager extends EventEmitter implements PageDelegate {
readonly rawMouse: RawMouseImpl;
readonly rawKeyboard: RawKeyboardImpl;
readonly screenshotterDelegate: WKScreenshotDelegate;
_session: TargetSession;
readonly _page: Page;
private readonly _networkManager: NetworkManager;
@ -72,7 +72,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
super();
this.rawKeyboard = new RawKeyboardImpl();
this.rawMouse = new RawMouseImpl();
this.screenshotterDelegate = new WKScreenshotDelegate();
this._networkManager = new NetworkManager(this);
this._frames = new Map();
this._contextIdToContext = new Map();
@ -90,7 +89,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
this._session = session;
this.rawKeyboard.setSession(session);
this.rawMouse.setSession(session);
this.screenshotterDelegate.setSession(session);
this._addSessionListeners();
this._networkManager.setSession(session);
this._isolatedWorlds = new Set();
@ -222,7 +220,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
return;
assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId);
const frame = new frames.Frame(this, this._page, parentFrame);
const frame = new frames.Frame(this._page, parentFrame);
const data: FrameData = {
id: frameId,
};
@ -250,7 +248,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
}
} else if (isMainFrame) {
// Initial frame navigation.
frame = new frames.Frame(this, this._page, null);
frame = new frames.Frame(this._page, null);
const data: FrameData = {
id: framePayload.id,
};
@ -526,4 +524,31 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
throw new Error('Not implemented');
(this._page.browser() as Browser)._closePage(this._page);
}
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
return handle.boundingBox();
}
canScreenshotOutsideViewport(): 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 takeScreenshot(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;
}
async resetViewport(oldSize: types.Size): Promise<void> {
await this._session.send('Emulation.setDeviceMetricsOverride', { ...oldSize, deviceScaleFactor: 0 });
}
}

View File

@ -1,44 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
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';
export class WKScreenshotDelegate implements ScreenshotterDelegate {
private _session: TargetSession;
setSession(session: TargetSession) {
this._session = session;
}
getBoundingBox(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
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;
}
async resetViewport(oldSize: types.Size): Promise<void> {
await this._session.send('Emulation.setDeviceMetricsOverride', { ...oldSize, deviceScaleFactor: 0 });
}
}