browser(firefox): follow-up with assorted simplifications (#4066)

This patch:
- moves `SimpleChannel` to synchronously dispatch buffered commands
  instead of a `await Promise.resolve()` hack
- moves dialog & screencast handling from `PageHandler` to
  `TargetManager`. This leaves `PageHandler` to be concerned solely about
  protocol.
- removes `attach` and `detach` methods for worker channels: since
  channels are buffering messages until the namespace registers, there's
  no chance to loose any events.
- slightly simplifies `PageNetwork` class: it's lifetime is now
  identical to the lifetime of the associated `PageTarget`, so a lot can
  be simplified later on.

References #3995
This commit is contained in:
Andrey Lushnikov 2020-10-06 01:53:25 -07:00 committed by GitHub
parent c8a64b88e1
commit 4ab66a4fe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 240 additions and 273 deletions

View File

@ -1,2 +1,2 @@
1182
Changed: lushnikov@chromium.org Mon Oct 5 23:55:54 PDT 2020
1183
Changed: lushnikov@chromium.org Tue Oct 6 01:20:41 PDT 2020

View File

@ -50,36 +50,14 @@ class PageNetwork {
constructor(target) {
EventEmitter.decorate(this);
this._target = target;
this._sessionCount = 0;
this._extraHTTPHeaders = null;
this._responseStorage = null;
this._responseStorage = new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10);
this._requestInterceptionEnabled = false;
// This is requestId => NetworkRequest map, only contains requests that are
// awaiting interception action (abort, resume, fulfill) over the protocol.
this._interceptedRequests = new Map();
}
addSession() {
if (this._sessionCount === 0)
this._responseStorage = new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10);
++this._sessionCount;
return () => this._stopTracking();
}
_stopTracking() {
--this._sessionCount;
if (this._sessionCount === 0) {
this._extraHTTPHeaders = null;
this._responseStorage = null;
this._requestInterceptionEnabled = false;
this._interceptedRequests.clear();
}
}
_isActive() {
return this._sessionCount > 0;
}
setExtraHTTPHeaders(headers) {
this._extraHTTPHeaders = headers;
}
@ -479,7 +457,7 @@ class NetworkRequest {
}
_activePageNetwork() {
if (!this._maybeInactivePageNetwork || !this._maybeInactivePageNetwork._isActive())
if (!this._maybeInactivePageNetwork)
return undefined;
return this._maybeInactivePageNetwork;
}

View File

@ -72,13 +72,11 @@ class SimpleChannel {
throw new Error('ERROR: double-register for namespace ' + namespace);
this._handlers.set(namespace, handler);
// Try to re-deliver all pending messages.
Promise.resolve().then(() => {
const bufferedRequests = this._bufferedRequests;
this._bufferedRequests = [];
for (const data of bufferedRequests) {
this._onMessage(data);
}
});
const bufferedRequests = this._bufferedRequests;
this._bufferedRequests = [];
for (const data of bufferedRequests) {
this._onMessage(data);
}
return () => this.unregister(namespace);
}

View File

