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 1183
Changed: lushnikov@chromium.org Mon Oct 5 23:55:54 PDT 2020 Changed: lushnikov@chromium.org Tue Oct 6 01:20:41 PDT 2020

View File

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

View File

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

View File

@ -117,7 +117,7 @@ class TargetRegistry {
const target = this._browserToTarget.get(browser); const target = this._browserToTarget.get(browser);
if (!target) if (!target)
return; return;
target.emit('crashed'); target.emit(PageTarget.Events.Crashed);
target.dispose(); target.dispose();
} }
}, 'oop-frameloader-crashed'); }, 'oop-frameloader-crashed');
@ -157,6 +157,8 @@ class TargetRegistry {
target.updateUserAgent(); target.updateUserAgent();
if (!hasExplicitSize) if (!hasExplicitSize)
target.updateViewportSize(); target.updateViewportSize();
if (browserContext.screencastOptions)
target._startVideoRecording(browserContext.screencastOptions);
}; };
const onTabCloseListener = event => { const onTabCloseListener = event => {
@ -329,6 +331,7 @@ class PageTarget {
this._openerId = opener ? opener.id() : undefined; this._openerId = opener ? opener.id() : undefined;
this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager); this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager);
this._screencastInfo = undefined; this._screencastInfo = undefined;
this._dialogs = new Map();
const navigationListener = { const navigationListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
@ -336,6 +339,12 @@ class PageTarget {
}; };
this._eventListeners = [ this._eventListeners = [
helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION), 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; this._disposed = false;
@ -346,6 +355,14 @@ class PageTarget {
this._registry.emit(TargetRegistry.Events.TargetCreated, this); this._registry.emit(TargetRegistry.Events.TargetCreated, this);
} }
dialog(dialogId) {
return this._dialogs.get(dialogId);
}
dialogs() {
return [...this._dialogs.values()];
}
async windowReady() { async windowReady() {
await waitForWindowReady(this._window); await waitForWindowReady(this._window);
} }
@ -362,6 +379,25 @@ class PageTarget {
this._linkedBrowser.browsingContext.customUserAgent = this._browserContext.defaultUserAgent; 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() { async updateViewportSize() {
// Viewport size is defined by three arguments: // Viewport size is defined by three arguments:
// 1. default size. Could be explicit if set as part of `window.open` call, e.g. // 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); 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 // 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 // NSWindow.windowNumber may be -1, so we wait until the window is known
// to be initialized and visible. // to be initialized and visible.
@ -451,10 +487,10 @@ class PageTarget {
const devicePixelRatio = this._window.devicePixelRatio; const devicePixelRatio = this._window.devicePixelRatio;
const videoSessionId = screencast.startVideoRecording(docShell, file, width, height, scale || 0, devicePixelRatio * rect.top); const videoSessionId = screencast.startVideoRecording(docShell, file, width, height, scale || 0, devicePixelRatio * rect.top);
this._screencastInfo = { videoSessionId, file }; this._screencastInfo = { videoSessionId, file };
this.emit('screencastStarted'); this.emit(PageTarget.Events.ScreencastStarted);
} }
async stopVideoRecording() { async _stopVideoRecording() {
if (!this._screencastInfo) if (!this._screencastInfo)
throw new Error('No video recording in progress'); throw new Error('No video recording in progress');
const screencastInfo = this._screencastInfo; const screencastInfo = this._screencastInfo;
@ -479,6 +515,8 @@ class PageTarget {
dispose() { dispose() {
this._disposed = true; this._disposed = true;
if (this._screencastInfo)
this._stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`));
this._browserContext.pages.delete(this); this._browserContext.pages.delete(this);
this._registry._browserToTarget.delete(this._linkedBrowser); this._registry._browserToTarget.delete(this._linkedBrowser);
this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext); 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 { class BrowserContext {
constructor(registry, browserContextId, removeOnDetach) { constructor(registry, browserContextId, removeOnDetach) {
this._registry = registry; this._registry = registry;
@ -702,11 +747,67 @@ class BrowserContext {
return; return;
const promises = []; const promises = [];
for (const page of this.pages) for (const page of this.pages)
promises.push(page.startVideoRecording(options)); promises.push(page._startVideoRecording(options));
await Promise.all(promises); 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) { function dirPath(path) {
return path.substring(0, path.lastIndexOf('/') + 1); return path.substring(0, path.lastIndexOf('/') + 1);
} }
@ -755,5 +856,6 @@ TargetRegistry.Events = {
DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'), DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
}; };
var EXPORTED_SYMBOLS = ['TargetRegistry']; var EXPORTED_SYMBOLS = ['TargetRegistry', 'PageTarget'];
this.TargetRegistry = TargetRegistry; this.TargetRegistry = TargetRegistry;
this.PageTarget = PageTarget;

View File

@ -40,11 +40,9 @@ class WorkerData {
disposeObject: (options) =>this._workerRuntime.send('disposeObject', options), disposeObject: (options) =>this._workerRuntime.send('disposeObject', options),
}), }),
]; ];
worker.channel().connect('').emit('attach');
} }
dispose() { dispose() {
this._worker.channel().connect('').emit('detach');
this._workerRuntime.dispose(); this._workerRuntime.dispose();
this._browserWorker.dispose(); this._browserWorker.dispose();
helper.removeListeners(this._eventListeners); helper.removeListeners(this._eventListeners);
@ -115,7 +113,6 @@ class PageAgent {
this._messageManager = messageManager; this._messageManager = messageManager;
this._browserChannel = browserChannel; this._browserChannel = browserChannel;
this._browserPage = browserChannel.connect('page'); this._browserPage = browserChannel.connect('page');
this._browserRuntime = browserChannel.connect('runtime');
this._frameTree = frameTree; this._frameTree = frameTree;
this._runtime = frameTree.runtime(); this._runtime = frameTree.runtime();
@ -124,7 +121,76 @@ class PageAgent {
this._scriptsToEvaluateOnNewDocument = new Map(); this._scriptsToEvaluateOnNewDocument = new Map();
this._isolatedWorlds = 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 = [ 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', { browserChannel.register('page', {
addBinding: ({ name, script }) => this._frameTree.addBinding(name, script), addBinding: ({ name, script }) => this._frameTree.addBinding(name, script),
addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this), addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this),
@ -149,21 +215,12 @@ class PageAgent {
setEmulatedMedia: this._setEmulatedMedia.bind(this), setEmulatedMedia: this._setEmulatedMedia.bind(this),
setFileInputFiles: this._setFileInputFiles.bind(this), setFileInputFiles: this._setFileInputFiles.bind(this),
setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this), setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this),
}),
browserChannel.register('runtime', {
evaluate: this._runtime.evaluate.bind(this._runtime), evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime), callFunction: this._runtime.callFunction.bind(this._runtime),
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
disposeObject: this._runtime.disposeObject.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}) { async _setEmulatedMedia({type, colorScheme}) {
@ -206,75 +263,6 @@ class PageAgent {
docShell.defaultLoadFlags = cacheDisabled ? disable : enable; 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) { _emitAllEvents(frame) {
this._browserPage.emit('pageEventFired', { this._browserPage.emit('pageEventFired', {
frameId: frame.id(), frameId: frame.id(),
@ -287,14 +275,14 @@ class PageAgent {
} }
_onExecutionContextCreated(executionContext) { _onExecutionContextCreated(executionContext) {
this._browserRuntime.emit('runtimeExecutionContextCreated', { this._browserPage.emit('runtimeExecutionContextCreated', {
executionContextId: executionContext.id(), executionContextId: executionContext.id(),
auxData: executionContext.auxData(), auxData: executionContext.auxData(),
}); });
} }
_onExecutionContextDestroyed(executionContext) { _onExecutionContextDestroyed(executionContext) {
this._browserRuntime.emit('runtimeExecutionContextDestroyed', { this._browserPage.emit('runtimeExecutionContextDestroyed', {
executionContextId: executionContext.id(), executionContextId: executionContext.id(),
}); });
} }

View File

@ -35,19 +35,21 @@ class RuntimeAgent {
constructor(runtime, channel) { constructor(runtime, channel) {
this._runtime = runtime; this._runtime = runtime;
this._browserRuntime = channel.connect('runtime'); this._browserRuntime = channel.connect('runtime');
for (const context of this._runtime.executionContexts())
this._onExecutionContextCreated(context);
this._eventListeners = [ 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', { channel.register('runtime', {
evaluate: this._runtime.evaluate.bind(this._runtime), evaluate: this._runtime.evaluate.bind(this._runtime),
callFunction: this._runtime.callFunction.bind(this._runtime), callFunction: this._runtime.callFunction.bind(this._runtime),
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
disposeObject: this._runtime.disposeObject.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) { _onExecutionContextCreated(executionContext) {
@ -70,15 +72,5 @@ class RuntimeAgent {
} }
} }
let runtimeAgent; new RuntimeAgent(runtime, channel);
channel.register('', {
attach: () => {
runtimeAgent = new RuntimeAgent(runtime, channel);
},
detach: () => {
runtimeAgent.dispose();
},
});

View File

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

View File

@ -7,6 +7,7 @@
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js'); const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
const Cc = Components.classes; const Cc = Components.classes;
const Ci = Components.interfaces; const Ci = Components.interfaces;
@ -60,12 +61,9 @@ class PageHandler {
this._session = session; this._session = session;
this._contentChannel = contentChannel; this._contentChannel = contentChannel;
this._contentPage = contentChannel.connect('page'); this._contentPage = contentChannel.connect('page');
this._contentRuntime = contentChannel.connect('runtime');
this._workers = new Map(); this._workers = new Map();
this._pageTarget = target; this._pageTarget = target;
this._browser = target.linkedBrowser();
this._dialogs = new Map();
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
const emitProtocolEvent = eventName => { const emitProtocolEvent = eventName => {
@ -75,7 +73,23 @@ class PageHandler {
this._reportedFrameIds = new Set(); this._reportedFrameIds = new Set();
this._networkEventsForUnreportedFrameIds = new Map(); this._networkEventsForUnreportedFrameIds = new Map();
for (const dialog of this._pageTarget.dialogs())
this._onDialogOpened(dialog);
if (this._pageTarget.screencastInfo())
this._onScreencastStarted();
this._eventListeners = [ 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', { contentChannel.register('page', {
pageBindingCalled: emitProtocolEvent('Page.bindingCalled'), pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'), pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
@ -93,36 +107,34 @@ class PageHandler {
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'), pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
pageWorkerCreated: this._onWorkerCreated.bind(this), pageWorkerCreated: this._onWorkerCreated.bind(this),
pageWorkerDestroyed: this._onWorkerDestroyed.bind(this), pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
}),
contentChannel.register('runtime', {
runtimeConsole: emitProtocolEvent('Runtime.console'), runtimeConsole: emitProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), 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(); async dispose() {
}), this._contentPage.dispose();
helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), helper.removeListeners(this._eventListeners);
helper.on(this._pageTarget, 'crashed', () => { }
this._session.emitEvent('Page.crashed', {});
}), _onScreencastStarted() {
helper.on(this._pageTarget, 'screencastStarted', () => {
const info = this._pageTarget.screencastInfo(); const info = this._pageTarget.screencastInfo();
this._session.emitEvent('Page.screencastStarted', { screencastId: '' + info.videoSessionId, file: info.file }); 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(); _onDialogOpened(dialog) {
const options = this._pageTarget.browserContext().screencastOptions; this._session.emitEvent('Page.dialogOpened', {
if (options) dialogId: dialog.id(),
this._pageTarget.startVideoRecording(options); type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
_onDialogClosed(dialog) {
this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), });
} }
_onWorkerCreated({workerId, frameId, url}) { _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}) { async ['Page.setViewportSize']({viewportSize}) {
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : 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) { async ['Runtime.evaluate'](options) {
return await this._contentRuntime.send('evaluate', options); return await this._contentPage.send('evaluate', options);
} }
async ['Runtime.callFunction'](options) { async ['Runtime.callFunction'](options) {
return await this._contentRuntime.send('callFunction', options); return await this._contentPage.send('callFunction', options);
} }
async ['Runtime.getObjectProperties'](options) { async ['Runtime.getObjectProperties'](options) {
return await this._contentRuntime.send('getObjectProperties', options); return await this._contentPage.send('getObjectProperties', options);
} }
async ['Runtime.disposeObject'](options) { async ['Runtime.disposeObject'](options) {
return await this._contentRuntime.send('disposeObject', options); return await this._contentPage.send('disposeObject', options);
} }
async ['Network.getResponseBody']({requestId}) { async ['Network.getResponseBody']({requestId}) {
@ -291,30 +268,18 @@ class PageHandler {
return await this._contentPage.send('getContentQuads', options); return await this._contentPage.send('getContentQuads', options);
} }
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.navigate'](options) { async ['Page.navigate'](options) {
return await this._contentPage.send('navigate', options); return await this._contentPage.send('navigate', options);
} }
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.goBack'](options) { async ['Page.goBack'](options) {
return await this._contentPage.send('goBack', options); return await this._contentPage.send('goBack', options);
} }
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.goForward'](options) { async ['Page.goForward'](options) {
return await this._contentPage.send('goForward', options); return await this._contentPage.send('goForward', options);
} }
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.reload'](options) { async ['Page.reload'](options) {
return await this._contentPage.send('reload', options); return await this._contentPage.send('reload', options);
} }
@ -356,7 +321,7 @@ class PageHandler {
} }
async ['Page.handleDialog']({dialogId, accept, promptText}) { async ['Page.handleDialog']({dialogId, accept, promptText}) {
const dialog = this._dialogs.get(dialogId); const dialog = this._pageTarget.dialog(dialogId);
if (!dialog) if (!dialog)
throw new Error('Failed to find dialog with id = ' + dialogId); throw new Error('Failed to find dialog with id = ' + dialogId);
if (accept) 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']; var EXPORTED_SYMBOLS = ['PageHandler'];
this.PageHandler = PageHandler; this.PageHandler = PageHandler;