Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 25 Feb 2016 14:32:51 -0800
changeset 321933 918df3a0bc1c4d07299e4f66274a7da923534577
parent 321913 97cf677ee66802809808a3e61a0ccb89542ca54e (current diff)
parent 321932 93aaf03b7399b9e2efd8d91b2ae060787b002281 (diff)
child 321981 7381731bbb411698aa74352841f79a7fb13020c3
child 322040 8e34b12969bf345b0cd765f612d59f8c0a6de444
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone47.0a1
first release with
nightly linux32
918df3a0bc1c / 47.0a1 / 20160226030256 / files
nightly linux64
918df3a0bc1c / 47.0a1 / 20160226030256 / files
nightly mac
918df3a0bc1c / 47.0a1 / 20160226030256 / files
nightly win32
918df3a0bc1c / 47.0a1 / 20160226030256 / files
nightly win64
918df3a0bc1c / 47.0a1 / 20160226030256 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge
devtools/client/inspector/test/browser_inspector_scrolling.js
--- a/browser/components/downloads/test/browser/browser.ini
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -1,14 +1,11 @@
 [DEFAULT]
 support-files = head.js
 
 [browser_basic_functionality.js]
-skip-if = buildapp == "mulet" || e10s
+skip-if = buildapp == "mulet"
 [browser_first_download_panel.js]
 skip-if = os == "linux" # Bug 949434
 [browser_overflow_anchor.js]
 skip-if = os == "linux" # Bug 952422
 [browser_confirm_unblock_download.js]
-
 [browser_iframe_gone_mid_download.js]