@ -117,7 +117,7 @@ class TargetRegistry {
const target = this._browserToTarget.get(browser);
if (!target)
return;
target.emit('crashed');
target.emit(PageTarget.Events.Crashed);
target.dispose();
}
}, 'oop-frameloader-crashed');
@ -157,6 +157,8 @@ class TargetRegistry {
target.updateUserAgent();
if (!hasExplicitSize)
target.updateViewportSize();
if (browserContext.screencastOptions)
target._startVideoRecording(browserContext.screencastOptions);
};
const onTabCloseListener = event => {
@ -329,6 +331,7 @@ class PageTarget {
this._openerId = opener ? opener.id() : undefined;
this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager);
this._screencastInfo = undefined;
this._dialogs = new Map();
const navigationListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
@ -336,6 +339,12 @@ class PageTarget {
};
this._eventListeners = [
helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
helper.addEventListener(this._linkedBrowser, 'DOMWillOpenModalDialog', async (event) => {
// wait for the dialog to be actually added to DOM.
await Promise.resolve();
this._updateModalDialogs();
}),
helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
];
this._disposed = false;
@ -346,6 +355,14 @@ class PageTarget {
this._registry.emit(TargetRegistry.Events.TargetCreated, this);
}
dialog(dialogId) {
return this._dialogs.get(dialogId);
}
dialogs() {
return [...this._dialogs.values()];
}
async windowReady() {
await waitForWindowReady(this._window);
}
@ -362,6 +379,25 @@ class PageTarget {
this._linkedBrowser.browsingContext.customUserAgent = this._browserContext.defaultUserAgent;
}
_updateModalDialogs() {
const prompts = new Set(this._linkedBrowser.tabModalPromptBox ? this._linkedBrowser.tabModalPromptBox.listPrompts() : []);
for (const dialog of this._dialogs.values()) {
if (!prompts.has(dialog.prompt())) {
this._dialogs.delete(dialog.id());
this.emit(PageTarget.Events.DialogClosed, dialog);
} else {
prompts.delete(dialog.prompt());
}
}
for (const prompt of prompts) {
const dialog = Dialog.createIfSupported(prompt);
if (!dialog)
continue;
this._dialogs.set(dialog.id(), dialog);
this.emit(PageTarget.Events.DialogOpened, dialog);
}
}
async updateViewportSize() {
// Viewport size is defined by three arguments:
// 1. default size. Could be explicit if set as part of `window.open` call, e.g.
@ -433,7 +469,7 @@ class PageTarget {
return await this._channel.connect('').send('hasFailedToOverrideTimezone').catch(e => true);
}
async startVideoRecording({width, height, scale, dir}) {
async _startVideoRecording({width, height, scale, dir}) {
// On Mac the window may not yet be visible when TargetCreated and its
// NSWindow.windowNumber may be -1, so we wait until the window is known
// to be initialized and visible.
@ -451,10 +487,10 @@ class PageTarget {
const devicePixelRatio = this._window.devicePixelRatio;
const videoSessionId = screencast.startVideoRecording(docShell, file, width, height, scale || 0, devicePixelRatio * rect.top);
this._screencastInfo = { videoSessionId, file };
this.emit('screencastStarted');
this.emit(PageTarget.Events.ScreencastStarted);
}
async stopVideoRecording() {
async _stopVideoRecording() {
if (!this._screencastInfo)
throw new Error('No video recording in progress');
const screencastInfo = this._screencastInfo;
@ -479,6 +515,8 @@ class PageTarget {
dispose() {
this._disposed = true;
if (this._screencastInfo)
this._stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`));
this._browserContext.pages.delete(this);
this._registry._browserToTarget.delete(this._linkedBrowser);
this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext);
@ -487,6 +525,13 @@ class PageTarget {
}
}
PageTarget.Events = {
ScreencastStarted: Symbol('PageTarget.ScreencastStarted'),
Crashed: Symbol('PageTarget.Crashed'),
DialogOpened: Symbol('PageTarget.DialogOpened'),
DialogClosed: Symbol('PageTarget.DialogClosed'),
};
class BrowserContext {
constructor(registry, browserContextId, removeOnDetach) {
this._registry = registry;
@ -702,11 +747,67 @@ class BrowserContext {
return;
const promises = [];
for (const page of this.pages)
promises.push(page.startVideoRecording(options));
promises.push(page._startVideoRecording(options));
await Promise.all(promises);
}
}
class Dialog {
static createIfSupported(prompt) {
const type = prompt.args.promptType;
switch (type) {
case 'alert':
case 'prompt':
case 'confirm':
return new Dialog(prompt, type);
case 'confirmEx':
return new Dialog(prompt, 'beforeunload');
default:
return null;
};
}
constructor(prompt, type) {
this._id = helper.generateId();
this._type = type;
this._prompt = prompt;
}
id() {
return this._id;
}
message() {
return this._prompt.ui.infoBody.textContent;
}
type() {
return this._type;
}
prompt() {
return this._prompt;
}
dismiss() {
if (this._prompt.ui.button1)
this._prompt.ui.button1.click();
else
this._prompt.ui.button0.click();
}
defaultValue() {
return this._prompt.ui.loginTextbox.value;
}
accept(promptValue) {
if (typeof promptValue === 'string' && this._type === 'prompt')
this._prompt.ui.loginTextbox.value = promptValue;
this._prompt.ui.button0.click();
}
}
function dirPath(path) {
return path.substring(0, path.lastIndexOf('/') + 1);
}
@ -755,5 +856,6 @@ TargetRegistry.Events = {
DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
};
var EXPORTED_SYMBOLS = ['TargetRegistry'];
var EXPORTED_SYMBOLS = ['TargetRegistry', 'PageTarget'];
this.TargetRegistry = TargetRegistry;
this.PageTarget = PageTarget;

View File

@ -40,11 +40,9 @@ class WorkerData {
disposeObject: (options) =>this._workerRuntime.send('disposeObject', options),
}),
];
worker.channel().connect('').emit('attach');
}
dispose() {
this._worker.channel().connect('').emit('detach');
this._workerRuntime.dispose();
this._browserWorker.dispose();
helper.removeListeners(this._eventListeners);
@ -115,7 +113,6 @@ class PageAgent {
this._messageManager = messageManager;
this._browserChannel = browserChannel;
this._browserPage = browserChannel.connect('page');
this._browserRuntime = browserChannel.connect('runtime');
this._frameTree = frameTree;
this._runtime = frameTree.runtime();
@ -124,7 +121,76 @@ class PageAgent {
this._scriptsToEvaluateOnNewDocument = new Map();
this._isolatedWorlds = new Map();
const docShell = frameTree.mainFrame().docShell();
this._docShell = docShell;
this._initialDPPX = docShell.contentViewer.overrideDPPX;
this._customScrollbars = null;
this._dataTransfer = null;
// Dispatch frameAttached events for all initial frames
for (const frame of this._frameTree.frames()) {
this._onFrameAttached(frame);
if (frame.url())
this._onNavigationCommitted(frame);
if (frame.pendingNavigationId())
this._onNavigationStarted(frame);
}
// Report created workers.
for (const worker of this._frameTree.workers())
this._onWorkerCreated(worker);
// Report execution contexts.
for (const context of this._runtime.executionContexts())
this._onExecutionContextCreated(context);
if (this._frameTree.isPageReady()) {
this._browserPage.emit('pageReady', {});
const mainFrame = this._frameTree.mainFrame();
const domWindow = mainFrame.domWindow();
const document = domWindow ? domWindow.document : null;
const readyState = document ? document.readyState : null;
// Sometimes we initialize later than the first about:blank page is opened.
// In this case, the page might've been loaded already, and we need to issue
// the `DOMContentLoaded` and `load` events.
if (mainFrame.url() === 'about:blank' && readyState === 'complete')
this._emitAllEvents(this._frameTree.mainFrame());
}
this._eventListeners = [
helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'),
helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'),
helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'),
helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
helper.addEventListener(this._messageManager, 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
helper.addEventListener(this._messageManager, 'pageshow', this._onLoad.bind(this)),
helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
helper.addEventListener(this._messageManager, 'error', this._onError.bind(this)),
helper.on(this._frameTree, 'bindingcalled', this._onBindingCalled.bind(this)),
helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
helper.on(this._frameTree, 'globalobjectcreated', this._onGlobalObjectCreated.bind(this)),
helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})),
helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)),
helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)),
helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
const frame = this._frameTree.frameForDocShell(domWindow.docShell);
if (!frame)
return;
this._browserPage.emit('pageUncaughtError', {
frameId: frame.id(),
message,
stack,
});
}),
this._runtime.events.onConsoleMessage(msg => this._browserPage.emit('runtimeConsole', msg)),
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
browserChannel.register('page', {
addBinding: ({ name, script }) => this._frameTree.addBinding(name, script),
addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this),
@ -149,21 +215,12 @@ class PageAgent {
setEmulatedMedia: this._setEmulatedMedia.bind(this),
setFileInputFiles: this._setFileInputFiles.bind(this),
setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this),
}),
browserChannel.register('runtime', {
evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime),
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
disposeObject: this._runtime.disposeObject.bind(this._runtime),
}),
];
this._enabled = false;
const docShell = frameTree.mainFrame().docShell();
this._docShell = docShell;
this._initialDPPX = docShell.contentViewer.overrideDPPX;
this._customScrollbars = null;
this._dataTransfer = null;
}
async _setEmulatedMedia({type, colorScheme}) {
@ -206,75 +263,6 @@ class PageAgent {
docShell.defaultLoadFlags = cacheDisabled ? disable : enable;
}
enable() {
if (this._enabled)
return;
this._enabled = true;
// Dispatch frameAttached events for all initial frames
for (const frame of this._frameTree.frames()) {
this._onFrameAttached(frame);
if (frame.url())
this._onNavigationCommitted(frame);
if (frame.pendingNavigationId())
this._onNavigationStarted(frame);
}
for (const worker of this._frameTree.workers())
this._onWorkerCreated(worker);
this._eventListeners.push(...[
helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'),
helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'),
helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'),
helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
helper.addEventListener(this._messageManager, 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
helper.addEventListener(this._messageManager, 'pageshow', this._onLoad.bind(this)),
helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
helper.addEventListener(this._messageManager, 'error', this._onError.bind(this)),
helper.on(this._frameTree, 'bindingcalled', this._onBindingCalled.bind(this)),
helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
helper.on(this._frameTree, 'globalobjectcreated', this._onGlobalObjectCreated.bind(this)),
helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})),
helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)),
helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)),
helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
const frame = this._frameTree.frameForDocShell(domWindow.docShell);
if (!frame)
return;
this._browserPage.emit('pageUncaughtError', {
frameId: frame.id(),
message,
stack,
});
}),
this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
]);
for (const context of this._runtime.executionContexts())
this._onExecutionContextCreated(context);
if (this._frameTree.isPageReady()) {
this._browserPage.emit('pageReady', {});
const mainFrame = this._frameTree.mainFrame();
const domWindow = mainFrame.domWindow();
const document = domWindow ? domWindow.document : null;
const readyState = document ? document.readyState : null;
// Sometimes we initialize later than the first about:blank page is opened.
// In this case, the page might've been loaded already, and we need to issue
// the `DOMContentLoaded` and `load` events.
if (mainFrame.url() === 'about:blank' && readyState === 'complete')
this._emitAllEvents(this._frameTree.mainFrame());
}
}
_emitAllEvents(frame) {
this._browserPage.emit('pageEventFired', {
frameId: frame.id(),
@ -287,14 +275,14 @@ class PageAgent {
}
_onExecutionContextCreated(executionContext) {
this._browserRuntime.emit('runtimeExecutionContextCreated', {
this._browserPage.emit('runtimeExecutionContextCreated', {
executionContextId: executionContext.id(),
auxData: executionContext.auxData(),
});
}
_onExecutionContextDestroyed(executionContext) {
this._browserRuntime.emit('runtimeExecutionContextDestroyed', {
this._browserPage.emit('runtimeExecutionContextDestroyed', {
executionContextId: executionContext.id(),
});
}

View File

@ -35,19 +35,21 @@ class RuntimeAgent {
constructor(runtime, channel) {
this._runtime = runtime;
this._browserRuntime = channel.connect('runtime');
for (const context of this._runtime.executionContexts())
this._onExecutionContextCreated(context);
this._eventListeners = [
this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
channel.register('runtime', {
evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime),
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
disposeObject: this._runtime.disposeObject.bind(this._runtime),
}),
this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
];
for (const context of this._runtime.executionContexts())
this._onExecutionContextCreated(context);
}
_onExecutionContextCreated(executionContext) {
@ -70,15 +72,5 @@ class RuntimeAgent {
}
}
let runtimeAgent;
channel.register('', {
attach: () => {
runtimeAgent = new RuntimeAgent(runtime, channel);
},
detach: () => {
runtimeAgent.dispose();
},
});
new RuntimeAgent(runtime, channel);

View File

@ -103,7 +103,6 @@ function initialize() {
frameTree.addBinding(name, script);
pageAgent = new PageAgent(messageManager, channel, frameTree);
pageAgent.enable();
channel.register('', {
addScriptToEvaluateOnNewDocument(script) {

View File

@ -7,6 +7,7 @@
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
const Cc = Components.classes;
const Ci = Components.interfaces;
@ -60,12 +61,9 @@ class PageHandler {
this._session = session;
this._contentChannel = contentChannel;
this._contentPage = contentChannel.connect('page');
this._contentRuntime = contentChannel.connect('runtime');
this._workers = new Map();
this._pageTarget = target;
this._browser = target.linkedBrowser();
this._dialogs = new Map();
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
const emitProtocolEvent = eventName => {
@ -75,7 +73,23 @@ class PageHandler {
this._reportedFrameIds = new Set();
this._networkEventsForUnreportedFrameIds = new Map();
for (const dialog of this._pageTarget.dialogs())
this._onDialogOpened(dialog);
if (this._pageTarget.screencastInfo())
this._onScreencastStarted();
this._eventListeners = [
helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)),
helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)),
helper.on(this._pageTarget, PageTarget.Events.Crashed, () => {
this._session.emitEvent('Page.crashed', {});
}),
helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onScreencastStarted.bind(this)),
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')),
contentChannel.register('page', {
pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
@ -93,36 +107,34 @@ class PageHandler {
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
pageWorkerCreated: this._onWorkerCreated.bind(this),
pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
}),
contentChannel.register('runtime', {
runtimeConsole: emitProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
}),
helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => {
// wait for the dialog to be actually added to DOM.
await Promise.resolve();
this._updateModalDialogs();
}),
helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
helper.on(this._pageTarget, 'crashed', () => {
this._session.emitEvent('Page.crashed', {});
}),
helper.on(this._pageTarget, 'screencastStarted', () => {
const info = this._pageTarget.screencastInfo();
this._session.emitEvent('Page.screencastStarted', { screencastId: '' + info.videoSessionId, file: info.file });
}),
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')),
this._pageNetwork.addSession(),
];
}
this._updateModalDialogs();
const options = this._pageTarget.browserContext().screencastOptions;
if (options)
this._pageTarget.startVideoRecording(options);
async dispose() {
this._contentPage.dispose();
helper.removeListeners(this._eventListeners);
}
_onScreencastStarted() {
const info = this._pageTarget.screencastInfo();
this._session.emitEvent('Page.screencastStarted', { screencastId: '' + info.videoSessionId, file: info.file });
}
_onDialogOpened(dialog) {
this._session.emitEvent('Page.dialogOpened', {
dialogId: dialog.id(),
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
_onDialogClosed(dialog) {
this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), });
}
_onWorkerCreated({workerId, frameId, url}) {
@ -169,59 +181,24 @@ class PageHandler {
});
}
async dispose() {
this._contentPage.dispose();
this._contentRuntime.dispose();
helper.removeListeners(this._eventListeners);
if (this._pageTarget.screencastInfo())
await this._pageTarget.stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`));
}
async ['Page.setViewportSize']({viewportSize}) {
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
}
_updateModalDialogs() {
const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []);
for (const dialog of this._dialogs.values()) {
if (!prompts.has(dialog.prompt())) {
this._dialogs.delete(dialog.id());
this._session.emitEvent('Page.dialogClosed', {
dialogId: dialog.id(),
});
} else {
prompts.delete(dialog.prompt());
}
}
for (const prompt of prompts) {
const dialog = Dialog.createIfSupported(prompt);
if (!dialog)
continue;
this._dialogs.set(dialog.id(), dialog);
this._session.emitEvent('Page.dialogOpened', {
dialogId: dialog.id(),
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
}
async ['Runtime.evaluate'](options) {
return await this._contentRuntime.send('evaluate', options);
return await this._contentPage.send('evaluate', options);
}
async ['Runtime.callFunction'](options) {
return await this._contentRuntime.send('callFunction', options);
return await this._contentPage.send('callFunction', options);
}
async ['Runtime.getObjectProperties'](options) {
return await this._contentRuntime.send('getObjectProperties', options);
return await this._contentPage.send('getObjectProperties', options);
}
async ['Runtime.disposeObject'](options) {
return await this._contentRuntime.send('disposeObject', options);
return await this._contentPage.send('disposeObject', options);
}
async ['Network.getResponseBody']({requestId}) {
@ -291,30 +268,18 @@ class PageHandler {
return await this._contentPage.send('getContentQuads', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.navigate'](options) {
return await this._contentPage.send('navigate', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.goBack'](options) {
return await this._contentPage.send('goBack', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.goForward'](options) {
return await this._contentPage.send('goForward', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.reload'](options) {
return await this._contentPage.send('reload', options);
}
@ -356,7 +321,7 @@ class PageHandler {
}
async ['Page.handleDialog']({dialogId, accept, promptText}) {
const dialog = this._dialogs.get(dialogId);
const dialog = this._pageTarget.dialog(dialogId);
if (!dialog)
throw new Error('Failed to find dialog with id = ' + dialogId);
if (accept)
@ -381,60 +346,5 @@ class PageHandler {
}
}
class Dialog {
static createIfSupported(prompt) {
const type = prompt.args.promptType;
switch (type) {
case 'alert':
case 'prompt':
case 'confirm':
return new Dialog(prompt, type);
case 'confirmEx':
return new Dialog(prompt, 'beforeunload');
default:
return null;
};
}
constructor(prompt, type) {
this._id = helper.generateId();
this._type = type;
this._prompt = prompt;
}
id() {
return this._id;
}
message() {
return this._prompt.ui.infoBody.textContent;
}
type() {
return this._type;
}
prompt() {
return this._prompt;
}
dismiss() {
if (this._prompt.ui.button1)
this._prompt.ui.button1.click();
else
this._prompt.ui.button0.click();
}
defaultValue() {
return this._prompt.ui.loginTextbox.value;
}
accept(promptValue) {
if (typeof promptValue === 'string' && this._type === 'prompt')
this._prompt.ui.loginTextbox.value = promptValue;
this._prompt.ui.button0.click();
}
}
var EXPORTED_SYMBOLS = ['PageHandler'];
this.PageHandler = PageHandler;