Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 25 Feb 2016 14:38:16 -0800
changeset 321981 7381731bbb411698aa74352841f79a7fb13020c3
parent 321980 d1a8a43006ec0b186429362e3cc241dacf39ae94 (current diff)
parent 321933 918df3a0bc1c4d07299e4f66274a7da923534577 (diff)
child 321982 a48d1a9ea9063396b6600357a2b5014274f2d777
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
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound, 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);
   },