chore: refactor impl-side events to be per-class (#3569)

This commit is contained in:
Dmitry Gozman 2020-08-21 16:26:33 -07:00 committed by GitHub
parent d4dac04212
commit 57e8617474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 165 additions and 244 deletions

View file

@ -19,7 +19,6 @@ import { BrowserContext } from './browserContext';
import { Page } from './page'; import { Page } from './page';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
import { Events } from './events';
import { ProxySettings } from './types'; import { ProxySettings } from './types';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
@ -40,6 +39,10 @@ export type BrowserOptions = types.UIOptions & {
}; };
export abstract class Browser extends EventEmitter { export abstract class Browser extends EventEmitter {
static Events = {
Disconnected: 'disconnected',
};
readonly _options: BrowserOptions; readonly _options: BrowserOptions;
private _downloads = new Map<string, Download>(); private _downloads = new Map<string, Download>();
_defaultContext: BrowserContext | null = null; _defaultContext: BrowserContext | null = null;
@ -87,7 +90,7 @@ export abstract class Browser extends EventEmitter {
context._browserClosed(); context._browserClosed();
if (this._defaultContext) if (this._defaultContext)
this._defaultContext._browserClosed(); this._defaultContext._browserClosed();
this.emit(Events.Browser.Disconnected); this.emit(Browser.Events.Disconnected);
} }
async close() { async close() {
@ -96,7 +99,7 @@ export abstract class Browser extends EventEmitter {
await this._options.browserProcess.close(); await this._options.browserProcess.close();
} }
if (this.isConnected()) if (this.isConnected())
await new Promise(x => this.once(Events.Browser.Disconnected, x)); await new Promise(x => this.once(Browser.Events.Disconnected, x));
} }
} }

View file

@ -23,7 +23,6 @@ import { Page, PageBinding } from './page';
import { TimeoutSettings } from './timeoutSettings'; import { TimeoutSettings } from './timeoutSettings';
import * as frames from './frames'; import * as frames from './frames';
import * as types from './types'; import * as types from './types';
import { Events } from './events';
import { Download } from './download'; import { Download } from './download';
import { Browser } from './browser'; import { Browser } from './browser';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
@ -40,6 +39,13 @@ export class Screencast {
} }
export abstract class BrowserContext extends EventEmitter { export abstract class BrowserContext extends EventEmitter {
static Events = {
Close: 'close',
Page: 'page',
ScreencastStarted: 'screencaststarted',
ScreencastStopped: 'screencaststopped',
};
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>(); readonly _pageBindings = new Map<string, PageBinding>();
readonly _options: types.BrowserContextOptions; readonly _options: types.BrowserContextOptions;
@ -81,7 +87,7 @@ export abstract class BrowserContext extends EventEmitter {
this._closedStatus = 'closed'; this._closedStatus = 'closed';
this._downloads.clear(); this._downloads.clear();
this._closePromiseFulfill!(new Error('Context closed')); this._closePromiseFulfill!(new Error('Context closed'));
this.emit(Events.BrowserContext.Close); this.emit(BrowserContext.Events.Close);
} }
// BrowserContext methods. // BrowserContext methods.
@ -160,7 +166,7 @@ export abstract class BrowserContext extends EventEmitter {
async _loadDefaultContext(progress: Progress) { async _loadDefaultContext(progress: Progress) {
if (!this.pages().length) { if (!this.pages().length) {
const waitForEvent = helper.waitForEvent(progress, this, Events.BrowserContext.Page); const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);
progress.cleanupWhenAborted(() => waitForEvent.dispose); progress.cleanupWhenAborted(() => waitForEvent.dispose);
await waitForEvent.promise; await waitForEvent.promise;
} }

View file

@ -17,7 +17,6 @@
import { Browser, BrowserOptions } from '../browser'; import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events as CommonEvents } from '../events';
import { assert } from '../helper'; import { assert } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding, Worker } from '../page'; import { Page, PageBinding, Worker } from '../page';
@ -26,7 +25,6 @@ import * as types from '../types';
import { ConnectionEvents, CRConnection, CRSession } from './crConnection'; import { ConnectionEvents, CRConnection, CRSession } from './crConnection';
import { CRPage } from './crPage'; import { CRPage } from './crPage';
import { readProtocolStream } from './crProtocolHelper'; import { readProtocolStream } from './crProtocolHelper';
import { Events } from './events';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { CRExecutionContext } from './crExecutionContext'; import { CRExecutionContext } from './crExecutionContext';
import { CRDevTools } from './crDevTools'; import { CRDevTools } from './crDevTools';
@ -151,7 +149,7 @@ export class CRBrowser extends Browser {
const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, false); const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, false);
this._backgroundPages.set(targetInfo.targetId, backgroundPage); this._backgroundPages.set(targetInfo.targetId, backgroundPage);
backgroundPage.pageOrError().then(() => { backgroundPage.pageOrError().then(() => {
context!.emit(Events.ChromiumBrowserContext.BackgroundPage, backgroundPage._page); context!.emit(CRBrowserContext.CREvents.BackgroundPage, backgroundPage._page);
}); });
return; return;
} }
@ -164,11 +162,11 @@ export class CRBrowser extends Browser {
const page = crPage._page; const page = crPage._page;
if (pageOrError instanceof Error) if (pageOrError instanceof Error)
page._setIsError(); page._setIsError();
context!.emit(CommonEvents.BrowserContext.Page, page); context!.emit(BrowserContext.Events.Page, page);
if (opener) { if (opener) {
opener.pageOrError().then(openerPage => { opener.pageOrError().then(openerPage => {
if (openerPage instanceof Page && !openerPage.isClosed()) if (openerPage instanceof Page && !openerPage.isClosed())
openerPage.emit(CommonEvents.Page.Popup, page); openerPage.emit(Page.Events.Popup, page);
}); });
} }
}); });
@ -178,7 +176,7 @@ export class CRBrowser extends Browser {
if (targetInfo.type === 'service_worker') { if (targetInfo.type === 'service_worker') {
const serviceWorker = new CRServiceWorker(context, session, targetInfo.url); const serviceWorker = new CRServiceWorker(context, session, targetInfo.url);
this._serviceWorkers.set(targetInfo.targetId, serviceWorker); this._serviceWorkers.set(targetInfo.targetId, serviceWorker);
context.emit(Events.ChromiumBrowserContext.ServiceWorker, serviceWorker); context.emit(CRBrowserContext.CREvents.ServiceWorker, serviceWorker);
return; return;
} }
@ -202,7 +200,7 @@ export class CRBrowser extends Browser {
const serviceWorker = this._serviceWorkers.get(targetId); const serviceWorker = this._serviceWorkers.get(targetId);
if (serviceWorker) { if (serviceWorker) {
this._serviceWorkers.delete(targetId); this._serviceWorkers.delete(targetId);
serviceWorker.emit(CommonEvents.Worker.Close); serviceWorker.emit(Worker.Events.Close);
return; return;
} }
} }
@ -280,6 +278,11 @@ class CRServiceWorker extends Worker {
} }
export class CRBrowserContext extends BrowserContext { export class CRBrowserContext extends BrowserContext {
static CREvents = {
BackgroundPage: 'backgroundpage',
ServiceWorker: 'serviceworker',
};
readonly _browser: CRBrowser; readonly _browser: CRBrowser;
readonly _browserContextId: string | null; readonly _browserContextId: string | null;
readonly _evaluateOnNewDocumentSources: string[]; readonly _evaluateOnNewDocumentSources: string[];
@ -432,7 +435,7 @@ export class CRBrowserContext extends BrowserContext {
// asynchronously and we get detached from them later. // asynchronously and we get detached from them later.
// To avoid the wrong order of notifications, we manually fire // To avoid the wrong order of notifications, we manually fire
// "close" event here and forget about the serivce worker. // "close" event here and forget about the serivce worker.
serviceWorker.emit(CommonEvents.Worker.Close); serviceWorker.emit(Worker.Events.Close);
this._browser._serviceWorkers.delete(targetId); this._browser._serviceWorkers.delete(targetId);
} }
} }

