bug 1523104: remote: allow Domains to be implemented in either parent or content processes; r=ato
authorAlexandre Poirot <poirot.alex@gmail.com>
Thu, 21 Feb 2019 15:04:13 -0800
changeset 524103 c26cc4c7f0212a46a076bbce894f505cf199f6a6
parent 524102 e3b2b601748cc5263650927d6de6b485975fd6c4
child 524104 0d1d92205068322902f909a7b171a65ea765486f
push id2032
push userffxbld-merge
push dateMon, 13 May 2019 09:36:57 +0000
treeherdermozilla-release@455c1065dcbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersato
bugs1523104
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1523104: remote: allow Domains to be implemented in either parent or content processes; r=ato
remote/ContentProcessSession.jsm
remote/Domain.jsm
remote/Domains.jsm
remote/Session.jsm
remote/domain/Log.jsm
remote/domain/Page.jsm
remote/domains/ContentProcessDomains.jsm
remote/domains/Domain.jsm
remote/domains/Domains.jsm
remote/domains/ParentProcessDomains.jsm
remote/domains/content/Log.jsm
remote/domains/content/Page.jsm
remote/domains/parent/Browser.jsm
remote/jar.mn
remote/test/browser/browser_cdp.js
--- a/remote/ContentProcessSession.jsm
+++ b/remote/ContentProcessSession.jsm
@@ -1,26 +1,27 @@
 /* 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/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["ContentProcessSession"];
 
-const {Domains} = ChromeUtils.import("chrome://remote/content/Domains.jsm");
+const {Domains} = ChromeUtils.import("chrome://remote/content/domains/Domains.jsm");
+const {ContentProcessDomains} = ChromeUtils.import("chrome://remote/content/domains/ContentProcessDomains.jsm");
 
 class ContentProcessSession {
   constructor(messageManager, browsingContext, content, docShell) {
     this.messageManager = messageManager;
     this.browsingContext = browsingContext;
     this.content = content;
     this.docShell = docShell;
 
-    this.domains = new Domains(this);
+    this.domains = new Domains(this, ContentProcessDomains);
     this.messageManager.addMessageListener("remote-protocol:request", this);
     this.messageManager.addMessageListener("remote-protocol:destroy", this);
 
     this.destroy = this.destroy.bind(this);
     this.content.addEventListener("unload", this.destroy);
   }
 
   destroy() {
deleted file mode 100644
--- a/remote/Domain.jsm
+++ /dev/null
@@ -1,39 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = ["Domain"];
-
-const {EventEmitter} = ChromeUtils.import("chrome://remote/content/EventEmitter.jsm");
-const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-class Domain {
-  constructor(session, target) {
-    this.session = session;
-    this.target = target;
-    this.name = this.constructor.name;
-
-    EventEmitter.decorate(this);
-  }
-
-  destructor() {}
-
-  get content() {
-    return this.session.content;
-  }
-
-  get docShell() {
-    return this.session.docShell;
-  }
-
-  get chromeEventHandler() {
-    return this.docShell.chromeEventHandler;
-  }
-}
-
-XPCOMUtils.defineLazyModuleGetters(Domain, {
-  Log: "chrome://remote/content/domain/Log.jsm",
-  Page: "chrome://remote/content/domain/Page.jsm",
-});
deleted file mode 100644
--- a/remote/Domains.jsm
+++ /dev/null
@@ -1,56 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = ["Domains"];
-
-const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
-
-class Domains extends Map {
-  constructor(session) {
-    super();
-    this.session = session;
-  }
-
-  get(name) {
-    let inst = super.get(name);
-    if (!inst) {
-      inst = this.new(name);
-      this.set(inst);
-    }
-    return inst;
-  }
-
-  set(domain) {
-    super.set(domain.name, domain);
-  }
-
-  new(name) {
-    const Cls = Domain[name];
-    if (!Cls) {
-      throw new Error("No such domain: " + name);
-    }
-
-    const inst = new Cls(this.session, this.session.target);
-    inst.on("*", this.session);
-
-    return inst;
-  }
-
-  delete(name) {
-    const inst = super.get(name);
-    if (inst) {
-      inst.off("*");
-      inst.destructor();
-      super.delete(inst.name);
-    }
-  }
-
-  clear() {
-    for (const domainName of this.keys()) {
-      this.delete(domainName);
-    }
-  }
-}
--- a/remote/Session.jsm
+++ b/remote/Session.jsm
@@ -1,32 +1,35 @@
 /* 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/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["Session"];
 
-const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
+const {ParentProcessDomains} = ChromeUtils.import("chrome://remote/content/domains/ParentProcessDomains.jsm");
+const {Domains} = ChromeUtils.import("chrome://remote/content/domains/Domains.jsm");
 const {formatError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
 
 class Session {
   constructor(connection, target) {
     this.connection = connection;
     this.target = target;
 
     this.browsingContext = target.browser.browsingContext;
     this.messageManager = target.browser.messageManager;
     this.messageManager.loadFrameScript("chrome://remote/content/frame-script.js", false);
     this.messageManager.addMessageListener("remote-protocol:event", this);
     this.messageManager.addMessageListener("remote-protocol:result", this);
     this.messageManager.addMessageListener("remote-protocol:error", this);
 
     this.connection.onmessage = this.dispatch.bind(this);
+
+    this.domains = new Domains(this, ParentProcessDomains);
   }
 
   destructor() {
     this.connection.onmessage = null;
 
     this.messageManager.sendAsyncMessage("remote-protocol:destroy", {
       browsingContextId: this.browsingContext.id,
     });
@@ -40,25 +43,31 @@ class Session {
       if (typeof id == "undefined") {
         throw new TypeError("Message missing 'id' field");
       }
       if (typeof method == "undefined") {
         throw new TypeError("Message missing 'method' field");
       }
 
       const [domainName, methodName] = split(method, ".", 1);
-      const domain = Domain[domainName];
-      if (!domain) {
-        throw new TypeError("No such domain: " + domainName);
-      }
+      if (this.domains.domainSupportsMethod(domainName, methodName)) {
+        const inst = this.domains.get(domainName);
+        const methodFn = inst[methodName];
+        if (!methodFn || typeof methodFn != "function") {
+          throw new Error(`Method implementation of ${methodName} missing`);
+        }
 
-      this.messageManager.sendAsyncMessage("remote-protocol:request", {
-        browsingContextId: this.browsingContext.id,
-        request: {id, domainName, methodName, params},
-      });
+        const result = await methodFn.call(inst, params);
+        this.connection.send({id, result});
+      } else {
+        this.messageManager.sendAsyncMessage("remote-protocol:request", {
+          browsingContextId: this.browsingContext.id,
+          request: {id, domainName, methodName, params},
+        });
+      }
     } catch (e) {
       const error = formatError(e, {stack: true});
       this.connection.send({id, error});
     }
   }
 
   receiveMessage({name, data}) {
     const {id, result, event, error} = data;
@@ -72,16 +81,23 @@ class Session {
       this.connection.send(event);
       break;
 
     case "remote-protocol:error":
       this.connection.send({id, error: formatError(error, {stack: true})});
       break;
     }
   }
+
+  onevent(eventName, params) {
+    this.connection.send({
+      method: eventName,
+      params,
+    });
+  }
 }
 
 /**
  * Split s by sep, returning list of substrings.
  * If max is given, at most max splits are done.
  * If max is 0, there is no limit on the number of splits.
  */
 function split(s, sep, max = 0) {
deleted file mode 100644
--- a/remote/domain/Log.jsm
+++ /dev/null
@@ -1,136 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = ["Log"];
-
-const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-class Log extends Domain {
-  constructor(session, target) {
-    super(session, target);
-    this.enabled = false;
-  }
-
-  destructor() {
-    this.disable();
-  }
-
-  enable() {
-    if (!this.enabled) {
-      this.enabled = true;
-
-      Services.console.registerListener(this);
-      Services.obs.addObserver(this, "console-api-log-event");
-    }
-  }
-
-  disable() {
-    if (this.enabled) {
-      this.enabled = false;
-      Services.console.unregisterListener(this);
-      Services.obs.removeObserver(this, "console-api-log-event");
-    }
-  }
-
-  // nsIObserver
-
-  /**
-   * Takes all script error messages belonging to the current window
-   * and emits a "console-service-message" event.
-   *
-   * @param {nsIConsoleMessage} message
-   *     Message originating from the nsIConsoleService.
-   */
-  observe(message, topic) {
-    let entry;
-    if (message instanceof Ci.nsIScriptError) {
-      entry = fromScriptError(message);
-    } else if (message instanceof Ci.nsIConsoleMessage) {
-      entry = fromConsoleMessage(message);
-    } else if (topic == "console-api-log-event") {
-      entry = fromConsoleAPI(message.wrappedJSObject);
-    }
-
-    this.emit("Log.entryAdded", {entry});
-  }
-
-  // XPCOM
-
-  get QueryInterface() {
-    return ChromeUtils.generateQI([Ci.nsIConsoleListener]);
-  }
-}
-
-function fromConsoleMessage(message) {
-  const levels = {
-    [Ci.nsIConsoleMessage.debug]: "verbose",
-    [Ci.nsIConsoleMessage.info]: "info",
-    [Ci.nsIConsoleMessage.warn]: "warning",
-    [Ci.nsIConsoleMessage.error]: "error",
-  };
-  const level = levels[message.logLevel];
-
-  return {
-    source: "javascript",
-    level,
-    text: message.message,
-    timestamp: Date.now(),
-  };
-}
-
-function fromConsoleAPI(message) {
-  // message is a ConsoleEvent instance:
-  // https://searchfox.org/mozilla-central/rev/00c0d068ece99717bea7475f7dc07e61f7f35984/dom/webidl/Console.webidl#83-107
-
-  // A couple of possible level are defined here:
-  // https://searchfox.org/mozilla-central/rev/00c0d068ece99717bea7475f7dc07e61f7f35984/dom/console/Console.cpp#1086-1100
-  const levels = {
-    "log": "verbose",
-    "info": "info",
-    "warn": "warning",
-    "error": "error",
-    "exception": "error",
-
-  };
-  const level = levels[message.level] || "info";
-
-  return {
-    source: "javascript",
-    level,
-    text: message.arguments,
-    url: message.filename,
-    lineNumber: message.lineNumber,
-    stackTrace: message.stacktrace,
-    timestamp: Date.now(),
-  };
-}
-
-function fromScriptError(error) {
-  const {flags, errorMessage, sourceName, lineNumber, stack} = error;
-
-  // lossy reduction from bitmask to CDP string level
-  let level = "verbose";
-  if ((flags & Ci.nsIScriptError.exceptionFlag) ||
-      (flags & Ci.nsIScriptError.errorFlag)) {
-    level = "error";
-  } else if ((flags & Ci.nsIScriptError.warningFlag) ||
-      (flags & Ci.nsIScriptError.strictFlag)) {
-    level = "warning";
-  } else if (flags & Ci.nsIScriptError.infoFlag) {
-    level = "info";
-  }
-
-  return {
-    source: "javascript",
-    level,
-    text: errorMessage,
-    timestamp: Date.now(),
-    url: sourceName,
-    lineNumber,
-    stackTrace: stack,
-  };
-}
deleted file mode 100644
--- a/remote/domain/Page.jsm
+++ /dev/null
@@ -1,97 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = ["Page"];
-
-const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const {UnsupportedError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
-
-class Page extends Domain {
-  constructor(session, target) {
-    super(session, target);
-    this.enabled = false;
-  }
-
-  destructor() {
-    this.disable();
-  }
-
-  QueryInterface(iid) {
-    if (iid.equals(Ci.nsIWebProgressListener) ||
-      iid.equals(Ci.nsISupportsWeakReference) ||
-      iid.equals(Ci.nsIObserver)) {
-      return this;
-    }
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  }
-
-  // commands
-
-  async enable() {
-    if (!this.enabled) {
-      this.enabled = true;
-      this.chromeEventHandler.addEventListener("DOMContentLoaded", this,
-        {mozSystemGroup: true});
-      this.chromeEventHandler.addEventListener("pageshow", this,
-        {mozSystemGroup: true});
-    }
-  }
-
-  disable() {
-    if (this.enabled) {
-      this.chromeEventHandler.removeEventListener("DOMContentLoaded", this,
-        {mozSystemGroup: true});
-      this.chromeEventHandler.removeEventListener("pageshow", this,
-        {mozSystemGroup: true});
-      this.enabled = false;
-    }
-  }
-
-  async navigate({url, referrer, transitionType, frameId} = {}) {
-    if (frameId) {
-      throw new UnsupportedError("frameId not supported");
-    }
-
-    const opts = {
-      loadFlags: transitionToLoadFlag(transitionType),
-      referrerURI: referrer,
-      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
-    };
-    this.docShell.QueryInterface(Ci.nsIWebNavigation);
-    this.docShell.loadURI(url, opts);
-
-    return {frameId: "42"};
-  }
-
-  url() {
-    return this.content.location.href;
-  }
-
-  handleEvent({type}) {
-    const timestamp = Date.now();
-
-    switch (type) {
-    case "DOMContentLoaded":
-      this.emit("Page.domContentEventFired", {timestamp});
-      break;
-
-    case "pageshow":
-      this.emit("Page.loadEventFired", {timestamp});
-      break;
-    }
-  }
-}
-
-function transitionToLoadFlag(transitionType) {
-  switch (transitionType) {
-  case "reload":
-    return Ci.nsIWebNavigation.LOAD_FLAG_IS_REFRESH;
-  case "link":
-  default:
-    return Ci.nsIWebNavigation.LOAD_FLAG_IS_LINK;
-  }
-}
new file mode 100644
--- /dev/null
+++ b/remote/domains/ContentProcessDomains.jsm
@@ -0,0 +1,16 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ContentProcessDomains"];
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const ContentProcessDomains = {};
+
+XPCOMUtils.defineLazyModuleGetters(ContentProcessDomains, {
+  Log: "chrome://remote/content/domains/content/Log.jsm",
+  Page: "chrome://remote/content/domains/content/Page.jsm",
+});
new file mode 100644
--- /dev/null
+++ b/remote/domains/Domain.jsm
@@ -0,0 +1,33 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Domain"];
+
+const {EventEmitter} = ChromeUtils.import("chrome://remote/content/EventEmitter.jsm");
+
+class Domain {
+  constructor(session, target) {
+    this.session = session;
+    this.target = target;
+    this.name = this.constructor.name;
+
+    EventEmitter.decorate(this);
+  }
+
+  destructor() {}
+
+  get content() {
+    return this.session.content;
+  }
+
+  get docShell() {
+    return this.session.docShell;
+  }
+
+  get chromeEventHandler() {
+    return this.docShell.chromeEventHandler;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/remote/domains/Domains.jsm
@@ -0,0 +1,60 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Domains"];
+
+class Domains extends Map {
+  constructor(session, modules) {
+    super();
+    this.session = session;
+    this.modules = modules;
+  }
+
+  domainSupportsMethod(name, method) {
+    const domain = this.modules[name];
+    return domain && !!domain.prototype[method];
+  }
+
+  get(name) {
+    let inst = super.get(name);
+    if (!inst) {
+      inst = this.new(name);
+      this.set(inst);
+    }
+    return inst;
+  }
+
+  set(domain) {
+    super.set(domain.name, domain);
+  }
+
+  new(name) {
+    const Cls = this.modules[name];
+    if (!Cls) {
+      throw new Error("No such domain: " + name);
+    }
+
+    const inst = new Cls(this.session, this.session.target);
+    inst.on("*", this.session);
+
+    return inst;
+  }
+
+  delete(name) {
+    const inst = super.get(name);
+    if (inst) {
+      inst.off("*");
+      inst.destructor();
+      super.delete(inst.name);
+    }
+  }
+
+  clear() {
+    for (const domainName of this.keys()) {
+      this.delete(domainName);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/remote/domains/ParentProcessDomains.jsm
@@ -0,0 +1,15 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ParentProcessDomains"];
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const ParentProcessDomains = {};
+
+XPCOMUtils.defineLazyModuleGetters(ParentProcessDomains, {
+  Browser: "chrome://remote/content/domains/parent/Browser.jsm",
+});
new file mode 100644
--- /dev/null
+++ b/remote/domains/content/Log.jsm
@@ -0,0 +1,136 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Log"];
+
+const {Domain} = ChromeUtils.import("chrome://remote/content/domains/Domain.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+class Log extends Domain {
+  constructor(session, target) {
+    super(session, target);
+    this.enabled = false;
+  }
+
+  destructor() {
+    this.disable();
+  }
+
+  enable() {
+    if (!this.enabled) {
+      this.enabled = true;
+
+      Services.console.registerListener(this);
+      Services.obs.addObserver(this, "console-api-log-event");
+    }
+  }
+
+  disable() {
+    if (this.enabled) {
+      this.enabled = false;
+      Services.console.unregisterListener(this);
+      Services.obs.removeObserver(this, "console-api-log-event");
+    }
+  }
+
+  // nsIObserver
+
+  /**
+   * Takes all script error messages belonging to the current window
+   * and emits a "console-service-message" event.
+   *
+   * @param {nsIConsoleMessage} message
+   *     Message originating from the nsIConsoleService.
+   */
+  observe(message, topic) {
+    let entry;
+    if (message instanceof Ci.nsIScriptError) {
+      entry = fromScriptError(message);
+    } else if (message instanceof Ci.nsIConsoleMessage) {
+      entry = fromConsoleMessage(message);
+    } else if (topic == "console-api-log-event") {
+      entry = fromConsoleAPI(message.wrappedJSObject);
+    }
+
+    this.emit("Log.entryAdded", {entry});
+  }
+
+  // XPCOM
+
+  get QueryInterface() {
+    return ChromeUtils.generateQI([Ci.nsIConsoleListener]);
+  }
+}
+
+function fromConsoleMessage(message) {
+  const levels = {
+    [Ci.nsIConsoleMessage.debug]: "verbose",
+    [Ci.nsIConsoleMessage.info]: "info",
+    [Ci.nsIConsoleMessage.warn]: "warning",
+    [Ci.nsIConsoleMessage.error]: "error",
+  };
+  const level = levels[message.logLevel];
+
+  return {
+    source: "javascript",
+    level,
+    text: message.message,
+    timestamp: Date.now(),
+  };
+}
+
+function fromConsoleAPI(message) {
+  // message is a ConsoleEvent instance:
+  // https://searchfox.org/mozilla-central/rev/00c0d068ece99717bea7475f7dc07e61f7f35984/dom/webidl/Console.webidl#83-107
+
+  // A couple of possible level are defined here:
+  // https://searchfox.org/mozilla-central/rev/00c0d068ece99717bea7475f7dc07e61f7f35984/dom/console/Console.cpp#1086-1100
+  const levels = {
+    "log": "verbose",
+    "info": "info",
+    "warn": "warning",
+    "error": "error",
+    "exception": "error",
+
+  };
+  const level = levels[message.level] || "info";
+
+  return {
+    source: "javascript",
+    level,
+    text: message.arguments,
+    url: message.filename,
+    lineNumber: message.lineNumber,
+    stackTrace: message.stacktrace,
+    timestamp: Date.now(),
+  };
+}
+
+function fromScriptError(error) {
+  const {flags, errorMessage, sourceName, lineNumber, stack} = error;
+
+  // lossy reduction from bitmask to CDP string level
+  let level = "verbose";
+  if ((flags & Ci.nsIScriptError.exceptionFlag) ||
+      (flags & Ci.nsIScriptError.errorFlag)) {
+    level = "error";
+  } else if ((flags & Ci.nsIScriptError.warningFlag) ||
+      (flags & Ci.nsIScriptError.strictFlag)) {
+    level = "warning";
+  } else if (flags & Ci.nsIScriptError.infoFlag) {
+    level = "info";
+  }
+
+  return {
+    source: "javascript",
+    level,
+    text: errorMessage,
+    timestamp: Date.now(),
+    url: sourceName,
+    lineNumber,
+    stackTrace: stack,
+  };
+}
new file mode 100644
--- /dev/null
+++ b/remote/domains/content/Page.jsm
@@ -0,0 +1,97 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Page"];
+
+const {Domain} = ChromeUtils.import("chrome://remote/content/domains/Domain.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {UnsupportedError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
+
+class Page extends Domain {
+  constructor(session, target) {
+    super(session, target);
+    this.enabled = false;
+  }
+
+  destructor() {
+    this.disable();
+  }
+
+  QueryInterface(iid) {
+    if (iid.equals(Ci.nsIWebProgressListener) ||
+      iid.equals(Ci.nsISupportsWeakReference) ||
+      iid.equals(Ci.nsIObserver)) {
+      return this;
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+
+  // commands
+
+  async enable() {
+    if (!this.enabled) {
+      this.enabled = true;
+      this.chromeEventHandler.addEventListener("DOMContentLoaded", this,
+        {mozSystemGroup: true});
+      this.chromeEventHandler.addEventListener("pageshow", this,
+        {mozSystemGroup: true});
+    }
+  }
+
+  disable() {
+    if (this.enabled) {
+      this.chromeEventHandler.removeEventListener("DOMContentLoaded", this,
+        {mozSystemGroup: true});
+      this.chromeEventHandler.removeEventListener("pageshow", this,
+        {mozSystemGroup: true});
+      this.enabled = false;
+    }
+  }
+
+  async navigate({url, referrer, transitionType, frameId} = {}) {
+    if (frameId) {
+      throw new UnsupportedError("frameId not supported");
+    }
+
+    const opts = {
+      loadFlags: transitionToLoadFlag(transitionType),
+      referrerURI: referrer,
+      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+    };
+    this.docShell.QueryInterface(Ci.nsIWebNavigation);
+    this.docShell.loadURI(url, opts);
+
+    return {frameId: "42"};
+  }
+
+  url() {
+    return this.content.location.href;
+  }
+
+  handleEvent({type}) {
+    const timestamp = Date.now();
+
+    switch (type) {
+    case "DOMContentLoaded":
+      this.emit("Page.domContentEventFired", {timestamp});
+      break;
+
+    case "pageshow":
+      this.emit("Page.loadEventFired", {timestamp});
+      break;
+    }
+  }
+}
+
+function transitionToLoadFlag(transitionType) {
+  switch (transitionType) {
+  case "reload":
+    return Ci.nsIWebNavigation.LOAD_FLAG_IS_REFRESH;
+  case "link":
+  default:
+    return Ci.nsIWebNavigation.LOAD_FLAG_IS_LINK;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/remote/domains/parent/Browser.jsm
@@ -0,0 +1,28 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Browser"];
+
+const {Domain} = ChromeUtils.import("chrome://remote/content/domains/Domain.jsm");
+
+class Browser extends Domain {
+  constructor(session, target) {
+    super(session, target);
+  }
+
+  destructor() {
+  }
+
+  getVersion() {
+    return {
+      protocolVersion: "1",
+      product: "Firefox",
+      revision: "1",
+      userAgent: "Firefox",
+      jsVersion: "1.8.5",
+    };
+  }
+}
--- a/remote/jar.mn
+++ b/remote/jar.mn
@@ -1,18 +1,16 @@
 # 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/.
 
 remote.jar:
 % content remote %content/
   content/Connection.jsm (Connection.jsm)
   content/ContentProcessSession.jsm (ContentProcessSession.jsm)
-  content/Domain.jsm (Domain.jsm)
-  content/Domains.jsm (Domains.jsm)
   content/Error.jsm (Error.jsm)
   content/EventEmitter.jsm (EventEmitter.jsm)
   content/Handler.jsm (Handler.jsm)
   content/Log.jsm (Log.jsm)
   content/Observer.jsm (Observer.jsm)
   content/Prefs.jsm (Prefs.jsm)
   content/Protocol.jsm (Protocol.jsm)
   content/Session.jsm (Session.jsm)
@@ -21,18 +19,23 @@ remote.jar:
   content/TargetListener.jsm (TargetListener.jsm)
   content/Targets.jsm (Targets.jsm)
   content/WindowManager.jsm (WindowManager.jsm)
 
   # Frame scripts
   content/frame-script.js (frame-script.js)
 
   # domains
-  content/domain/Log.jsm (domain/Log.jsm)
-  content/domain/Page.jsm (domain/Page.jsm)
+  content/domains/Domain.jsm (domains/Domain.jsm)
+  content/domains/Domains.jsm (domains/Domains.jsm)
+  content/domains/ContentProcessDomains.jsm (domains/ContentProcessDomains.jsm)
+  content/domains/ParentProcessDomains.jsm (domains/ParentProcessDomains.jsm)
+  content/domains/content/Log.jsm (domains/content/Log.jsm)
+  content/domains/content/Page.jsm (domains/content/Page.jsm)
+  content/domains/parent/Browser.jsm (domains/parent/Browser.jsm)
 
   # transport layer
   content/server/HTTPD.jsm (../netwerk/test/httpserver/httpd.js)
   content/server/Packet.jsm (server/Packet.jsm)
   content/server/Socket.jsm (server/Socket.jsm)
   content/server/Stream.jsm (server/Stream.jsm)
   content/server/Transport.jsm (server/Transport.jsm)
   content/server/WebSocket.jsm (server/WebSocket.jsm)
--- a/remote/test/browser/browser_cdp.js
+++ b/remote/test/browser/browser_cdp.js
@@ -41,20 +41,24 @@ async function testCDP() {
       // Ensure debugging the right target, i.e. the one for our test tab.
       return list.find(target => {
         return target.url == TEST_URI;
       });
     },
   });
   ok(true, "CDP client has been instantiated");
 
-  const {Log, Page} = client;
+  const {Browser, Log, Page} = client;
+  ok("Browser" in client, "Browser domain is available");
   ok("Log" in client, "Log domain is available");
   ok("Page" in client, "Page domain is available");
 
+  const version = await Browser.getVersion();
+  is(version.product, "Firefox", "Browser.getVersion works");
+
   // receive console.log messages and print them
   Log.enable();
   ok(true, "Log domain has been enabled");
 
   Log.entryAdded(({entry}) => {
     const {timestamp, level, text, args} = entry;
     const msg = text || args.join(" ");
     console.log(`${new Date(timestamp)}\t${level.toUpperCase()}\t${msg}`);