browser(firefox): support workers (#526)

a0549a0f64
This commit is contained in:
Dmitry Gozman 2020-01-17 16:40:30 -08:00 committed by GitHub
parent c517542f39
commit 2520dedb41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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,
+ },
+ },
+ },
+};
+