View file

@ -24,7 +24,6 @@ import { CRExecutionContext } from './crExecutionContext';
import { CRNetworkManager } from './crNetworkManager'; import { CRNetworkManager } from './crNetworkManager';
import { Page, Worker, PageBinding } from '../page'; import { Page, Worker, PageBinding } from '../page';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { Events } from '../events';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper'; import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import { PageDelegate } from '../page'; import { PageDelegate } from '../page';
@ -590,7 +589,7 @@ class FrameSession {
const args = event.args.map(o => worker._existingExecutionContext!.createHandle(o)); const args = event.args.map(o => worker._existingExecutionContext!.createHandle(o));
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
}); });
session.on('Runtime.exceptionThrown', exception => this._page.emit(Events.Page.PageError, exceptionToError(exception.exceptionDetails))); session.on('Runtime.exceptionThrown', exception => this._page.emit(Page.Events.PageError, exceptionToError(exception.exceptionDetails)));
// TODO: attribute workers to the right frame. // TODO: attribute workers to the right frame.
this._networkManager.instrumentNetworkEvents(session, this._page._frameManager.frame(this._targetId)!); this._networkManager.instrumentNetworkEvents(session, this._page._frameManager.frame(this._targetId)!);
} }
@ -664,7 +663,7 @@ class FrameSession {
} }
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) { _onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog( this._page.emit(Page.Events.Dialog, new dialog.Dialog(
event.type, event.type,
event.message, event.message,
async (accept: boolean, promptText?: string) => { async (accept: boolean, promptText?: string) => {
@ -674,7 +673,7 @@ class FrameSession {
} }
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails)); this._page.emit(Page.Events.PageError, exceptionToError(exceptionDetails));
} }
async _onTargetCrashed() { async _onTargetCrashed() {
@ -692,7 +691,7 @@ class FrameSession {
lineNumber: lineNumber || 0, lineNumber: lineNumber || 0,
columnNumber: 0, columnNumber: 0,
}; };
this._page.emit(Events.Page.Console, new ConsoleMessage(level, text, [], location)); this._page.emit(Page.Events.Console, new ConsoleMessage(level, text, [], location));
} }
} }

View file

@ -1,23 +0,0 @@
/**
* 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.
*/
export const Events = {
ChromiumBrowserContext: {
BackgroundPage: 'backgroundpage',
ServiceWorker: 'serviceworker',
}
};

View file

@ -15,7 +15,6 @@
*/ */
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { Events } from '../events';
import * as frames from '../frames'; import * as frames from '../frames';
import * as js from '../javascript'; import * as js from '../javascript';
import { Page } from '../page'; import { Page } from '../page';
@ -23,10 +22,10 @@ import DebugScript from './injected/debugScript';
export class DebugController { export class DebugController {
constructor(context: BrowserContext) { constructor(context: BrowserContext) {
context.on(Events.BrowserContext.Page, (page: Page) => { context.on(BrowserContext.Events.Page, (page: Page) => {
for (const frame of page.frames()) for (const frame of page.frames())
this.ensureInstalledInFrame(frame); this.ensureInstalledInFrame(frame);
page.on(Events.Page.FrameNavigated, frame => this.ensureInstalledInFrame(frame)); page.on(Page.Events.FrameNavigated, frame => this.ensureInstalledInFrame(frame));
}); });
} }

View file

@ -18,7 +18,6 @@ import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as util from 'util'; import * as util from 'util';
import { Page } from './page'; import { Page } from './page';
import { Events } from './events';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { assert, mkdirIfNeeded } from './helper'; import { assert, mkdirIfNeeded } from './helper';
@ -47,13 +46,13 @@ export class Download {
page._browserContext._downloads.add(this); page._browserContext._downloads.add(this);
this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads; this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads;
if (suggestedFilename !== undefined) if (suggestedFilename !== undefined)
this._page.emit(Events.Page.Download, this); this._page.emit(Page.Events.Download, this);
} }
_filenameSuggested(suggestedFilename: string) { _filenameSuggested(suggestedFilename: string) {
assert(this._suggestedFilename === undefined); assert(this._suggestedFilename === undefined);
this._suggestedFilename = suggestedFilename; this._suggestedFilename = suggestedFilename;
this._page.emit(Events.Page.Download, this); this._page.emit(Page.Events.Download, this);
} }
url(): string { url(): string {

View file

@ -1,60 +0,0 @@
/**
* 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.
*/
export const Events = {
Browser: {
Disconnected: 'disconnected'
},
BrowserContext: {
Close: 'close',
Page: 'page',
ScreencastStarted: 'screencaststarted',
ScreencastStopped: 'screencaststopped',
},
BrowserServer: {
Close: 'close',
},
Page: {
Close: 'close',
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
Download: 'download',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: 'pageerror',
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Popup: 'popup',
Worker: 'worker',
},
Worker: {
Close: 'close',
},
};

View file

@ -17,7 +17,6 @@
import { Browser, BrowserOptions } from '../browser'; import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events } from '../events';
import { assert, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding } from '../page'; import { Page, PageBinding } from '../page';
@ -129,12 +128,12 @@ export class FFBrowser extends Browser {
const page = ffPage._page; const page = ffPage._page;
if (pageOrError instanceof Error) if (pageOrError instanceof Error)
page._setIsError(); page._setIsError();
context.emit(Events.BrowserContext.Page, page); context.emit(BrowserContext.Events.Page, page);
if (!opener) if (!opener)
return; return;
const openerPage = await opener.pageOrError(); const openerPage = await opener.pageOrError();
if (openerPage instanceof Page && !openerPage.isClosed()) if (openerPage instanceof Page && !openerPage.isClosed())
openerPage.emit(Events.Page.Popup, page); openerPage.emit(Page.Events.Popup, page);
}); });
} }