-skip-if = e10s
-
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-commands.js
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+   PlatformInfo,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> Map[name => Command]]
+var commandsMap = new WeakMap();
+
+function Command(description, shortcut) {
+  this.description = description;
+  this.shortcut = shortcut;
+}
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_commands", (type, directive, extension, manifest) => {
+  let commands = new Map();
+  for (let name of Object.keys(manifest.commands)) {
+    let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
+    let manifestCommand = manifest.commands[name];
+    let description = manifestCommand.description;
+    let shortcut = manifestCommand.suggested_key[os] || manifestCommand.suggested_key.default;
+    let command = new Command(description, shortcut);
+    commands.set(name, command);
+  }
+  commandsMap.set(extension, commands);
+});
+
+extensions.on("shutdown", (type, extension) => {
+  commandsMap.delete(extension);
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("commands", null, (extension, context) => {
+  return {
+    commands: {
+      getAll() {
+        let commands = Array.from(commandsMap.get(extension), ([name, command]) => {
+          return ({
+            name,
+            description: command.description,
+            shortcut: command.shortcut,
+          });
+        });
+        return Promise.resolve(commands);
+      },
+    },
+  };
+});
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -1,14 +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/.
 
 browser.jar:
     content/browser/extension.svg
     content/browser/ext-utils.js
+    content/browser/ext-commands.js
     content/browser/ext-contextMenus.js
     content/browser/ext-browserAction.js
     content/browser/ext-pageAction.js
     content/browser/ext-desktop-runtime.js
     content/browser/ext-tabs.js
     content/browser/ext-windows.js
     content/browser/ext-bookmarks.js
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -4,16 +4,17 @@ support-files =
   context.html
   ctxmenu-image.png
   context_tabs_onUpdated_page.html
   context_tabs_onUpdated_iframe.html
   file_popup_api_injection_a.html
   file_popup_api_injection_b.html
 
 [browser_ext_simple.js]
+[browser_ext_commands.js]
 [browser_ext_currentWindow.js]
 [browser_ext_browserAction_simple.js]
 [browser_ext_browserAction_pageAction_icon.js]
 [browser_ext_browserAction_context.js]
 [browser_ext_browserAction_disabled.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_browserAction_popup.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands.js
@@ -0,0 +1,81 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm");
+
+add_task(function* () {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "name": "Commands Extension",
+      "commands": {
+        "with-desciption": {
+          "suggested_key": {
+            "default": "Ctrl+Shift+Y",
+          },
+          "description": "should have a description",
+        },
+        "without-description": {
+          "suggested_key": {
+            "default": "Ctrl+Shift+D",
+          },
+        },
+        "with-platform-info": {
+          "suggested_key": {
+            "mac": "Ctrl+Shift+M",
+            "linux": "Ctrl+Shift+L",
+            "windows": "Ctrl+Shift+W",
+            "android": "Ctrl+Shift+A",
+          },
+        },
+      },
+    },
+
+    background: function() {
+      browser.test.onMessage.addListener((message, additionalScope) => {
+        browser.commands.getAll((commands) => {
+          let errorMessage = "getAll should return an array of commands";
+          browser.test.assertEq(commands.length, 3, errorMessage);
+
+          let command = commands.find(c => c.name == "with-desciption");
+
+          errorMessage = "The description should match what is provided in the manifest";
+          browser.test.assertEq("should have a description", command.description, errorMessage);
+
+          errorMessage = "The shortcut should match the default shortcut provided in the manifest";
+          browser.test.assertEq("Ctrl+Shift+Y", command.shortcut, errorMessage);
+
+          command = commands.find(c => c.name == "without-description");
+
+          errorMessage = "The description should be empty when it is not provided";
+          browser.test.assertEq(null, command.description, errorMessage);
+
+          errorMessage = "The shortcut should match the default shortcut provided in the manifest";
+          browser.test.assertEq("Ctrl+Shift+D", command.shortcut, errorMessage);
+
+          let platformKeys = {
+            macosx: "M",
+            linux: "L",
+            win: "W",
+            android: "A",
+          };
+
+          command = commands.find(c => c.name == "with-platform-info");
+          let platformKey = platformKeys[additionalScope.platform];
+          let shortcut = `Ctrl+Shift+${platformKey}`;
+          errorMessage = `The shortcut should match the one provided in the manifest for OS='${additionalScope.platform}'`;
+          browser.test.assertEq(shortcut, command.shortcut, errorMessage);
+
+          browser.test.notifyPass("commands");
+        });
+      });
+      browser.test.sendMessage("ready");
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+  extension.sendMessage("additional-scope", {platform: AppConstants.platform});
+  yield extension.awaitFinish("commands");
+  yield extension.unload();
+});
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -541,16 +541,17 @@ BrowserGlue.prototype = {
 
     if (AppConstants.NIGHTLY_BUILD) {
       os.addObserver(this, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false);
     }
 
     ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
+    ExtensionManagement.registerScript("chrome://browser/content/ext-commands.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-desktop-runtime.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-bookmarks.js");
 
     ExtensionManagement.registerSchema("chrome://browser/content/schemas/bookmarks.json");
     ExtensionManagement.registerSchema("chrome://browser/content/schemas/browser_action.json");
--- a/devtools/client/aboutdebugging/components/aboutdebugging.js
+++ b/devtools/client/aboutdebugging/components/aboutdebugging.js
@@ -1,24 +1,23 @@
 /* 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/. */
 
-/* global React */
+/* eslint-env browser */
 
 "use strict";
 
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
-loader.lazyRequireGetter(this, "AddonsTab",
-  "devtools/client/aboutdebugging/components/addons-tab", true);
-loader.lazyRequireGetter(this, "TabMenu",
-  "devtools/client/aboutdebugging/components/tab-menu", true);
-loader.lazyRequireGetter(this, "WorkersTab",
-  "devtools/client/aboutdebugging/components/workers-tab", true);
+const Services = require("Services");
+
+const React = require("devtools/client/shared/vendor/react");
+const { TabMenu } = require("./tab-menu");
+
+loader.lazyRequireGetter(this, "AddonsTab", "./components/addons-tab", true);
+loader.lazyRequireGetter(this, "WorkersTab", "./components/workers-tab", true);
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 const tabs = [
   { id: "addons", name: Strings.GetStringFromName("addons"),
     icon: "chrome://devtools/skin/images/debugging-addons.svg",
     component: AddonsTab },
@@ -33,23 +32,23 @@ exports.AboutDebuggingApp = React.create
 
   getInitialState() {
     return {
       selectedTabId: defaultTabId
     };
   },
 
   componentDidMount() {
-    this.props.window.addEventListener("hashchange", this.onHashChange);
+    window.addEventListener("hashchange", this.onHashChange);
     this.onHashChange();
     this.props.telemetry.toolOpened("aboutdebugging");
   },
 
   componentWillUnmount() {
-    this.props.window.removeEventListener("hashchange", this.onHashChange);
+    window.removeEventListener("hashchange", this.onHashChange);
     this.props.telemetry.toolClosed("aboutdebugging");
     this.props.telemetry.destroy();
   },
 
   render() {
     let { client } = this.props;
     let { selectedTabId } = this.state;
     let selectTab = this.selectTab;
@@ -60,25 +59,24 @@ exports.AboutDebuggingApp = React.create
       "div", { className: "app"},
         React.createElement(TabMenu, { tabs, selectedTabId, selectTab }),
         React.createElement("div", { className: "main-content" },
           React.createElement(selectedTab.component, { client }))
         );
   },
 
   onHashChange() {
-    let tabId = this.props.window.location.hash.substr(1);
+    let tabId = window.location.hash.substr(1);
 
     let isValid = tabs.some(t => t.id == tabId);
     if (isValid) {
       this.setState({ selectedTabId: tabId });
     } else {
       // If the current hash matches no valid category, navigate to the default
       // tab.
       this.selectTab(defaultTabId);
     }
   },
 
   selectTab(tabId) {
-    let win = this.props.window;
-    win.location.hash = "#" + tabId;
+    window.location.hash = "#" + tabId;
   }
 });
--- a/devtools/client/aboutdebugging/components/addons-controls.js
+++ b/devtools/client/aboutdebugging/components/addons-controls.js
@@ -1,24 +1,24 @@
 /* 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/. */
 
-/* global React */
+/* eslint-env browser */
 
 "use strict";
 
-loader.lazyRequireGetter(this, "Ci", "chrome", true);
-loader.lazyRequireGetter(this, "Cc", "chrome", true);
-loader.lazyRequireGetter(this, "React", "devtools/client/shared/vendor/react");
-loader.lazyRequireGetter(this, "Services");
-
 loader.lazyImporter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 
+const { Cc, Ci } = require("chrome");
+const Services = require("Services");
+
+const React = require("devtools/client/shared/vendor/react");
+
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
                       "/about:debugging#Enabling_add-on_debugging";
 
 exports.AddonsControls = React.createClass({
   displayName: "AddonsControls",
@@ -53,33 +53,31 @@ exports.AddonsControls = React.createCla
   },
 
   onEnableAddonDebuggingChange(event) {
     let enabled = event.target.checked;
     Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
     Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
   },
 
-  loadAddonFromFile(event) {
-    let win = event.target.ownerDocument.defaultView;
-
+  loadAddonFromFile() {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-    fp.init(win,
+    fp.init(window,
       Strings.GetStringFromName("selectAddonFromFile"),
       Ci.nsIFilePicker.modeOpen);
     let res = fp.show();
     if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
       return;
     }
     let file = fp.file;
     // AddonManager.installTemporaryAddon accepts either
     // addon directory or final xpi file.
     if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
       file = file.parent;
     }
     try {
       AddonManager.installTemporaryAddon(file);
     } catch (e) {
-      win.alert("Error while installing the addon:\n" + e.message + "\n");
+      window.alert("Error while installing the addon:\n" + e.message + "\n");
       throw e;
     }
   },
 });
--- a/devtools/client/aboutdebugging/components/addons-tab.js
+++ b/devtools/client/aboutdebugging/components/addons-tab.js
@@ -1,28 +1,23 @@
 /* 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/. */
 
-/* global AddonManager, React */
+/* global React */
 
 "use strict";
 
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
-loader.lazyRequireGetter(this, "TargetList",
-  "devtools/client/aboutdebugging/components/target-list", true);
-loader.lazyRequireGetter(this, "TabHeader",
-  "devtools/client/aboutdebugging/components/tab-header", true);
-loader.lazyRequireGetter(this, "AddonsControls",
-  "devtools/client/aboutdebugging/components/addons-controls", true);
-loader.lazyRequireGetter(this, "Services");
+const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const Services = require("Services");
 
-loader.lazyImporter(this, "AddonManager",
-  "resource://gre/modules/AddonManager.jsm");
+const React = require("devtools/client/shared/vendor/react");
+const { AddonsControls } = require("./addons-controls");
+const { TabHeader } = require("./tab-header");
+const { TargetList } = require("./target-list");
 
 const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
 const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
 
--- a/devtools/client/aboutdebugging/components/tab-header.js
+++ b/devtools/client/aboutdebugging/components/tab-header.js
@@ -1,18 +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/. */
 
-/* global React */
-
 "use strict";
 
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
+const React = require("devtools/client/shared/vendor/react");
 
 exports.TabHeader = React.createClass({
   displayName: "TabHeader",
 
   render() {
     let { name, id } = this.props;
 
     return React.createElement(
--- a/devtools/client/aboutdebugging/components/tab-menu-entry.js
+++ b/devtools/client/aboutdebugging/components/tab-menu-entry.js
@@ -1,18 +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/. */
 
-/* global React */
-
 "use strict";
 
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
+const React = require("devtools/client/shared/vendor/react");
 
 exports.TabMenuEntry = React.createClass({
   displayName: "TabMenuEntry",
 
   render() {
     let { icon, name, selected } = this.props;
 
     // Here .category, .category-icon, .category-name classnames are used to
--- a/devtools/client/aboutdebugging/components/tab-menu.js
+++ b/devtools/client/aboutdebugging/components/tab-menu.js
@@ -1,20 +1,18 @@
 /* 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/. */
 
 /* global React */
 
 "use strict";
 
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
-loader.lazyRequireGetter(this, "TabMenuEntry",
-  "devtools/client/aboutdebugging/components/tab-menu-entry", true);
+const React = require("devtools/client/shared/vendor/react");
+const { TabMenuEntry } = require("./tab-menu-entry");
 
 exports.TabMenu = React.createClass({
   displayName: "TabMenu",
 
   render() {
     let { tabs, selectedTabId, selectTab } = this.props;
     let tabLinks = tabs.map(({ id, name, icon }) => {
       let selected = id == selectedTabId;
--- a/devtools/client/aboutdebugging/components/target-list.js
+++ b/devtools/client/aboutdebugging/components/target-list.js
@@ -1,24 +1,24 @@
 /* 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/. */
 
 /* global React */
 
 "use strict";
 
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
-loader.lazyRequireGetter(this, "Target",
-  "devtools/client/aboutdebugging/components/target", true);
-loader.lazyRequireGetter(this, "Services");
+const Services = require("Services");
+
+const React = require("devtools/client/shared/vendor/react");
+const { Target } = require("./target");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
+
 const LocaleCompare = (a, b) => {
   return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
 };
 
 exports.TargetList = React.createClass({
   displayName: "TargetList",
 
   render() {
--- a/devtools/client/aboutdebugging/components/target.js
+++ b/devtools/client/aboutdebugging/components/target.js
@@ -1,72 +1,84 @@
 /* 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/. */
 
-/* global alert, BrowserToolboxProcess, gDevTools, React, TargetFactory,
-   Toolbox */
+/* eslint-env browser */
 
 "use strict";
 
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
 loader.lazyRequireGetter(this, "TargetFactory",
-  "devtools/client/framework/target", true);
+      "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "gDevTools",
+      "devtools/client/framework/devtools", true);
 loader.lazyRequireGetter(this, "Toolbox",
-  "devtools/client/framework/toolbox", true);
-loader.lazyRequireGetter(this, "Services");
+      "devtools/client/framework/toolbox", true);
 
 loader.lazyImporter(this, "BrowserToolboxProcess",
-  "resource://devtools/client/framework/ToolboxProcess.jsm");
-loader.lazyRequireGetter(this, "gDevTools",
-  "devtools/client/framework/devtools", true);
+      "resource://devtools/client/framework/ToolboxProcess.jsm");
+
+const Services = require("Services");
+const React = require("devtools/client/shared/vendor/react");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 exports.Target = React.createClass({
   displayName: "Target",
 
-  debug() {
-    let { client, target } = this.props;
-    switch (target.type) {
-      case "extension":
-        BrowserToolboxProcess.init({ addonID: target.addonID });
-        break;
-      case "serviceworker":
-        // Fall through.
-      case "sharedworker":
-        // Fall through.
-      case "worker":
-        let workerActor = this.props.target.actorID;
-        client.attachWorker(workerActor, (response, workerClient) => {
-          gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
-            "jsdebugger", Toolbox.HostType.WINDOW)
-            .then(toolbox => {
-              toolbox.once("destroy", () => workerClient.detach());
-            });
-        });
-        break;
-      default:
-        alert("Not implemented yet!");
-    }
-  },
-
   render() {
     let { target, debugDisabled } = this.props;
+    let isServiceWorker = (target.type === "serviceworker");
+    let isRunning = (!isServiceWorker || target.workerActor);
     return React.createElement("div", { className: "target" },
       React.createElement("img", {
         className: "target-icon",
         role: "presentation",
         src: target.icon }),
       React.createElement("div", { className: "target-details" },
-        React.createElement("div", { className: "target-name" }, target.name),
-        React.createElement("div", { className: "target-url" }, target.url)
+        React.createElement("div", { className: "target-name" }, target.name)
       ),
-      React.createElement("button", {
-        className: "debug-button",
-        onClick: this.debug,
-        disabled: debugDisabled,
-      }, Strings.GetStringFromName("debug"))
+      (isRunning ?
+        React.createElement("button", {
+          className: "debug-button",
+          onClick: this.debug,
+          disabled: debugDisabled,
+        }, Strings.GetStringFromName("debug")) :
+        null
+      )
     );
   },
+
+  debug() {
+    let { target } = this.props;
+    switch (target.type) {
+      case "extension":
+        BrowserToolboxProcess.init({ addonID: target.addonID });
+        break;
+      case "serviceworker":
+        if (target.workerActor) {
+          this.openWorkerToolbox(target.workerActor);
+        }
+        break;
+      case "sharedworker":
+        this.openWorkerToolbox(target.workerActor);
+        break;
+      case "worker":
+        this.openWorkerToolbox(target.workerActor);
+        break;
+      default:
+        window.alert("Not implemented yet!");
+        break;
+    }
+  },
+
+  openWorkerToolbox(workerActor) {
+    let { client } = this.props;
+    client.attachWorker(workerActor, (response, workerClient) => {
+      gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+        "jsdebugger", Toolbox.HostType.WINDOW)
+        .then(toolbox => {
+          toolbox.once("destroy", () => workerClient.detach());
+        });
+    });
+  },
 });
--- a/devtools/client/aboutdebugging/components/workers-tab.js
+++ b/devtools/client/aboutdebugging/components/workers-tab.js
@@ -1,27 +1,21 @@
 /* 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/. */
 
-/* global React */
-
 "use strict";
 
-loader.lazyRequireGetter(this, "Ci",
-  "chrome", true);
-loader.lazyRequireGetter(this, "React",
-  "devtools/client/shared/vendor/react");
-loader.lazyRequireGetter(this, "TargetList",
-  "devtools/client/aboutdebugging/components/target-list", true);
-loader.lazyRequireGetter(this, "TabHeader",
-  "devtools/client/aboutdebugging/components/tab-header", true);
-loader.lazyRequireGetter(this, "Services");
+const { Ci } = require("chrome");
+const { Task } = require("resource://gre/modules/Task.jsm");
+const Services = require("Services");
 
-loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm");
+const React = require("devtools/client/shared/vendor/react");
+const { TargetList } = require("./target-list");
+const { TabHeader } = require("./tab-header");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
 
 exports.WorkersTab = React.createClass({
   displayName: "WorkersTab",
 
@@ -33,23 +27,25 @@ exports.WorkersTab = React.createClass({
         other: []
       }
     };
   },
 
   componentDidMount() {
     let client = this.props.client;
     client.addListener("workerListChanged", this.update);
+    client.addListener("serviceWorkerRegistrationListChanged", this.update);
     client.addListener("processListChanged", this.update);
     this.update();
   },
 
   componentWillUnmount() {
     let client = this.props.client;
     client.removeListener("processListChanged", this.update);
+    client.removeListener("serviceWorkerRegistrationListChanged", this.update);
     client.removeListener("workerListChanged", this.update);
   },
 
   render() {
     let { client } = this.props;
     let { workers } = this.state;
 
     return React.createElement(
@@ -72,57 +68,95 @@ exports.WorkersTab = React.createClass({
             id: "other-workers",
             name: Strings.GetStringFromName("otherWorkers"),
             targets: workers.other, client }))
       );
   },
 
   update() {
     let workers = this.getInitialState().workers;
+
     this.getWorkerForms().then(forms => {
-      forms.forEach(form => {
+      forms.registrations.forEach(form => {
+        workers.service.push({
+          type: "serviceworker",
+          icon: WorkerIcon,
+          name: form.url,
+          url: form.url,
+          scope: form.scope,
+          registrationActor: form.actor
+        });
+      });
+
+      forms.workers.forEach(form => {
         let worker = {
-          name: form.url,
+          type: "worker",
           icon: WorkerIcon,
-          actorID: form.actor
+          name: form.url,
+          url: form.url,
+          workerActor: form.actor
         };
         switch (form.type) {
           case Ci.nsIWorkerDebugger.TYPE_SERVICE:
-            worker.type = "serviceworker";
-            workers.service.push(worker);
+            for (let registration of workers.service) {
+              if (registration.scope === form.scope) {
+                // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
+                // have a scriptSpec, but its associated WorkerDebugger does.
+                if (!registration.url) {
+                  registration.name = registration.url = form.url;
+                }
+                registration.workerActor = form.actor;
+                break;
+              }
+            }
             break;
           case Ci.nsIWorkerDebugger.TYPE_SHARED:
             worker.type = "sharedworker";
             workers.shared.push(worker);
             break;
           default:
-            worker.type = "worker";
             workers.other.push(worker);
         }
       });
+
+      // XXX: Filter out the service worker registrations for which we couldn't
+      // find the scriptSpec.
+      workers.service = workers.service.filter(reg => !!reg.url);
+
       this.setState({ workers });
     });
   },
 
   getWorkerForms: Task.async(function*() {
     let client = this.props.client;
+    let registrations = [];
+    let workers = [];
 
-    // List workers from the Parent process
-    let result = yield client.mainRoot.listWorkers();
-    let forms = result.workers;
+    try {
+      // List service worker registrations
+      ({ registrations } =
+        yield client.mainRoot.listServiceWorkerRegistrations());
+
+      // List workers from the Parent process
+      ({ workers } = yield client.mainRoot.listWorkers());
 
-    // And then from the Child processes
-    let { processes } = yield client.mainRoot.listProcesses();
-    for (let process of processes) {
-      // Ignore parent process
-      if (process.parent) {
-        continue;
+      // And then from the Child processes
+      let { processes } = yield client.mainRoot.listProcesses();
+      for (let process of processes) {
+        // Ignore parent process
+        if (process.parent) {
+          continue;
+        }
+        let { form } = yield client.getProcess(process.id);
+        let processActor = form.actor;
+        let response = yield client.request({
+          to: processActor,
+          type: "listWorkers"
+        });
+        workers = workers.concat(response.workers);
       }
-      let { form } = yield client.getProcess(process.id);
-      let processActor = form.actor;
-      let { workers } = yield client.request({to: processActor,
-                                              type: "listWorkers"});
-      forms = forms.concat(workers);
+    } catch (e) {
+      // Something went wrong, maybe our client is disconnected?
     }
 
-    return forms;
+    return { registrations, workers };
   }),
 });
--- a/devtools/client/aboutdebugging/initializer.js
+++ b/devtools/client/aboutdebugging/initializer.js
@@ -1,43 +1,50 @@
 /* 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/. */
 
 /* eslint-env browser */
-/* global DebuggerClient, DebuggerServer, React */
+/* global DebuggerClient, DebuggerServer */
 
 "use strict";
 
 const { loader } = Components.utils.import(
   "resource://devtools/shared/Loader.jsm", {});
 
 loader.lazyRequireGetter(this, "DebuggerClient",
   "devtools/shared/client/main", true);
 loader.lazyRequireGetter(this, "DebuggerServer",
   "devtools/server/main", true);
 loader.lazyRequireGetter(this, "Telemetry",
   "devtools/client/shared/telemetry");
-loader.lazyRequireGetter(this, "AboutDebuggingApp",
-  "devtools/client/aboutdebugging/components/aboutdebugging", true);
+
+const { BrowserLoader } = Components.utils.import(
+  "resource://devtools/client/shared/browser-loader.js", {});
+const { require } =
+  BrowserLoader("resource://devtools/client/aboutdebugging/", window);
+
+const React = require("devtools/client/shared/vendor/react");
+const { AboutDebuggingApp } = require("./components/aboutdebugging");
 
 var AboutDebugging = {
   init() {
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
     }
     DebuggerServer.allowChromeProcess = true;
     this.client = new DebuggerClient(DebuggerServer.connectPipe());
 
     this.client.connect().then(() => {
       let client = this.client;
       let telemetry = new Telemetry();
-      React.render(React.createElement(AboutDebuggingApp,
-        { client, telemetry, window }), document.querySelector("#body"));
+
+      let app = React.createElement(AboutDebuggingApp, { client, telemetry });
+      React.render(app, document.querySelector("#body"));
     });
   },
 
   destroy() {
     React.unmountComponentAtNode(document.querySelector("#body"));
 
     this.client.close();
     this.client = null;
--- a/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
@@ -62,17 +62,18 @@ add_task(function* () {
     });
   });
   ok(true, "Service worker registration resolved");
 
   // Retrieve the DEBUG button for the worker
   let names = [...document.querySelectorAll("#service-workers .target-name")];
   let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
   ok(name, "Found the service worker in the list");
-  let debugBtn = name.parentNode.parentNode.querySelector("button");
+  let targetElement = name.parentNode.parentNode;
+  let debugBtn = targetElement.querySelector(".debug-button");
   ok(debugBtn, "Found its debug button");
 
   // Click on it and wait for the toolbox to be ready
   let onToolboxReady = new Promise(done => {
     gDevTools.once("toolbox-ready", function(e, toolbox) {
       done(toolbox);
     });
   });
@@ -83,26 +84,28 @@ add_task(function* () {
   // Wait for more than the regular timeout,
   // so that if the worker freezing doesn't work,
   // it will be destroyed and removed from the list
   yield new Promise(done => {
     setTimeout(done, SW_TIMEOUT * 2);
   });
 
   assertHasWorker(true, document, "service-workers", SERVICE_WORKER);
+  ok(targetElement.querySelector(".debug-button"),
+    "The debug button is still there");
 
   yield toolbox.destroy();
   toolbox = null;
 
   // Now ensure that the worker is correctly destroyed
   // after we destroy the toolbox.
-  // The list should update once it get destroyed.
-  yield waitForMutation(serviceWorkersElement, { childList: true });
-
-  assertHasWorker(false, document, "service-workers", SERVICE_WORKER);
+  // The DEBUG button should disappear once the worker is destroyed.
+  yield waitForMutation(targetElement, { childList: true });
+  ok(!targetElement.querySelector(".debug-button"),
+    "The debug button was removed when the worker was killed");
 
   // Finally, unregister the service worker itself
   // Use message manager to work with e10s
   frameScript = function() {
     // Retrieve the `sw` promise created in the html page
     let { sw } = content.wrappedJSObject;
     sw.then(function(registration) {
       registration.unregister().then(function() {
@@ -119,11 +122,16 @@ add_task(function* () {
   yield new Promise(done => {
     mm.addMessageListener("sw-unregistered", function listener() {
       mm.removeMessageListener("sw-unregistered", listener);
       done();
     });
   });
   ok(true, "Service worker registration unregistered");
 
+  // Now ensure that the worker registration is correctly removed.
+  // The list should update once the registration is destroyed.
+  yield waitForMutation(serviceWorkersElement, { childList: true });
+  assertHasWorker(false, document, "service-workers", SERVICE_WORKER);
+
   yield removeTab(swTab);
   yield closeAboutDebugging(tab);
 });
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -37,17 +37,16 @@ support-files =
 [browser_inspector_breadcrumbs_mutations.js]
 [browser_inspector_delete-selected-node-01.js]
 [browser_inspector_delete-selected-node-02.js]
 [browser_inspector_delete-selected-node-03.js]
 [browser_inspector_destroy-after-navigation.js]
 [browser_inspector_destroy-before-ready.js]
 [browser_inspector_expand-collapse.js]
 [browser_inspector_gcli-inspect-command.js]
-skip-if = e10s # GCLI isn't e10s compatible. See bug 1128988.
 [browser_inspector_highlighter-01.js]
 [browser_inspector_highlighter-02.js]
 [browser_inspector_highlighter-03.js]
 [browser_inspector_highlighter-04.js]
 [browser_inspector_highlighter-by-type.js]
 [browser_inspector_highlighter-comments.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
@@ -97,18 +96,16 @@ skip-if = e10s && debug && os == 'win' #
 [browser_inspector_pane-toggle-03.js]
 [browser_inspector_picker-stop-on-destroy.js]
 [browser_inspector_picker-stop-on-tool-change.js]
 [browser_inspector_pseudoclass-lock.js]
 [browser_inspector_pseudoclass-menu.js]
 [browser_inspector_reload-01.js]
 [browser_inspector_reload-02.js]
 [browser_inspector_remove-iframe-during-load.js]
-[browser_inspector_scrolling.js]
-skip-if = e10s # Test synthesize scrolling events in content. Also, see bug 1035661.
 [browser_inspector_search-01.js]
 [browser_inspector_search-02.js]
 [browser_inspector_search-03.js]
 [browser_inspector_search-04.js]
 [browser_inspector_search-05.js]
 [browser_inspector_search-06.js]
 [browser_inspector_search-07.js]
 [browser_inspector_search-reserved.js]
deleted file mode 100644
--- a/devtools/client/inspector/test/browser_inspector_scrolling.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80: */
-/* 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";
-
-// Test that highlighted nodes can be scrolled.
-// TODO: This doesn't test anything useful. See b.m.o 1035661.
-const IFRAME_SRC = "data:text/html;charset=utf-8," +
-  "<div style='height:500px; width:500px; border:1px solid gray;'>" +
-    "big div" +
-  "</div>";
-
-const TEST_URI = "data:text/html;charset=utf-8," +
-  "<p>browser_inspector_scrolling.js</p>" +
-  "<iframe src=\"" + IFRAME_SRC + "\" />";
-
-add_task(function* () {
-  let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
-
-  let iframe = getNode("iframe");
-  let div = getNode("div", { document: iframe.contentDocument });
-  let divFront = yield getNodeFrontInFrame("div", "iframe", inspector);
-
-  info("Waiting for highlighter box model to appear.");
-  yield toolbox.highlighter.showBoxModel(divFront);
-
-  let scrolled = once(gBrowser.selectedBrowser, "scroll");
-
-  info("Scrolling iframe.");
-  EventUtils.synthesizeWheel(div, 10, 10,
-    { deltaY: 50.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL },
-    iframe.contentWindow);
-
-  info("Waiting for scroll event");
-  yield scrolled;
-
-  let isRetina = devicePixelRatio === 2;
-  is(iframe.contentDocument.body.scrollTop,
-    isRetina ? 25 : 50, "inspected iframe scrolled");
-
-  info("Hiding box model.");
-  yield toolbox.highlighter.hideBoxModel();
-});
--- a/devtools/client/responsivedesign/responsivedesign.jsm
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -58,22 +58,28 @@ var Manager = {
     }
   },
 
   /**
    * Launches the responsive mode.
    *
    * @param aWindow the main window.
    * @param aTab the tab targeted.
+   * @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab.
    */
-  runIfNeeded: function(aWindow, aTab) {
+  runIfNeeded: Task.async(function*(aWindow, aTab) {
+    let ui;
     if (!this.isActiveForTab(aTab)) {
-      new ResponsiveUI(aWindow, aTab);
+      ui = new ResponsiveUI(aWindow, aTab);
+      yield ui.inited;
+    } else {
+      ui = this.getResponsiveUIForTab(aTab);
     }
-  },
+    return ui;
+  }),
 
   /**
    * Returns true if responsive view is active for the provided tab.
    *
    * @param aTab the tab targeted.
    */
   isActiveForTab: function(aTab) {
     return ActiveTabs.has(aTab);
@@ -92,19 +98,17 @@ var Manager = {
    * @param aWindow the browser window.
    * @param aTab the tab targeted.
    * @param aCommand the command name.
    * @param aArgs command arguments.
    */
   handleGcliCommand: Task.async(function*(aWindow, aTab, aCommand, aArgs) {
     switch (aCommand) {
       case "resize to":
-        this.runIfNeeded(aWindow, aTab);
-        let ui = ActiveTabs.get(aTab);
-        yield ui.inited;
+        let ui = yield this.runIfNeeded(aWindow, aTab);
         ui.setSize(aArgs.width, aArgs.height);
         break;
       case "resize on":
         this.runIfNeeded(aWindow, aTab);
         break;
       case "resize off":
         if (this.isActiveForTab(aTab)) {
           yield ActiveTabs.get(aTab).close();
--- a/devtools/client/shared/browser-loader.js
+++ b/devtools/client/shared/browser-loader.js
@@ -13,50 +13,16 @@ Cu.import("resource://gre/modules/AppCon
 
 const BROWSER_BASED_DIRS = [
   "resource://devtools/client/jsonview",
   "resource://devtools/client/shared/vendor",
   "resource://devtools/client/shared/components",
   "resource://devtools/client/shared/redux"
 ];
 
-function clearCache() {
-  Services.obs.notifyObservers(null, "startupcache-invalidate", null);
-}
-
-function hotReloadFile(window, require, loader, componentProxies, fileURI) {
-  dump("Hot reloading: " + fileURI + "\n");
-
-  if (fileURI.match(/\.js$/)) {
-    // Test for React proxy components
-    const proxy = componentProxies.get(fileURI);
-    if (proxy) {
-      // Remove the old module and re-require the new one; the require
-      // hook in the loader will take care of the rest
-      delete loader.modules[fileURI];
-      clearCache();
-      require(fileURI);
-    }
-  } else if (fileURI.match(/\.css$/)) {
-    const links = [...window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "link")];
-    links.forEach(link => {
-      if (link.href.indexOf(fileURI) === 0) {
-        const parentNode = link.parentNode;
-        const newLink = window.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
-        newLink.rel = "stylesheet";
-        newLink.type = "text/css";
-        newLink.href = fileURI + "?s=" + Math.random();
-
-        parentNode.insertBefore(newLink, link);
-        parentNode.removeChild(link);
-      }
-    });
-  }
-}
-
 /*
  * Create a loader to be used in a browser environment. This evaluates
  * modules in their own environment, but sets window (the normal
  * global object) as the sandbox prototype, so when a variable is not
  * defined it checks `window` before throwing an error. This makes all
  * browser APIs available to modules by default, like a normal browser
  * environment, but modules are still evaluated in their own scope.
  *
@@ -73,16 +39,33 @@ function hotReloadFile(window, require, 
  * @param Object window
  *        The window instance to evaluate modules within
  * @return Object
  *         An object with two properties:
  *         - loader: the Loader instance
  *         - require: a function to require modules with
  */
 function BrowserLoader(baseURI, window) {
+  const browserLoaderBuilder = new BrowserLoaderBuilder(baseURI, window);
+  return {
+    loader: browserLoaderBuilder.loader,
+    require: browserLoaderBuilder.require
+  };
+}
+
+/**
+ * Private class used to build the Loader instance and require method returned
+ * by BrowserLoader(baseURI, window).
+ *
+ * @param string baseURI
+ *        Base path to load modules from.
+ * @param Object window
+ *        The window instance to evaluate modules within
+ */
+function BrowserLoaderBuilder(baseURI, window) {
   const loaderOptions = devtools.require("@loader/options");
   const dynamicPaths = {};
   const componentProxies = new Map();
   const hotReloadEnabled = Services.prefs.getBoolPref("devtools.loader.hotreload");
 
   if(AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES) {
     dynamicPaths["devtools/client/shared/vendor/react"] =
       "resource://devtools/client/shared/vendor/react-dev";
@@ -121,16 +104,23 @@ function BrowserLoader(baseURI, window) 
       //     ... code ...
       //   });
       //
       // Bug 1248830 will work out a better plan here for our content module
       // loading needs, especially as we head towards devtools.html.
       define(factory) {
         factory(this.require, this.exports, this.module);
       },
+      // Allow modules to use the DevToolsLoader lazy loading helpers.
+      loader: {
+        lazyGetter: devtools.lazyGetter,
+        lazyImporter: devtools.lazyImporter,
+        lazyServiceGetter: devtools.lazyServiceGetter,
+        lazyRequireGetter: this.lazyRequireGetter.bind(this),
+      },
     }
   };
 
   if(hotReloadEnabled) {
     opts.loadModuleHook = (module, require) => {
       const { uri, exports } = module;
 
       if (exports.prototype &&
@@ -150,34 +140,86 @@ function BrowserLoader(baseURI, window) 
           instances.forEach(getForceUpdate(React));
           module.exports = proxy.get();
         }
       }
       return exports;
     }
   }
 
-
   const mainModule = loaders.Module(baseURI, joinURI(baseURI, "main.js"));
-  const mainLoader = loaders.Loader(opts);
-  const require = loaders.Require(mainLoader, mainModule);
+  this.loader = loaders.Loader(opts);
+  this.require = loaders.Require(this.loader, mainModule);
 
   if (hotReloadEnabled) {
     const watcher = devtools.require("devtools/client/shared/file-watcher");
-    function onFileChanged(_, fileURI) {
-      hotReloadFile(window, require, mainLoader, componentProxies, fileURI);
-    }
+    const onFileChanged = (_, fileURI) => {
+      this.hotReloadFile(window, componentProxies, fileURI);
+    };
     watcher.on("file-changed", onFileChanged);
 
     window.addEventListener("unload", () => {
       watcher.off("file-changed", onFileChanged);
     });
   }
+}
 
-  return {
-    loader: mainLoader,
-    require: require
-  };
-}
+BrowserLoaderBuilder.prototype = {
+  /**
+   * Define a getter property on the given object that requires the given
+   * module. This enables delaying importing modules until the module is
+   * actually used.
+   *
+   * @param Object obj
+   *    The object to define the property on.
+   * @param String property
+   *    The property name.
+   * @param String module
+   *    The module path.
+   * @param Boolean destructure
+   *    Pass true if the property name is a member of the module's exports.
+   */
+  lazyRequireGetter: function(obj, property, module, destructure) {
+    devtools.lazyGetter(obj, property, () => {
+      return destructure
+          ? this.require(module)[property]
+          : this.require(module || property);
+    });
+  },
+
+  clearCache: function() {
+    Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+  },
+
+  hotReloadFile: function(window, componentProxies, fileURI) {
+    dump("Hot reloading: " + fileURI + "\n");
+
+    if (fileURI.match(/\.js$/)) {
+      // Test for React proxy components
+      const proxy = componentProxies.get(fileURI);
+      if (proxy) {
+        // Remove the old module and re-require the new one; the require
+        // hook in the loader will take care of the rest
+        delete this.loader.modules[fileURI];
+        this.clearCache();
+        this.require(fileURI);
+      }
+    } else if (fileURI.match(/\.css$/)) {
+      const links = [...window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "link")];
+      links.forEach(link => {
+        if (link.href.indexOf(fileURI) === 0) {
+          const parentNode = link.parentNode;
+          const newLink = window.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
+          newLink.rel = "stylesheet";
+          newLink.type = "text/css";
+          newLink.href = fileURI + "?s=" + Math.random();
+
+          parentNode.insertBefore(newLink, link);
+          parentNode.removeChild(link);
+        }
+      });
+    }
+  }
+};
 
 this.BrowserLoader = BrowserLoader;
 
 this.EXPORTED_SYMBOLS = ["BrowserLoader"];
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -940,30 +940,30 @@ StyleEditorUI.prototype = {
   },
 
   /**
    * Launches the responsive mode with a specific width or height
    *
    * @param  {object} options
    *         Object with width or/and height properties.
    */
-  _launchResponsiveMode: function(options = {}) {
+  _launchResponsiveMode: Task.async(function*(options = {}) {
     let tab = this._target.tab;
     let win = this._target.tab.ownerGlobal;
 
-    ResponsiveUIManager.runIfNeeded(win, tab);
+    yield ResponsiveUIManager.runIfNeeded(win, tab);
     if (options.width && options.height) {
       ResponsiveUIManager.getResponsiveUIForTab(tab).setSize(options.width,
                                                              options.height);
     } else if (options.width) {
       ResponsiveUIManager.getResponsiveUIForTab(tab).setWidth(options.width);
     } else if (options.height) {
       ResponsiveUIManager.getResponsiveUIForTab(tab).setHeight(options.height);
     }
-  },
+  }),
 
   /**
    * Jump cursor to the editor for a stylesheet and line number for a rule.
    *
    * @param  {object} location
    *         Location object with 'line', 'column', and 'source' properties.
    */
   _jumpToLocation: function(location) {
--- a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
@@ -2,74 +2,111 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /* Tests responsive mode links for
  * @media sidebar width and height related conditions */
 
-const {ResponsiveUIManager} =
-      Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+const mgr = "resource://devtools/client/responsivedesign/responsivedesign.jsm";
+const {ResponsiveUIManager} = Cu.import(mgr, {});
 const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
-
-waitForExplicitFinish();
+const responsiveModeToggleClass = ".media-responsive-mode-toggle";
 
 add_task(function*() {
   let {ui} = yield openStyleEditorForURL(TESTCASE_URI);
 
-  let mediaEditor = ui.editors[1];
-  yield openEditor(mediaEditor);
+  let editor = ui.editors[1];
+  yield openEditor(editor);
 
-  yield testLinkifiedConditions(mediaEditor, gBrowser.selectedTab, ui);
+  let tab = gBrowser.selectedTab;
+  testNumberOfLinks(editor);
+  yield testMediaLink(editor, tab, ui, 2, "width", 400);
+  yield testMediaLink(editor, tab, ui, 3, "height", 200);
+
+  yield closeRDM(tab, ui);
+  doFinalChecks(editor);
 });
 
-function* testLinkifiedConditions(editor, tab, ui) {
+function testNumberOfLinks(editor) {
   let sidebar = editor.details.querySelector(".stylesheet-sidebar");
   let conditions = sidebar.querySelectorAll(".media-rule-condition");
-  let responsiveModeToggleClass = ".media-responsive-mode-toggle";
 
   info("Testing if media rules have the appropriate number of links");
   ok(!conditions[0].querySelector(responsiveModeToggleClass),
     "There should be no links in the first media rule.");
   ok(!conditions[1].querySelector(responsiveModeToggleClass),
      "There should be no links in the second media rule.");
   ok(conditions[2].querySelector(responsiveModeToggleClass),
      "There should be 1 responsive mode link in the media rule");
   is(conditions[3].querySelectorAll(responsiveModeToggleClass).length, 2,
-       "There should be 2 resposnive mode links in the media rule");
+       "There should be 2 responsive mode links in the media rule");
+}
+
+function* testMediaLink(editor, tab, ui, itemIndex, type, value) {
+  let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+  let conditions = sidebar.querySelectorAll(".media-rule-condition");
+
+  let onMediaChange = once("media-list-changed", ui);
+  let ruiEvent = !ResponsiveUIManager.isActiveForTab(tab) ?
+                    once("on", ResponsiveUIManager) :
+                    once("contentResize", ResponsiveUIManager);
 
   info("Launching responsive mode");
-  conditions[2].querySelector(responsiveModeToggleClass).click();
+  conditions[itemIndex].querySelector(responsiveModeToggleClass).click();
 
   info("Waiting for the @media list to update");
-  let onMediaChange = once("media-list-changed", ui);
-  yield once("on", ResponsiveUIManager);
+  yield ruiEvent;
   yield onMediaChange;
 
+  ResponsiveUIManager.getResponsiveUIForTab(tab).transitionsEnabled = false;
+
   ok(ResponsiveUIManager.isActiveForTab(tab),
     "Responsive mode should be active.");
   conditions = sidebar.querySelectorAll(".media-rule-condition");
-  ok(!conditions[2].classList.contains("media-condition-unmatched"),
+  ok(!conditions[itemIndex].classList.contains("media-condition-unmatched"),
      "media rule should now be matched after responsive mode is active");
 
+  let dimension = (yield getSizing())[type];
+  is(dimension, value, `${type} should be properly set.`);
+}
+
+function* closeRDM(tab, ui) {
   info("Closing responsive mode");
   ResponsiveUIManager.toggle(window, tab);
-  onMediaChange = once("media-list-changed", ui);
+  let onMediaChange = once("media-list-changed", ui);
   yield once("off", ResponsiveUIManager);
   yield onMediaChange;
-
   ok(!ResponsiveUIManager.isActiveForTab(tab),
      "Responsive mode should no longer be active.");
+}
+
+function doFinalChecks(editor) {
+  let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+  let conditions = sidebar.querySelectorAll(".media-rule-condition");
   conditions = sidebar.querySelectorAll(".media-rule-condition");
   ok(conditions[2].classList.contains("media-condition-unmatched"),
-       "media rule should now be unmatched after responsive mode is closed");
+     "The width condition should now be unmatched");
+  ok(conditions[3].classList.contains("media-condition-unmatched"),
+     "The height condition should now be unmatched");
 }
 
 /* Helpers */
+function* getSizing() {
+  let browser = gBrowser.selectedBrowser;
+  let sizing = yield ContentTask.spawn(browser, {}, function*() {
+    return {
+      width: content.innerWidth,
+      height: content.innerHeight
+    };
+  });
+  return sizing;
+}
+
 function once(event, target) {
   let deferred = promise.defer();
   target.once(event, () => {
     deferred.resolve();
   });
   return deferred.promise;
 }
 
--- a/devtools/client/webconsole/test/browser.ini
+++ b/devtools/client/webconsole/test/browser.ini
@@ -214,17 +214,16 @@ tags = mcb
 [browser_webconsole_bug_587617_output_copy.js]
 [browser_webconsole_bug_588342_document_focus.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_588730_text_node_insertion.js]
 [browser_webconsole_bug_588967_input_expansion.js]
 [browser_webconsole_bug_589162_css_filter.js]
 [browser_webconsole_bug_592442_closing_brackets.js]
 [browser_webconsole_bug_593003_iframe_wrong_hud.js]
-skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_594497_history_arrow_keys.js]
 [browser_webconsole_bug_595223_file_uri.js]
 [browser_webconsole_bug_595350_multiple_windows_and_tabs.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_595934_message_categories.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js]
 [browser_webconsole_bug_597136_external_script_errors.js]
@@ -293,17 +292,16 @@ skip-if = e10s && os == 'win'
 [browser_webconsole_certificate_messages.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_show_subresource_security_errors.js]
 skip-if = e10s && os == 'win'
 [browser_webconsole_cached_autocomplete.js]
 [browser_webconsole_change_font_size.js]
 [browser_webconsole_chrome.js]
 [browser_webconsole_clickable_urls.js]
-skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
 [browser_webconsole_closure_inspection.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_completion.js]
 [browser_webconsole_console_extras.js]
 [browser_webconsole_console_logging_api.js]
 [browser_webconsole_console_logging_workers_api.js]
 [browser_webconsole_console_trace_async.js]
 [browser_webconsole_count.js]
--- a/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
@@ -10,62 +10,59 @@ const TEST_URI = "http://example.com/bro
 
 const TEST_IFRAME_URI = "http://example.com/browser/devtools/client/" +
                         "webconsole/test/test-bug-593003-iframe-wrong-" +
                         "hud-iframe.html";
 
 const TEST_DUMMY_URI = "http://example.com/browser/devtools/client/" +
                        "webconsole/test/test-console.html";
 
-var tab1, tab2;
+add_task(function*() {
 
-function test() {
-  loadTab(TEST_URI).then(({tab}) => {
-    tab1 = tab;
-
+  let tab1 = (yield loadTab(TEST_URI)).tab;
+  yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
     content.console.log("FOO");
-    openConsole().then(() => {
-      tab2 = gBrowser.addTab(TEST_DUMMY_URI);
-      gBrowser.selectedTab = tab2;
-      gBrowser.selectedBrowser.addEventListener("load", tab2Loaded, true);
-    });
   });
+  yield openConsole();
+
+  let tab2 = (yield loadTab(TEST_DUMMY_URI)).tab;
+  yield openConsole(gBrowser.selectedTab);
+
+  info("Reloading tab 1");
+  yield reloadTab(tab1);
+
+  info("Checking for messages");
+  yield checkMessages(tab1, tab2);
+
+  info("Cleaning up");
+  yield closeConsole(tab1);
+  yield closeConsole(tab2);
+});
+
+function* reloadTab(tab) {
+  let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  tab.linkedBrowser.reload();
+  yield loaded;
 }
 
-function tab2Loaded(aEvent) {
-  tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true);
-
-  openConsole(gBrowser.selectedTab).then(() => {
-    tab1.linkedBrowser.addEventListener("load", tab1Reloaded, true);
-    tab1.linkedBrowser.contentWindow.location.reload();
-  });
-}
-
-function tab1Reloaded(aEvent) {
-  tab1.linkedBrowser.removeEventListener(aEvent.type, tab1Reloaded, true);
-
-  let hud1 = HUDService.getHudByWindow(tab1.linkedBrowser.contentWindow);
+function* checkMessages(tab1, tab2) {
+  let hud1 = yield openConsole(tab1);
   let outputNode1 = hud1.outputNode;
 
-  waitForMessages({
+  info("Waiting for messages");
+  yield waitForMessages({
     webconsole: hud1,
     messages: [{
       text: TEST_IFRAME_URI,
       category: CATEGORY_NETWORK,
       severity: SEVERITY_LOG,
-    }],
-  }).then(() => {
-    let hud2 = HUDService.getHudByWindow(tab2.linkedBrowser.contentWindow);
-    let outputNode2 = hud2.outputNode;
+    }]
+  });
 
-    isnot(outputNode1, outputNode2,
-      "the two HUD outputNodes must be different");
+  let hud2 = yield openConsole(tab2);
+  let outputNode2 = hud2.outputNode;
 
-    let msg = "Didn't find the iframe network request in tab2";
-    testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true);
+  isnot(outputNode1, outputNode2,
+    "the two HUD outputNodes must be different");
 
-    closeConsole(tab2).then(() => {
-      gBrowser.removeTab(tab2);
-      tab1 = tab2 = null;
-      executeSoon(finishTest);
-    });
-  });
+  let msg = "Didn't find the iframe network request in tab2";
+  testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true);
 }
--- a/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js
+++ b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js
@@ -3,19 +3,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // When strings containing URLs are entered into the webconsole, check
 // its output and ensure that the output can be clicked to open those URLs.
 
 "use strict";
 
-const TEST_URI = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS";
-
-var inputTests = [
+const inputTests = [
 
   // 0: URL opens page when clicked.
   {
     input: "'http://example.com'",
     output: "http://example.com",
     expectedTab: "http://example.com/",
   },
 
@@ -91,16 +89,15 @@ var inputTests = [
     noClick: true,
     consoleLogClick: true,
     expectedTab: "http://example.com/abcdefghijabcdefghij",
     getClickableNode: (msg) => msg.querySelectorAll("a")[1],
   },
 
 ];
 
-function test() {
-  Task.spawn(function*() {
-    let {tab} = yield loadTab(TEST_URI);
-    let hud = yield openConsole(tab);
-    yield checkOutputForInputs(hud, inputTests);
-    inputTests = null;
-  }).then(finishTest);
-}
+const url = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS";
+
+add_task(function* () {
+  yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+  let hud = yield openConsole();
+  yield checkOutputForInputs(hud, inputTests);
+});
--- a/devtools/server/actors/child-process.js
+++ b/devtools/server/actors/child-process.js
@@ -90,17 +90,17 @@ ChildProcessActor.prototype = {
         highlightable: false,
         networkMonitor: false,
       },
     };
   },
 
   onListWorkers: function () {
     if (!this._workerList) {
-      this._workerList = new WorkerActorList({});
+      this._workerList = new WorkerActorList(this.conn, {});
     }
     return this._workerList.getList().then(actors => {
       let pool = new ActorPool(this.conn);
       for (let actor of actors) {
         pool.addActor(actor);
       }
 
       this.conn.removeActorPool(this._workerActorPool);
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -126,18 +126,19 @@ exports.sendShutdownEvent = sendShutdown
  *        The conection to the client.
  */
 function createRootActor(aConnection)
 {
   return new RootActor(aConnection,
                        {
                          tabList: new BrowserTabList(aConnection),
                          addonList: new BrowserAddonList(aConnection),
-                         workerList: new WorkerActorList({}),
-                         serviceWorkerRegistrationList: new ServiceWorkerRegistrationActorList(),
+                         workerList: new WorkerActorList(aConnection, {}),
+                         serviceWorkerRegistrationList:
+                           new ServiceWorkerRegistrationActorList(aConnection),
                          processList: new ProcessActorList(),
                          globalActorFactories: DebuggerServer.globalActorFactories,
                          onShutdown: sendShutdownEvent
                        });
 }
 
 /**
  * A live list of BrowserTabActors representing the current browser tabs,
@@ -1093,17 +1094,17 @@ TabActor.prototype = {
   },
 
   onListWorkers: function BTA_onListWorkers(aRequest) {
     if (!this.attached) {
       return { error: "wrongState" };
     }
 
     if (this._workerActorList === null) {
-      this._workerActorList = new WorkerActorList({
+      this._workerActorList = new WorkerActorList(this.conn, {
         type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
         window: this.window
       });
     }
 
     return this._workerActorList.getList().then((actors) => {
       let pool = new ActorPool(this.conn);
       for (let actor of actors) {
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -1,12 +1,14 @@
 "use strict";
 
 var { Ci, Cu } = require("chrome");
 var { DebuggerServer } = require("devtools/server/main");
+const protocol = require("devtools/server/protocol");
+const { Arg, method, RetVal } = protocol;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(
   this, "wdm",
   "@mozilla.org/dom/workers/workerdebuggermanager;1",
   "nsIWorkerDebuggerManager"
 );
@@ -30,113 +32,138 @@ function matchWorkerDebugger(dbg, option
     if (window !== options.window) {
       return false;
     }
   }
 
   return true;
 }
 
-function WorkerActor(dbg) {
-  this._dbg = dbg;
-  this._isAttached = false;
-  this._threadActor = null;
-  this._transport = null;
-}
+let WorkerActor = protocol.ActorClass({
+  typeName: "worker",
 
-WorkerActor.prototype = {
-  actorPrefix: "worker",
+  initialize: function (conn, dbg) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this._dbg = dbg;
+    this._attached = false;
+    this._threadActor = null;
+    this._transport = null;
+    this.manage(this);
+  },
 
-  form: function () {
-    return {
+  form: function (detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+    let form = {
       actor: this.actorID,
       consoleActor: this._consoleActor,
       url: this._dbg.url,
       type: this._dbg.type
     };
+    if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) {
+      let registration = this._getServiceWorkerRegistrationInfo();
+      form.scope = registration.scope;
+    }
+    return form;
   },
 
-  onAttach: function () {
+  attach: method(function () {
     if (this._dbg.isClosed) {
       return { error: "closed" };
     }
 
-    if (!this._isAttached) {
+    if (!this._attached) {
       // Automatically disable their internal timeout that shut them down
       // Should be refactored by having actors specific to service workers
       if (this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE) {
         let worker = this._getServiceWorkerInfo();
         if (worker) {
           worker.attachDebugger();
         }
       }
       this._dbg.addListener(this);
-      this._isAttached = true;
+      this._attached = true;
     }
 
     return {
       type: "attached",
       url: this._dbg.url
     };
-  },
+  }, {
+    request: {},
+    response: RetVal("json")
+  }),
 
-  onDetach: function () {
-    if (!this._isAttached) {
+  detach: method(function () {
+    if (!this._attached) {
       return { error: "wrongState" };
     }
 
     this._detach();
 
     return { type: "detached" };
-  },
+  }, {
+    request: {},
+    response: RetVal("json")
+  }),
 
-  onConnect: function (request) {
-    if (!this._isAttached) {
+  connect: method(function (options) {
+    if (!this._attached) {
       return { error: "wrongState" };
     }
 
     if (this._threadActor !== null) {
       return {
         type: "connected",
         threadActor: this._threadActor
       };
     }
 
     return DebuggerServer.connectToWorker(
-      this.conn, this._dbg, this.actorID, request.options
+      this.conn, this._dbg, this.actorID, options
     ).then(({ threadActor, transport, consoleActor }) => {
       this._threadActor = threadActor;
       this._transport = transport;
       this._consoleActor = consoleActor;
 
       return {
         type: "connected",
         threadActor: this._threadActor,
         consoleActor: this._consoleActor
       };
     }, (error) => {
       return { error: error.toString() };
     });
-  },
+  }, {
+    request: {
+      options: Arg(0, "json"),
+    },
+    response: RetVal("json")
+  }),
 
   onClose: function () {
-    if (this._isAttached) {
+    if (this._attached) {
       this._detach();
     }
 
     this.conn.sendActorEvent(this.actorID, "close");
   },
 
   onError: function (filename, lineno, message) {
     reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n");
   },
 
+  _getServiceWorkerRegistrationInfo() {
+    return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url);
+  },
+
   _getServiceWorkerInfo: function () {
-    let info = swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url);
-    return info.getWorkerByID(this._dbg.serviceWorkerID);
+    let registration = this._getServiceWorkerRegistrationInfo();
+    return registration.getWorkerByID(this._dbg.serviceWorkerID);
   },
 
   _detach: function () {
     if (this._threadActor !== null) {
       this._transport.close();
       this._transport = null;
       this._threadActor = null;
     }
@@ -151,29 +178,24 @@ WorkerActor.prototype = {
     if (type == Ci.nsIWorkerDebugger.TYPE_SERVICE) {
       let worker = this._getServiceWorkerInfo();
       if (worker) {
         worker.detachDebugger();
       }
     }
 
     this._dbg.removeListener(this);
-    this._isAttached = false;
+    this._attached = false;
   }
-};
-
-WorkerActor.prototype.requestTypes = {
-  "attach": WorkerActor.prototype.onAttach,
-  "detach": WorkerActor.prototype.onDetach,
-  "connect": WorkerActor.prototype.onConnect
-};
+});
 
 exports.WorkerActor = WorkerActor;
 
-function WorkerActorList(options) {
+function WorkerActorList(conn, options) {
+  this._conn = conn;
   this._options = options;
   this._actors = new Map();
   this._onListChanged = null;
   this._mustNotify = false;
   this.onRegister = this.onRegister.bind(this);
   this.onUnregister = this.onUnregister.bind(this);
 }
 
@@ -194,17 +216,17 @@ WorkerActorList.prototype = {
       if (!dbgs.has(dbg)) {
         this._actors.delete(dbg);
       }
     }
 
     // Create an actor for each debugger for which we don't have one.
     for (let dbg of dbgs) {
       if (!this._actors.has(dbg)) {
-        this._actors.set(dbg, new WorkerActor(dbg));
+        this._actors.set(dbg, new WorkerActor(this._conn, dbg));
       }
     }
 
     let actors = [];
     for (let [, actor] of this._actors) {
       actors.push(actor);
     }
 
@@ -260,32 +282,40 @@ WorkerActorList.prototype = {
     if (matchWorkerDebugger(dbg, this._options)) {
       this._notifyListChanged();
     }
   }
 };
 
 exports.WorkerActorList = WorkerActorList;
 
-function ServiceWorkerRegistrationActor(registration) {
-  this._registration = registration;
-};
+let ServiceWorkerRegistrationActor = protocol.ActorClass({
+  typeName: "serviceWorkerRegistration",
 
-ServiceWorkerRegistrationActor.prototype = {
-  actorPrefix: "serviceWorkerRegistration",
+  initialize: function(conn, registration) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this._registration = registration;
+    this.manage(this);
+  },
 
-  form: function () {
+  form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
     return {
       actor: this.actorID,
-      scope: this._registration.scope
+      scope: this._registration.scope,
+      url: this._registration.scriptSpec
     };
-  }
-};
+  },
 
-function ServiceWorkerRegistrationActorList() {
+});
+
+function ServiceWorkerRegistrationActorList(conn) {
+  this._conn = conn;
   this._actors = new Map();
   this._onListChanged = null;
   this._mustNotify = false;
   this.onRegister = this.onRegister.bind(this);
   this.onUnregister = this.onUnregister.bind(this);
 };
 
 ServiceWorkerRegistrationActorList.prototype = {
@@ -304,17 +334,17 @@ ServiceWorkerRegistrationActorList.proto
         this._actors.delete(registration);
       }
     }
 
     // Create an actor for each registration for which we don't have one.
     for (let registration of registrations) {
       if (!this._actors.has(registration)) {
         this._actors.set(registration,
-          new ServiceWorkerRegistrationActor(registration));
+          new ServiceWorkerRegistrationActor(this._conn, registration));
       }
     }
 
     if (!this._mustNotify) {
       if (this._onListChanged !== null) {
         swm.addListener(this);
       }
       this._mustNotify = true;
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -1411,16 +1411,19 @@ WorkerClient.prototype = {
   },
 
   get isClosed() {
     return this._isClosed;
   },
 
   detach: DebuggerClient.requester({ type: "detach" }, {
     after: function (aResponse) {
+      if (this.thread) {
+        this.client.unregisterClient(this.thread);
+      }
       this.client.unregisterClient(this);
       return aResponse;
     },
 
     telemetry: "WORKERDETACH"
   }),
 
   attachThread: function(aOptions = {}, aOnResponse = noop) {
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -379,19 +379,27 @@ GlobalManager = {
       // does not.
       let injectObject = (name, defaultCallback) => {
         let browserObj = Cu.createObjectIn(contentWindow, {defineAs: name});
 
         let api = Management.generateAPIs(extension, context, Management.apis);
         injectAPI(api, browserObj);
 
         let schemaApi = Management.generateAPIs(extension, context, Management.schemaApis);
+
+        // Add in any extra API namespaces which do not have implementations
+        // outside of their schema file.
+        schemaApi.extensionTypes = {};
+
         function findPath(path) {
           let obj = schemaApi;
           for (let elt of path) {
+            if (!(elt in obj)) {
+              return null;
+            }
             obj = obj[elt];
           }
           return obj;
         }
         let schemaWrapper = {
           get cloneScope() {
             return context.cloneScope;
           },
@@ -418,16 +426,20 @@ GlobalManager = {
                 Cu.reportError(e);
                 promise = Promise.reject({message: "An unexpected error occurred"});
               }
             }
 
             return context.wrapPromise(promise || Promise.resolve(), callback);
           },
 
+          shouldInject(path, name) {
+            return findPath(path) != null;
+          },
+
           getProperty(path, name) {
             return findPath(path)[name];
           },
 
           setProperty(path, name, value) {
             findPath(path)[name] = value;
           },
 
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -115,16 +115,21 @@ var api = context => {
       inIncognitoContext: PrivateBrowsingUtils.isContentWindowPrivate(context.contentWindow),
     },
 
     i18n: {
       getMessage: function(messageName, substitutions) {
         return context.extension.localizeMessage(messageName, substitutions);
       },
 
+      getAcceptLanguages: function(callback) {
+        let result = context.extension.localeData.acceptLanguages;
+        return context.wrapPromise(Promise.resolve(result), callback);
+      },
+
       getUILanguage: function() {
         return context.extension.localeData.uiLocale;
       },
 
       detectLanguage: function(text, callback) {
         let result = detectLanguage(text);
         return context.wrapPromise(result, callback);
       },
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -15,16 +15,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
                                   "resource:///modules/translation/LanguageDetector.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
 
 function filterStack(error) {
   return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
 }
 
 // Run a function and report exceptions.
 function runSafeSyncWithoutClone(f, ...args) {
   try {
@@ -424,16 +426,26 @@ LocaleData.prototype = {
       // Message names are also case-insensitive, so normalize them to lower-case.
       result.set(key.toLowerCase(), value);
     }
 
     this.messages.set(locale, result);
     return result;
   },
 
+  get acceptLanguages() {
+    let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString);
+    result = result.split(",");
+    result = result.map(lang => {
+      return lang.replace(/-/g, "_").trim();
+    });
+    return result;
+  },
+
+
   get uiLocale() {
     // Return the browser locale, but convert it to a Chrome-style
     // locale code.
     return Locale.getLocale().replace(/-/g, "_");
   },
 };
 
 // This is a generic class for managing event listeners. Example usage:
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -1314,17 +1314,19 @@ this.Schemas = {
       }
     });
   },
 
   inject(dest, wrapperFuncs) {
     for (let [namespace, ns] of this.namespaces) {
       let obj = Cu.createObjectIn(dest, {defineAs: namespace});
       for (let [name, entry] of ns) {
-        entry.inject([namespace], name, obj, new Context(wrapperFuncs));
+        if (wrapperFuncs.shouldInject([namespace], name)) {
+          entry.inject([namespace], name, obj, new Context(wrapperFuncs));
+        }
       }
 
       if (!Object.keys(obj).length) {
         delete dest[namespace];
       }
     }
   },
 
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -1,20 +1,110 @@
 "use strict";
 
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+                                  "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   ignoreEvent,
 } = ExtensionUtils;
 
+let currentId = 0;
+
 extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
   return {
     downloads: {
+      download(options) {
+        if (options.filename != null) {
+          if (options.filename.length == 0) {
+            return Promise.reject({message: "filename must not be empty"});
+          }
+
+          let path = OS.Path.split(options.filename);
+          if (path.absolute) {
+            return Promise.reject({message: "filename must not be an absolute path"});
+          }
+
+          if (path.components.some(component => component == "..")) {
+            return Promise.reject({message: "filename must not contain back-references (..)"});
+          }
+        }
+
+        if (options.conflictAction == "prompt") {
+          // TODO
+          return Promise.reject({message: "conflictAction prompt not yet implemented"});
+        }
+
+        function createTarget(downloadsDir) {
+          // TODO
+          // if (options.saveAs) { }
+
+          let target;
+          if (options.filename) {
+            target = OS.Path.join(downloadsDir, options.filename);
+          } else {
+            let uri = NetUtil.newURI(options.url).QueryInterface(Ci.nsIURL);
+            target = OS.Path.join(downloadsDir, uri.fileName);
+          }
+
+          // This has a race, something else could come along and create
+          // the file between this test and them time the download code
+          // creates the target file.  But we can't easily fix it without
+          // modifying DownloadCore so we live with it for now.
+          return OS.File.exists(target).then(exists => {
+            if (exists) {
+              switch (options.conflictAction) {
+                case "uniquify":
+                default:
+                  target = DownloadPaths.createNiceUniqueFile(new FileUtils.File(target)).path;
+                  break;
+
+                case "overwrite":
+                  break;
+              }
+            }
+            return target;
+          });
+        }
+
+        let download;
+        return Downloads.getPreferredDownloadsDirectory()
+          .then(downloadsDir => createTarget(downloadsDir))
+          .then(target => Downloads.createDownload({
+            source: options.url,
+            target: target,
+          })).then(dl => {
+            download = dl;
+            return Downloads.getList(Downloads.ALL);
+          }).then(list => {
+            list.add(download);
+
+            // This is necessary to make pause/resume work.
+            download.tryToKeepPartialData = true;
+            download.start();
+
+            // Without other chrome.downloads methods, we can't actually
+            // do anything with the id so just return a dummy value for now.
+            return currentId++;
+          });
+      },
+
       // When we do open(), check for additional downloads.open permission.
       // i.e.:
       // open(downloadId) {
       //   if (!extension.hasPermission("downloads.open")) {
       //     throw new context.cloneScope.Error("Permission denied because 'downloads.open' permission is missing.");
       //   }
       //   ...
       // }
--- a/toolkit/components/extensions/ext-i18n.js
+++ b/toolkit/components/extensions/ext-i18n.js
@@ -7,16 +7,21 @@ var {
 
 extensions.registerSchemaAPI("i18n", null, (extension, context) => {
   return {
     i18n: {
       getMessage: function(messageName, substitutions) {
         return extension.localizeMessage(messageName, substitutions);
       },
 
+      getAcceptLanguages: function() {
+        let result = extension.localeData.acceptLanguages;
+        return Promise.resolve(result);
+      },
+
       getUILanguage: function() {
         return extension.localeData.uiLocale;
       },
 
       detectLanguage: function(text) {
         return detectLanguage(text);
       },
     },
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -74,16 +74,17 @@ extensions.registerSchemaAPI("webNavigat
   return {
     webNavigation: {
       onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
       onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
       onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
       onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
       onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
       onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
+      onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
       onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
       getAllFrames(details) {
         let tab = TabManager.getTab(details.tabId);
         if (!tab) {
           return Promise.reject({message: `No tab found with tabId: ${details.tabId}`});
         }
 
         let {innerWindowID, messageManager} = tab.linkedBrowser;
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -14,9 +14,10 @@ EXTRA_JS_MODULES += [
     'Schemas.jsm',
 ]
 
 DIRS += ['schemas']
 
 JAR_MANIFESTS += ['jar.mn']
 
 MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
--- a/toolkit/components/extensions/schemas/downloads.json
+++ b/toolkit/components/extensions/schemas/downloads.json
@@ -17,17 +17,17 @@
   },
   {
     "namespace": "downloads",
     "types": [
       {
         "id": "FilenameConflictAction",
         "type": "string",
         "enum": [
-          "uniqify",
+          "uniquify",
           "overwrite",
           "prompt"
         ]
       },
       {
         "id": "InterruptReason",
         "type": "string",
         "enum": [
@@ -209,52 +209,56 @@
           }
         }
       }
     ],
     "functions": [
       {
         "name": "download",
         "type": "function",
-        "unsupported": true,
+        "async": "callback",
         "description": "Download a URL. If the URL uses the HTTP[S] protocol, then the request will include all cookies currently set for its hostname. If both <code>filename</code> and <code>saveAs</code> are specified, then the Save As dialog will be displayed, pre-populated with the specified <code>filename</code>. If the download started successfully, <code>callback</code> will be called with the new <a href='#type-DownloadItem'>DownloadItem</a>'s <code>downloadId</code>. If there was an error starting the download, then <code>callback</code> will be called with <code>downloadId=undefined</code> and <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain a descriptive string. The error strings are not guaranteed to remain backwards compatible between releases. You must not parse it.",
         "parameters": [
           {
             "description": "What to download and how.",
             "name": "options",
             "type": "object",
             "properties": {
               "url": {
                 "description": "The URL to download.",
-                "type": "string"
+                "type": "string",
+                "format": "url"
               },
               "filename": {
                 "description": "A file path relative to the Downloads directory to contain the downloaded file.",
                 "optional": true,
                 "type": "string"
               },
               "conflictAction": {
                 "$ref": "FilenameConflictAction",
                 "optional": true
               },
               "saveAs": {
+                "unsupported": true,
                 "description": "Use a file-chooser to allow the user to select a filename.",
                 "optional": true,
                 "type": "boolean"
               },
               "method": {
+                "unsupported": true,
                 "description": "The HTTP method to use if the URL uses the HTTP[S] protocol.",
                 "enum": [
                   "GET",
                   "POST"
                 ],
                 "optional": true,
                 "type": "string"
               },
               "headers": {
+                "unsupported": true,
                 "optional": true,
                 "type": "array",
                 "description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.",
                 "items": {
                   "type": "object",
                   "properties": {
                     "name": {
                       "description": "Name of the HTTP header.",
@@ -263,16 +267,17 @@
                     "value": {
                       "description": "Value of the HTTP header.",
                       "type": "string"
                     }
                   }
                 }
               },
               "body": {
+                "unsupported": true,
                 "description": "Post body.",
                 "optional": true,
                 "type": "string"
               }
             }
           },
           {
             "name": "callback",
--- a/toolkit/components/extensions/schemas/i18n.json
+++ b/toolkit/components/extensions/schemas/i18n.json
@@ -25,17 +25,16 @@
         "id": "LanguageCode",
         "type": "string",
         "description": "An ISO language code such as <code>en</code> or <code>fr</code>. For a complete list of languages supported by this method, see <a href='http://src.chromium.org/viewvc/chrome/trunk/src/third_party/cld/languages/internal/languages.cc'>kLanguageInfoTable</a>. For an unknown language, <code>und</code> will be returned, which means that [percentage] of the text is unknown to CLD"
       }
     ],
     "functions": [
       {
         "name": "getAcceptLanguages",
-        "unsupported": true,
         "type": "function",
         "description": "Gets the accept-languages of the browser. This is different from the locale used by the browser; to get the locale, use $(ref:i18n.getUILanguage).",
         "async": "callback",
         "parameters": [
           {
             "type": "function",
             "name": "callback",
             "parameters": [
--- a/toolkit/components/extensions/schemas/web_navigation.json
+++ b/toolkit/components/extensions/schemas/web_navigation.json
@@ -340,17 +340,16 @@
               "tabId": {"type": "integer", "description": "The ID of the tab that replaced the old tab."},
               "timeStamp": {"type": "number", "description": "The time when the replacement happened, in milliseconds since the epoch."}
             }
           }
         ]
       },
       {
         "name": "onHistoryStateUpdated",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when the frame's history was updated to a new URL. All future events for that frame will use the updated URL.",
         "filters": [
           {
             "name": "url",
             "type": "array",
             "items": { "$ref": "events.UrlFilter" },
             "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+  file_download.txt
+
+[test_chrome_ext_downloads_download.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_download.txt
@@ -0,0 +1,1 @@
+This is a sample file used in download tests.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_download.html
@@ -0,0 +1,221 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>WebExtension test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {
+  interfaces: Ci,
+  utils: Cu,
+} = Components;
+
+/* global OS */
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const WINDOWS = (AppConstants.platform == "win");
+
+const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
+const FILE_NAME = "file_download.txt";
+const FILE_URL = BASE + "/" + FILE_NAME;
+const FILE_NAME_UNIQUE = "file_download(1).txt";
+const FILE_LEN = 46;
+
+let downloadDir;
+
+function setup() {
+  downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+  downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+  info(`Using download directory ${downloadDir.path}`);
+
+  Services.prefs.setIntPref("browser.download.folderList", 2);
+  Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir);
+
+  SimpleTest.registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("browser.download.folderList");
+    Services.prefs.clearUserPref("browser.download.dir");
+  });
+}
+
+function backgroundScript() {
+  browser.test.onMessage.addListener(function(msg) {
+    if (msg == "download.request") {
+      // download() throws on bad arguments, we can remove the extra
+      // promise when bug 1250223 is fixed.
+      return Promise.resolve().then(() => browser.downloads.download(arguments[1]))
+             .then((id) => browser.test.sendMessage("download.done", {status: "success", id}))
+             .catch(error => browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}));
+    }
+  });
+
+  browser.test.sendMessage("ready");
+}
+
+// This function is a bit of a sledgehammer, it looks at every download
+// the browser knows about and waits for all active downloads to complete.
+// But we only start one at a time and only do a handful in total, so
+// this lets us test download() without depending on anything else.
+function waitForDownloads() {
+  return Downloads.getList(Downloads.ALL)
+                  .then(list => list.getAll())
+                  .then(downloads => {
+                    let inprogress = downloads.filter(dl => !dl.stopped);
+                    return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
+                  });
+}
+
+// Create a file in the downloads directory.
+function touch(filename) {
+  let file = downloadDir.clone();
+  file.append(filename);
+  file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+}
+
+// Remove a file in the downloads directory.
+function remove(filename) {
+  let file = downloadDir.clone();
+  file.append(filename);
+  file.remove(false);
+}
+
+add_task(function* test_downloads() {
+  setup();
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["downloads"],
+    },
+  });
+
+  function download(options) {
+    extension.sendMessage("download.request", options);
+    return extension.awaitMessage("download.done");
+  }
+
+  function testDownload(options, localFile, expectedSize, description) {
+    return download(options).then(msg => {
+      is(msg.status, "success", `downloads.download() works with ${description}`);
+      return waitForDownloads();
+    }).then(() => {
+      let localPath = downloadDir.clone();
+      localPath.append(localFile);
+      is(localPath.fileSize, expectedSize, "Downloaded file has expected size");
+      localPath.remove(false);
+    });
+  }
+
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+  info("extension started");
+
+  // Call download() with just the url property.
+  yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source");
+
+  // Call download() with a filename property.
+  yield testDownload({
+    url: FILE_URL,
+    filename: "newpath.txt",
+  }, "newpath.txt", FILE_LEN, "source and filename");
+
+  // Check conflictAction of "uniquify".
+  touch(FILE_NAME);
+  yield testDownload({
+    url: FILE_URL,
+    conflictAction: "uniquify",
+  }, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify");
+  // todo check that preexisting file was not modified?
+  remove(FILE_NAME);
+
+  // Check conflictAction of "overwrite".
+  touch(FILE_NAME);
+  yield testDownload({
+    url: FILE_URL,
+    conflictAction: "overwrite",
+  }, FILE_NAME, FILE_LEN, "conflictAction=overwrite");
+
+  // Try to download in invalid url
+  yield download({url: "this is not a valid URL"}).then(msg => {
+    is(msg.status, "error", "downloads.download() fails with invalid url");
+    ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct");
+  });
+
+  // Try to download to an empty path.
+  yield download({
+    url: FILE_URL,
+    filename: "",
+  }).then(msg => {
+    is(msg.status, "error", "downloads.download() fails with empty filename");
+    is(msg.errmsg, "filename must not be empty", "error message for empty filename is correct");
+  });
+
+  // Try to download to an absolute path.
+  const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt");
+  yield download({
+    url: FILE_URL,
+    filename: absolutePath,
+  }).then(msg => {
+    is(msg.status, "error", "downloads.download() fails with absolute filename");
+    is(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`);
+  });
+
+  if (WINDOWS) {
+    yield download({
+      url: FILE_URL,
+      filename: "C:\\file_download.txt",
+    }).then(msg => {
+      is(msg.status, "error", "downloads.download() fails with absolute filename");
+      is(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct");
+    });
+  }
+
+  // Try to download to a relative path containing ..
+  yield download({
+    url: FILE_URL,
+    filename: OS.Path.join("..", "file_download.txt"),
+  }).then(msg => {
+    is(msg.status, "error", "downloads.download() fails with back-references");
+    is(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
+  });
+
+  // Try to download to a long relative path containing ..
+  yield download({
+    url: FILE_URL,
+    filename: OS.Path.join("foo", "..", "..", "file_download.txt"),
+  }).then(msg => {
+    is(msg.status, "error", "downloads.download() fails with back-references");
+    is(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
+  });
+
+  yield extension.unload();
+});
+
+// check for leftover files in the download directory
+add_task(function*() {
+  let entries = downloadDir.directoryEntries;
+  while (entries.hasMoreElements()) {
+    let entry = entries.getNext().QueryInterface(Ci.nsIFile);
+    ok(false, `Leftover file ${entry.path} in download directory`);
+    entry.remove(false);
+  }
+
+  downloadDir.remove(false);
+});
+
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_alarms.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_alarms.html
@@ -10,104 +10,210 @@
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_alarm_without_permissions() {
   function backgroundScript() {
-    browser.test.log("running alarm script");
-
     browser.test.assertTrue(!browser.alarms,
-                            "alarm API should not be available if the alarm permission is not required");
+                            "alarm API is not available when the alarm permission is not required");
     browser.test.notifyPass("alarms_permission");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${backgroundScript})()`,
     manifest: {
       permissions: [],
     },
   });
 
   yield extension.startup();
-  info("extension loaded");
   yield extension.awaitFinish("alarms_permission");
   yield extension.unload();
-  info("extension unloaded");
 });
 
 
 add_task(function* test_alarm_fires() {
   function backgroundScript() {
     let ALARM_NAME = "test_ext_alarms";
-    browser.test.log("running alarm script");
 
-    chrome.alarms.onAlarm.addListener(function(alarm) {
-      browser.test.assertEq(alarm.name, ALARM_NAME, "alarm should have the correct name");
+    browser.alarms.onAlarm.addListener(alarm => {
+      browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the correct name");
       browser.test.notifyPass("alarms");
     });
-    chrome.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
+    browser.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
     setTimeout(() => {
-      browser.test.notifyFail("alarms test failed, took too long");
+      browser.test.fail("alarm fired within expected time");
     }, 10000);
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${backgroundScript})()`,
     manifest: {
       permissions: ["alarms"],
     },
   });
 
   yield extension.startup();
-  info("extension loaded");
+  yield extension.awaitFinish("alarms");
+  yield extension.unload();
+});
+
+
+add_task(function* test_cleared_alarm_does_not_fire() {
+  function backgroundScript() {
+    let ALARM_NAME = "test_ext_alarms";
+
+    browser.alarms.onAlarm.addListener(alarm => {
+      browser.test.fail("cleared alarm does not fire");
+    });
+    browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
+    browser.alarms.clear(ALARM_NAME, wasCleared => {
+      browser.test.assertTrue(wasCleared, "alarm was cleared");
+      setTimeout(() => {
+        browser.test.notifyPass("alarms");
+      }, 2000);
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["alarms"],
+    },
+  });
+
+  yield extension.startup();
   yield extension.awaitFinish("alarms");
   yield extension.unload();
-  info("extension unloaded");
+});
+
+
+add_task(function* test_alarm_fires_with_when() {
+  function backgroundScript() {
+    let ALARM_NAME = "test_ext_alarms";
+
+    browser.alarms.onAlarm.addListener(alarm => {
+      browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name");
+      browser.test.notifyPass("alarms");
+    });
+    browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
+    setTimeout(() => {
+      browser.test.fail("alarm fired within expected time");
+      browser.alarms.clear(ALARM_NAME, (wasCleared) => {
+        browser.test.assertTrue(wasCleared, "alarm was cleared");
+      });
+    }, 10000);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["alarms"],
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("alarms");
+  yield extension.unload();
+});
+
+
+add_task(function* test_alarm_clear_non_matching_name() {
+  function backgroundScript() {
+    let ALARM_NAME = "test_ext_alarms";
+
+    browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000});
+
+    browser.alarms.clear(ALARM_NAME + "1", wasCleared => {
+      browser.test.assertFalse(wasCleared, "alarm was not cleared");
+      browser.alarms.getAll(alarms => {
+        browser.test.assertEq(1, alarms.length, "alarm was not removed");
+        browser.test.notifyPass("alarms");
+      });
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["alarms"],
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("alarms");
+  yield extension.unload();
+});
+
+
+add_task(function* test_alarm_get_and_clear_single_argument() {
+  function backgroundScript() {
+    browser.alarms.create({when: Date.now() + 2000});
+
+    browser.alarms.get(alarm => {
+      browser.test.assertEq("", alarm.name, "expected alarm returned");
+      browser.alarms.clear(wasCleared => {
+        browser.test.assertTrue(wasCleared, "alarm was cleared");
+        browser.alarms.getAll(alarms => {
+          browser.test.assertEq(0, alarms.length, "alarm was removed");
+          browser.test.notifyPass("alarms");
+        });
+      });
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["alarms"],
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("alarms");
+  yield extension.unload();
 });
 
 
 add_task(function* test_periodic_alarm_fires() {
   function backgroundScript() {
     const ALARM_NAME = "test_ext_alarms";
-    browser.test.log("running alarm script");
 
     let count = 0;
-    chrome.alarms.onAlarm.addListener(function(alarm) {
-      browser.test.assertEq(alarm.name, ALARM_NAME, "alarm should have the correct name");
+    browser.alarms.onAlarm.addListener(alarm => {
+      browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name");
       if (count++ === 3) {
-        chrome.alarms.clear(ALARM_NAME, (wasCleared) => {
-          browser.test.assertTrue(wasCleared, "alarm should be cleared");
+        browser.alarms.clear(ALARM_NAME, (wasCleared) => {
+          browser.test.assertTrue(wasCleared, "alarm was cleared");
           browser.test.notifyPass("alarms");
         });
       }
     });
-    chrome.alarms.create(ALARM_NAME, {periodInMinutes: 0.02});
+    browser.alarms.create(ALARM_NAME, {periodInMinutes: 0.02});
     setTimeout(() => {
-      browser.test.notifyFail("alarms test failed, took too long");
-      chrome.alarms.clear(ALARM_NAME, (wasCleared) => {
-        browser.test.assertTrue(wasCleared, "alarm should be cleared");
+      browser.test.notify("alarm fired within expected time");
+      browser.alarms.clear(ALARM_NAME, (wasCleared) => {
+        browser.test.assertTrue(wasCleared, "alarm was cleared");
       });
     }, 30000);
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${backgroundScript})()`,
     manifest: {
       permissions: ["alarms"],
     },
   });
 
   yield extension.startup();
-  info("extension loaded");
   yield extension.awaitFinish("alarms");
   yield extension.unload();
-  info("extension unloaded");
 });
 
 
 add_task(function* test_get_get_all_clear_all_alarms() {
   function backgroundScript() {
     const ALARM_NAME = "test_alarm";
 
     let suffixes = [0, 1, 2];
@@ -124,38 +230,38 @@ add_task(function* test_get_get_all_clea
 
     suffixes.forEach(suffix => {
       browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000});
     });
 
     promiseAlarms.getAll().then(alarms => {
       browser.test.assertEq(suffixes.length, alarms.length);
       alarms.forEach((alarm, index) => {
-        browser.test.assertEq(ALARM_NAME + index, alarm.name, "expected alarm returned");
+        browser.test.assertEq(ALARM_NAME + index, alarm.name, "alarm has the expected name");
       });
 
       return Promise.all(
         suffixes.map(suffix => {
           return promiseAlarms.get(ALARM_NAME + suffix).then(alarm => {
-            browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "expected alarm returned");
+            browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "alarm has the expected name");
             browser.test.sendMessage(`get-${suffix}`);
           });
         }));
     }).then(() => {
       return promiseAlarms.clear(ALARM_NAME + suffixes[0]);
     }).then(wasCleared => {
       browser.test.assertTrue(wasCleared, "alarm was cleared");
 
       return promiseAlarms.getAll();
     }).then(alarms => {
       browser.test.assertEq(2, alarms.length, "alarm was removed");
 
       return promiseAlarms.get(ALARM_NAME + suffixes[0]);
     }).then(alarm => {
-      browser.test.assertEq(undefined, alarm, "non-existent alarm should be undefined");
+      browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined");
       browser.test.sendMessage(`get-invalid`);
 
       return promiseAlarms.clearAll();
     }).then(wasCleared => {
       browser.test.assertTrue(wasCleared, "alarms were cleared");
 
       return promiseAlarms.getAll();
     }).then(alarms => {
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -9,16 +9,17 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
+SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("intl.accept_languages"); });
 SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("general.useragent.locale"); });
 
 add_task(function* test_i18n() {
   function runTests(assertEq) {
     let _ = browser.i18n.getMessage.bind(browser.i18n);
 
     let url = browser.runtime.getURL("/");
     assertEq(url, `moz-extension://${_("@@extension_id")}/`, "@@extension_id builtin message");
@@ -158,16 +159,95 @@ add_task(function* test_i18n() {
   let win = window.open("file_sample.html");
   yield extension.awaitMessage("content-script-finished");
   win.close();
 
   yield extension.awaitFinish("l10n");
   yield extension.unload();
 });
 
+add_task(function* test_get_accept_languages() {
+  function background() {
+    function checkResults(source, results, expected) {
+      browser.test.assertEq(
+        expected.length,
+        results.length,
+        `got expected number of languages in ${source}`);
+      results.forEach((lang, index) => {
+        browser.test.assertEq(
+          expected[index],
+          lang,
+          `got expected language in ${source}`);
+      });
+    }
+
+    let tabId;
+
+    browser.tabs.query({currentWindow: true, active: true}, tabs => {
+      tabId = tabs[0].id;
+      browser.test.sendMessage("ready");
+    });
+
+    browser.test.onMessage.addListener(([msg, expected]) => {
+      Promise.all([
+        new Promise(
+          resolve => browser.tabs.sendMessage(tabId, "get-results", resolve)),
+        browser.i18n.getAcceptLanguages(),
+      ]).then(([contentResults, backgroundResults]) => {
+        checkResults("contentScript", contentResults, expected);
+        checkResults("background", backgroundResults, expected);
+
+        browser.test.sendMessage("done");
+      });
+    });
+  }
+
+  function content() {
+    browser.runtime.onMessage.addListener((msg, sender, respond) => {
+      browser.i18n.getAcceptLanguages(respond);
+      return true;
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "content_scripts": [{
+        "matches": ["http://mochi.test/*/file_sample.html"],
+        "run_at": "document_start",
+        "js": ["content_script.js"],
+      }],
+    },
+
+    background: `(${background})()`,
+
+    files: {
+      "content_script.js": `(${content})()`,
+    },
+  });
+
+  let win = window.open("file_sample.html");
+
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+
+  let expectedLangs = ["en_US", "en"];
+  extension.sendMessage(["expect-results", expectedLangs]);
+  yield extension.awaitMessage("done");
+
+  expectedLangs = ["en_US", "en", "fr_CA", "fr"];
+  SpecialPowers.setCharPref("intl.accept_languages", expectedLangs.toString());
+  extension.sendMessage(["expect-results", expectedLangs]);
+  yield extension.awaitMessage("done");
+  SpecialPowers.clearUserPref("intl.accept_languages");
+
+  win.close();
+
+  yield extension.unload();
+});
+
 add_task(function* test_get_ui_language() {
   function getResults() {
     return {
       getUILanguage: browser.i18n.getUILanguage(),
       getMessage: browser.i18n.getMessage("@@ui_locale"),
     };
   }
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_schema.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
@@ -11,28 +11,30 @@
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* testEmptySchema() {
   function background() {
     browser.test.assertTrue(!("manifest" in browser), "browser.manifest is not defined");
+    browser.test.assertTrue("storage" in browser, "browser.storage should be defined");
+    browser.test.assertTrue(!("contextMenus" in browser), "browser.contextMenus should not be defined");
     browser.test.notifyPass("schema");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${background})()`,
+    manifest: {
+      permissions: ["storage"],
+    },
   });
 
-
   yield extension.startup();
-
   yield extension.awaitFinish("schema");
-
   yield extension.unload();
 });
 
 add_task(function* testUnknownProperties() {
   function background() {
     browser.test.notifyPass("loaded");
   }
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -19,16 +19,17 @@ function backgroundScript() {
 
   const EVENTS = [
     "onBeforeNavigate",
     "onCommitted",
     "onDOMContentLoaded",
     "onCompleted",
     "onErrorOccurred",
     "onReferenceFragmentUpdated",
+    "onHistoryStateUpdated",
   ];
 
   let expectedTabId = -1;
 
   function gotEvent(event, details) {
     if (!details.url.startsWith(BASE)) {
       return;
     }
@@ -56,23 +57,24 @@ function backgroundScript() {
   }
 
   let listeners = {};
   for (let event of EVENTS) {
     listeners[event] = gotEvent.bind(null, event);
     browser.webNavigation[event].addListener(listeners[event]);
   }
 
-  browser.test.sendMessage("ready", browser.webRequest.ResourceType);
+  browser.test.sendMessage("ready");
 }
 
 const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
 const URL = BASE + "/file_WebNavigation_page1.html";
 const FRAME = BASE + "/file_WebNavigation_page2.html";
 const FRAME2 = BASE + "/file_WebNavigation_page3.html";
+const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html";
 
 const REQUIRED = [
   "onBeforeNavigate",
   "onCommitted",
   "onDOMContentLoaded",
   "onCompleted",
 ];
 
@@ -150,20 +152,81 @@ add_task(function* webnav_ordering() {
 
   checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"});
   checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"});
 
   yield loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; });
 
   checkRequired(FRAME2);
 
-  yield loadAndWait(win, "onReferenceFragmentUpdated", FRAME2 + "#ref",
-                    () => { win.frames[0].document.getElementById("elt").click(); });
+  let navigationSequence = [
+    {
+      action: () => { win.frames[0].document.getElementById("elt").click(); },
+      waitURL: `${FRAME2}#ref`,
+      expectedEvent: "onReferenceFragmentUpdated",
+      description: "clicked an anchor link",
+    },
+    {
+      action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+      waitURL: `${FRAME2}#ref2`,
+      expectedEvent: "onReferenceFragmentUpdated",
+      description: "history.pushState, same pathname, different hash",
+    },
+    {
+      action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+      waitURL: `${FRAME2}#ref2`,
+      expectedEvent: "onHistoryStateUpdated",
+      description: "history.pushState, same pathname, same hash",
+    },
+    {
+      action: () => {
+        win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`);
+      },
+      waitURL: `${FRAME2}?query_param1=value#ref2`,
+      expectedEvent: "onHistoryStateUpdated",
+      description: "history.pushState, same pathname, same hash, different query params",
+    },
+    {
+      action: () => {
+        win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`);
+      },
+      waitURL: `${FRAME2}?query_param2=value#ref3`,
+      expectedEvent: "onHistoryStateUpdated",
+      description: "history.pushState, same pathname, different hash, different query params",
+    },
+    {
+      action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); },
+      waitURL: FRAME_PUSHSTATE,
+      expectedEvent: "onHistoryStateUpdated",
+      description: "history.pushState, different pathname",
+    },
+  ];
 
-  info("Received onReferenceFragmentUpdated from FRAME2");
+  for (let navigation of navigationSequence) {
+    let {expectedEvent, waitURL, action, description} = navigation;
+    info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`);
+    yield loadAndWait(win, expectedEvent, waitURL, action);
+    info(`Received ${expectedEvent} from ${waitURL} - ${description}`);
+  }
+
+  for (let i = navigationSequence.length - 1; i > 0; i--) {
+    let {waitURL: fromURL, expectedEvent} = navigationSequence[i];
+    let {waitURL} = navigationSequence[i - 1];
+    info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+    yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); });
+    info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+  }
+
+  for (let i = 0; i < navigationSequence.length - 1; i++) {
+    let {waitURL: fromURL} = navigationSequence[i];
+    let {waitURL, expectedEvent} = navigationSequence[i + 1];
+    info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+    yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); });
+    info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+  }
 
   win.close();
 
   yield extension.unload();
   info("webnavigation extension unloaded");
 });
 </script>
 
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -276,16 +276,28 @@ let json = [
        type: "function",
        extraParameters: [{
          name: "filter",
          type: "integer",
        }],
      },
    ],
   },
+  {
+    namespace: "inject",
+    properties: {
+      PROP1: {value: "should inject"},
+    },
+  },
+  {
+    namespace: "do-not-inject",
+    properties: {
+      PROP1: {value: "should not inject"},
+    },
+  },
 ];
 
 let tallied = null;
 
 function tally(kind, ns, name, args) {
   tallied = [kind, ns, name, args];
 }
 
@@ -317,16 +329,21 @@ let wrapper = {
     talliedErrors.push(message);
   },
 
   callFunction(path, name, args) {
     let ns = path.join(".");
     tally("call", ns, name, args);
   },
 
+  shouldInject(path) {
+    let ns = path.join(".");
+    return ns != "do-not-inject";
+  },
+
   getProperty(path, name) {
     let ns = path.join(".");
     tally("get", ns, name);
   },
 
   setProperty(path, name, value) {
     let ns = path.join(".");
     tally("set", ns, name, value);
@@ -353,16 +370,19 @@ add_task(function* () {
 
   let root = {};
   Schemas.inject(root, wrapper);
 
   do_check_eq(root.testing.PROP1, 20, "simple value property");
   do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
   do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
 
+  do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
+  do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
+
   root.testing.foo(11, true);
   verify("call", "testing", "foo", [11, true]);
 
   root.testing.foo(true);
   verify("call", "testing", "foo", [null, true]);
 
   root.testing.foo(null, true);
   verify("call", "testing", "foo", [null, true]);
--- a/toolkit/components/satchel/FormHistory.jsm
+++ b/toolkit/components/satchel/FormHistory.jsm
@@ -837,37 +837,34 @@ this.FormHistory = {
         }
       }
     };
 
     stmt.executeAsync(handlers);
   },
 
   update : function formHistoryUpdate(aChanges, aCallbacks) {
+    if (!Prefs.enabled) {
+      return;
+    }
+
     // Used to keep track of how many searches have been started. When that number
     // are finished, updateFormHistoryWrite can be called.
     let numSearches = 0;
     let completedSearches = 0;
     let searchFailed = false;
 
     function validIdentifier(change) {
       // The identifier is only valid if one of either the guid or the (fieldname/value) are set
       return Boolean(change.guid) != Boolean(change.fieldname && change.value);
     }
 
     if (!("length" in aChanges))
       aChanges = [aChanges];
 
-    let isRemoveOperation = aChanges.every(change => change && change.op && change.op == "remove");
-    if (!Prefs.enabled && !isRemoveOperation) {
-      throw Components.Exception(
-        "Form history is disabled, only remove operations are allowed",
-        Cr.NS_ERROR_ILLEGAL_VALUE);
-    }
-
     for (let change of aChanges) {
       switch (change.op) {
         case "remove":
           validateSearchData(change, "Remove");
           continue;
         case "update":
           if (validIdentifier(change)) {
             validateOpData(change, "Update");
--- a/toolkit/components/satchel/test/unit/test_history_api.js
+++ b/toolkit/components/satchel/test/unit/test_history_api.js
@@ -400,46 +400,16 @@ add_task(function* ()
   do_check_eq(13, results[3].timesUsed);
   do_check_eq(230, results[2].firstUsed);
   do_check_eq(430, results[3].firstUsed);
   do_check_true(results[2].lastUsed > 600);
   do_check_true(results[3].lastUsed > 700);
 
   yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
 
-  // ===== 21 =====
-  // Check update throws if form history is disabled and the operation is not a
-  // pure removal.
-  testnum++;
-  Services.prefs.setBoolPref("browser.formfill.enable", false);
-  Assert.throws(() => promiseUpdate(
-                { op : "bump", fieldname: "field5", value: "value5" }),
-                /NS_ERROR_ILLEGAL_VALUE/);
-  Assert.throws(() => promiseUpdate(
-                { op : "add", fieldname: "field5", value: "value5" }),
-                /NS_ERROR_ILLEGAL_VALUE/);
-  Assert.throws(() => promiseUpdate([
-                  { op : "update", fieldname: "field5", value: "value5" },
-                  { op : "remove", fieldname: "field5", value: "value5" }
-                ]),
-                /NS_ERROR_ILLEGAL_VALUE/);
-  Assert.throws(() => promiseUpdate([
-                  null,
-                  undefined,
-                  "",
-                  1,
-                  {},
-                  { op : "remove", fieldname: "field5", value: "value5" }
-                ]),
-                /NS_ERROR_ILLEGAL_VALUE/);
-  // Remove should work though.
-  yield promiseUpdate([{ op: "remove", fieldname: "field5", value: null },
-                       { op: "remove", fieldname: null, value: null }]);
-  Services.prefs.clearUserPref("browser.formfill.enable");
-
   } catch (e) {
     throw "FAILED in test #" + testnum + " -- " + e;
   }
   finally {
     FormHistory._supportsDeletedTable = oldSupportsDeletedTable;
     dbConnection.asyncClose(do_test_finished);
   }
 });
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -94,18 +94,21 @@ var Manager = {
           this.fire("onErrorOccurred", browser, data, {error, url});
         }
       }
     }
   },
 
   onLocationChange(browser, data) {
     let url = data.location;
-    if (data.flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+
+    if (data.isReferenceFragmentUpdated) {
       this.fire("onReferenceFragmentUpdated", browser, data, {url});
+    } else if (data.isHistoryStateUpdated) {
+      this.fire("onHistoryStateUpdated", browser, data, {url});
     } else {
       this.fire("onCommitted", browser, data, {url});
     }
   },
 
   onLoad(browser, data) {
     this.fire("onDOMContentLoaded", browser, data, {url: data.url});
   },
@@ -137,19 +140,18 @@ var Manager = {
 
 const EVENTS = [
   "onBeforeNavigate",
   "onCommitted",
   "onDOMContentLoaded",
   "onCompleted",
   "onErrorOccurred",
   "onReferenceFragmentUpdated",
-
+  "onHistoryStateUpdated",
   // "onCreatedNavigationTarget",
-  // "onHistoryStateUpdated",
 ];
 
 var WebNavigation = {};
 
 for (let event of EVENTS) {
   WebNavigation[event] = {
     addListener: Manager.addListener.bind(Manager, event),
     removeListener: Manager.removeListener.bind(Manager, event),
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -20,16 +20,29 @@ function loadListener(event) {
 
 addEventListener("DOMContentLoaded", loadListener);
 addMessageListener("Extension:DisableWebNavigation", () => {
   removeEventListener("DOMContentLoaded", loadListener);
 });
 
 var WebProgressListener = {
   init: function() {
+    // This WeakMap (DOMWindow -> nsIURI) keeps track of the pathname and hash
+    // of the previous location for all the existent docShells.
+    this.previousURIMap = new WeakMap();
+
+    // Populate the above previousURIMap by iterating over the docShells tree.
+    for (let currentDocShell of WebNavigationFrames.iterateDocShellTree(docShell)) {
+      let win = currentDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIDOMWindow);
+      let {currentURI} = currentDocShell.QueryInterface(Ci.nsIWebNavigation);
+
+      this.previousURIMap.set(win, currentURI);
+    }
+
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
     webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
                                           Ci.nsIWebProgress.NOTIFY_LOCATION);
   },
 
   uninit() {
     if (!docShell) {
@@ -43,42 +56,70 @@ var WebProgressListener = {
   onStateChange: function onStateChange(webProgress, request, stateFlags, status) {
     let data = {
       requestURL: request.QueryInterface(Ci.nsIChannel).URI.spec,
       windowId: webProgress.DOMWindowID,
       parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
       status,
       stateFlags,
     };
+
     sendAsyncMessage("Extension:StateChange", data);
 
     if (webProgress.DOMWindow.top != webProgress.DOMWindow) {
       let webNav = webProgress.QueryInterface(Ci.nsIWebNavigation);
       if (!webNav.canGoBack) {
         // For some reason we don't fire onLocationChange for the
         // initial navigation of a sub-frame. So we need to simulate
         // it here.
-        let data = {
-          location: request.QueryInterface(Ci.nsIChannel).URI.spec,
-          windowId: webProgress.DOMWindowID,
-          parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
-          flags: 0,
-        };
-        sendAsyncMessage("Extension:LocationChange", data);
+        this.onLocationChange(webProgress, request, request.QueryInterface(Ci.nsIChannel).URI, 0);
       }
     }
   },
 
   onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
+    let {DOMWindow, loadType} = webProgress;
+
+    // Get the previous URI loaded in the DOMWindow.
+    let previousURI = this.previousURIMap.get(DOMWindow);
+
+    // Update the URI in the map with the new locationURI.
+    this.previousURIMap.set(DOMWindow, locationURI);
+
+    let isSameDocument = (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
+    let isHistoryStateUpdated = false;
+    let isReferenceFragmentUpdated = false;
+
+    if (isSameDocument) {
+      let pathChanged = !(previousURI && locationURI.equalsExceptRef(previousURI));
+      let hashChanged = !(previousURI && previousURI.ref == locationURI.ref);
+
+      // When the location changes but the document is the same:
+      // - path not changed and hash changed -> |onReferenceFragmentUpdated|
+      //   (even if it changed using |history.pushState|)
+      // - path not changed and hash not changed -> |onHistoryStateUpdated|
+      //   (only if it changes using |history.pushState|)
+      // - path changed -> |onHistoryStateUpdated|
+
+      if (!pathChanged && hashChanged) {
+        isReferenceFragmentUpdated = true;
+      } else if (loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
+        isHistoryStateUpdated = true;
+      } else if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
+        isHistoryStateUpdated = true;
+      }
+    }
+
     let data = {
+      isHistoryStateUpdated, isReferenceFragmentUpdated,
       location: locationURI ? locationURI.spec : "",
       windowId: webProgress.DOMWindowID,
       parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
-      flags,
     };
+
     sendAsyncMessage("Extension:LocationChange", data);
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
 };
 
 var disabled = false;
 WebProgressListener.init();
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -92,16 +92,18 @@ function findFrame(windowId, rootDocShel
       return convertDocShellToFrameDetail(docShell);
     }
   }
 
   return null;
 }
 
 var WebNavigationFrames = {
+  iterateDocShellTree,
+
   getFrame(docShell, frameId) {
     if (frameId == 0) {
       return convertDocShellToFrameDetail(docShell);
     }
 
     return findFrame(frameId, docShell);
   },