chore: encapsulate target business in Browser class (#151)

Page and BrowserContext are now closer to be reused between browsers.
This commit is contained in:
Dmitry Gozman 2019-12-05 14:11:48 -08:00 committed by GitHub
parent ed39499cea
commit 51ca756efe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 155 additions and 115 deletions

View File

@ -119,7 +119,7 @@ export class Browser extends EventEmitter {
const target = this._targets.get(event.targetId);
target._initializedCallback(false);
this._targets.delete(event.targetId);
target._closedCallback();
target._didClose();
if (await target._initializedPromise)
this.chromium.emit(Events.Chromium.TargetDestroyed, target);
}
@ -146,14 +146,24 @@ export class Browser extends EventEmitter {
return page;
}
async _closeTarget(target: Target) {
await this._client.send('Target.closeTarget', { targetId: target._targetId });
async _closePage(page: Page) {
await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId });
}
_allTargets(): Target[] {
return Array.from(this._targets.values()).filter(target => target._isInitialized);
}
async _pages(context: BrowserContext): Promise<Page[]> {
const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
}
async _activatePage(page: Page) {
await page._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId});
}
async _waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise<Target> {
const {
timeout = 30000

View File

@ -21,7 +21,6 @@ import { Browser } from './Browser';
import { CDPSession } from './Connection';
import { Permissions } from './features/permissions';
import { Page } from './Page';
import { Target } from './Target';
export class BrowserContext {
readonly permissions: Permissions;
@ -35,17 +34,8 @@ export class BrowserContext {
this.permissions = new Permissions(client, contextId);
}
_targets(): Target[] {
return this._browser._allTargets().filter(target => target.browserContext() === this);
}
async pages(): Promise<Page[]> {
const pages = await Promise.all(
this._targets()
.filter(target => target.type() === 'page')
.map(target => target.page())
);
return pages.filter(page => !!page);
pages(): Promise<Page[]> {
return this._browser._pages(this);
}
isIncognito(): boolean {

View File

@ -35,7 +35,6 @@ import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { NetworkManagerEvents } from './NetworkManager';
import { Protocol } from './protocol';
import { getExceptionMessage, releaseObject } from './protocolHelper';
import { Target } from './Target';
import * as input from '../input';
import * as types from '../types';
import * as frames from '../frames';
@ -58,8 +57,10 @@ export type Viewport = {
export class Page extends EventEmitter {
private _closed = false;
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
_client: CDPSession;
_target: Target;
private _browserContext: BrowserContext;
private _keyboard: input.Keyboard;
private _mouse: input.Mouse;
private _timeoutSettings: TimeoutSettings;
@ -79,18 +80,19 @@ export class Page extends EventEmitter {
private _disconnectPromise: Promise<Error> | undefined;
private _emulatedMediaType: string | undefined;
static async create(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
const page = new Page(client, target, ignoreHTTPSErrors, screenshotter);
static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise<Page> {
const page = new Page(client, browserContext, ignoreHTTPSErrors, screenshotter);
await page._initialize();
if (defaultViewport)
await page.setViewport(defaultViewport);
return page;
}
constructor(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, screenshotter: Screenshotter) {
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, screenshotter: Screenshotter) {
super();
this._client = client;
this._target = target;
this._closedPromise = new Promise(f => this._closedCallback = f);
this._browserContext = browserContext;
this._keyboard = new input.Keyboard(new RawKeyboardImpl(client));
this._mouse = new input.Mouse(new RawMouseImpl(client), this._keyboard);
this._timeoutSettings = new TimeoutSettings();
@ -134,10 +136,13 @@ export class Page extends EventEmitter {
client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event));
this._target._isClosedPromise.then(() => {
this.emit(Events.Page.Close);
this._closed = true;
});
}
_didClose() {
assert(!this._closed, 'Page closed twice');
this._closed = true;
this.emit(Events.Page.Close);
this._closedCallback();
}
async _initialize() {
@ -179,11 +184,11 @@ export class Page extends EventEmitter {
}
browser(): Browser {
return this._target.browser();
return this._browserContext.browser();
}
browserContext(): BrowserContext {
return this._target.browserContext();
return this._browserContext;
}
_onTargetCrashed() {
@ -518,8 +523,8 @@ export class Page extends EventEmitter {
if (runBeforeUnload) {
await this._client.send('Page.close');
} else {
await this.browser()._closeTarget(this._target);
await this._target._isClosedPromise;
await this.browser()._closePage(this);
await this._closedPromise;
}
}

View File

@ -85,7 +85,7 @@ export class Screenshotter {
}
private async _screenshot(page: Page, format: 'png' | 'jpeg', options: ScreenshotOptions): Promise<Buffer | string> {
await page._client.send('Target.activateTarget', {targetId: page._target._targetId});
await page.browser()._activatePage(page);
let clip = options.clip ? processClip(options.clip) : undefined;
const viewport = page.viewport();

View File

@ -24,6 +24,8 @@ import { Page, Viewport } from './Page';
import { Protocol } from './protocol';
import { Screenshotter } from './Screenshotter';
const targetSymbol = Symbol('target');
export class Target {
private _targetInfo: Protocol.Target.TargetInfo;
private _browserContext: BrowserContext;
@ -36,10 +38,12 @@ export class Target {
private _workerPromise: Promise<Worker> | null = null;
_initializedPromise: Promise<boolean>;
_initializedCallback: (value?: unknown) => void;
_isClosedPromise: Promise<void>;
_closedCallback: (value?: unknown) => void;
_isInitialized: boolean;
static fromPage(page: Page): Target {
return (page as any)[targetSymbol];
}
constructor(
targetInfo: Protocol.Target.TargetInfo,
browserContext: BrowserContext,
@ -67,16 +71,23 @@ export class Target {
openerPage.emit(Events.Page.Popup, popupPage);
return true;
});
this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill);
this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== '';
if (this._isInitialized)
this._initializedCallback(true);
}
_didClose() {
if (this._pagePromise)
this._pagePromise.then(page => page._didClose());
}
async page(): Promise<Page | null> {
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
this._pagePromise = this._sessionFactory()
.then(client => Page.create(client, this, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotter));
this._pagePromise = this._sessionFactory().then(async client => {
const page = await Page.create(client, this._browserContext, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotter);
page[targetSymbol] = this;
return page;
});
}
return this._pagePromise;
}

View File

@ -92,7 +92,7 @@ export class Chromium extends EventEmitter {
}
pageTarget(page: Page): Target {
return page._target;
return Target.fromPage(page);
}
waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise<Target> {

View File

@ -150,6 +150,12 @@ export class Browser extends EventEmitter {
return Array.from(this._targets.values());
}
async _pages(context: BrowserContext): Promise<Page[]> {
const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
}
async _onTargetCreated({targetId, url, browserContextId, openerId, type}) {
const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext;
const target = new Target(this._connection, this, context, targetId, type, url, openerId);
@ -166,7 +172,7 @@ export class Browser extends EventEmitter {
_onTargetDestroyed({targetId}) {
const target = this._targets.get(targetId);
this._targets.delete(targetId);
target._closedCallback();
target._didClose();
}
_onTargetInfoChanged({targetId, url}) {
@ -189,8 +195,6 @@ export class Target {
private _type: 'page' | 'browser';
_url: string;
private _openerId: string;
_isClosedPromise: Promise<unknown>;
_closedCallback: (value?: unknown) => void;
constructor(connection: any, browser: Browser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) {
this._browser = browser;
@ -200,9 +204,12 @@ export class Target {
this._type = type;
this._url = url;
this._openerId = openerId;
this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill);
}
_didClose() {
if (this._pagePromise)
this._pagePromise.then(page => page._didClose());
}
opener(): Target | null {
return this._openerId ? this._browser._targets.get(this._openerId) : null;
@ -225,7 +232,7 @@ export class Target {
async page() {
if (this._type === 'page' && !this._pagePromise) {
const session = await this._connection.createSession(this._targetId);
this._pagePromise = Page.create(session, this, this._browser._defaultViewport);
this._pagePromise = Page.create(session, this._context, this._browser._defaultViewport);
}
return this._pagePromise;
}
@ -248,17 +255,8 @@ export class BrowserContext {
this.permissions = new Permissions(connection, browserContextId);
}
_targets(): Array<Target> {
return this._browser._allTargets().filter(target => target.browserContext() === this);
}
async pages(): Promise<Array<Page>> {
const pages = await Promise.all(
this._targets()
.filter(target => target.type() === 'page')
.map(target => target.page())
);
return pages.filter(page => !!page);
pages(): Promise<Page[]> {
return this._browser._pages(this);
}
isIncognito(): boolean {

View File

@ -21,7 +21,7 @@ import * as mime from 'mime';
import { TimeoutError } from '../Errors';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import { TimeoutSettings } from '../TimeoutSettings';
import { BrowserContext, Target } from './Browser';
import { BrowserContext } from './Browser';
import { JugglerSession, JugglerSessionEvents } from './Connection';
import { Events } from './events';
import { Accessibility } from './features/accessibility';
@ -44,12 +44,14 @@ const writeFileAsync = helper.promisify(fs.writeFile);
export class Page extends EventEmitter {
private _timeoutSettings: TimeoutSettings;
private _session: JugglerSession;
private _target: Target;
private _browserContext: BrowserContext;
private _keyboard: input.Keyboard;
private _mouse: input.Mouse;
readonly accessibility: Accessibility;
readonly interception: Interception;
private _closed: boolean;
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
private _pageBindings: Map<string, Function>;
private _networkManager: NetworkManager;
_frameManager: FrameManager;
@ -59,8 +61,8 @@ export class Page extends EventEmitter {
private _disconnectPromise: Promise<Error>;
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
static async create(session: JugglerSession, target: Target, defaultViewport: Viewport | null) {
const page = new Page(session, target);
static async create(session: JugglerSession, browserContext: BrowserContext, defaultViewport: Viewport | null) {
const page = new Page(session, browserContext);
await Promise.all([
session.send('Runtime.enable'),
session.send('Network.enable'),
@ -73,15 +75,16 @@ export class Page extends EventEmitter {
return page;
}
constructor(session: JugglerSession, target: Target) {
constructor(session: JugglerSession, browserContext: BrowserContext) {
super();
this._timeoutSettings = new TimeoutSettings();
this._session = session;
this._target = target;
this._browserContext = browserContext;
this._keyboard = new input.Keyboard(new RawKeyboardImpl(session));
this._mouse = new input.Mouse(new RawMouseImpl(session), this._keyboard);
this.accessibility = new Accessibility(session);
this._closed = false;
this._closedPromise = new Promise(f => this._closedCallback = f);
this._pageBindings = new Map();
this._networkManager = new NetworkManager(session);
this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings);
@ -104,13 +107,16 @@ export class Page extends EventEmitter {
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)),
];
this._viewport = null;
this._target._isClosedPromise.then(() => {
this._closed = true;
this._frameManager.dispose();
this._networkManager.dispose();
helper.removeEventListeners(this._eventListeners);
this.emit(Events.Page.Close);
});
}
_didClose() {
assert(!this._closed, 'Page closed twice');
this._closed = true;
this._frameManager.dispose();
this._networkManager.dispose();
helper.removeEventListeners(this._eventListeners);
this.emit(Events.Page.Close);
this._closedCallback();
}
async setExtraHTTPHeaders(headers) {
@ -250,7 +256,7 @@ export class Page extends EventEmitter {
}
browserContext(): BrowserContext {
return this._target.browserContext();
return this._browserContext;
}
_onUncaughtError(params) {
@ -288,7 +294,7 @@ export class Page extends EventEmitter {
}
browser() {
return this._target.browser();
return this._browserContext.browser();
}
url() {
@ -535,7 +541,7 @@ export class Page extends EventEmitter {
} = options;
await this._session.send('Page.close', { runBeforeUnload });
if (!runBeforeUnload)
await this._target._isClosedPromise;
await this._closedPromise;
}
async content() {

View File

@ -17,7 +17,7 @@
import * as childProcess from 'child_process';
import { EventEmitter } from 'events';
import { assert, helper, RegisteredListener } from '../helper';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network';
import { Connection } from './Connection';
import { Page, Viewport } from './Page';
@ -167,7 +167,23 @@ export class Browser extends EventEmitter {
_onTargetDestroyed({targetId}) {
const target = this._targets.get(targetId);
this._targets.delete(targetId);
target._closedCallback();
target._didClose();
}
_closePage(page: Page) {
this._connection.send('Target.close', {
targetId: Target.fromPage(page)._targetId
}).catch(debugError);
}
async _pages(context: BrowserContext): Promise<Page[]> {
const targets = this.targets().filter(target => target.browserContext() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
}
async _activatePage(page: Page): Promise<void> {
await this._connection.send('Target.activate', { targetId: Target.fromPage(page)._targetId });
}
async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) {
@ -177,8 +193,12 @@ export class Browser extends EventEmitter {
const page = await oldTarget._pagePromise;
const newTarget = this._targets.get(newTargetId);
const newSession = this._connection.session(newTargetId);
page._swapTargetOnNavigation(newSession, newTarget);
page._swapSessionOnNavigation(newSession);
newTarget._pagePromise = oldTarget._pagePromise;
newTarget._adoptPage(page);
// Old target should not be accessed by anyone. Reset page promise so that
// old target does not close the page on connection reset.
oldTarget._pagePromise = null;
}
disconnect() {
@ -204,17 +224,8 @@ export class BrowserContext {
this._id = contextId;
}
_targets(): Target[] {
return this._browser.targets().filter(target => target.browserContext() === this);
}
async pages(): Promise<Page[]> {
const pages = await Promise.all(
this._targets()
.filter(target => target.type() === 'page')
.map(target => target.page())
);
return pages.filter(page => !!page);
pages(): Promise<Page[]> {
return this._browser._pages(this);
}
isIncognito(): boolean {

View File

@ -91,7 +91,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
];
}
async _swapTargetOnNavigation(newSession) {
async _swapSessionOnNavigation(newSession) {
helper.removeEventListeners(this._sessionListeners);
this.disconnectFromTarget();
this._session = newSession;

View File

@ -28,7 +28,6 @@ import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawKeyboardImpl, RawMouseImpl } from './Input';
import { NetworkManagerEvents } from './NetworkManager';
import { Protocol } from './protocol';
import { Target } from './Target';
import { TaskQueue } from './TaskQueue';
import * as input from '../input';
import * as types from '../types';
@ -48,8 +47,10 @@ export type Viewport = {
export class Page extends EventEmitter {
private _closed = false;
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
_session: TargetSession;
private _target: Target;
private _browserContext: BrowserContext;
private _keyboard: input.Keyboard;
private _mouse: input.Mouse;
private _timeoutSettings: TimeoutSettings;
@ -64,16 +65,17 @@ export class Page extends EventEmitter {
private _emulatedMediaType: string | undefined;
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
static async create(session: TargetSession, target: Target, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise<Page> {
const page = new Page(session, target, screenshotTaskQueue);
static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise<Page> {
const page = new Page(session, browserContext, screenshotTaskQueue);
await page._initialize();
if (defaultViewport)
await page.setViewport(defaultViewport);
return page;
}
constructor(session: TargetSession, target: Target, screenshotTaskQueue: TaskQueue) {
constructor(session: TargetSession, browserContext: BrowserContext, screenshotTaskQueue: TaskQueue) {
super();
this._closedPromise = new Promise(f => this._closedCallback = f);
this._keyboard = new input.Keyboard(new RawKeyboardImpl(session));
this._mouse = new input.Mouse(new RawMouseImpl(session), this._keyboard);
this._timeoutSettings = new TimeoutSettings();
@ -82,7 +84,7 @@ export class Page extends EventEmitter {
this._screenshotTaskQueue = screenshotTaskQueue;
this._setSession(session);
this._setTarget(target);
this._browserContext = browserContext;
this._frameManager.on(FrameManagerEvents.FrameAttached, event => this.emit(Events.Page.FrameAttached, event));
this._frameManager.on(FrameManagerEvents.FrameDetached, event => this.emit(Events.Page.FrameDetached, event));
@ -95,6 +97,13 @@ export class Page extends EventEmitter {
networkManager.on(NetworkManagerEvents.RequestFinished, event => this.emit(Events.Page.RequestFinished, event));
}
_didClose() {
assert(!this._closed, 'Page closed twice');
this._closed = true;
this.emit(Events.Page.Close);
this._closedCallback();
}
async _initialize() {
return Promise.all([
this._frameManager.initialize(),
@ -127,29 +136,18 @@ export class Page extends EventEmitter {
event.defaultPrompt));
}
_setTarget(newTarget: Target) {
this._target = newTarget;
this._target._isClosedPromise.then(() => {
if (this._target !== newTarget)
return;
this.emit(Events.Page.Close);
this._closed = true;
});
}
async _swapTargetOnNavigation(newSession : TargetSession, newTarget : Target) {
async _swapSessionOnNavigation(newSession: TargetSession) {
this._setSession(newSession);
this._setTarget(newTarget);
await this._frameManager._swapTargetOnNavigation(newSession);
await this._frameManager._swapSessionOnNavigation(newSession);
await this._initialize().catch(e => debugError('failed to enable agents after swap: ' + e));
}
browser(): Browser {
return this._target.browser();
return this._browserContext.browser();
}
browserContext(): BrowserContext {
return this._target.browserContext();
return this._browserContext;
}
_onTargetCrashed() {
@ -419,7 +417,7 @@ export class Page extends EventEmitter {
Object.assign(params, this._viewport);
}
const [, result] = await Promise.all([
this._session._connection.send('Target.activate', { targetId: this._target._targetId }),
this.browser()._activatePage(this),
this._session.send('Page.snapshotRect', params),
]).catch(e => {
debugError('Failed to take screenshot: ' + e);
@ -437,12 +435,8 @@ export class Page extends EventEmitter {
}
async close() {
this.browser()._connection.send('Target.close', {
targetId: this._target._targetId
}).catch(e => {
debugError(e);
});
await this._target._isClosedPromise;
this.browser()._closePage(this);
await this._closedPromise;
}
isClosed(): boolean {

View File

@ -20,6 +20,8 @@ import { Browser, BrowserContext } from './Browser';
import { Page } from './Page';
import { Protocol } from './protocol';
const targetSymbol = Symbol('target');
export class Target {
private _browserContext: BrowserContext;
_targetId: string;
@ -28,11 +30,13 @@ export class Target {
private _url: string;
_initializedPromise: Promise<boolean>;
_initializedCallback: (value?: unknown) => void;
_isClosedPromise: Promise<void>;
_closedCallback: (value?: unknown) => void;
_isInitialized: boolean;
_eventListeners: RegisteredListener[];
static fromPage(page: Page): Target {
return (page as any)[targetSymbol];
}
constructor(targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext) {
const {targetId, url, type} = targetInfo;
this._browserContext = browserContext;
@ -41,13 +45,24 @@ export class Target {
/** @type {?Promise<!Page>} */
this._pagePromise = null;
this._url = url;
this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill);
}
_didClose() {
if (this._pagePromise)
this._pagePromise.then(page => page._didClose());
}
_adoptPage(page: Page) {
(page as any)[targetSymbol] = this;
}
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, this.browser()._defaultViewport, this.browser()._screenshotTaskQueue);
this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport, this.browser()._screenshotTaskQueue).then(page => {
this._adoptPage(page);
return page;
});
}
return this._pagePromise;
}