View file

@ -17,7 +17,6 @@
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import * as dom from '../dom'; import * as dom from '../dom';
import { Events } from '../events';
import * as frames from '../frames'; import * as frames from '../frames';
import { assert, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import { Page, PageBinding, PageDelegate, Worker } from '../page'; import { Page, PageBinding, PageDelegate, Worker } from '../page';
@ -32,7 +31,7 @@ import { FFNetworkManager } from './ffNetworkManager';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { selectors } from '../selectors'; import { selectors } from '../selectors';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
import { Screencast } from '../browserContext'; import { Screencast, BrowserContext } from '../browserContext';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -62,7 +61,7 @@ export class FFPage implements PageDelegate {
this._browserContext = browserContext; this._browserContext = browserContext;
this._page = new Page(this, browserContext); this._page = new Page(this, browserContext);
this._networkManager = new FFNetworkManager(session, this._page); this._networkManager = new FFNetworkManager(session, this._page);
this._page.on(Events.Page.FrameDetached, frame => this._removeContextsForFrame(frame)); this._page.on(Page.Events.FrameDetached, frame => this._removeContextsForFrame(frame));
// TODO: remove Page.willOpenNewWindowAsynchronously from the protocol. // TODO: remove Page.willOpenNewWindowAsynchronously from the protocol.
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)), helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
@ -179,7 +178,7 @@ export class FFPage implements PageDelegate {
const message = params.message.startsWith('Error: ') ? params.message.substring(7) : params.message; const message = params.message.startsWith('Error: ') ? params.message.substring(7) : params.message;
const error = new Error(message); const error = new Error(message);
error.stack = params.stack; error.stack = params.stack;
this._page.emit(Events.Page.PageError, error); this._page.emit(Page.Events.PageError, error);
} }
_onConsole(payload: Protocol.Runtime.consolePayload) { _onConsole(payload: Protocol.Runtime.consolePayload) {
@ -189,7 +188,7 @@ export class FFPage implements PageDelegate {
} }
_onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog( this._page.emit(Page.Events.Dialog, new dialog.Dialog(
params.type, params.type,
params.message, params.message,
async (accept: boolean, promptText?: string) => { async (accept: boolean, promptText?: string) => {
@ -262,7 +261,7 @@ export class FFPage implements PageDelegate {
_onScreencastStarted(event: Protocol.Page.screencastStartedPayload) { _onScreencastStarted(event: Protocol.Page.screencastStartedPayload) {
const screencast = new Screencast(event.file, this._page); const screencast = new Screencast(event.file, this._page);
this._idToScreencast.set(event.uid, screencast); this._idToScreencast.set(event.uid, screencast);
this._browserContext.emit(Events.BrowserContext.ScreencastStarted, screencast); this._browserContext.emit(BrowserContext.Events.ScreencastStarted, screencast);
} }
_onScreencastStopped(event: Protocol.Page.screencastStoppedPayload) { _onScreencastStopped(event: Protocol.Page.screencastStoppedPayload) {
@ -270,7 +269,7 @@ export class FFPage implements PageDelegate {
if (!screencast) if (!screencast)
return; return;
this._idToScreencast.delete(event.uid); this._idToScreencast.delete(event.uid);
this._browserContext.emit(Events.BrowserContext.ScreencastStopped, screencast); this._browserContext.emit(BrowserContext.Events.ScreencastStopped, screencast);
} }
async exposeBinding(binding: PageBinding) { async exposeBinding(binding: PageBinding) {

View file

@ -17,7 +17,6 @@
import { ConsoleMessage } from './console'; import { ConsoleMessage } from './console';
import * as dom from './dom'; import * as dom from './dom';
import { Events } from './events';
import { assert, helper, RegisteredListener, debugLogger } from './helper'; import { assert, helper, RegisteredListener, debugLogger } from './helper';
import * as js from './javascript'; import * as js from './javascript';
import * as network from './network'; import * as network from './network';
@ -50,7 +49,6 @@ type ConsoleTagHandler = () => void;
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame}, ...args: any) => any; export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame}, ...args: any) => any;
export const kNavigationEvent = Symbol('navigation');
export type NavigationEvent = { export type NavigationEvent = {
// New frame url after navigation. // New frame url after navigation.
url: string, url: string,
@ -63,8 +61,6 @@ export type NavigationEvent = {
// the navigation did not commit. // the navigation did not commit.
error?: Error, error?: Error,
}; };
export const kAddLifecycleEvent = Symbol('addLifecycle');
export const kRemoveLifecycleEvent = Symbol('removeLifecycle');
export class FrameManager { export class FrameManager {
private _page: Page; private _page: Page;
@ -120,7 +116,7 @@ export class FrameManager {
assert(!this._frames.has(frameId)); assert(!this._frames.has(frameId));
const frame = new Frame(this._page, frameId, parentFrame); const frame = new Frame(this._page, frameId, parentFrame);
this._frames.set(frameId, frame); this._frames.set(frameId, frame);
this._page.emit(Events.Page.FrameAttached, frame); this._page.emit(Page.Events.FrameAttached, frame);
return frame; return frame;
} }
} }
@ -197,10 +193,10 @@ export class FrameManager {
frame._onClearLifecycle(); frame._onClearLifecycle();
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument }; const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
frame._eventEmitter.emit(kNavigationEvent, navigationEvent); frame.emit(Frame.Events.Navigation, navigationEvent);
if (!initial) { if (!initial) {
debugLogger.log('api', ` navigated to "${url}"`); debugLogger.log('api', ` navigated to "${url}"`);
this._page.emit(Events.Page.FrameNavigated, frame); this._page.emit(Page.Events.FrameNavigated, frame);
} }
// Restore pending if any - see comments above about keepPending. // Restore pending if any - see comments above about keepPending.
frame._pendingDocument = keepPending; frame._pendingDocument = keepPending;
@ -212,9 +208,9 @@ export class FrameManager {
return; return;
frame._url = url; frame._url = url;
const navigationEvent: NavigationEvent = { url, name: frame._name }; const navigationEvent: NavigationEvent = { url, name: frame._name };
frame._eventEmitter.emit(kNavigationEvent, navigationEvent); frame.emit(Frame.Events.Navigation, navigationEvent);
debugLogger.log('api', ` navigated to "${url}"`); debugLogger.log('api', ` navigated to "${url}"`);
this._page.emit(Events.Page.FrameNavigated, frame); this._page.emit(Page.Events.FrameNavigated, frame);
} }
frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) { frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) {
@ -230,7 +226,7 @@ export class FrameManager {
error: new Error(errorText), error: new Error(errorText),
}; };
frame._pendingDocument = undefined; frame._pendingDocument = undefined;
frame._eventEmitter.emit(kNavigationEvent, navigationEvent); frame.emit(Frame.Events.Navigation, navigationEvent);
} }
frameDetached(frameId: string) { frameDetached(frameId: string) {
@ -266,13 +262,13 @@ export class FrameManager {
requestReceivedResponse(response: network.Response) { requestReceivedResponse(response: network.Response) {
if (!response.request()._isFavicon) if (!response.request()._isFavicon)
this._page.emit(Events.Page.Response, response); this._page.emit(Page.Events.Response, response);
} }
requestFinished(request: network.Request) { requestFinished(request: network.Request) {
this._inflightRequestFinished(request); this._inflightRequestFinished(request);
if (!request._isFavicon) if (!request._isFavicon)
this._page.emit(Events.Page.RequestFinished, request); this._page.emit(Page.Events.RequestFinished, request);
} }
requestFailed(request: network.Request, canceled: boolean) { requestFailed(request: network.Request, canceled: boolean) {
@ -285,7 +281,7 @@ export class FrameManager {
this.frameAbortedNavigation(frame._id, errorText, frame._pendingDocument.documentId); this.frameAbortedNavigation(frame._id, errorText, frame._pendingDocument.documentId);
} }
if (!request._isFavicon) if (!request._isFavicon)
this._page.emit(Events.Page.RequestFailed, request); this._page.emit(Page.Events.RequestFailed, request);
} }
removeChildFramesRecursively(frame: Frame) { removeChildFramesRecursively(frame: Frame) {
@ -297,7 +293,7 @@ export class FrameManager {
this.removeChildFramesRecursively(frame); this.removeChildFramesRecursively(frame);
frame._onDetached(); frame._onDetached();
this._frames.delete(frame._id); this._frames.delete(frame._id);
this._page.emit(Events.Page.FrameDetached, frame); this._page.emit(Page.Events.FrameDetached, frame);
} }
private _inflightRequestFinished(request: network.Request) { private _inflightRequestFinished(request: network.Request) {
@ -333,8 +329,13 @@ export class FrameManager {
} }
} }
export class Frame { export class Frame extends EventEmitter {
readonly _eventEmitter: EventEmitter; static Events = {
Navigation: 'navigation',
AddLifecycle: 'addlifecycle',
RemoveLifecycle: 'removelifecycle',
};
_id: string; _id: string;
private _firedLifecycleEvents = new Set<types.LifecycleEvent>(); private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>(); _subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
@ -354,8 +355,8 @@ export class Frame {
private _detachedCallback = () => {}; private _detachedCallback = () => {};
constructor(page: Page, id: string, parentFrame: Frame | null) { constructor(page: Page, id: string, parentFrame: Frame | null) {
this._eventEmitter = new EventEmitter(); super();
this._eventEmitter.setMaxListeners(0); this.setMaxListeners(0);
this._id = id; this._id = id;
this._page = page; this._page = page;
this._parentFrame = parentFrame; this._parentFrame = parentFrame;
@ -406,18 +407,18 @@ export class Frame {
for (const event of events) { for (const event of events) {
// Checking whether we have already notified about this event. // Checking whether we have already notified about this event.
if (!this._subtreeLifecycleEvents.has(event)) { if (!this._subtreeLifecycleEvents.has(event)) {
this._eventEmitter.emit(kAddLifecycleEvent, event); this.emit(Frame.Events.AddLifecycle, event);
if (this === mainFrame && this._url !== 'about:blank') if (this === mainFrame && this._url !== 'about:blank')
debugLogger.log('api', ` "${event}" event fired`); debugLogger.log('api', ` "${event}" event fired`);
if (this === mainFrame && event === 'load') if (this === mainFrame && event === 'load')
this._page.emit(Events.Page.Load); this._page.emit(Page.Events.Load);
if (this === mainFrame && event === 'domcontentloaded') if (this === mainFrame && event === 'domcontentloaded')
this._page.emit(Events.Page.DOMContentLoaded); this._page.emit(Page.Events.DOMContentLoaded);
} }
} }
for (const event of this._subtreeLifecycleEvents) { for (const event of this._subtreeLifecycleEvents) {
if (!events.has(event)) if (!events.has(event))
this._eventEmitter.emit(kRemoveLifecycleEvent, event); this.emit(Frame.Events.RemoveLifecycle, event);
} }
this._subtreeLifecycleEvents = events; this._subtreeLifecycleEvents = events;
} }
@ -436,13 +437,13 @@ export class Frame {
} }
url = helper.completeUserURL(url); url = helper.completeUserURL(url);
const sameDocument = helper.waitForEvent(progress, this._eventEmitter, kNavigationEvent, (e: NavigationEvent) => !e.newDocument); const sameDocument = helper.waitForEvent(progress, this, Frame.Events.Navigation, (e: NavigationEvent) => !e.newDocument);
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer); const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
let event: NavigationEvent; let event: NavigationEvent;
if (navigateResult.newDocumentId) { if (navigateResult.newDocumentId) {
sameDocument.dispose(); sameDocument.dispose();
event = await helper.waitForEvent(progress, this._eventEmitter, kNavigationEvent, (event: NavigationEvent) => { event = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
// We are interested either in this specific document, or any other document that // We are interested either in this specific document, or any other document that
// did commit and replaced the expected document. // did commit and replaced the expected document.
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error); return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
@ -460,7 +461,7 @@ export class Frame {
} }
if (!this._subtreeLifecycleEvents.has(waitUntil)) if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise; await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
const request = event.newDocument ? event.newDocument.request : undefined; const request = event.newDocument ? event.newDocument.request : undefined;
const response = request ? request._finalRequest().response() : null; const response = request ? request._finalRequest().response() : null;
@ -474,7 +475,7 @@ export class Frame {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`waiting for navigation until "${waitUntil}"`); progress.log(`waiting for navigation until "${waitUntil}"`);
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this._eventEmitter, kNavigationEvent, (event: NavigationEvent) => { const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
// Any failed navigation results in a rejection. // Any failed navigation results in a rejection.
if (event.error) if (event.error)
return true; return true;
@ -485,7 +486,7 @@ export class Frame {
throw navigationEvent.error; throw navigationEvent.error;
if (!this._subtreeLifecycleEvents.has(waitUntil)) if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise; await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined; const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined;
return request ? request._finalRequest().response() : null; return request ? request._finalRequest().response() : null;
@ -499,7 +500,7 @@ export class Frame {
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> { async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
const waitUntil = verifyLifecycle('state', state); const waitUntil = verifyLifecycle('state', state);
if (!this._subtreeLifecycleEvents.has(waitUntil)) if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this._eventEmitter, kAddLifecycleEvent, (e: types.LifecycleEvent) => e === waitUntil).promise; await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
} }
async frameElement(): Promise<dom.ElementHandle> { async frameElement(): Promise<dom.ElementHandle> {
@ -752,7 +753,7 @@ export class Frame {
resolve(); resolve();
}); });
const errorPromise = new Promise(resolve => { const errorPromise = new Promise(resolve => {
listeners.push(helper.addEventListener(this._page, Events.Page.Console, (message: ConsoleMessage) => { listeners.push(helper.addEventListener(this._page, Page.Events.Console, (message: ConsoleMessage) => {
if (message.type() === 'error' && message.text().includes('Content Security Policy')) { if (message.type() === 'error' && message.text().includes('Content Security Policy')) {
cspMessage = message; cspMessage = message;
resolve(); resolve();
@ -1045,7 +1046,7 @@ class SignalBarrier {
async addFrameNavigation(frame: Frame) { async addFrameNavigation(frame: Frame) {
this.retain(); this.retain();
const waiter = helper.waitForEvent(null, frame._eventEmitter, kNavigationEvent, (e: NavigationEvent) => { const waiter = helper.waitForEvent(null, frame, Frame.Events.Navigation, (e: NavigationEvent) => {
if (!e.error && this._progress) if (!e.error && this._progress)
this._progress.log(` navigated to "${frame._url}"`); this._progress.log(` navigated to "${frame._url}"`);
return true; return true;

View file

@ -24,7 +24,6 @@ import * as network from './network';
import { Screenshotter } from './screenshotter'; import { Screenshotter } from './screenshotter';
import { TimeoutSettings } from './timeoutSettings'; import { TimeoutSettings } from './timeoutSettings';
import * as types from './types'; import * as types from './types';
import { Events } from './events';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
import { ConsoleMessage } from './console'; import { ConsoleMessage } from './console';
import * as accessibility from './accessibility'; import * as accessibility from './accessibility';
@ -91,6 +90,29 @@ type PageState = {
}; };
export class Page extends EventEmitter { export class Page extends EventEmitter {
static Events = {
Close: 'close',
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
Download: 'download',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: 'pageerror',
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Popup: 'popup',
Worker: 'worker',
};
private _closedState: 'open' | 'closing' | 'closed' = 'open'; private _closedState: 'open' | 'closing' | 'closed' = 'open';
private _closedCallback: () => void; private _closedCallback: () => void;
private _closedPromise: Promise<void>; private _closedPromise: Promise<void>;
@ -154,13 +176,13 @@ export class Page extends EventEmitter {
this._frameManager.dispose(); this._frameManager.dispose();
assert(this._closedState !== 'closed', 'Page closed twice'); assert(this._closedState !== 'closed', 'Page closed twice');
this._closedState = 'closed'; this._closedState = 'closed';
this.emit(Events.Page.Close); this.emit(Page.Events.Close);
this._closedCallback(); this._closedCallback();
} }
_didCrash() { _didCrash() {
this._frameManager.dispose(); this._frameManager.dispose();
this.emit(Events.Page.Crash); this.emit(Page.Events.Crash);
this._crashedCallback(new Error('Page crashed')); this._crashedCallback(new Error('Page crashed'));
} }
@ -179,12 +201,12 @@ export class Page extends EventEmitter {
async _onFileChooserOpened(handle: dom.ElementHandle) { async _onFileChooserOpened(handle: dom.ElementHandle) {
const multiple = await handle.evaluate(element => !!(element as HTMLInputElement).multiple); const multiple = await handle.evaluate(element => !!(element as HTMLInputElement).multiple);
if (!this.listenerCount(Events.Page.FileChooser)) { if (!this.listenerCount(Page.Events.FileChooser)) {
handle.dispose(); handle.dispose();
return; return;
} }
const fileChooser = new FileChooser(this, handle, multiple); const fileChooser = new FileChooser(this, handle, multiple);
this.emit(Events.Page.FileChooser, fileChooser); this.emit(Page.Events.FileChooser, fileChooser);
} }
context(): BrowserContext { context(): BrowserContext {
@ -235,10 +257,10 @@ export class Page extends EventEmitter {
_addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { _addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) {
const message = new ConsoleMessage(type, text, args, location); const message = new ConsoleMessage(type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message); const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Events.Page.Console)) if (intercepted || !this.listenerCount(Page.Events.Console))
args.forEach(arg => arg.dispose()); args.forEach(arg => arg.dispose());
else else
this.emit(Events.Page.Console, message); this.emit(Page.Events.Console, message);
} }
async reload(options?: types.NavigateOptions): Promise<network.Response | null> { async reload(options?: types.NavigateOptions): Promise<network.Response | null> {
@ -315,7 +337,7 @@ export class Page extends EventEmitter {
} }
_requestStarted(request: network.Request) { _requestStarted(request: network.Request) {
this.emit(Events.Page.Request, request); this.emit(Page.Events.Request, request);
const route = request._route(); const route = request._route();
if (!route) if (!route)
return; return;
@ -362,20 +384,20 @@ export class Page extends EventEmitter {
_addWorker(workerId: string, worker: Worker) { _addWorker(workerId: string, worker: Worker) {
this._workers.set(workerId, worker); this._workers.set(workerId, worker);
this.emit(Events.Page.Worker, worker); this.emit(Page.Events.Worker, worker);
} }
_removeWorker(workerId: string) { _removeWorker(workerId: string) {
const worker = this._workers.get(workerId); const worker = this._workers.get(workerId);
if (!worker) if (!worker)
return; return;
worker.emit(Events.Worker.Close, worker); worker.emit(Worker.Events.Close, worker);
this._workers.delete(workerId); this._workers.delete(workerId);
} }
_clearWorkers() { _clearWorkers() {
for (const [workerId, worker] of this._workers) { for (const [workerId, worker] of this._workers) {
worker.emit(Events.Worker.Close, worker); worker.emit(Worker.Events.Close, worker);
this._workers.delete(workerId); this._workers.delete(workerId);
} }
} }
@ -386,6 +408,10 @@ export class Page extends EventEmitter {
} }
export class Worker extends EventEmitter { export class Worker extends EventEmitter {
static Events = {
Close: 'close',
};
private _url: string; private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>; private _executionContextPromise: Promise<js.ExecutionContext>;
private _executionContextCallback: (value?: js.ExecutionContext) => void; private _executionContextCallback: (value?: js.ExecutionContext) => void;

View file

@ -15,14 +15,12 @@
*/ */
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import { Events } from '../../events';
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher'; import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
import * as channels from '../channels'; import * as channels from '../channels';
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers'; import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
import { CRBrowserContext } from '../../chromium/crBrowser'; import { CRBrowserContext } from '../../chromium/crBrowser';
import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Events as ChromiumEvents } from '../../chromium/events';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext; private _context: BrowserContext;
@ -33,8 +31,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
for (const page of context.pages()) for (const page of context.pages())
this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) }); this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) });
context.on(Events.BrowserContext.Page, page => this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) })); context.on(BrowserContext.Events.Page, page => this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) }));
context.on(Events.BrowserContext.Close, () => { context.on(BrowserContext.Events.Close, () => {
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });
@ -42,10 +40,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
if (context._browser._options.name === 'chromium') { if (context._browser._options.name === 'chromium') {
for (const page of (context as CRBrowserContext).backgroundPages()) for (const page of (context as CRBrowserContext).backgroundPages())
this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) }); this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) });
context.on(ChromiumEvents.ChromiumBrowserContext.BackgroundPage, page => this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) })); context.on(CRBrowserContext.CREvents.BackgroundPage, page => this._dispatchEvent('crBackgroundPage', { page: new PageDispatcher(this._scope, page) }));
for (const serviceWorker of (context as CRBrowserContext).serviceWorkers()) for (const serviceWorker of (context as CRBrowserContext).serviceWorkers())
this._dispatchEvent('crServiceWorker', new WorkerDispatcher(this._scope, serviceWorker)); this._dispatchEvent('crServiceWorker', new WorkerDispatcher(this._scope, serviceWorker));
context.on(ChromiumEvents.ChromiumBrowserContext.ServiceWorker, serviceWorker => this._dispatchEvent('crServiceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) })); context.on(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('crServiceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) }));
} }
} }

View file

@ -15,7 +15,6 @@
*/ */
import { Browser } from '../../browser'; import { Browser } from '../../browser';
import { Events } from '../../events';
import * as channels from '../channels'; import * as channels from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher'; import { BrowserContextDispatcher } from './browserContextDispatcher';
import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { CDPSessionDispatcher } from './cdpSessionDispatcher';
@ -26,7 +25,7 @@ import { PageDispatcher } from './pageDispatcher';
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserInitializer> implements channels.BrowserChannel { export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserInitializer> implements channels.BrowserChannel {
constructor(scope: DispatcherScope, browser: Browser, guid?: string) { constructor(scope: DispatcherScope, browser: Browser, guid?: string) {
super(scope, browser, 'Browser', { version: browser.version() }, true, guid); super(scope, browser, 'Browser', { version: browser.version() }, true, guid);
browser.on(Events.Browser.Disconnected, () => this._didClose()); browser.on(Browser.Events.Disconnected, () => this._didClose());
} }
_didClose() { _didClose() {

View file

@ -15,7 +15,7 @@
*/ */
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { Electron, ElectronApplication, ElectronEvents, ElectronPage } from '../../server/electron'; import { Electron, ElectronApplication, ElectronPage } from '../../server/electron';
import * as channels from '../channels'; import * as channels from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher'; import { BrowserContextDispatcher } from './browserContextDispatcher';
import { PageDispatcher } from './pageDispatcher'; import { PageDispatcher } from './pageDispatcher';
@ -37,11 +37,11 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
constructor(scope: DispatcherScope, electronApplication: ElectronApplication) { constructor(scope: DispatcherScope, electronApplication: ElectronApplication) {
super(scope, electronApplication, 'ElectronApplication', {}, true); super(scope, electronApplication, 'ElectronApplication', {}, true);
this._dispatchEvent('context', { context: new BrowserContextDispatcher(this._scope, electronApplication.context()) }); this._dispatchEvent('context', { context: new BrowserContextDispatcher(this._scope, electronApplication.context()) });
electronApplication.on(ElectronEvents.ElectronApplication.Close, () => { electronApplication.on(ElectronApplication.Events.Close, () => {
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });
electronApplication.on(ElectronEvents.ElectronApplication.Window, (page: ElectronPage) => { electronApplication.on(ElectronApplication.Events.Window, (page: ElectronPage) => {
this._dispatchEvent('window', { this._dispatchEvent('window', {
page: lookupDispatcher<PageDispatcher>(page), page: lookupDispatcher<PageDispatcher>(page),
browserWindow: createHandle(this._scope, page.browserWindow), browserWindow: createHandle(this._scope, page.browserWindow),

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Frame, kAddLifecycleEvent, kRemoveLifecycleEvent, kNavigationEvent, NavigationEvent } from '../../frames'; import { Frame, NavigationEvent } from '../../frames';
import * as types from '../../types'; import * as types from '../../types';
import * as channels from '../channels'; import * as channels from '../channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
@ -38,13 +38,13 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
loadStates: Array.from(frame._subtreeLifecycleEvents), loadStates: Array.from(frame._subtreeLifecycleEvents),
}); });
this._frame = frame; this._frame = frame;
frame._eventEmitter.on(kAddLifecycleEvent, (event: types.LifecycleEvent) => { frame.on(Frame.Events.AddLifecycle, (event: types.LifecycleEvent) => {
this._dispatchEvent('loadstate', { add: event }); this._dispatchEvent('loadstate', { add: event });
}); });
frame._eventEmitter.on(kRemoveLifecycleEvent, (event: types.LifecycleEvent) => { frame.on(Frame.Events.RemoveLifecycle, (event: types.LifecycleEvent) => {
this._dispatchEvent('loadstate', { remove: event }); this._dispatchEvent('loadstate', { remove: event });
}); });
frame._eventEmitter.on(kNavigationEvent, (event: NavigationEvent) => { frame.on(Frame.Events.Navigation, (event: NavigationEvent) => {
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined }; const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };
if (event.newDocument) if (event.newDocument)
(params as any).newDocument = { request: RequestDispatcher.fromNullable(this._scope, event.newDocument.request || null) }; (params as any).newDocument = { request: RequestDispatcher.fromNullable(this._scope, event.newDocument.request || null) };

View file

@ -15,7 +15,6 @@
*/ */
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import { Events } from '../../events';
import { Frame } from '../../frames'; import { Frame } from '../../frames';
import { Request } from '../../network'; import { Request } from '../../network';
import { Page, Worker } from '../../page'; import { Page, Worker } from '../../page';
@ -44,29 +43,29 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
isClosed: page.isClosed() isClosed: page.isClosed()
}); });
this._page = page; this._page = page;
page.on(Events.Page.Close, () => this._dispatchEvent('close')); page.on(Page.Events.Close, () => this._dispatchEvent('close'));
page.on(Events.Page.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this._scope, message) })); page.on(Page.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this._scope, message) }));
page.on(Events.Page.Crash, () => this._dispatchEvent('crash')); page.on(Page.Events.Crash, () => this._dispatchEvent('crash'));
page.on(Events.Page.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded')); page.on(Page.Events.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded'));
page.on(Events.Page.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) })); page.on(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) }));
page.on(Events.Page.Download, dialog => this._dispatchEvent('download', { download: new DownloadDispatcher(this._scope, dialog) })); page.on(Page.Events.Download, dialog => this._dispatchEvent('download', { download: new DownloadDispatcher(this._scope, dialog) }));
this._page.on(Events.Page.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { this._page.on(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
element: new ElementHandleDispatcher(this._scope, fileChooser.element()), element: new ElementHandleDispatcher(this._scope, fileChooser.element()),
isMultiple: fileChooser.isMultiple() isMultiple: fileChooser.isMultiple()
})); }));
page.on(Events.Page.FrameAttached, frame => this._onFrameAttached(frame)); page.on(Page.Events.FrameAttached, frame => this._onFrameAttached(frame));
page.on(Events.Page.FrameDetached, frame => this._onFrameDetached(frame)); page.on(Page.Events.FrameDetached, frame => this._onFrameDetached(frame));
page.on(Events.Page.Load, () => this._dispatchEvent('load')); page.on(Page.Events.Load, () => this._dispatchEvent('load'));
page.on(Events.Page.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) })); page.on(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) }));
page.on(Events.Page.Popup, page => this._dispatchEvent('popup', { page: lookupDispatcher<PageDispatcher>(page) })); page.on(Page.Events.Popup, page => this._dispatchEvent('popup', { page: lookupDispatcher<PageDispatcher>(page) }));
page.on(Events.Page.Request, request => this._dispatchEvent('request', { request: RequestDispatcher.from(this._scope, request) })); page.on(Page.Events.Request, request => this._dispatchEvent('request', { request: RequestDispatcher.from(this._scope, request) }));
page.on(Events.Page.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', { page.on(Page.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', {
request: RequestDispatcher.from(this._scope, request), request: RequestDispatcher.from(this._scope, request),
failureText: request._failureText failureText: request._failureText
})); }));
page.on(Events.Page.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) })); page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) }));
page.on(Events.Page.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) })); page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) }));
page.on(Events.Page.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) })); page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
} }
async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams): Promise<void> { async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams): Promise<void> {
@ -232,7 +231,7 @@ export class WorkerDispatcher extends Dispatcher<Worker, channels.WorkerInitiali
super(scope, worker, 'Worker', { super(scope, worker, 'Worker', {
url: worker.url() url: worker.url()
}); });
worker.on(Events.Worker.Close, () => this._dispatchEvent('close')); worker.on(Worker.Events.Close, () => this._dispatchEvent('close'));
} }
async evaluateExpression(params: channels.WorkerEvaluateExpressionParams): Promise<channels.WorkerEvaluateExpressionResult> { async evaluateExpression(params: channels.WorkerEvaluateExpressionParams): Promise<channels.WorkerEvaluateExpressionResult> {

View file

@ -18,7 +18,6 @@ import * as path from 'path';
import { CRBrowser, CRBrowserContext } from '../chromium/crBrowser'; import { CRBrowser, CRBrowserContext } from '../chromium/crBrowser';
import { CRConnection, CRSession } from '../chromium/crConnection'; import { CRConnection, CRSession } from '../chromium/crConnection';
import { CRExecutionContext } from '../chromium/crExecutionContext'; import { CRExecutionContext } from '../chromium/crExecutionContext';
import { Events } from '../events';
import * as js from '../javascript'; import * as js from '../javascript';
import { Page } from '../page'; import { Page } from '../page';
import { TimeoutSettings } from '../timeoutSettings'; import { TimeoutSettings } from '../timeoutSettings';
@ -42,19 +41,17 @@ export type ElectronLaunchOptionsBase = {
timeout?: number, timeout?: number,
}; };
export const ElectronEvents = {
ElectronApplication: {
Close: 'close',
Window: 'window',
}
};
export interface ElectronPage extends Page { export interface ElectronPage extends Page {
browserWindow: js.JSHandle<BrowserWindow>; browserWindow: js.JSHandle<BrowserWindow>;
_browserWindowId: number; _browserWindowId: number;
} }
export class ElectronApplication extends EventEmitter { export class ElectronApplication extends EventEmitter {
static Events = {
Close: 'close',
Window: 'window',
};
private _browserContext: CRBrowserContext; private _browserContext: CRBrowserContext;
private _nodeConnection: CRConnection; private _nodeConnection: CRConnection;
private _nodeSession: CRSession; private _nodeSession: CRSession;
@ -67,11 +64,11 @@ export class ElectronApplication extends EventEmitter {
constructor(browser: CRBrowser, nodeConnection: CRConnection) { constructor(browser: CRBrowser, nodeConnection: CRConnection) {
super(); super();
this._browserContext = browser._defaultContext as CRBrowserContext; this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(Events.BrowserContext.Close, () => { this._browserContext.on(BrowserContext.Events.Close, () => {
// Emit application closed after context closed. // Emit application closed after context closed.
Promise.resolve().then(() => this.emit(ElectronEvents.ElectronApplication.Close)); Promise.resolve().then(() => this.emit(ElectronApplication.Events.Close));
}); });
this._browserContext.on(Events.BrowserContext.Page, event => this._onPage(event)); this._browserContext.on(BrowserContext.Events.Page, event => this._onPage(event));
this._nodeConnection = nodeConnection; this._nodeConnection = nodeConnection;
this._nodeSession = nodeConnection.rootSession; this._nodeSession = nodeConnection.rootSession;
} }
@ -85,13 +82,13 @@ export class ElectronApplication extends EventEmitter {
return; return;
page.browserWindow = handle; page.browserWindow = handle;
page._browserWindowId = windowId; page._browserWindowId = windowId;
page.on(Events.Page.Close, () => { page.on(Page.Events.Close, () => {
page.browserWindow.dispose(); page.browserWindow.dispose();
this._windows.delete(page); this._windows.delete(page);
}); });
this._windows.add(page); this._windows.add(page);
await page.mainFrame().waitForLoadState('domcontentloaded').catch(e => {}); // can happen after detach await page.mainFrame().waitForLoadState('domcontentloaded').catch(e => {}); // can happen after detach
this.emit(ElectronEvents.ElectronApplication.Window, page); this.emit(ElectronApplication.Events.Window, page);
} }
async newBrowserWindow(options: any): Promise<Page> { async newBrowserWindow(options: any): Promise<Page> {
@ -106,7 +103,7 @@ export class ElectronApplication extends EventEmitter {
return page; return page;
} }
return await this._waitForEvent(ElectronEvents.ElectronApplication.Window, (page: ElectronPage) => page._browserWindowId === windowId); return await this._waitForEvent(ElectronApplication.Events.Window, (page: ElectronPage) => page._browserWindowId === windowId);
} }
context(): BrowserContext { context(): BrowserContext {
@ -114,7 +111,7 @@ export class ElectronApplication extends EventEmitter {
} }
async close() { async close() {
const closed = this._waitForEvent(ElectronEvents.ElectronApplication.Close); const closed = this._waitForEvent(ElectronApplication.Events.Close);
await this._nodeElectronHandle!.evaluate(({ app }) => app.quit()); await this._nodeElectronHandle!.evaluate(({ app }) => app.quit());
this._nodeConnection.close(); this._nodeConnection.close();
await closed; await closed;
@ -122,7 +119,7 @@ export class ElectronApplication extends EventEmitter {
private async _waitForEvent(event: string, predicate?: Function): Promise<any> { private async _waitForEvent(event: string, predicate?: Function): Promise<any> {
const progressController = new ProgressController(this._timeoutSettings.timeout({})); const progressController = new ProgressController(this._timeoutSettings.timeout({}));
if (event !== ElectronEvents.ElectronApplication.Close) if (event !== ElectronApplication.Events.Close)
this._browserContext._closePromise.then(error => progressController.abort(error)); this._browserContext._closePromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise); return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise);
} }

View file

@ -17,7 +17,6 @@
import { Browser, BrowserOptions } from '../browser'; import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events } from '../events';
import { helper, RegisteredListener, assert } from '../helper'; import { helper, RegisteredListener, assert } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding } from '../page'; import { Page, PageBinding } from '../page';
@ -150,13 +149,13 @@ export class WKBrowser extends Browser {
const page = wkPage._page; const page = wkPage._page;
if (pageOrError instanceof Error) if (pageOrError instanceof Error)
page._setIsError(); page._setIsError();
context!.emit(Events.BrowserContext.Page, page); context!.emit(BrowserContext.Events.Page, page);
if (!opener) if (!opener)
return; return;
await opener.pageOrError(); await opener.pageOrError();
const openerPage = opener._page; const openerPage = opener._page;
if (!openerPage.isClosed()) if (!openerPage.isClosed())
openerPage.emit(Events.Page.Popup, page); openerPage.emit(Page.Events.Popup, page);
}); });
} }

