diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index 3930e87f3a..62055f11d3 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -1824,10 +1824,10 @@ index 0000000000000000000000000000000000000000..2508cce41565023b7fee9c7b85afe8ec + diff --git a/testing/juggler/content/PageAgent.js b/testing/juggler/content/PageAgent.js new file mode 100644 -index 0000000000000000000000000000000000000000..03c4c9717148169110f7e7d19306a76984ed4860 +index 0000000000000000000000000000000000000000..37ab5f56739cfd16200a4ada9f4cf83436688eba --- /dev/null +++ b/testing/juggler/content/PageAgent.js -@@ -0,0 +1,721 @@ +@@ -0,0 +1,843 @@ +"use strict"; +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const Ci = Components.interfaces; @@ -1839,11 +1839,28 @@ index 0000000000000000000000000000000000000000..03c4c9717148169110f7e7d19306a769 + +const helper = new Helper(); + ++const registeredWorkerListeners = new Map(); ++const workerListener = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]), ++ onMessage: (wrapped) => { ++ const message = JSON.parse(wrapped); ++ const listener = registeredWorkerListeners.get(message.workerId); ++ if (listener) ++ listener(message); ++ }, ++ onClose: () => { ++ }, ++ onError: (filename, lineno, message) => { ++ dump(`Error in worker: ${message} @${filename}:${lineno}\n`); ++ }, ++}; ++ +class FrameData { + constructor(agent, frame) { + this._agent = agent; + this._frame = frame; + this._isolatedWorlds = new Map(); ++ this._workers = new Map(); + this.reset(); + } + @@ -1913,6 +1930,59 @@ index 0000000000000000000000000000000000000000..03c4c9717148169110f7e7d19306a769 + } + throw new Error('Cannot find object with id = ' + objectId); + } ++ ++ workerCreated(workerDebugger) { ++ const workerId = helper.generateId(); ++ this._workers.set(workerId, workerDebugger); ++ this._agent._session.emitEvent('Page.workerCreated', { ++ workerId, ++ frameId: this._frame.id(), ++ url: workerDebugger.url, ++ }); ++ // Note: this does not interoperate with firefox devtools. ++ if (!workerDebugger.isInitialized) { ++ workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js'); ++ workerDebugger.addListener(workerListener); ++ } ++ registeredWorkerListeners.set(workerId, message => { ++ if (message.command === 'dispatch') { ++ this._agent._session.emitEvent('Page.dispatchMessageFromWorker', { ++ workerId, ++ message: message.message, ++ }); ++ } ++ if (message.command === 'console') ++ this._agent._runtime.filterConsoleMessage(message.hash); ++ }); ++ workerDebugger.postMessage(JSON.stringify({command: 'connect', workerId})); ++ } ++ ++ workerDestroyed(wd) { ++ for (const [workerId, workerDebugger] of this._workers) { ++ if (workerDebugger === wd) { ++ this._agent._session.emitEvent('Page.workerDestroyed', { ++ workerId, ++ }); ++ this._workers.delete(workerId); ++ registeredWorkerListeners.delete(workerId); ++ } ++ } ++ } ++ ++ sendMessageToWorker(workerId, message) { ++ const workerDebugger = this._workers.get(workerId); ++ if (!workerDebugger) ++ throw new Error('Cannot find worker with id "' + workerId + '"'); ++ workerDebugger.postMessage(JSON.stringify({command: 'dispatch', workerId, message})); ++ } ++ ++ dispose() { ++ for (const [workerId, workerDebugger] of this._workers) { ++ workerDebugger.postMessage(JSON.stringify({command: 'disconnect', workerId})); ++ registeredWorkerListeners.delete(workerId); ++ } ++ this._workers.clear(); ++ } +} + +class PageAgent { @@ -1934,6 +2004,24 @@ index 0000000000000000000000000000000000000000..03c4c9717148169110f7e7d19306a769 + this._docShell = docShell; + this._initialDPPX = docShell.contentViewer.overrideDPPX; + this._customScrollbars = null; ++ ++ this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager); ++ this._wdmListener = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]), ++ onRegister: this._onWorkerCreated.bind(this), ++ onUnregister: this._onWorkerDestroyed.bind(this), ++ }; ++ ++ this._runtime.setOnErrorFromWorker((domWindow, message, stack) => { ++ const frame = this._frameTree.frameForDocShell(domWindow.docShell); ++ if (!frame) ++ return; ++ this._session.emitEvent('Page.uncaughtError', { ++ frameId: frame.id(), ++ message, ++ stack, ++ }); ++ }); + } + + async awaitViewportDimensions({width, height}) { @@ -2042,12 +2130,43 @@ index 0000000000000000000000000000000000000000..03c4c9717148169110f7e7d19306a769 + helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)), + helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)), + ]; ++ ++ this._wdm.addListener(this._wdmListener); ++ for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator()) ++ this._onWorkerCreated(workerDebugger); + } + + setInterceptFileChooserDialog({enabled}) { + this._docShell.fileInputInterceptionEnabled = !!enabled; + } + ++ _frameForWorker(workerDebugger) { ++ if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED) ++ return null; ++ const docShell = workerDebugger.window.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ return frame ? this._frameData.get(frame) : null; ++ } ++ ++ _onWorkerCreated(workerDebugger) { ++ const frameData = this._frameForWorker(workerDebugger); ++ if (frameData) ++ frameData.workerCreated(workerDebugger); ++ } ++ ++ _onWorkerDestroyed(workerDebugger) { ++ const frameData = this._frameForWorker(workerDebugger); ++ if (frameData) ++ frameData.workerDestroyed(workerDebugger); ++ } ++ ++ sendMessageToWorker({frameId, workerId, message}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ this._frameData.get(frame).sendMessageToWorker(workerId, message); ++ } ++ + _filePickerShown(inputElement) { + if (inputElement.ownerGlobal.docShell !== this._docShell) + return; @@ -2166,7 +2285,10 @@ index 0000000000000000000000000000000000000000..03c4c9717148169110f7e7d19306a769 + } + + dispose() { ++ for (const frameData of this._frameData.values()) ++ frameData.dispose(); + helper.removeListeners(this._eventListeners); ++ this._wdm.removeListener(this._wdmListener); + } + + async navigate({frameId, url, referer}) { @@ -2551,20 +2673,24 @@ index 0000000000000000000000000000000000000000..03c4c9717148169110f7e7d19306a769 + diff --git a/testing/juggler/content/RuntimeAgent.js b/testing/juggler/content/RuntimeAgent.js new file mode 100644 -index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac357fb9b60 +index 0000000000000000000000000000000000000000..5765d5c3b1de7b9383a80435b37b034d6951d981 --- /dev/null +++ b/testing/juggler/content/RuntimeAgent.js -@@ -0,0 +1,478 @@ +@@ -0,0 +1,545 @@ +"use strict"; -+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -+const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {}); ++// Note: this file should be loadabale with eval() into worker environment. ++// Avoid Components.*, ChromeUtils and global const variables. + -+const Ci = Components.interfaces; -+const Cr = Components.results; -+const Cu = Components.utils; -+addDebuggerToGlobal(Cu.getGlobalForObject(this)); -+const helper = new Helper(); ++if (!this.Debugger) { ++ // Worker has a Debugger defined already. ++ const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {}); ++ addDebuggerToGlobal(Components.utils.getGlobalForObject(this)); ++} ++ ++let lastId = 0; ++function generateId() { ++ return 'id-' + (++lastId); ++} + +const consoleLevelToProtocolType = { + 'dir': 'dir', @@ -2603,13 +2729,39 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 +]); + +class RuntimeAgent { -+ constructor(session) { ++ constructor(session, onWorkerConsoleMessage) { + this._debugger = new Debugger(); + this._pendingPromises = new Map(); + this._session = session; + this._executionContexts = new Map(); + this._windowToExecutionContext = new Map(); -+ this._consoleServiceListener = { ++ this._eventListeners = []; ++ this._enabled = false; ++ this._filteredConsoleMessageHashes = new Set(); ++ this._onErrorFromWorker = null; ++ this._onWorkerConsoleMessage = onWorkerConsoleMessage; ++ } ++ ++ enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ for (const executionContext of this._executionContexts.values()) ++ this._notifyExecutionContextCreated(executionContext); ++ ++ const isWorker = !!this._onWorkerConsoleMessage; ++ if (isWorker) { ++ this._registerConsoleEventHandler(); ++ } else { ++ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ this._registerConsoleServiceListener(Services); ++ this._registerConsoleObserver(Services); ++ } ++ } ++ ++ _registerConsoleServiceListener(Services) { ++ const Ci = Components.interfaces; ++ const consoleServiceListener = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]), + + observe: message => { @@ -2618,6 +2770,11 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + return; + } + const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID); ++ if (message.category === 'Web Worker' && (message.flags & Ci.nsIScriptError.exceptionFlag)) { ++ if (this._onErrorFromWorker) ++ this._onErrorFromWorker(errorWindow, message.message, '' + message.stack); ++ return; ++ } + const executionContext = this._windowToExecutionContext.get(errorWindow); + if (!executionContext) + return; @@ -2641,47 +2798,67 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + }); + }, + }; -+ -+ this._eventListeners = []; -+ this._enabled = false; ++ Services.console.registerListener(consoleServiceListener); ++ this._eventListeners.push(() => Services.console.unregisterListener(consoleServiceListener)); + } + -+ _consoleAPICalled({wrappedJSObject}, topic, data) { -+ const type = consoleLevelToProtocolType[wrappedJSObject.level]; ++ _registerConsoleObserver(Services) { ++ const consoleObserver = ({wrappedJSObject}, topic, data) => { ++ const hash = this._consoleMessageHash(wrappedJSObject); ++ if (this._filteredConsoleMessageHashes.has(hash)) { ++ this._filteredConsoleMessageHashes.delete(hash); ++ return; ++ } ++ const executionContext = Array.from(this._executionContexts.values()).find(context => { ++ const domWindow = context._domWindow; ++ return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID; ++ }); ++ if (!executionContext) ++ return; ++ this._onConsoleMessage(executionContext, wrappedJSObject); ++ }; ++ Services.obs.addObserver(consoleObserver, "console-api-log-event"); ++ this._eventListeners.push(() => Services.obs.removeObserver(consoleObserver, "console-api-log-event")); ++ } ++ ++ _registerConsoleEventHandler() { ++ setConsoleEventHandler(message => { ++ this._onWorkerConsoleMessage(this._consoleMessageHash(message)); ++ const executionContext = Array.from(this._executionContexts.values())[0]; ++ this._onConsoleMessage(executionContext, message); ++ }); ++ this._eventListeners.push(() => setConsoleEventHandler(null)); ++ } ++ ++ filterConsoleMessage(messageHash) { ++ this._filteredConsoleMessageHashes.add(messageHash); ++ } ++ ++ setOnErrorFromWorker(onErrorFromWorker) { ++ this._onErrorFromWorker = onErrorFromWorker; ++ } ++ ++ _consoleMessageHash(message) { ++ return `${message.timeStamp}/${message.filename}/${message.lineNumber}/${message.columnNumber}/${message.sourceId}/${message.level}`; ++ } ++ ++ _onConsoleMessage(executionContext, message) { ++ const type = consoleLevelToProtocolType[message.level]; + if (!type) + return; -+ const executionContext = Array.from(this._executionContexts.values()).find(context => { -+ const domWindow = context._domWindow; -+ return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID; -+ }); -+ if (!executionContext) -+ return; -+ const args = wrappedJSObject.arguments.map(arg => executionContext.rawValueToRemoteObject(arg)); ++ const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg)); + this._session.emitEvent('Runtime.console', { + args, + type, + executionContextId: executionContext.id(), + location: { -+ lineNumber: wrappedJSObject.lineNumber - 1, -+ columnNumber: wrappedJSObject.columnNumber - 1, -+ url: wrappedJSObject.filename, ++ lineNumber: message.lineNumber - 1, ++ columnNumber: message.columnNumber - 1, ++ url: message.filename, + }, + }); + } + -+ enable() { -+ if (this._enabled) -+ return; -+ this._enabled = true; -+ for (const executionContext of this._executionContexts.values()) -+ this._notifyExecutionContextCreated(executionContext); -+ Services.console.registerListener(this._consoleServiceListener); -+ this._eventListeners = [ -+ () => Services.console.unregisterListener(this._consoleServiceListener), -+ helper.addObserver(this._consoleAPICalled.bind(this), "console-api-log-event"), -+ ]; -+ } -+ + _notifyExecutionContextCreated(executionContext) { + if (!this._enabled) + return; @@ -2700,7 +2877,9 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + } + + dispose() { -+ helper.removeListeners(this._eventListeners); ++ for (const tearDown of this._eventListeners) ++ tearDown.call(null); ++ this._eventListeners = []; + } + + async _awaitPromise(executionContext, obj, exceptionDetails = {}) { @@ -2742,9 +2921,11 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + } + + createExecutionContext(domWindow, contextGlobal, auxData) { -+ const context = new ExecutionContext(this, domWindow, this._debugger.addDebuggee(contextGlobal), auxData); ++ // Note: domWindow is null for workers. ++ const context = new ExecutionContext(this, domWindow, contextGlobal, this._debugger.addDebuggee(contextGlobal), auxData); + this._executionContexts.set(context._id, context); -+ this._windowToExecutionContext.set(domWindow, context); ++ if (domWindow) ++ this._windowToExecutionContext.set(domWindow, context); + this._notifyExecutionContextCreated(context); + return context; + } @@ -2765,9 +2946,10 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + } + if (!this._pendingPromises.size) + this._debugger.onPromiseSettled = undefined; -+ this._debugger.removeDebuggee(destroyedContext._domWindow); ++ this._debugger.removeDebuggee(destroyedContext._contextGlobal); + this._executionContexts.delete(destroyedContext._id); -+ this._windowToExecutionContext.delete(destroyedContext._domWindow); ++ if (destroyedContext._domWindow) ++ this._windowToExecutionContext.delete(destroyedContext._domWindow); + this._notifyExecutionContextDestroyed(destroyedContext); + } + @@ -2813,12 +2995,13 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 +} + +class ExecutionContext { -+ constructor(runtime, domWindow, global, auxData) { ++ constructor(runtime, domWindow, contextGlobal, global, auxData) { + this._runtime = runtime; + this._domWindow = domWindow; ++ this._contextGlobal = contextGlobal; + this._global = global; + this._remoteObjects = new Map(); -+ this._id = helper.generateId(); ++ this._id = generateId(); + this._auxData = auxData; + this._jsonStringifyObject = this._global.executeInGlobal(`((stringify, dateProto, object) => { + const oldToJson = dateProto.toJSON; @@ -2839,9 +3022,9 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + } + + async evaluateScript(script, exceptionDetails = {}) { -+ const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true); ++ const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null; + let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails); -+ userInputHelper.destruct(); ++ userInputHelper && userInputHelper.destruct(); + if (!success) + return null; + if (obj && obj.isPromise) { @@ -2873,9 +3056,9 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + default: return this._toDebugger(arg.value); + } + }); -+ const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true); ++ const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null; + let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails); -+ userInputHelper.destruct(); ++ userInputHelper && userInputHelper.destruct(); + if (!success) + return null; + if (obj && obj.isPromise) { @@ -2898,9 +3081,15 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + return this._createRemoteObject(debuggerObj); + } + ++ _instanceOf(debuggerObj, rawObj, className) { ++ if (this._domWindow) ++ return rawObj instanceof this._domWindow[className]; ++ return this._global.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._global.makeDebuggeeValue(className)}).return; ++ } ++ + _createRemoteObject(debuggerObj) { + if (debuggerObj instanceof Debugger.Object) { -+ const objectId = helper.generateId(); ++ const objectId = generateId(); + this._remoteObjects.set(objectId, debuggerObj); + const rawObj = debuggerObj.unsafeDereference(); + const type = typeof rawObj; @@ -2911,35 +3100,35 @@ index 0000000000000000000000000000000000000000..262011d8fda346078a6cfcb7aae5dac3 + subtype = 'array'; + else if (Object.is(rawObj, null)) + subtype = 'null'; -+ else if (rawObj instanceof this._domWindow.Node) ++ else if (this._instanceOf(debuggerObj, rawObj, 'Node')) + subtype = 'node'; -+ else if (rawObj instanceof this._domWindow.RegExp) ++ else if (this._instanceOf(debuggerObj, rawObj, 'RegExp')) + subtype = 'regexp'; -+ else if (rawObj instanceof this._domWindow.Date) ++ else if (this._instanceOf(debuggerObj, rawObj, 'Date')) + subtype = 'date'; -+ else if (rawObj instanceof this._domWindow.Map) ++ else if (this._instanceOf(debuggerObj, rawObj, 'Map')) + subtype = 'map'; -+ else if (rawObj instanceof this._domWindow.Set) ++ else if (this._instanceOf(debuggerObj, rawObj, 'Set')) + subtype = 'set'; -+ else if (rawObj instanceof this._domWindow.WeakMap) ++ else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap')) + subtype = 'weakmap'; -+ else if (rawObj instanceof this._domWindow.WeakSet) ++ else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet')) + subtype = 'weakset'; -+ else if (rawObj instanceof this._domWindow.Error) ++ else if (this._instanceOf(debuggerObj, rawObj, 'Error')) + subtype = 'error'; -+ else if (rawObj instanceof this._domWindow.Promise) ++ else if (this._instanceOf(debuggerObj, rawObj, 'Promise')) + subtype = 'promise'; -+ else if ((rawObj instanceof this._domWindow.Int8Array) || (rawObj instanceof this._domWindow.Uint8Array) || -+ (rawObj instanceof this._domWindow.Uint8ClampedArray) || (rawObj instanceof this._domWindow.Int16Array) || -+ (rawObj instanceof this._domWindow.Uint16Array) || (rawObj instanceof this._domWindow.Int32Array) || -+ (rawObj instanceof this._domWindow.Uint32Array) || (rawObj instanceof this._domWindow.Float32Array) || -+ (rawObj instanceof this._domWindow.Float64Array)) { ++ else if ((this._instanceOf(debuggerObj, rawObj, 'Int8Array')) || (this._instanceOf(debuggerObj, rawObj, 'Uint8Array')) || ++ (this._instanceOf(debuggerObj, rawObj, 'Uint8ClampedArray')) || (this._instanceOf(debuggerObj, rawObj, 'Int16Array')) || ++ (this._instanceOf(debuggerObj, rawObj, 'Uint16Array')) || (this._instanceOf(debuggerObj, rawObj, 'Int32Array')) || ++ (this._instanceOf(debuggerObj, rawObj, 'Uint32Array')) || (this._instanceOf(debuggerObj, rawObj, 'Float32Array')) || ++ (this._instanceOf(debuggerObj, rawObj, 'Float64Array'))) { + subtype = 'typedarray'; + } + return {objectId, type, subtype}; + } + if (typeof debuggerObj === 'symbol') { -+ const objectId = helper.generateId(); ++ const objectId = generateId(); + this._remoteObjects.set(objectId, debuggerObj); + return {objectId, type: 'symbol'}; + } @@ -3124,6 +3313,79 @@ index 0000000000000000000000000000000000000000..caee4df323d0a526ed7e38947c41c643 +var EXPORTED_SYMBOLS = ['ScrollbarManager']; +this.ScrollbarManager = ScrollbarManager; + +diff --git a/testing/juggler/content/WorkerMain.js b/testing/juggler/content/WorkerMain.js +new file mode 100644 +index 0000000000000000000000000000000000000000..73cdce649608f068e59e1ff7808883c4482bff7e +--- /dev/null ++++ b/testing/juggler/content/WorkerMain.js +@@ -0,0 +1,67 @@ ++"use strict"; ++loadSubScript('chrome://juggler/content/content/RuntimeAgent.js'); ++ ++class WorkerSession { ++ constructor(workerId) { ++ this._workerId = workerId; ++ this._agents = { ++ Runtime: new RuntimeAgent(this, hash => this._send({command: 'console', hash})), ++ }; ++ this._agents.Runtime.enable(); ++ this._agents.Runtime.createExecutionContext(null /* domWindow */, global, {}); ++ } ++ ++ _send(command) { ++ postMessage(JSON.stringify({...command, workerId: this._workerId})); ++ } ++ ++ _dispatchProtocolMessage(protocolMessage) { ++ this._send({command: 'dispatch', message: JSON.stringify(protocolMessage)}); ++ } ++ ++ emitEvent(eventName, params) { ++ this._dispatchProtocolMessage({method: eventName, params}); ++ } ++ ++ async _onMessage(message) { ++ const object = JSON.parse(message); ++ const id = object.id; ++ try { ++ const [domainName, methodName] = object.method.split('.'); ++ const agent = this._agents[domainName]; ++ if (!agent) ++ throw new Error(`unknown domain: ${domainName}`); ++ const handler = agent[methodName]; ++ if (!handler) ++ throw new Error(`unknown method: ${domainName}.${methodName}`); ++ const result = await handler.call(agent, object.params); ++ this._dispatchProtocolMessage({id, result}); ++ } catch (e) { ++ this._dispatchProtocolMessage({id, error: e.message + '\n' + e.stack}); ++ } ++ } ++ ++ dispose() { ++ for (const agent of Object.values(this._agents)) ++ agent.dispose(); ++ } ++} ++ ++const workerSessions = new Map(); ++ ++this.addEventListener('message', event => { ++ const data = JSON.parse(event.data); ++ if (data.command === 'connect') { ++ const session = new WorkerSession(data.workerId); ++ workerSessions.set(data.workerId, session); ++ } ++ if (data.command === 'disconnect') { ++ const session = workerSessions.get(data.workerId); ++ session.dispose(); ++ workerSessions.delete(data.workerId); ++ } ++ if (data.command === 'dispatch') { ++ const session = workerSessions.get(data.workerId); ++ session._onMessage(data.message); ++ } ++}); diff --git a/testing/juggler/content/floating-scrollbars.css b/testing/juggler/content/floating-scrollbars.css new file mode 100644 index 0000000000000000000000000000000000000000..7709bdd34c65062fc63684ef17fc792d3991d965 @@ -3243,10 +3505,10 @@ index 0000000000000000000000000000000000000000..8585092e04e7e763a0c115c28363e505 + diff --git a/testing/juggler/jar.mn b/testing/juggler/jar.mn new file mode 100644 -index 0000000000000000000000000000000000000000..27f5a15fd7f14385bb1f080d5965d90e60423d1a +index 0000000000000000000000000000000000000000..76377927a8c9af3cac3b028ff754491966d03ba3 --- /dev/null +++ b/testing/juggler/jar.mn -@@ -0,0 +1,29 @@ +@@ -0,0 +1,30 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -3272,6 +3534,7 @@ index 0000000000000000000000000000000000000000..27f5a15fd7f14385bb1f080d5965d90e + content/content/NetworkMonitor.js (content/NetworkMonitor.js) + content/content/PageAgent.js (content/PageAgent.js) + content/content/RuntimeAgent.js (content/RuntimeAgent.js) ++ content/content/WorkerMain.js (content/WorkerMain.js) + content/content/ScrollbarManager.js (content/ScrollbarManager.js) + content/content/floating-scrollbars.css (content/floating-scrollbars.css) + content/content/hidden-scrollbars.css (content/hidden-scrollbars.css) @@ -3392,10 +3655,10 @@ index 0000000000000000000000000000000000000000..708059a95b3a01f3d9c7b7ef4714ee6f +this.BrowserHandler = BrowserHandler; diff --git a/testing/juggler/protocol/Dispatcher.js b/testing/juggler/protocol/Dispatcher.js new file mode 100644 -index 0000000000000000000000000000000000000000..7b3a6fa4fe7a2b50a78ed446fbf5537504994798 +index 0000000000000000000000000000000000000000..956988738079272be8d3998dcbbaa91abc415fcc --- /dev/null +++ b/testing/juggler/protocol/Dispatcher.js -@@ -0,0 +1,255 @@ +@@ -0,0 +1,254 @@ +const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); +const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); @@ -3630,7 +3893,6 @@ index 0000000000000000000000000000000000000000..7b3a6fa4fe7a2b50a78ed446fbf55375 + + _onMessage({data}) { + if (data.id) { -+ let id = data.id; + const {resolve, reject} = this._pendingMessages.get(data.id); + this._pendingMessages.delete(data.id); + if (data.error) @@ -3813,10 +4075,10 @@ index 0000000000000000000000000000000000000000..f5e7e919594b3778fd3046bf69d34878 +this.NetworkHandler = NetworkHandler; diff --git a/testing/juggler/protocol/PageHandler.js b/testing/juggler/protocol/PageHandler.js new file mode 100644 -index 0000000000000000000000000000000000000000..bf59b2afa8692d02fd0ce664eec2e9827a8209d2 +index 0000000000000000000000000000000000000000..23a32be2200e90e2e05d31aec85874a829cb1bbe --- /dev/null +++ b/testing/juggler/protocol/PageHandler.js -@@ -0,0 +1,281 @@ +@@ -0,0 +1,285 @@ +"use strict"; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); @@ -4039,6 +4301,10 @@ index 0000000000000000000000000000000000000000..bf59b2afa8692d02fd0ce664eec2e982 + async handleFileChooser(options) { + return await this._contentSession.send('Page.handleFileChooser', options); + } ++ ++ async sendMessageToWorker(options) { ++ return await this._contentSession.send('Page.sendMessageToWorker', options); ++ } +} + +class Dialog { @@ -4249,10 +4515,10 @@ index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07 +this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js new file mode 100644 -index 0000000000000000000000000000000000000000..75b5276d085bd4217389cd05099895ebec2438b6 +index 0000000000000000000000000000000000000000..1eecb6120f101cb7506fcf8d40c177089e62671b --- /dev/null +++ b/testing/juggler/protocol/Protocol.js -@@ -0,0 +1,712 @@ +@@ -0,0 +1,731 @@ +const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); + +// Protocol-specific types. @@ -4738,6 +5004,18 @@ index 0000000000000000000000000000000000000000..75b5276d085bd4217389cd05099895eb + executionContextId: t.String, + element: runtimeTypes.RemoteObject + }, ++ 'workerCreated': { ++ workerId: t.String, ++ frameId: t.String, ++ url: t.String, ++ }, ++ 'workerDestroyed': { ++ workerId: t.String, ++ }, ++ 'dispatchMessageFromWorker': { ++ workerId: t.String, ++ message: t.String, ++ }, + }, + + methods: { @@ -4940,6 +5218,13 @@ index 0000000000000000000000000000000000000000..75b5276d085bd4217389cd05099895eb + enabled: t.Boolean, + }, + }, ++ 'sendMessageToWorker': { ++ params: { ++ frameId: t.String, ++ workerId: t.String, ++ message: t.String, ++ }, ++ }, + }, +}; +