From 655ded6f15c16fa667d55546d2e6edc0295c1880 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 6 Oct 2020 01:21:46 -0700 Subject: [PATCH] browser(firefox): follow-up with assorted simplifications 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 --- browser_patches/firefox/BUILD_NUMBER | 4 +- .../firefox/juggler/NetworkObserver.js | 26 +-- .../firefox/juggler/SimpleChannel.js | 12 +- .../firefox/juggler/TargetRegistry.js | 114 ++++++++++- .../firefox/juggler/content/PageAgent.js | 154 +++++++-------- .../firefox/juggler/content/WorkerMain.js | 24 +-- .../firefox/juggler/content/main.js | 1 - .../firefox/juggler/protocol/PageHandler.js | 178 +++++------------- 8 files changed, 240 insertions(+), 273 deletions(-) diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index 02913a01833ee..032a786bde251 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -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 diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js index 522a8f2a14a32..0264c1d82bf2e 100644 --- a/browser_patches/firefox/juggler/NetworkObserver.js +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -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; } diff --git a/browser_patches/firefox/juggler/SimpleChannel.js b/browser_patches/firefox/juggler/SimpleChannel.js index 9cf9b75eb5d91..7cd5ae26c6f67 100644 --- a/browser_patches/firefox/juggler/SimpleChannel.js +++ b/browser_patches/firefox/juggler/SimpleChannel.js @@ -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); } diff --git a/browser_patches/firefox/juggler/TargetRegistry.js b/browser_patches/firefox/juggler/TargetRegistry.js index 9387a2a5d1252..cd37492e00885 100644 --- a/browser_patches/firefox/juggler/TargetRegistry.js +++ b/browser_patches/firefox/juggler/TargetRegistry.js @@ -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; diff --git a/browser_patches/firefox/juggler/content/PageAgent.js b/browser_patches/firefox/juggler/content/PageAgent.js index a600ab8af4c36..d252d1b433679 100644 --- a/browser_patches/firefox/juggler/content/PageAgent.js +++ b/browser_patches/firefox/juggler/content/PageAgent.js @@ -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(), }); } diff --git a/browser_patches/firefox/juggler/content/WorkerMain.js b/browser_patches/firefox/juggler/content/WorkerMain.js index b78bc3923e2ef..9b9bcf2900d65 100644 --- a/browser_patches/firefox/juggler/content/WorkerMain.js +++ b/browser_patches/firefox/juggler/content/WorkerMain.js @@ -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); diff --git a/browser_patches/firefox/juggler/content/main.js b/browser_patches/firefox/juggler/content/main.js index 6985964e2ea8e..fdd86716a3897 100644 --- a/browser_patches/firefox/juggler/content/main.js +++ b/browser_patches/firefox/juggler/content/main.js @@ -103,7 +103,6 @@ function initialize() { frameTree.addBinding(name, script); pageAgent = new PageAgent(messageManager, channel, frameTree); - pageAgent.enable(); channel.register('', { addScriptToEvaluateOnNewDocument(script) { diff --git a/browser_patches/firefox/juggler/protocol/PageHandler.js b/browser_patches/firefox/juggler/protocol/PageHandler.js index e7496af607344..cf517ce4fc91d 100644 --- a/browser_patches/firefox/juggler/protocol/PageHandler.js +++ b/browser_patches/firefox/juggler/protocol/PageHandler.js @@ -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(), ]; + } + + async dispose() { + this._contentPage.dispose(); + helper.removeListeners(this._eventListeners); + } - this._updateModalDialogs(); - const options = this._pageTarget.browserContext().screencastOptions; - if (options) - this._pageTarget.startVideoRecording(options); + _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;