View file

@ -15,13 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { Screencast } from '../browserContext'; import { Screencast, BrowserContext } from '../browserContext';
import * as frames from '../frames'; import * as frames from '../frames';
import { helper, RegisteredListener, assert, debugAssert } from '../helper'; import { helper, RegisteredListener, assert, debugAssert } from '../helper';
import * as dom from '../dom'; import * as dom from '../dom';
import * as network from '../network'; import * as network from '../network';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
import { Events } from '../events';
import { WKExecutionContext } from './wkExecutionContext'; import { WKExecutionContext } from './wkExecutionContext';
import { WKInterceptableRequest } from './wkInterceptableRequest'; import { WKInterceptableRequest } from './wkInterceptableRequest';
import { WKWorkers } from './wkWorkers'; import { WKWorkers } from './wkWorkers';
@ -81,7 +80,7 @@ export class WKPage implements PageDelegate {
this._workers = new WKWorkers(this._page); this._workers = new WKWorkers(this._page);
this._session = undefined as any as WKSession; this._session = undefined as any as WKSession;
this._browserContext = browserContext; this._browserContext = browserContext;
this._page.on(Events.Page.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false)); this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)), helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)),
helper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), helper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
@ -474,7 +473,7 @@ export class WKPage implements PageDelegate {
} else { } else {
error.stack = ''; error.stack = '';
} }
this._page.emit(Events.Page.PageError, error); this._page.emit(Page.Events.PageError, error);
return; return;
} }
@ -524,7 +523,7 @@ export class WKPage implements PageDelegate {
} }
_onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) { _onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog( this._page.emit(Page.Events.Dialog, new dialog.Dialog(
event.type as dialog.DialogType, event.type as dialog.DialogType,
event.message, event.message,
async (accept: boolean, promptText?: string) => { async (accept: boolean, promptText?: string) => {
@ -718,7 +717,7 @@ export class WKPage implements PageDelegate {
height: options.height, height: options.height,
scale: options.scale, scale: options.scale,
}); });
this._browserContext.emit(Events.BrowserContext.ScreencastStarted, new Screencast(options.outputFile, this._initializedPage!)); this._browserContext.emit(BrowserContext.Events.ScreencastStarted, new Screencast(options.outputFile, this._initializedPage!));
} catch (e) { } catch (e) {
this._recordingVideoFile = null; this._recordingVideoFile = null;
throw e; throw e;
@ -731,7 +730,7 @@ export class WKPage implements PageDelegate {
const fileName = this._recordingVideoFile; const fileName = this._recordingVideoFile;
this._recordingVideoFile = null; this._recordingVideoFile = null;
await this._pageProxySession.send('Screencast.stopVideoRecording'); await this._pageProxySession.send('Screencast.stopVideoRecording');
this._browserContext.emit(Events.BrowserContext.ScreencastStopped, new Screencast(fileName, this._initializedPage!)); this._browserContext.emit(BrowserContext.Events.ScreencastStopped, new Screencast(fileName, this._initializedPage!));
} }
async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> { async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {

View file

@ -61,32 +61,11 @@ function traceAPICoverage(apiCoverage, api, events) {
* @param {string} browserName * @param {string} browserName
*/ */
function apiForBrowser(browserName) { function apiForBrowser(browserName) {
const BROWSER_CONFIGS = [ const events = require('../lib/rpc/client/events').Events;
{ const api = require('../lib/rpc/client/api');
name: 'Firefox', const otherBrowsers = ['chromium', 'webkit', 'firefox'].filter(name => name.toLowerCase() !== browserName.toLowerCase());
events: require('../lib/events').Events,
},
{
name: 'WebKit',
events: require('../lib/events').Events,
},
{
name: 'Chromium',
events: {
...require('../lib/events').Events,
...require('../lib/chromium/events').Events,
}
},
];
const browserConfig = BROWSER_CONFIGS.find(config => config.name.toLowerCase() === browserName);
const events = browserConfig.events;
// TODO: we should rethink our api.ts approach to ensure coverage and async stacks.
const api = {
...require('../lib/rpc/client/api'),
};
const filteredKeys = Object.keys(api).filter(apiName => { const filteredKeys = Object.keys(api).filter(apiName => {
return !BROWSER_CONFIGS.some(config => apiName.startsWith(config.name)) || apiName.startsWith(browserConfig.name); return !otherBrowsers.some(otherName => apiName.toLowerCase().startsWith(otherName));
}); });
const filteredAPI = {}; const filteredAPI = {};
for (const key of filteredKeys) for (const key of filteredKeys)