Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 18 Apr 2016 15:05:36 +0200
changeset 331471 755eb5d58f6f3ee4e41f9844e4491aa2bbd8e9a1
parent 331470 317bc3f69953ab30354ae88bfe30061cabb29bb2 (current diff)
parent 331425 6066850740cd4711ee5502fda89f422440b7c2cc (diff)
child 331472 826d16396107def2873839344ebb6306832114d0
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone48.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 mozilla-central to mozilla-inbound
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -207,22 +207,25 @@ class BasePopup {
 
   handleEvent(event) {
     switch (event.type) {
       case this.DESTROY_EVENT:
         this.destroy();
         break;
 
       case "DOMWindowCreated":
-        let winUtils = this.browser.contentWindow
-            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
-        for (let stylesheet of global.stylesheets) {
-          winUtils.addSheet(stylesheet, winUtils.AGENT_SHEET);
+        if (event.target === this.browser.contentDocument) {
+          let winUtils = this.browser.contentWindow
+              .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+          for (let stylesheet of global.stylesheets) {
+            winUtils.addSheet(stylesheet, winUtils.AGENT_SHEET);
+          }
         }
         break;
+
       case "DOMWindowClose":
         if (event.target === this.browser.contentWindow) {
           event.preventDefault();
           this.closePopup();
         }
         break;
 
       case "DOMTitleChanged":
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1183,21 +1183,23 @@ html|span.ac-tag {
 }
 
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-type-icon[type=switchtab] {
+.ac-type-icon[type=switchtab],
+.ac-type-icon[type=remotetab] {
   list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
 }
 
-.ac-type-icon[type=switchtab][selected] {
+.ac-type-icon[type=switchtab][selected],
+.ac-type-icon[type=remotetab][selected] {
   list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(suggesthint, treecolAutoCompleteComment),
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1830,21 +1830,23 @@ html|span.ac-emphasize-text-url {
 .autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected) {
   list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon-inverted);
 }
 
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
 }
 
-.ac-type-icon[type=switchtab] {
+.ac-type-icon[type=switchtab],
+.ac-type-icon[type=remotetab] {
   list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
 }
 
-.ac-type-icon[type=switchtab][selected] {
+.ac-type-icon[type=switchtab][selected],
+.ac-type-icon[type=remotetab][selected] {
   list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(suggesthint, treecolAutoCompleteComment),
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1568,21 +1568,23 @@ html|span.ac-emphasize-text-url {
 }
 
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-type-icon[type=switchtab] {
+.ac-type-icon[type=switchtab],
+.ac-type-icon[type=remotetab] {
   list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
 }
 
-.ac-type-icon[type=switchtab][selected] {
+.ac-type-icon[type=switchtab][selected],
+.ac-type-icon[type=remotetab][selected] {
   list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(suggesthint, treecolAutoCompleteComment),
--- a/devtools/client/aboutdebugging/components/addon-target.js
+++ b/devtools/client/aboutdebugging/components/addon-target.js
@@ -20,28 +20,45 @@ const Strings = Services.strings.createB
 module.exports = createClass({
   displayName: "AddonTarget",
 
   debug() {
     let { target } = this.props;
     BrowserToolboxProcess.init({ addonID: target.addonID });
   },
 
+  reload() {
+    let { client, target } = this.props;
+    // This function sometimes returns a partial promise that only
+    // implements then().
+    client.request({
+      to: target.addonActor,
+      type: "reload"
+    }).then(() => {}, error => {
+      throw new Error(
+        "Error reloading addon " + target.addonID + ": " + error);
+    });
+  },
+
   render() {
     let { target, debugDisabled } = this.props;
 
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
       }),
       dom.div({ className: "target" },
         dom.div({ className: "target-name" }, target.name)
       ),
       dom.button({
         className: "debug-button",
         onClick: this.debug,
         disabled: debugDisabled,
-      }, Strings.GetStringFromName("debug"))
+      }, Strings.GetStringFromName("debug")),
+      dom.button({
+        className: "reload-button",
+        onClick: this.reload
+      }, Strings.GetStringFromName("reload"))
     );
   }
 });
--- a/devtools/client/aboutdebugging/components/addons-tab.js
+++ b/devtools/client/aboutdebugging/components/addons-tab.js
@@ -55,27 +55,31 @@ module.exports = createClass({
     let debugDisabled =
       !Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
       !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
 
     this.setState({ debugDisabled });
   },
 
   updateAddonsList() {
-    AddonManager.getAllAddons(addons => {
-      let extensions = addons.filter(addon => addon.isDebuggable).map(addon => {
-        return {
-          name: addon.name,
-          icon: addon.iconURL || ExtensionIcon,
-          addonID: addon.id
-        };
+    this.props.client.listAddons()
+      .then(({addons}) => {
+        let extensions = addons.filter(addon => addon.debuggable).map(addon => {
+          return {
+            name: addon.name,
+            icon: addon.iconURL || ExtensionIcon,
+            addonID: addon.id,
+            addonActor: addon.actor
+          };
+        });
+
+        this.setState({ extensions });
+      }, error => {
+        throw new Error("Client error while listing addons: " + error);
       });
-
-      this.setState({ extensions });
-    });
   },
 
   /**
    * Mandatory callback as AddonManager listener.
    */
   onInstalled() {
     this.updateAddonsList();
   },
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -9,15 +9,16 @@ support-files =
   service-workers/empty-sw.html
   service-workers/empty-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
 
 [browser_addons_debug_bootstrapped.js]
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
+[browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_service_workers.js]
 [browser_service_workers_push.js]
 [browser_service_workers_start.js]
 [browser_service_workers_timeout.js]
 skip-if = true # Bug 1232931
 [browser_service_workers_unregister.js]
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
@@ -17,16 +17,17 @@ add_task(function* () {
       ["devtools.debugger.prompt-connection", false],
       // Enable Browser toolbox test script execution via env variable
       ["devtools.browser-toolbox.allow-unsafe-script", true],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   // Retrieve the DEBUG button for the addon
   let names = [...document.querySelectorAll("#addons .target-name")];
   let name = names.filter(element => element.textContent === ADDON_NAME)[0];
   ok(name, "Found the addon in the list");
   let targetElement = name.parentNode.parentNode;
--- a/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
@@ -42,16 +42,17 @@ function* testCheckboxState(testData) {
     let options = {"set": [
       ["devtools.chrome.enabled", testData.chromeEnabled],
       ["devtools.debugger.remote-enabled", testData.debuggerRemoteEnable],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   info("Test checkbox checked state.");
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   is(addonDebugCheckbox.checked, testData.expected,
--- a/devtools/client/aboutdebugging/test/browser_addons_install.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -2,29 +2,31 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const ADDON_ID = "test-devtools@mozilla.org";
 const ADDON_NAME = "test-devtools";
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   // Install this add-on, and verify that it appears in the about:debugging UI
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   // Install the add-on, and verify that it disappears in the about:debugging UI
   yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
 
   yield closeAboutDebugging(tab);
 });
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   // Start an observer that looks for the install error before
   // actually doing the install
   let top = document.querySelector(".addons-top");
   let promise = waitForMutation(top, { childList: true });
 
   // Mock the file picker to select a test addon
   let MockFilePicker = SpecialPowers.MockFilePicker;
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+/**
+ * Returns a promise that resolves when the given add-on event is fired. The
+ * resolved value is an array of arguments passed for the event.
+ */
+function promiseAddonEvent(event) {
+  return new Promise(resolve => {
+    let listener = {
+      [event]: function(...args) {
+        AddonManager.removeAddonListener(listener);
+        resolve(args);
+      }
+    };
+
+    AddonManager.addAddonListener(listener);
+  });
+}
+
+add_task(function* () {
+  const { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
+  yield installAddon(document, "addons/unpacked/install.rdf",
+                     ADDON_NAME, ADDON_NAME);
+
+  // Retrieve the Reload button.
+  const names = [...document.querySelectorAll("#addons .target-name")];
+  const name = names.filter(element => element.textContent === ADDON_NAME)[0];
+  ok(name, "Found " + ADDON_NAME + " add-on in the list");
+  const targetElement = name.parentNode.parentNode;
+  const reloadButton = targetElement.querySelector(".reload-button");
+  ok(reloadButton, "Found its reload button");
+
+  const onDisabled = promiseAddonEvent("onDisabled");
+  const onEnabled = promiseAddonEvent("onEnabled");
+
+  const onBootstrapInstallCalled = new Promise(done => {
+    Services.obs.addObserver(function listener() {
+      Services.obs.removeObserver(listener, ADDON_NAME, false);
+      ok(true, "Add-on was installed: " + ADDON_NAME);
+      done();
+    }, ADDON_NAME, false);
+  });
+
+  reloadButton.click();
+
+  const [disabledAddon] = yield onDisabled;
+  ok(disabledAddon.name === ADDON_NAME,
+     "Add-on was disabled: " + disabledAddon.name);
+
+  const [enabledAddon] = yield onEnabled;
+  ok(enabledAddon.name === ADDON_NAME,
+     "Add-on was re-enabled: " + enabledAddon.name);
+
+  yield onBootstrapInstallCalled;
+
+  info("Uninstall addon installed earlier.");
+  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
@@ -15,16 +15,17 @@ add_task(function* () {
     let options = {"set": [
       ["devtools.chrome.enabled", false],
       ["devtools.debugger.remote-enabled", false],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
   yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
                      "test-devtools");
 
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
 
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
 /* eslint-disable mozilla/no-cpows-in-tests */
 /* exported openAboutDebugging, closeAboutDebugging, installAddon,
    uninstallAddon, waitForMutation, assertHasTarget,
-   waitForServiceWorkerRegistered, unregisterServiceWorker */
+   waitForInitialAddonList, waitForServiceWorkerRegistered,
+   unregisterServiceWorker */
 /* global sendAsyncMessage */
 
 "use strict";
 
 var { utils: Cu, classes: Cc, interfaces: Ci } = Components;
 
 const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
@@ -151,16 +152,40 @@ function* uninstallAddon(document, addon
   let names = [...addonList.querySelectorAll(".target-name")];
   names = names.map(element => element.textContent);
   ok(!names.includes(addonName),
     "After uninstall, the addon name disappears from the list of addons: "
     + names);
 }
 
 /**
+ * Returns a promise that will resolve when the add-on list has been updated.
+ *
+ * @param {Node} document
+ * @return {Promise}
+ */
+function waitForInitialAddonList(document) {
+  const addonListContainer = document.querySelector("#addons .targets");
+  let addonCount = addonListContainer.querySelectorAll(".target");
+  addonCount = addonCount ? [...addonCount].length : -1;
+  info("Waiting for add-ons to load. Current add-on count: " + addonCount);
+
+  // This relies on the network speed of the actor responding to the
+  // listAddons() request and also the speed of openAboutDebugging().
+  let result;
+  if (addonCount > 0) {
+    info("Actually, the add-ons have already loaded");
+    result = Promise.resolve();
+  } else {
+    result = waitForMutation(addonListContainer, { childList: true });
+  }
+  return result;
+}
+
+/**
  * Returns a promise that will resolve after receiving a mutation matching the
  * provided mutation options on the provided target.
  * @param {Node} target
  * @param {Object} mutationOptions
  * @return {Promise}
  */
 function waitForMutation(target, mutationOptions) {
   return new Promise(resolve => {
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -122,19 +122,20 @@ HTMLBreadcrumbs.prototype = {
     };
   },
 
   /**
    * Warn if rejection was caused by selection change, print an error otherwise.
    * @param {Error} err
    */
   selectionGuardEnd: function(err) {
-    if (err === "selection-changed") {
-      console.warn("Asynchronous operation was aborted as selection changed.");
-    } else {
+    // If the error is selection-changed, this is expected, the selection
+    // changed while we were waiting for a promise to resolve, so there's no
+    // need to proceed with the current update, and we should be silent.
+    if (err !== "selection-changed") {
       console.error(err);
     }
   },
 
   /**
    * Build a string that represents the node: tagName#id.class1.class2.
    * @param {NodeFront} node The node to pretty-print
    * @return {String}
@@ -613,17 +614,17 @@ HTMLBreadcrumbs.prototype = {
           }
           lastNode = node;
         }
         if (response.hasLast) {
           deferred.resolve(fallback);
           return;
         }
         moreChildren();
-      }).then(null, this.selectionGuardEnd);
+      }).catch(this.selectionGuardEnd);
     };
 
     moreChildren();
     return deferred.promise;
   },
 
   /**
    * Find the "youngest" ancestor of a node which is already in the breadcrumbs.
@@ -811,17 +812,17 @@ HTMLBreadcrumbs.prototype = {
       this.updateSelectors();
 
       // Make sure the selected node and its neighbours are visible.
       this.scroll();
       return resolveNextTick().then(() => {
         this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
         doneUpdating();
       });
-    }).then(null, err => {
+    }).catch(err => {
       doneUpdating(this.selection.nodeFront);
       this.selectionGuardEnd(err);
     });
   }
 };
 
 /**
  * Returns a promise that resolves at the next main thread tick.
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -1061,17 +1061,17 @@ InspectorPanel.prototype = {
 
     // Insert the html and expect a childList markup mutation.
     let onMutations = this.once("markupmutation");
     let {nodes} = yield this.walker.insertAdjacentHTML(this.selection.nodeFront,
                                                        "beforeEnd", html);
     yield onMutations;
 
     // Select the new node (this will auto-expand its parent).
-    this.selection.setNodeFront(nodes[0]);
+    this.selection.setNodeFront(nodes[0], "node-inserted");
   }),
 
   /**
    * Toggle a pseudo class.
    */
   togglePseudoClass: function(aPseudo) {
     if (this.selection.isElementNode()) {
       let node = this.selection.nodeFront;
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -545,29 +545,35 @@ MarkupView.prototype = {
           "destroyed while showing the node.");
       }
     });
 
     promise.all([onShowBoxModel, onShow]).then(done);
   },
 
   /**
-   * Focus the current node selection's MarkupContainer if the selection
-   * happened because the user picked an element using the element picker or
-   * browser context menu.
+   * Maybe focus the current node selection's MarkupContainer depending on why
+   * the current node got selected.
    */
   maybeFocusNewSelection: function() {
     let {reason, nodeFront} = this._inspector.selection;
 
-    if (reason !== "browser-context-menu" &&
-        reason !== "picker-node-picked") {
-      return;
+    // The list of reasons that should lead to focusing the node.
+    let reasonsToFocus = [
+      // If the user picked an element with the element picker.
+      "picker-node-picked",
+      // If the user selected an element with the browser context menu.
+      "browser-context-menu",
+      // If the user added a new node by clicking in the inspector toolbar.
+      "node-inserted"
+    ];
+
+    if (reasonsToFocus.includes(reason)) {
+      this.getContainer(nodeFront).focus();
     }
-
-    this.getContainer(nodeFront).focus();
   },
 
   /**
    * Create a TreeWalker to find the next/previous
    * node for selection.
    */
   _selectionWalker: function(start) {
     let walker = this.doc.createTreeWalker(
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -431,16 +431,22 @@ Rule.prototype = {
    * to parse the style's authoredText.
    */
   _getTextProperties: function() {
     let textProps = [];
     let store = this.elementStyle.store;
     let props = parseDeclarations(this.style.authoredText, true);
     for (let prop of props) {
       let name = prop.name;
+      // If the authored text has an invalid property, it will show up
+      // as nameless.  Skip these as we don't currently have a good
+      // way to display them.
+      if (!name) {
+        continue;
+      }
       // In an inherited rule, we only show inherited properties.
       // However, we must keep all properties in order for rule
       // rewriting to work properly.  So, compute the "invisible"
       // property here.
       let invisible = this.inherited && !domUtils.isInheritedProperty(name);
       let value = store.userProperties.getProperty(this.style, name,
                                                    prop.value);
       let textProp = new TextProperty(this, name, value, prop.priority,
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -116,28 +116,30 @@ skip-if = os == "mac" # Bug 1245996 : cl
 [browser_rules_edit-selector-commit.js]
 [browser_rules_edit-selector_01.js]
 [browser_rules_edit-selector_02.js]
 [browser_rules_edit-selector_03.js]
 [browser_rules_edit-selector_04.js]
 [browser_rules_edit-selector_05.js]
 [browser_rules_edit-selector_06.js]
 [browser_rules_edit-selector_07.js]
+[browser_rules_edit-selector_08.js]
 [browser_rules_editable-field-focus_01.js]
 [browser_rules_editable-field-focus_02.js]
 [browser_rules_eyedropper.js]
 [browser_rules_filtereditor-appears-on-swatch-click.js]
 [browser_rules_filtereditor-commit-on-ENTER.js]
 [browser_rules_filtereditor-revert-on-ESC.js]
 skip-if = (os == "win" && debug) # bug 963492: win.
 [browser_rules_guessIndentation.js]
 [browser_rules_inherited-properties_01.js]
 [browser_rules_inherited-properties_02.js]
 [browser_rules_inherited-properties_03.js]
 [browser_rules_inline-source-map.js]
+[browser_rules_invalid.js]
 [browser_rules_invalid-source-map.js]
 [browser_rules_keybindings.js]
 [browser_rules_keyframes-rule_01.js]
 [browser_rules_keyframes-rule_02.js]
 [browser_rules_keyframeLineNumbers.js]
 [browser_rules_lineNumbers.js]
 [browser_rules_livepreview.js]
 [browser_rules_mark_overridden_01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js
@@ -0,0 +1,71 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that reverting a selector edit does the right thing.
+// Bug 1241046.
+
+const TEST_URI = `
+  <style type="text/css">
+    span {
+      color: chartreuse;
+    }
+  </style>
+  <span>
+    <div id="testid" class="testclass">Styled Node</div>
+  </span>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+
+  info("Selecting the test element");
+  yield selectNode("#testid", inspector);
+
+  let idRuleEditor = getRuleViewRuleEditor(view, 2);
+
+  info("Focusing an existing selector name in the rule-view");
+  let editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+  is(inplaceEditor(idRuleEditor.selectorText), editor,
+    "The selector editor got focused");
+
+  info("Entering a new selector name and committing");
+  editor.input.value = "pre";
+
+  info("Waiting for rule view to update");
+  let onRuleViewChanged = once(view, "ruleview-changed");
+
+  info("Entering the commit key");
+  EventUtils.synthesizeKey("VK_RETURN", {});
+  yield onRuleViewChanged;
+
+  info("Re-focusing the selector name in the rule-view");
+  idRuleEditor = getRuleViewRuleEditor(view, 2);
+  editor = yield focusEditableField(view, idRuleEditor.selectorText);
+
+  is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+  ok(getRuleViewRule(view, "pre"), "Rule with pre selector exists.");
+  is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
+     "true",
+     "Rule with pre does not match the current element.");
+
+  // Now change it back.
+  info("Re-entering original selector name and committing");
+  editor.input.value = "span";
+
+  info("Waiting for rule view to update");
+  onRuleViewChanged = once(view, "ruleview-changed");
+
+  info("Entering the commit key");
+  EventUtils.synthesizeKey("VK_RETURN", {});
+  yield onRuleViewChanged;
+
+  is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+  ok(getRuleViewRule(view, "span"), "Rule with span selector exists.");
+  is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
+     "false", "Rule with span matches the current element.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_invalid.js
@@ -0,0 +1,33 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that an invalid property still lets us display the rule view
+// Bug 1235603.
+
+const TEST_URI = `
+  <style>
+    div {
+	background: #fff;
+	font-family: sans-serif;
+	url(display-table.min.htc);
+   }
+ </style>
+ <body>
+    <div id="testid" class="testclass">Styled Node</div>
+ </body>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+
+  is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+  // Have to actually get the rule in order to ensure that the
+  // elements were created.
+  ok(getRuleViewRule(view, "div"), "Rule with div selector exists");
+});
--- a/devtools/client/inspector/test/browser_inspector_addNode_03.js
+++ b/devtools/client/inspector/test/browser_inspector_addNode_03.js
@@ -1,20 +1,20 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test that adding nodes does work as expected: the parent gets expanded and
-// the new node gets selected.
+// Test that adding nodes does work as expected: the parent gets expanded, the
+// new node gets selected and the corresponding markup-container focused.
 
 const TEST_URL = URL_ROOT + "doc_inspector_add_node.html";
 
-add_task(function*() {
+add_task(function* () {
   let {inspector} = yield openInspectorForURL(TEST_URL);
 
   info("Adding in element that has no children and is collapsed");
   let parentNode = yield getNodeFront("#foo", inspector);
   yield selectNode(parentNode, inspector);
   yield testAddNode(parentNode, inspector);
 
   info("Adding in element with children but that has not been expanded yet");
@@ -32,37 +32,45 @@ add_task(function*() {
   info("Adding in element with children that is expanded");
   parentNode = yield getNodeFront("#bar", inspector);
   yield selectNode(parentNode, inspector);
   yield testAddNode(parentNode, inspector);
 });
 
 function* testAddNode(parentNode, inspector) {
   let btn = inspector.panelDoc.querySelector("#inspector-element-add-button");
+  let markupWindow = inspector.markup.win;
 
-  info("Clicking on the 'add node' button and expecting a markup mutation");
+  info("Clicking 'add node' and expecting a markup mutation and focus event");
   let onMutation = inspector.once("markupmutation");
   btn.click();
   let mutations = yield onMutation;
 
-  info("Expecting an inspector-updated event right after the mutation event "+
+  info("Expecting an inspector-updated event right after the mutation event " +
        "to wait for the new node selection");
   yield inspector.once("inspector-updated");
 
   is(mutations.length, 1, "There is one mutation only");
   is(mutations[0].added.length, 1, "There is one new node only");
 
   let newNode = mutations[0].added[0];
 
   is(newNode, inspector.selection.nodeFront,
      "The new node is selected");
 
   ok(inspector.markup.getContainer(parentNode).expanded,
      "The parent node is now expanded");
 
   is(inspector.selection.nodeFront.parentNode(), parentNode,
      "The new node is inside the right parent");
+
+  let focusedElement = markupWindow.document.activeElement;
+  let focusedContainer = focusedElement.closest(".child").container;
+  is(focusedContainer.node, inspector.selection.nodeFront,
+     "The right container is focused in the markup-view");
+  ok(focusedElement.classList.contains("tag"),
+     "The tagName part of the container is focused");
 }
 
 function collapseNode(node, inspector) {
   let container = inspector.markup.getContainer(node);
   container.setExpanded(false);
 }
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -11,16 +11,17 @@ unregister = unregister
 
 addons = Add-ons
 addonDebugging.label = Enable add-on debugging
 addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
 addonDebugging.moreInfo = more info
 loadTemporaryAddon = Load Temporary Add-on
 extensions = Extensions
 selectAddonFromFile2 = Select Manifest File or Package (.xpi)
+reload = Reload
 
 workers = Workers
 serviceWorkers = Service Workers
 sharedWorkers = Shared Workers
 otherWorkers = Other Workers
 
 nothing = Nothing yet.
 
--- a/devtools/client/locales/en-US/memory.properties
+++ b/devtools/client/locales/en-US/memory.properties
@@ -22,16 +22,20 @@ memory.panelLabel=Memory Panel
 # LOCALIZATION NOTE (memory.tooltip): This string is displayed in the tooltip of
 # the tab when the memory tool is displayed inside the developer tools window.
 memory.tooltip=Memory
 
 # LOCALIZATION NOTE (snapshot.io.save): The label for the link that saves a
 # snapshot to disk.
 snapshot.io.save=Save
 
+# LOCALIZATION NOTE (snapshot.io.delete): The label for the link that deletes
+# a snapshot
+snapshot.io.delete=Delete
+
 # LOCALIZATION NOTE (snapshot.io.save.window): The title for the window
 # displayed when saving a snapshot to disk.
 snapshot.io.save.window=Save Heap Snapshot
 
 # LOCALIZATION NOTE (snapshot.io.import.window): The title for the window
 # displayed when importing a snapshot form disk.
 snapshot.io.import.window=Import Heap Snapshot
 
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -477,25 +477,27 @@ const refreshSelectedCensus = exports.re
  * Refresh the selected snapshot's tree map data, if need be (for example,
  * display configuration changed).
  *
  * @param {HeapAnalysesClient} heapWorker
  */
 const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWorker) {
   return function*(dispatch, getState) {
     let snapshot = getState().snapshots.find(s => s.selected);
+    if (!snapshot || snapshot.state !== states.READ) {
+      return;
+    }
 
     // Intermediate snapshot states will get handled by the task action that is
     // orchestrating them. For example, if the snapshot census's state is
     // SAVING, then the takeCensus action will keep taking a census until
     // the inverted property matches the inverted state. If the snapshot is
     // still in the process of being saved or read, the takeSnapshotAndCensus
     // task action will follow through and ensure that a census is taken.
-    if (snapshot &&
-        (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
+    if ((snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
         !snapshot.treeMap) {
       yield dispatch(takeTreeMap(heapWorker, snapshot.id));
     }
   };
 };
 
 /**
  * Request that the `HeapAnalysesWorker` compute the dominator tree for the
@@ -757,16 +759,37 @@ const clearSnapshots = exports.clearSnap
       });
     }));
 
     dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids });
   };
 };
 
 /**
+ * Delete a snapshot
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ * @param {snapshotModel} snapshot
+ */
+const deleteSnapshot = exports.deleteSnapshot = function (heapWorker, snapshot) {
+  return function*(dispatch, getState) {
+    dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] });
+
+    try {
+      yield heapWorker.deleteHeapSnapshot(snapshot.path);
+    } catch (error) {
+      reportException("deleteSnapshot", error);
+      dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
+    }
+
+    dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids: [snapshot.id] });
+  };
+};
+
+/**
  * Expand the given node in the snapshot's census report.
  *
  * @param {CensusTreeNode} node
  */
 const expandCensusNode = exports.expandCensusNode = function (id, node) {
   return {
     type: actions.EXPAND_CENSUS_NODE,
     id,
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.js
@@ -25,16 +25,17 @@ const {
   focusDiffingCensusNode,
 } = require("./actions/diffing");
 const { setFilterStringAndRefresh } = require("./actions/filter");
 const { pickFileAndExportSnapshot, pickFileAndImportSnapshotAndCensus } = require("./actions/io");
 const {
   selectSnapshotAndRefresh,
   takeSnapshotAndCensus,
   clearSnapshots,
+  deleteSnapshot,
   fetchImmediatelyDominated,
   expandCensusNode,
   collapseCensusNode,
   focusCensusNode,
   expandDominatorTreeNode,
   collapseDominatorTreeNode,
   focusDominatorTreeNode,
   fetchIndividuals,
@@ -205,16 +206,17 @@ const MemoryApp = createClass({
           {
             id: "memory-tool-container"
           },
 
           List({
             itemComponent: SnapshotListItem,
             items: snapshots,
             onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot)),
+            onDelete: snapshot => dispatch(deleteSnapshot(heapWorker, snapshot)),
             onClick: onClickSnapshotListItem,
             diffing,
           }),
 
           Heap({
             snapshot: selectedSnapshot,
             diffing,
             onViewSourceInDebugger: frame => toolbox.viewSourceInDebugger(frame.source, frame.line),
--- a/devtools/client/memory/components/snapshot-list-item.js
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -21,22 +21,23 @@ const {
 const { snapshot: snapshotModel } = require("../models");
 
 const SnapshotListItem = module.exports = createClass({
   displayName: "SnapshotListItem",
 
   propTypes: {
     onClick: PropTypes.func.isRequired,
     onSave: PropTypes.func.isRequired,
+    onDelete: PropTypes.func.isRequired,
     item: snapshotModel.isRequired,
     index: PropTypes.number.isRequired,
   },
 
   render() {
-    let { index, item: snapshot, onClick, onSave, diffing } = this.props;
+    let { index, item: snapshot, onClick, onSave, onDelete, diffing } = this.props;
     let className = `snapshot-list-item ${snapshot.selected ? " selected" : ""}`;
     let statusText = getStatusText(snapshot.state);
     let wantThrobber = !!statusText;
     let title = getSnapshotTitle(snapshot);
 
     const selectedForDiffing = diffing
           && (diffing.firstSnapshotId === snapshot.id
               || diffing.secondSnapshotId === snapshot.id);
@@ -83,23 +84,30 @@ const SnapshotListItem = module.exports 
     }
     if (!details) {
       details = dom.span({ className: "snapshot-state" }, statusText);
     }
 
     let saveLink = !snapshot.path ? void 0 : dom.a({
       onClick: () => onSave(snapshot),
       className: "save",
-    }, L10N.getFormatStr("snapshot.io.save"));
+    }, L10N.getStr("snapshot.io.save"));
+
+    let deleteButton = !snapshot.path ? void 0 : dom.button({
+      onClick: () => onDelete(snapshot),
+      className: "devtools-button delete",
+      title: L10N.getStr("snapshot.io.delete")
+    });
 
     return (
       dom.li({ className, onClick },
         dom.span({ className: `snapshot-title ${wantThrobber ? " devtools-throbber" : ""}` },
           checkbox,
-          title
+          title,
+          deleteButton
         ),
         dom.span({ className: "snapshot-info" },
           details,
           saveLink
         )
       )
     );
   }
--- a/devtools/client/memory/test/chrome/chrome.ini
+++ b/devtools/client/memory/test/chrome/chrome.ini
@@ -7,12 +7,14 @@ support-files =
 [test_DominatorTree_02.html]
 [test_DominatorTree_03.html]
 [test_DominatorTreeItem_01.html]
 [test_Heap_01.html]
 [test_Heap_02.html]
 [test_Heap_03.html]
 [test_Heap_04.html]
 [test_Heap_05.html]
+[test_List_01.html]
 [test_ShortestPaths_01.html]
 [test_ShortestPaths_02.html]
+[test_SnapshotListItem_01.html]
 [test_Toolbar_01.html]
 [test_TreeMap_01.html]
--- a/devtools/client/memory/test/chrome/head.js
+++ b/devtools/client/memory/test/chrome/head.js
@@ -48,16 +48,18 @@ var Immutable = require("devtools/client
 var React = require("devtools/client/shared/vendor/react");
 var ReactDOM = require("devtools/client/shared/vendor/react-dom");
 var Heap = React.createFactory(require("devtools/client/memory/components/heap"));
 var CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item"));
 var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
 var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
 var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
 var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map"));
+var SnapshotListItem = React.createFactory(require("devtools/client/memory/components/snapshot-list-item"));
+var List = React.createFactory(require("devtools/client/memory/components/list"));
 var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
 
 // All tests are asynchronous.
 SimpleTest.waitForExplicitFinish();
 
 var noop = () => {};
 
 var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
@@ -158,64 +160,66 @@ var TEST_SHORTEST_PATHS_PROPS = Object.f
     edges: [
       { from: 1, to: 2, name: "1->2" },
       { from: 1, to: 3, name: "1->3" },
       { from: 2, to: 3, name: "2->3" },
     ],
   }),
 });
 
+var TEST_SNAPSHOT = Object.freeze({
+  id: 1337,
+  selected: true,
+  path: "/fake/path/to/snapshot",
+  census: Object.freeze({
+    report: Object.freeze({
+      objects: Object.freeze({ count: 4, bytes: 400 }),
+      scripts: Object.freeze({ count: 3, bytes: 300 }),
+      strings: Object.freeze({ count: 2, bytes: 200 }),
+      other: Object.freeze({ count: 1, bytes: 100 }),
+    }),
+    display: Object.freeze({
+      displayName: "Test Display",
+      tooltip: "Test display tooltup",
+      inverted: false,
+      breakdown: Object.freeze({
+        by: "coarseType",
+        objects: Object.freeze({ by: "count", count: true, bytes: true }),
+        scripts: Object.freeze({ by: "count", count: true, bytes: true }),
+        strings: Object.freeze({ by: "count", count: true, bytes: true }),
+        other: Object.freeze({ by: "count", count: true, bytes: true }),
+      }),
+    }),
+    state: censusState.SAVED,
+    inverted: false,
+    filter: null,
+    expanded: new Set(),
+    focused: null,
+    parentMap: Object.freeze(Object.create(null))
+  }),
+  dominatorTree: TEST_DOMINATOR_TREE,
+  error: null,
+  imported: false,
+  creationTime: 0,
+  state: snapshotState.READ,
+});
+
 var TEST_HEAP_PROPS = Object.freeze({
   onSnapshotClick: noop,
   onLoadMoreSiblings: noop,
   onCensusExpand: noop,
   onCensusCollapse: noop,
   onDominatorTreeExpand: noop,
   onDominatorTreeCollapse: noop,
   onCensusFocus: noop,
   onDominatorTreeFocus: noop,
   onViewSourceInDebugger: noop,
   diffing: null,
   view: { state: viewState.CENSUS, },
-  snapshot: Object.freeze({
-    id: 1337,
-    selected: true,
-    path: "/fake/path/to/snapshot",
-    census: Object.freeze({
-      report: Object.freeze({
-        objects: Object.freeze({ count: 4, bytes: 400 }),
-        scripts: Object.freeze({ count: 3, bytes: 300 }),
-        strings: Object.freeze({ count: 2, bytes: 200 }),
-        other: Object.freeze({ count: 1, bytes: 100 }),
-      }),
-      display: Object.freeze({
-        displayName: "Test Display",
-        tooltip: "Test display tooltup",
-        inverted: false,
-        breakdown: Object.freeze({
-          by: "coarseType",
-          objects: Object.freeze({ by: "count", count: true, bytes: true }),
-          scripts: Object.freeze({ by: "count", count: true, bytes: true }),
-          strings: Object.freeze({ by: "count", count: true, bytes: true }),
-          other: Object.freeze({ by: "count", count: true, bytes: true }),
-        }),
-      }),
-      state: censusState.SAVED,
-      inverted: false,
-      filter: null,
-      expanded: new Set(),
-      focused: null,
-      parentMap: Object.freeze(Object.create(null))
-    }),
-    dominatorTree: TEST_DOMINATOR_TREE,
-    error: null,
-    imported: false,
-    creationTime: 0,
-    state: snapshotState.READ,
-  }),
+  snapshot: TEST_SNAPSHOT,
   sizes: Object.freeze({ shortestPathsSize: .5 }),
   onShortestPathsResize: noop,
 });
 
 var TEST_TOOLBAR_PROPS = Object.freeze({
   censusDisplays: [
     censusDisplays.coarseType,
     censusDisplays.allocationStack,
@@ -280,16 +284,24 @@ var TEST_TREE_MAP_PROPS = Object.freeze(
           totalCount: 200,
           children: [ makeTestCensusNode(), makeTestCensusNode() ],
         }
       ]
     }
   })
 });
 
+var TEST_SNAPSHOT_LIST_ITEM_PROPS = Object.freeze({
+  onClick: noop,
+  onSave: noop,
+  onDelete: noop,
+  item: TEST_SNAPSHOT,
+  index: 1234,
+});
+
 function onNextAnimationFrame(fn) {
   return () =>
     requestAnimationFrame(() =>
       requestAnimationFrame(fn));
 }
 
 /**
  * Render the provided ReactElement in the provided HTML container.
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_List_01.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test to verify the delete button calls the onDelete handler for an item
+-->
+<head>
+    <meta charset="utf-8">
+    <title>Tree component test</title>
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+    <div id="container"></div>
+
+    <pre id="test">
+        <script src="head.js" type="application/javascript;version=1.8"></script>
+        <script type="application/javascript;version=1.8">
+         window.onload = Task.async(function* () {
+          try {
+            const container = document.getElementById("container");
+
+            let deletedSnapshots = [];
+
+            let snapshots = [ TEST_SNAPSHOT, TEST_SNAPSHOT, TEST_SNAPSHOT ]
+              .map((snapshot, index) => immutableUpdate(snapshot, {
+                index: snapshot.index + index
+              }));
+
+            yield renderComponent(
+              List({
+                itemComponent: SnapshotListItem,
+                onClick: noop,
+                onDelete: (item) => deletedSnapshots.push(item),
+                items: snapshots
+              }),
+              container
+            );
+
+            let deleteButtons = container.querySelectorAll('.delete');
+
+            is(container.querySelectorAll('.snapshot-list-item').length, 3,
+              "There are 3 list items\n");
+            is(deletedSnapshots.length, 0,
+              "Not snapshots have been deleted\n");
+
+            deleteButtons[1].click();
+
+            is(deletedSnapshots.length, 1, "One snapshot was deleted\n");
+            is(deletedSnapshots[0], snapshots[1],
+              "Deleted snapshot was added to the deleted list\n");
+
+            deleteButtons[0].click();
+
+            is(deletedSnapshots.length, 2, "Two snapshots were deleted\n");
+            is(deletedSnapshots[1], snapshots[0],
+              "Deleted snapshot was added to the deleted list\n");
+
+            deleteButtons[2].click();
+
+            is(deletedSnapshots.length, 3, "Three snapshots were deleted\n");
+            is(deletedSnapshots[2], snapshots[2],
+              "Deleted snapshot was added to the deleted list\n");
+
+
+          } catch(e) {
+            ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+          } finally {
+            SimpleTest.finish();
+          }
+         });
+        </script>
+    </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test to verify that the delete button only shows up for a snapshot when it has a
+path.
+-->
+<head>
+    <meta charset="utf-8">
+    <title>Tree component test</title>
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+    <div id="container"></div>
+
+    <pre id="test">
+        <script src="head.js" type="application/javascript;version=1.8"></script>
+        <script type="application/javascript;version=1.8">
+         window.onload = Task.async(function* () {
+          try {
+            const container = document.getElementById("container");
+
+            yield renderComponent(
+              SnapshotListItem(TEST_SNAPSHOT_LIST_ITEM_PROPS),
+              container
+            );
+
+            ok(container.querySelector('.delete'),
+               "Should have delete button when there is a path");
+
+            const pathlessProps = immutableUpdate(
+                TEST_SNAPSHOT_LIST_ITEM_PROPS,
+                {item: immutableUpdate(TEST_SNAPSHOT, {path: null})}
+            );
+
+            yield renderComponent(
+              SnapshotListItem(pathlessProps),
+              container
+            );
+
+            ok(!container.querySelector('.delete'),
+               "No delete button should be found if there is no path\n");
+
+          } catch(e) {
+            ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+          } finally {
+            SimpleTest.finish();
+          }
+         });
+        </script>
+    </pre>
+</body>
+</html>
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -1,34 +1,36 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   storage-cache-error.html
   storage-complex-values.html
   storage-cookies.html
+  storage-empty-objectstores.html
   storage-listings.html
   storage-localstorage.html
   storage-overflow.html
   storage-search.html
   storage-secured-iframe.html
   storage-sessionstorage.html
   storage-unsecured-iframe.html
   storage-updates.html
   head.js
 
 [browser_storage_basic.js]
 [browser_storage_cache_error.js]
 [browser_storage_cookies_delete_all.js]
 [browser_storage_cookies_edit.js]
 [browser_storage_cookies_edit_keyboard.js]
 [browser_storage_cookies_tab_navigation.js]
-[browser_storage_dynamic_updates.js]
-[browser_storage_localstorage_edit.js]
 [browser_storage_delete.js]
 [browser_storage_delete_all.js]
 [browser_storage_delete_tree.js]
+[browser_storage_dynamic_updates.js]
+[browser_storage_empty_objectstores.js]
+[browser_storage_localstorage_edit.js]
 [browser_storage_overflow.js]
 [browser_storage_search.js]
 skip-if = os == "linux" && e10s # Bug 1240804 - unhandled promise rejections
 [browser_storage_sessionstorage_edit.js]
 [browser_storage_sidebar.js]
 [browser_storage_values.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_empty_objectstores.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+// Basic test to assert that the storage tree and table corresponding to each
+// item in the storage tree is correctly displayed.
+
+"use strict";
+
+// Entries that should be present in the tree for this test
+// Format for each entry in the array:
+// [
+//   ["path", "to", "tree", "item"],
+//   - The path to the tree item to click formed by id of each item
+//   ["key_value1", "key_value2", ...]
+//   - The value of the first (unique) column for each row in the table
+//     corresponding to the tree item selected.
+// ]
+// These entries are formed by the cookies, local storage, session storage and
+// indexedDB entries created in storage-listings.html,
+// storage-secured-iframe.html and storage-unsecured-iframe.html
+const storeItems = [
+  [["indexedDB", "http://test1.example.org"],
+   ["idb1", "idb2"]],
+  [["indexedDB", "http://test1.example.org", "idb1"],
+   ["obj1", "obj2"]],
+  [["indexedDB", "http://test1.example.org", "idb2"],
+   []],
+  [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+   [1, 2, 3]],
+  [["indexedDB", "http://test1.example.org", "idb1", "obj2"],
+   [1]]
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree() {
+  let doc = gPanelWindow.document;
+  for (let [item] of storeItems) {
+    ok(doc.querySelector(`[data-id='${JSON.stringify(item)}']`),
+      `Tree item ${item} should be present in the storage tree`);
+  }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+let testTables = function* () {
+  let doc = gPanelWindow.document;
+  // Expand all nodes so that the synthesized click event actually works
+  gUI.tree.expandAll();
+
+  // Click the tree items and wait for the table to be updated
+  for (let [item, ids] of storeItems) {
+    yield selectTreeItem(item);
+
+    // Check whether correct number of items are present in the table
+    is(doc.querySelectorAll(
+         ".table-widget-wrapper:first-of-type .table-widget-cell"
+       ).length, ids.length, "Number of items in table is correct");
+
+    // Check if all the desired items are present in the table
+    for (let id of ids) {
+      ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+        `Table item ${id} should be present`);
+    }
+  }
+};
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html");
+
+  testTree();
+  yield testTables();
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/storage-empty-objectstores.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for proper listing indexedDB databases with no object stores</title>
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+
+window.setup = function* () {
+  let request = indexedDB.open("idb1", 1);
+  let db = yield new Promise((resolve, reject) => {
+    request.onerror = e => reject(Error("error opening db connection"));
+    request.onupgradeneeded = event => {
+      let db = event.target.result;
+      let store1 = db.createObjectStore("obj1", { keyPath: "id" });
+      store1.createIndex("name", "name", { unique: false });
+      store1.createIndex("email", "email", { unique: true });
+      let store2 = db.createObjectStore("obj2", { keyPath: "id2" });
+      store1.transaction.oncomplete = () => resolve(db);
+    };
+  });
+
+  yield new Promise(resolve => request.onsuccess = resolve);
+
+  let transaction = db.transaction(["obj1", "obj2"], "readwrite");
+  let store1 = transaction.objectStore("obj1");
+  let store2 = transaction.objectStore("obj2");
+
+  store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+  store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+  store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+  store2.add({id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"});
+
+  yield new Promise(resolve => transaction.oncomplete = resolve);
+
+  db.close();
+
+  request = indexedDB.open("idb2", 1);
+  let db2 = yield new Promise((resolve, reject) => {
+    request.onerror = e => reject(Error("error opening db2 connection"));
+    request.onupgradeneeded = event => resolve(event.target.result);
+  });
+
+  yield new Promise(resolve => request.onsuccess = resolve);
+
+  db2.close();
+  dump("added indexedDB items from main page\n");
+};
+
+window.clear = function* () {
+  for (let dbName of ["idb1", "idb2"]) {
+    yield new Promise(resolve => {
+      indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+    });
+  }
+  dump("removed indexedDB items from main page\n");
+};
+
+</script>
+</body>
+</html>
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -192,21 +192,42 @@ html, body, #app, #memory-tool {
 }
 
 .snapshot-list-item .snapshot-info {
   display: flex;
   justify-content: space-between;
   font-size: 90%;
 }
 
+.snapshot-list-item .snapshot-title {
+  display: flex;
+  justify-content: space-between;
+}
+
 .snapshot-list-item .save {
   text-decoration: underline;
   cursor: pointer;
 }
 
+.snapshot-list-item .delete {
+  cursor: pointer;
+  position: relative;
+  min-height: 1em;
+  min-width: 1.3em;
+}
+
+.theme-light .snapshot-list-item.selected .delete {
+  filter: invert(100%);
+}
+
+.snapshot-list-item .delete::before {
+  background-image: url("chrome://devtools/skin/images/close.svg");
+  background-position: 0.2em 0;
+}
+
 .snapshot-list-item > .snapshot-title {
   margin-bottom: 14px;
 }
 
 .snapshot-list-item > .snapshot-title > input[type=checkbox] {
   margin: 0;
   margin-inline-end: 5px;
 }
--- a/devtools/server/actors/addon.js
+++ b/devtools/server/actors/addon.js
@@ -78,16 +78,17 @@ BrowserAddonActor.prototype = {
       this._contextPool.addActor(this._consoleActor);
     }
 
     return {
       actor: this.actorID,
       id: this.id,
       name: this._addon.name,
       url: this.url,
+      iconURL: this._addon.iconURL,
       debuggable: this._addon.isDebuggable,
       consoleActor: this._consoleActor.actorID,
 
       traits: {
         highlightable: false,
         networkMonitor: false,
       },
     };
@@ -150,16 +151,23 @@ BrowserAddonActor.prototype = {
     this._contextPool.removeActor(this.threadActor);
 
     this.threadActor = null;
     this._sources = null;
 
     return { type: "detached" };
   },
 
+  onReload: function BAA_onReload() {
+    return this._addon.reload()
+      .then(() => {
+        return {}; // send an empty response
+      });
+  },
+
   preNest: function() {
     let e = Services.wm.getEnumerator(null);
     while (e.hasMoreElements()) {
       let win = e.getNext();
       let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
       windowUtils.suppressEventHandling(true);
       windowUtils.suspendTimeouts();
@@ -244,17 +252,18 @@ BrowserAddonActor.prototype = {
    */
   _findDebuggees: function (dbg) {
     return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
   }
 };
 
 BrowserAddonActor.prototype.requestTypes = {
   "attach": BrowserAddonActor.prototype.onAttach,
-  "detach": BrowserAddonActor.prototype.onDetach
+  "detach": BrowserAddonActor.prototype.onDetach,
+  "reload": BrowserAddonActor.prototype.onReload
 };
 
 /**
  * The AddonConsoleActor implements capabilities needed for the add-on web
  * console feature.
  *
  * @constructor
  * @param object aAddon
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -1531,19 +1531,23 @@ StorageActors.createActor({
    * cannot be performed synchronously. Thus, the preListStores method exists to
    * do the same task asynchronously.
    */
   populateStoresForHosts: function() {},
 
   getNamesForHost: function(host) {
     let names = [];
 
-    for (let [dbName, metaData] of this.hostVsStores.get(host)) {
-      for (let objectStore of metaData.objectStores.keys()) {
-        names.push(JSON.stringify([dbName, objectStore]));
+    for (let [dbName, {objectStores}] of this.hostVsStores.get(host)) {
+      if (objectStores.size) {
+        for (let objectStore of objectStores.keys()) {
+          names.push(JSON.stringify([dbName, objectStore]));
+        }
+      } else {
+        names.push(JSON.stringify([dbName]));
       }
     }
     return names;
   },
 
   /**
    * Returns the total number of entries for various types of requests to
    * getStoreObjects for Indexed DB actor.
@@ -1629,26 +1633,26 @@ StorageActors.createActor({
   /**
    * Returns the over-the-wire implementation of the indexed db entity.
    */
   toStoreObject: function(item) {
     if (!item) {
       return null;
     }
 
-    if (item.indexes) {
+    if ("indexes" in item) {
       // Object store meta data
       return {
         objectStore: item.name,
         keyPath: item.keyPath,
         autoIncrement: item.autoIncrement,
         indexes: item.indexes
       };
     }
-    if (item.objectStores) {
+    if ("objectStores" in item) {
       // DB meta data
       return {
         db: item.name,
         origin: item.origin,
         version: item.version,
         objectStores: item.objectStores
       };
     }
@@ -2235,17 +2239,18 @@ var StorageActor = exports.StorageActor 
     return null;
   },
 
   getWindowFromHost: function(host) {
     for (let win of this.childWindowPool.values()) {
       let origin = win.document
                       .nodePrincipal
                       .originNoSuffix;
-      if (origin === host) {
+      let url = win.document.URL;
+      if (origin === host || url === host) {
         return win;
       }
     }
     return null;
   },
 
   /**
    * Event handler for any docshell update. This lets us figure out whenever
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -738,26 +738,50 @@ var PageStyleActor = protocol.ActorClass
         isSystem: isSystem,
         pseudoElement: pseudo
       });
     }
     return rules;
   },
 
   /**
+   * Given a node and a CSS rule, walk up the DOM looking for a
+   * matching element rule.  Return an array of all found entries, in
+   * the form generated by _getAllElementRules.  Note that this will
+   * always return an array of either zero or one element.
+   *
+   * @param {NodeActor} node the node
+   * @param {CSSStyleRule} filterRule the rule to filter for
+   * @return {Array} array of zero or one elements; if one, the element
+   *                 is the entry as returned by _getAllElementRules.
+   */
+  findEntryMatchingRule: function(node, filterRule) {
+    const options = {matchedSelectors: true, inherited: true};
+    let entries = [];
+    let parent = this.walker.parentNode(node);
+    while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) {
+      entries = entries.concat(this._getAllElementRules(parent, parent,
+                                                        options));
+      parent = this.walker.parentNode(parent);
+    }
+
+    return entries.filter(entry => entry.rule.rawRule === filterRule);
+  },
+
+  /**
    * Helper function for getApplied that fetches a set of style properties that
    * apply to the given node and associated rules
    * @param NodeActor node
    * @param object options
    *   `filter`: A string filter that affects the "matched" handling.
    *     'user': Include properties from user style sheets.
    *     'ua': Include properties from user and user-agent sheets.
    *     Default value is 'ua'
    *   `inherited`: Include styles inherited from parent nodes.
-   *   `matchedSeletors`: Include an array of specific selectors that
+   *   `matchedSelectors`: Include an array of specific selectors that
    *     caused this rule to match its node.
    * @param array entries
    *   List of appliedstyle objects that lists the rules that apply to the
    *   node. If adding a new rule to the stylesheet, only the new rule entry
    *   is provided and only the style properties that apply to the new
    *   rule is fetched.
    * @returns Object containing the list of rule entries, rule actors and
    *   stylesheet actors that applies to the given node and its associated
@@ -965,17 +989,17 @@ var PageStyleActor = protocol.ActorClass
 
     return this._styleElement;
   },
 
   /**
    * Helper function for adding a new rule and getting its applied style
    * properties
    * @param NodeActor node
-   * @param CSSStyleRUle rule
+   * @param CSSStyleRule rule
    * @returns Object containing its applied style properties
    */
   getNewAppliedProps: function(node, rule) {
     let ruleActor = this._styleRef(rule);
     return this.getAppliedProps(node, [{ rule: ruleActor }],
       { matchedSelectors: true });
   },
 
@@ -1621,48 +1645,48 @@ var StyleRuleActor = protocol.ActorClass
    *        authored text; false if the selector should be updated via
    *        CSSOM.
    * @returns {Object}
    *        Returns an object that contains the applied style properties of the
    *        new rule and a boolean indicating whether or not the new selector
    *        matches the current selected element
    */
   modifySelector2: method(function(node, value, editAuthored = false) {
-    let ruleProps = null;
-
     if (this.type === ELEMENT_STYLE ||
         this.rawRule.selectorText === value) {
-      return { ruleProps, isMatching: true };
+      return { ruleProps: null, isMatching: true };
     }
 
     let selectorPromise = this._addNewSelector(value, editAuthored);
 
     if (editAuthored) {
       selectorPromise = selectorPromise.then((newCssRule) => {
         if (newCssRule) {
           let style = this.pageStyle._styleRef(newCssRule);
           // See the comment in |form| to understand this.
           return style.getAuthoredCssText().then(() => newCssRule);
         }
         return newCssRule;
       });
     }
 
     return selectorPromise.then((newCssRule) => {
-      if (newCssRule) {
-        ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
-      }
+      let ruleProps = null;
+      let isMatching = false;
 
-      // Determine if the new selector value matches the current
-      // selected element
-      let isMatching = false;
-      try {
-        isMatching = node.rawNode.matches(value);
-      } catch (e) {
-        // This fails when value is an invalid selector.
+      if (newCssRule) {
+        let ruleEntry = this.pageStyle.findEntryMatchingRule(node, newCssRule);
+        if (ruleEntry.length === 1) {
+          isMatching = true;
+          ruleProps =
+            this.pageStyle.getAppliedProps(node, ruleEntry,
+                                           { matchedSelectors: true });
+        } else {
+          ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
+        }
       }
 
       return { ruleProps, isMatching };
     });
   }, {
     request: {
       node: Arg(0, "domnode"),
       value: Arg(1, "string"),
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -3454,26 +3454,33 @@ public class BrowserApp extends GeckoApp
         }
         Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
 
         mBrowserToolbar.cancelEdit();
 
         if (itemId == R.id.bookmark) {
             tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
+                final String extra;
+                if (AboutPages.isAboutReader(tab.getURL())) {
+                    extra = "bookmark_reader";
+                } else {
+                    extra = "bookmark";
+                }
+
                 if (item.isChecked()) {
-                    Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, "bookmark");
+                    Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, extra);
                     tab.removeBookmark();
                     item.setTitle(resolveBookmarkTitleID(false));
                     if (Versions.feature11Plus) {
                         // We don't use icons on GB builds so not resolving icons might conserve resources.
                         item.setIcon(resolveBookmarkIconID(false));
                     }
                 } else {
-                    Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "bookmark");
+                    Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, extra);
                     tab.addBookmark();
                     item.setTitle(resolveBookmarkTitleID(true));
                     if (Versions.feature11Plus) {
                         // We don't use icons on GB builds so not resolving icons might conserve resources.
                         item.setIcon(resolveBookmarkIconID(true));
                     }
                 }
             }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -375,21 +375,30 @@ public abstract class HomeFragment exten
                 mDB.unpinSite(cr, mPosition);
                 if (mDB.hideSuggestedSite(mUrl)) {
                     cr.notifyChange(SuggestedSites.CONTENT_URI, null);
                 }
             }
 
             switch (mType) {
                 case BOOKMARKS:
-                    Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, "bookmark");
+                    SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
+                    final boolean isReaderViewPage = rch.isURLCached(mUrl);
+
+                    final String extra;
+                    if (isReaderViewPage) {
+                        extra = "bookmark_reader";
+                    } else {
+                        extra = "bookmark";
+                    }
+
+                    Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, extra);
                     mDB.removeBookmarksWithURL(cr, mUrl);
 
-                    SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
-                    if (rch.isURLCached(mUrl)) {
+                    if (isReaderViewPage) {
                         ReadingListHelper.removeCachedReaderItem(mUrl, mContext);
                     }
 
                     break;
 
                 case HISTORY:
                     mDB.removeHistoryEntry(cr, mUrl);
                     break;
--- a/mobile/android/base/resources/layout/readinglistpanel_gone_fragment.xml
+++ b/mobile/android/base/resources/layout/readinglistpanel_gone_fragment.xml
@@ -8,17 +8,16 @@
             android:layout_height="wrap_content"
             android:layout_width="match_parent"
             android:orientation="vertical"
             android:fillViewport="true">
 
     <LinearLayout android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:minHeight="@dimen/firstrun_min_height"
-                  android:background="@color/about_page_header_grey"
                   android:gravity="center_horizontal"
                   android:orientation="vertical">
 
         <ImageView android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="40dp"
                    android:layout_marginBottom="40dp"
                    android:scaleType="fitCenter"
--- a/services/common/async.js
+++ b/services/common/async.js
@@ -202,9 +202,19 @@ this.Async = {
   querySpinningly: function querySpinningly(query, names) {
     // 'Synchronously' asyncExecute, fetching all results by name.
     let storageCallback = Object.create(Async._storageCallbackPrototype);
     storageCallback.names = names;
     storageCallback.syncCb = Async.makeSyncCallback();
     query.executeAsync(storageCallback);
     return Async.waitForSyncCallback(storageCallback.syncCb);
   },
+
+  promiseSpinningly(promise) {
+    let cb = Async.makeSpinningCallback();
+    promise.then(result =>  {
+      cb(null, result);
+    }, err => {
+      cb(err || new Error("Promise rejected without explicit error"));
+    });
+    return cb.wait();
+  },
 };
--- a/services/common/kinto-updater.js
+++ b/services/common/kinto-updater.js
@@ -7,74 +7,110 @@ this.EXPORTED_SYMBOLS = ["checkVersions"
 const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.importGlobalProperties(['fetch']);
 
 const PREF_KINTO_CHANGES_PATH = "services.kinto.changes.path";
 const PREF_KINTO_BASE = "services.kinto.base";
+const PREF_KINTO_BUCKET = "services.kinto.bucket";
 const PREF_KINTO_LAST_UPDATE = "services.kinto.last_update_seconds";
+const PREF_KINTO_LAST_ETAG = "services.kinto.last_etag";
 const PREF_KINTO_CLOCK_SKEW_SECONDS = "services.kinto.clock_skew_seconds";
+const PREF_KINTO_ONECRL_COLLECTION = "services.kinto.onecrl.collection";
 
 const kintoClients = {
 };
 
 // This is called by the ping mechanism.
 // returns a promise that rejects if something goes wrong
 this.checkVersions = function() {
+
   return Task.spawn(function *() {
     // Fetch a versionInfo object that looks like:
     // {"data":[
     //   {
     //     "host":"kinto-ota.dev.mozaws.net",
     //     "last_modified":1450717104423,
     //     "bucket":"blocklists",
     //     "collection":"certificates"
     //    }]}
     // Right now, we only use the collection name and the last modified info
     let kintoBase = Services.prefs.getCharPref(PREF_KINTO_BASE);
     let changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_KINTO_CHANGES_PATH);
+    let blocklistsBucket = Services.prefs.getCharPref(PREF_KINTO_BUCKET);
 
-    let response = yield fetch(changesEndpoint);
+    // Use ETag to obtain a `304 Not modified` when no change occurred.
+    const headers = {};
+    if (Services.prefs.prefHasUserValue(PREF_KINTO_LAST_ETAG)) {
+      const lastEtag = Services.prefs.getCharPref(PREF_KINTO_LAST_ETAG);
+      if (lastEtag) {
+        headers["If-None-Match"] = lastEtag;
+      }
+    }
+
+    let response = yield fetch(changesEndpoint, {headers});
+
+    let versionInfo;
+    // No changes since last time. Go on with empty list of changes.
+    if (response.status == 304) {
+      versionInfo = {data: []};
+    } else {
+      versionInfo = yield response.json();
+    }
+
+    // If the server is failing, the JSON response might not contain the
+    // expected data (e.g. error response - Bug 1259145)
+    if (!versionInfo.hasOwnProperty("data")) {
+      throw new Error("Polling for changes failed.");
+    }
 
     // Record new update time and the difference between local and server time
     let serverTimeMillis = Date.parse(response.headers.get("Date"));
     let clockDifference = Math.abs(Date.now() - serverTimeMillis) / 1000;
+    Services.prefs.setIntPref(PREF_KINTO_CLOCK_SKEW_SECONDS, clockDifference);
     Services.prefs.setIntPref(PREF_KINTO_LAST_UPDATE, serverTimeMillis / 1000);
-    Services.prefs.setIntPref(PREF_KINTO_CLOCK_SKEW_SECONDS, clockDifference);
-
-    let versionInfo = yield response.json();
 
     let firstError;
     for (let collectionInfo of versionInfo.data) {
+      // Skip changes that don't concern configured blocklist bucket.
+      if (collectionInfo.bucket != blocklistsBucket) {
+        continue;
+      }
+
       let collection = collectionInfo.collection;
       let kintoClient = kintoClients[collection];
       if (kintoClient && kintoClient.maybeSync) {
         let lastModified = 0;
         if (collectionInfo.last_modified) {
-          lastModified = collectionInfo.last_modified
+          lastModified = collectionInfo.last_modified;
         }
         try {
           yield kintoClient.maybeSync(lastModified, serverTimeMillis);
         } catch (e) {
           if (!firstError) {
             firstError = e;
           }
         }
       }
     }
     if (firstError) {
       // cause the promise to reject by throwing the first observed error
       throw firstError;
     }
+
+    // Save current Etag for next poll.
+    if (response.headers.has("ETag")) {
+      const currentEtag = response.headers.get("ETag");
+      Services.prefs.setCharPref(PREF_KINTO_LAST_ETAG, currentEtag);
+    }
   });
 };
 
 // Add a kintoClient for testing purposes. Do not use for any other purpose
 this.addTestKintoClient = function(name, kintoClient) {
   kintoClients[name] = kintoClient;
 };
 
 // Add the various things that we know want updates
-kintoClients.certificates =
-  Cu.import("resource://services-common/KintoCertificateBlocklist.js", {})
-  .OneCRLClient;
+const KintoBlocklist = Cu.import("resource://services-common/KintoCertificateBlocklist.js", {});
+kintoClients[Services.prefs.getCharPref(PREF_KINTO_ONECRL_COLLECTION)]  = KintoBlocklist.OneCRLClient;
--- a/services/common/tests/unit/test_kinto_updater.js
+++ b/services/common/tests/unit/test_kinto_updater.js
@@ -3,16 +3,17 @@
 
 Cu.import("resource://services-common/kinto-updater.js")
 Cu.import("resource://testing-common/httpd.js");
 
 var server;
 
 const PREF_KINTO_BASE = "services.kinto.base";
 const PREF_LAST_UPDATE = "services.kinto.last_update_seconds";
+const PREF_LAST_ETAG = "services.kinto.last_etag";
 const PREF_CLOCK_SKEW_SECONDS = "services.kinto.clock_skew_seconds";
 
 // Check to ensure maybeSync is called with correct values when a changes
 // document contains information on when a collection was last modified
 add_task(function* test_check_maybeSync(){
   const changesPath = "/v1/buckets/monitor/collections/changes/records";
 
   // register a handler
@@ -26,45 +27,46 @@ add_task(function* test_check_maybeSync(
       response.setStatusLine(null, sampled.status.status,
                              sampled.status.statusText);
       // send the headers
       for (let headerLine of sampled.sampleHeaders) {
         let headerElements = headerLine.split(':');
         response.setHeader(headerElements[0], headerElements[1].trimLeft());
       }
 
-      // set the
+      // set the server date
       response.setHeader("Date", (new Date(2000)).toUTCString());
 
       response.write(sampled.responseBody);
     } catch (e) {
       dump(`${e}\n`);
     }
   }
 
   server.registerPathHandler(changesPath, handleResponse);
 
   // set up prefs so the kinto updater talks to the test server
   Services.prefs.setCharPref(PREF_KINTO_BASE,
     `http://localhost:${server.identity.primaryPort}/v1`);
 
   // set some initial values so we can check these are updated appropriately
-  Services.prefs.setIntPref("services.kinto.last_update", 0);
-  Services.prefs.setIntPref("services.kinto.clock_difference", 0);
+  Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
+  Services.prefs.setIntPref(PREF_CLOCK_SKEW_SECONDS, 0);
+  Services.prefs.clearUserPref(PREF_LAST_ETAG);
 
 
   let startTime = Date.now();
 
+  let updater = Cu.import("resource://services-common/kinto-updater.js");
+
   let syncPromise = new Promise(function(resolve, reject) {
-    let updater = Cu.import("resource://services-common/kinto-updater.js");
     // add a test kinto client that will respond to lastModified information
     // for a collection called 'test-collection'
     updater.addTestKintoClient("test-collection", {
-      "maybeSync": function(lastModified, serverTime){
-        // ensire the lastModified and serverTime values are as expected
+      maybeSync(lastModified, serverTime) {
         do_check_eq(lastModified, 1000);
         do_check_eq(serverTime, 2000);
         resolve();
       }
     });
     updater.checkVersions();
   });
 
@@ -75,16 +77,54 @@ add_task(function* test_check_maybeSync(
   do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
 
   // How does the clock difference look?
   let endTime = Date.now();
   let clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
   // we previously set the serverTime to 2 (seconds past epoch)
   do_check_eq(clockDifference <= endTime / 1000
               && clockDifference >= Math.floor(startTime / 1000) - 2, true);
+  // Last timestamp was saved. An ETag header value is a quoted string.
+  let lastEtag = Services.prefs.getCharPref(PREF_LAST_ETAG);
+  do_check_eq(lastEtag, "\"1100\"");
+
+
+  // Simulate a poll with up-to-date collection.
+  Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
+  // If server has no change, a 304 is received, maybeSync() is not called.
+  updater.addTestKintoClient("test-collection", {
+    maybeSync: () => {throw new Error("Should not be called");}
+  });
+  yield updater.checkVersions();
+  // Last update is overwritten
+  do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
+
+
+  // Simulate a server error.
+  function simulateErrorResponse (request, response) {
+    response.setHeader("Date", (new Date(3000)).toUTCString());
+    response.setHeader("Content-Type", "application/json; charset=UTF-8");
+    response.write(JSON.stringify({
+      code: 503,
+      errno: 999,
+      error: "Service Unavailable",
+    }));
+    response.setStatusLine(null, 503, "Service Unavailable");
+  }
+  server.registerPathHandler(changesPath, simulateErrorResponse);
+  // checkVersions() fails with adequate error.
+  let error;
+  try {
+    yield updater.checkVersions();
+  } catch (e) {
+    error = e;
+  }
+  do_check_eq(error.message, "Polling for changes failed.");
+  // When an error occurs, last update was not overwritten (see Date header above).
+  do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
 });
 
 function run_test() {
   // Set up an HTTP Server
   server = new HttpServer();
   server.start(-1);
 
   run_next_test();
@@ -94,17 +134,34 @@ function run_test() {
   });
 }
 
 // get a response for a given request from sample data
 function getSampleResponse(req, port) {
   const responses = {
     "GET:/v1/buckets/monitor/collections/changes/records?": {
       "sampleHeaders": [
-        "Content-Type: application/json; charset=UTF-8"
+        "Content-Type: application/json; charset=UTF-8",
+        "ETag: \"1100\""
       ],
       "status": {status: 200, statusText: "OK"},
-      "responseBody": JSON.stringify({"data":[{"host":"localhost","last_modified":1000,"bucket":"blocklists","id":"330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a","collection":"test-collection"}]})
+      "responseBody": JSON.stringify({"data": [{
+        "host": "localhost",
+        "last_modified": 1100,
+        "bucket": "blocklists:aurora",
+        "id": "330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a",
+        "collection": "test-collection"
+      }, {
+        "host": "localhost",
+        "last_modified": 1000,
+        "bucket": "blocklists",
+        "id": "254cbb9e-6888-4d9f-8e60-58b74faa8778",
+        "collection": "test-collection"
+      }]})
     }
   };
+
+  if (req.hasHeader("if-none-match") && req.getHeader("if-none-match", "") == "\"1100\"")
+    return {sampleHeaders: [], status: {status: 304, statusText: "Not Modified"}, responseBody: ""};
+
   return responses[`${req.method}:${req.path}?${req.queryString}`] ||
          responses[req.method];
 }
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -267,79 +267,79 @@ BookmarksEngine.prototype = {
       }
     }
     return url;
   },
 
   _guidMapFailed: false,
   _buildGUIDMap: function _buildGUIDMap() {
     let guidMap = {};
-    for (let guid in this._store.getAllIDs()) {
-      // Figure out with which key to store the mapping.
-      let key;
-      let id = this._store.idForGUID(guid);
-      let itemType;
-      try {
-        itemType = PlacesUtils.bookmarks.getItemType(id);
-      } catch (ex) {
-        this._log.warn("Deleting invalid bookmark record with id", id);
-        try {
-          PlacesUtils.bookmarks.removeItem(id);
-        } catch (ex) {
-          this._log.warn("Failed to delete invalid bookmark", ex);
+    let tree = Async.promiseSpinningly(PlacesUtils.promiseBookmarksTree("", {
+      includeItemIds: true
+    }));
+    function* walkBookmarksTree(tree, parent=null) {
+      if (tree) {
+        // Skip root node
+        if (parent) {
+          yield [tree, parent];
         }
-        continue;
+        if (tree.children) {
+          for (let child of tree.children) {
+            yield* walkBookmarksTree(child, tree);
+          }
+        }
       }
-      switch (itemType) {
-        case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+    }
 
-          // Smart bookmarks map to their annotation value.
-          let queryId;
-          try {
-            queryId = PlacesUtils.annotations.getItemAnnotation(
-              id, SMART_BOOKMARKS_ANNO);
-          } catch(ex) {}
+    function* walkBookmarksRoots(tree, rootGUIDs) {
+      for (let guid of rootGUIDs) {
+        let id = kSpecialIds.specialIdForGUID(guid, false);
+        let bookmarkRoot = id === null ? null :
+          tree.children.find(child => child.id === id);
+        if (bookmarkRoot === null) {
+          continue;
+        }
+        yield* walkBookmarksTree(bookmarkRoot, tree);
+      }
+    }
 
-          if (queryId) {
-            key = "q" + queryId;
+    let rootsToWalk = kSpecialIds.guids.filter(guid =>
+      guid !== 'places' && guid !== 'tags');
+
+    for (let [node, parent] of walkBookmarksRoots(tree, rootsToWalk)) {
+      let {guid, id, type: placeType} = node;
+      guid = kSpecialIds.specialGUIDForId(id) || guid;
+      let key;
+      switch (placeType) {
+        case PlacesUtils.TYPE_X_MOZ_PLACE:
+          // Bookmark
+          let query = null;
+          if (node.annos && node.uri.startsWith("place:")) {
+            query = node.annos.find(({name}) => name === SMART_BOOKMARKS_ANNO);
+          }
+          if (query && query.value) {
+            key = "q" + query.value;
           } else {
-            let uri;
-            try {
-              uri = PlacesUtils.bookmarks.getBookmarkURI(id);
-            } catch (ex) {
-              // Bug 1182366 - NS_ERROR_MALFORMED_URI here stops bookmarks sync.
-              // Try and get the string value of the URL for diagnostic purposes.
-              let url = this._getStringUrlForId(id);
-              this._log.warn(`Deleting bookmark with invalid URI. url="${url}", id=${id}`);
-              try {
-                PlacesUtils.bookmarks.removeItem(id);
-              } catch (ex) {
-                this._log.warn("Failed to delete invalid bookmark", ex);
-              }
-              continue;
-            }
-            key = "b" + uri.spec + ":" + PlacesUtils.bookmarks.getItemTitle(id);
+            key = "b" + node.uri + ":" + node.title;
           }
           break;
-        case PlacesUtils.bookmarks.TYPE_FOLDER:
-          key = "f" + PlacesUtils.bookmarks.getItemTitle(id);
+        case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
+          // Folder
+          key = "f" + node.title;
           break;
-        case PlacesUtils.bookmarks.TYPE_SEPARATOR:
-          key = "s" + PlacesUtils.bookmarks.getItemIndex(id);
+        case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
+          // Separator
+          key = "s" + node.index;
           break;
         default:
+          this._log.error("Unknown place type: '"+placeType+"'");
           continue;
       }
 
-      // The mapping is on a per parent-folder-name basis.
-      let parent = PlacesUtils.bookmarks.getFolderIdForItem(id);
-      if (parent <= 0)
-        continue;
-
-      let parentName = PlacesUtils.bookmarks.getItemTitle(parent);
+      let parentName = parent.title;
       if (guidMap[parentName] == null)
         guidMap[parentName] = {};
 
       // If the entry already exists, remember that there are explicit dupes.
       let entry = new String(guid);
       entry.hasDupe = guidMap[parentName][key] != null;
 
       // Remember this item's GUID for its parent-name/key pair.
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -391,17 +391,19 @@ add_test(function test_bookmark_guidMap_
     PlacesUtils.bookmarks.toolbarFolder, "Folder 1", 0);
   let itemGUID    = store.GUIDForId(itemID);
   let itemPayload = store.createRecord(itemGUID).cleartext;
   coll.insert(itemGUID, encryptPayload(itemPayload));
 
   engine.lastSync = 1;   // So we don't back up.
 
   // Make building the GUID map fail.
-  store.getAllIDs = function () { throw "Nooo"; };
+
+  let pbt = PlacesUtils.promiseBookmarksTree;
+  PlacesUtils.promiseBookmarksTree = function() { return Promise.reject("Nooo"); };
 
   // Ensure that we throw when accessing _guidMap.
   engine._syncStartup();
   _("No error.");
   do_check_false(engine._guidMapFailed);
 
   _("We get an error if building _guidMap fails in use.");
   let err;
@@ -417,16 +419,17 @@ add_test(function test_bookmark_guidMap_
   err = undefined;
   try {
     engine._processIncoming();
   } catch (ex) {
     err = ex;
   }
   do_check_eq(err, "Nooo");
 
+  PlacesUtils.promiseBookmarksTree = pbt;
   server.stop(run_next_test);
 });
 
 add_test(function test_bookmark_is_taggable() {
   let engine = new BookmarksEngine(Service);
   let store = engine._store;
 
   do_check_true(store.isTaggable("bookmark"));
--- a/services/sync/tests/unit/test_bookmark_invalid.js
+++ b/services/sync/tests/unit/test_bookmark_invalid.js
@@ -7,56 +7,16 @@ Cu.import("resource://services-sync/serv
 Cu.import("resource://services-sync/util.js");
 
 Service.engineManager.register(BookmarksEngine);
 
 var engine = Service.engineManager.get("bookmarks");
 var store = engine._store;
 var tracker = engine._tracker;
 
-// Return a promise resolved when the specified message is written to the
-// bookmarks engine log.
-function promiseLogMessage(messagePortion) {
-  return new Promise(resolve => {
-    let appender;
-    let log = Log.repository.getLogger("Sync.Engine.Bookmarks");
-
-    function TestAppender() {
-      Log.Appender.call(this);
-    }
-    TestAppender.prototype = Object.create(Log.Appender.prototype);
-    TestAppender.prototype.doAppend = function(message) {
-      if (message.indexOf(messagePortion) >= 0) {
-        log.removeAppender(appender);
-        resolve();
-      }
-    };
-    TestAppender.prototype.level = Log.Level.Debug;
-    appender = new TestAppender();
-    log.addAppender(appender);
-  });
-}
-
-// Returns a promise that resolves if the specified ID does *not* exist and
-// rejects if it does.
-function promiseNoItem(itemId) {
-  return new Promise((resolve, reject) => {
-    try {
-      PlacesUtils.bookmarks.getFolderIdForItem(itemId);
-      reject("fetching the item didn't fail");
-    } catch (ex) {
-      if (ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
-        resolve("item doesn't exist");
-      } else {
-        reject("unexpected exception: " + ex);
-      }
-    }
-  });
-}
-
 add_task(function* test_ignore_invalid_uri() {
   _("Ensure that we don't die with invalid bookmarks.");
 
   // First create a valid bookmark.
   let bmid = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                                   Services.io.newURI("http://example.com/", null, null),
                                                   PlacesUtils.bookmarks.DEFAULT_INDEX,
                                                   "the title");
@@ -65,23 +25,19 @@ add_task(function* test_ignore_invalid_u
   yield PlacesUtils.withConnectionWrapper("test_ignore_invalid_uri", Task.async(function* (db) {
     yield db.execute(
       `UPDATE moz_places SET url = :url
        WHERE id = (SELECT b.fk FROM moz_bookmarks b
        WHERE b.id = :id LIMIT 1)`,
       { id: bmid, url: "<invalid url>" });
   }));
 
-  // DB is now "corrupt" - setup a log appender to capture what we log.
-  let promiseMessage = promiseLogMessage('Deleting bookmark with invalid URI. url="<invalid url>"');
-  // This should work and log our invalid id.
+  // Ensure that this doesn't throw even though the DB is now in a bad state (a
+  // bookmark has an illegal url).
   engine._buildGUIDMap();
-  yield promiseMessage;
-  // And we should have deleted the item.
-  yield promiseNoItem(bmid);
 });
 
 add_task(function* test_ignore_missing_uri() {
   _("Ensure that we don't die with a bookmark referencing an invalid bookmark id.");
 
   // First create a valid bookmark.
   let bmid = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                                   Services.io.newURI("http://example.com/", null, null),
@@ -91,21 +47,17 @@ add_task(function* test_ignore_missing_u
   // Now update moz_bookmarks to reference a non-existing places ID
   yield PlacesUtils.withConnectionWrapper("test_ignore_missing_uri", Task.async(function* (db) {
     yield db.execute(
       `UPDATE moz_bookmarks SET fk = 999999
        WHERE id = :id`
       , { id: bmid });
   }));
 
-  // DB is now "corrupt" - bookmarks will fail to locate a string url to log
-  // and use "<not found>" as a placeholder.
-  let promiseMessage = promiseLogMessage('Deleting bookmark with invalid URI. url="<not found>"');
+  // Ensure that this doesn't throw even though the DB is now in a bad state (a
+  // bookmark has an illegal url).
   engine._buildGUIDMap();
-  yield promiseMessage;
-  // And we should have deleted the item.
-  yield promiseNoItem(bmid);
 });
 
 function run_test() {
   initTestLogging('Trace');
   run_next_test();
 }
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -235,17 +235,16 @@ ExtensionPage = class extends BaseContex
     super();
 
     let {type, contentWindow, uri} = params;
     this.extension = extension;
     this.type = type;
     this.contentWindow = contentWindow || null;
     this.uri = uri || extension.baseURI;
     this.incognito = params.incognito || false;
-    this.unloaded = false;
 
     // This is the MessageSender property passed to extension.
     // It can be augmented by the "page-open" hook.
     let sender = {id: extension.uuid};
     if (uri) {
       sender.url = uri.spec;
     }
     let delegate = {
@@ -280,18 +279,16 @@ ExtensionPage = class extends BaseContex
   unload() {
     // Note that without this guard, we end up running unload code
     // multiple times for tab pages closed by the "page-unload" handlers
     // triggered below.
     if (this.unloaded) {
       return;
     }
 
-    this.unloaded = true;
-
     super.unload();
 
     Management.emit("page-unload", this);
 
     this.extension.views.delete(this);
   }
 };
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -49,16 +49,21 @@ function runSafeWithoutClone(f, ...args)
   Promise.resolve().then(() => {
     runSafeSyncWithoutClone(f, ...args);
   });
 }
 
 // Run a function, cloning arguments into context.cloneScope, and
 // report exceptions. |f| is expected to be in context.cloneScope.
 function runSafeSync(context, f, ...args) {
+  if (context.unloaded) {
+    Cu.reportError("runSafeSync called after context unloaded");
+    return;
+  }
+
   try {
     args = Cu.cloneInto(args, context.cloneScope);
   } catch (e) {
     Cu.reportError(e);
     dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
   }
   return runSafeSyncWithoutClone(f, ...args);
 }
@@ -67,16 +72,20 @@ function runSafeSync(context, f, ...args
 // report exceptions. |f| is expected to be in context.cloneScope.
 function runSafe(context, f, ...args) {
   try {
     args = Cu.cloneInto(args, context.cloneScope);
   } catch (e) {
     Cu.reportError(e);
     dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
   }
+  if (context.unloaded) {
+    dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
+    return undefined;
+  }
   return runSafeWithoutClone(f, ...args);
 }
 
 // Return true if the given value is an instance of the given
 // native type.
 function instanceOf(value, type) {
   return {}.toString.call(value) == `[object ${type}]`;
 }
@@ -130,26 +139,43 @@ class SpreadArgs extends Array {
 let gContextId = 0;
 
 class BaseContext {
   constructor() {
     this.onClose = new Set();
     this.checkedLastError = false;
     this._lastError = null;
     this.contextId = ++gContextId;
+    this.unloaded = false;
   }
 
   get cloneScope() {
     throw new Error("Not implemented");
   }
 
   get principal() {
     throw new Error("Not implemented");
   }
 
+  runSafe(...args) {
+    if (this.unloaded) {
+      Cu.reportError("context.runSafe called after context unloaded");
+    } else {
+      return runSafeSync(this, ...args);
+    }
+  }
+
+  runSafeWithoutClone(...args) {
+    if (this.unloaded) {
+      Cu.reportError("context.runSafeWithoutClone called after context unloaded");
+    } else {
+      return runSafeSyncWithoutClone(...args);
+    }
+  }
+
   checkLoadURL(url, options = {}) {
     let ssm = Services.scriptSecurityManager;
 
     let flags = ssm.STANDARD;
     if (!options.allowScript) {
       flags |= ssm.DISALLOW_SCRIPT;
     }
     if (!options.allowInheritsPrincipal) {
@@ -266,47 +292,65 @@ class BaseContext {
    * @param {function} [callback] The callback function to wrap
    *
    * @returns {Promise|undefined} If callback is null, a promise object
    *     belonging to the target scope. Otherwise, undefined.
    */
   wrapPromise(promise, callback = null) {
     // Note: `promise instanceof this.cloneScope.Promise` returns true
     // here even for promises that do not belong to the content scope.
-    let runSafe = runSafeSync.bind(null, this);
+    let runSafe = this.runSafe.bind(this);
     if (promise.constructor === this.cloneScope.Promise) {
-      runSafe = runSafeSyncWithoutClone;
+      runSafe = this.runSafeWithoutClone.bind(this);
     }
 
     if (callback) {
       promise.then(
         args => {
-          if (args instanceof SpreadArgs) {
+          if (this.unloaded) {
+            dump(`Promise resolved after context unloaded\n`);
+          } else if (args instanceof SpreadArgs) {
             runSafe(callback, ...args);
           } else {
             runSafe(callback, args);
           }
         },
         error => {
           this.withLastError(error, () => {
-            runSafeSyncWithoutClone(callback);
+            if (this.unloaded) {
+              dump(`Promise rejected after context unloaded\n`);
+            } else {
+              this.runSafeWithoutClone(callback);
+            }
           });
         });
     } else {
       return new this.cloneScope.Promise((resolve, reject) => {
         promise.then(
-          value => { runSafe(resolve, value); },
           value => {
-            runSafeSyncWithoutClone(reject, this.normalizeError(value));
+            if (this.unloaded) {
+              dump(`Promise resolved after context unloaded\n`);
+            } else {
+              runSafe(resolve, value);
+            }
+          },
+          value => {
+            if (this.unloaded) {
+              dump(`Promise rejected after context unloaded\n`);
+            } else {
+              this.runSafeWithoutClone(reject, this.normalizeError(value));
+            }
           });
       });
     }
   }
 
   unload() {
+    this.unloaded = true;
+
     MessageChannel.abortResponses({
       extensionId: this.extension.id,
       contextId: this.contextId,
     });
 
     for (let obj of this.onClose) {
       obj.close();
     }
@@ -565,23 +609,29 @@ EventManager.prototype = {
   },
 
   hasListener(callback) {
     return this.callbacks.has(callback);
   },
 
   fire(...args) {
     for (let callback of this.callbacks) {
-      runSafe(this.context, callback, ...args);
+      Promise.resolve(callback).then(callback => {
+        if (this.context.unloaded) {
+          dump(`${this.name} event fired after context unloaded.`);
+        } else if (this.callbacks.has(callback)) {
+          this.context.runSafe(callback, ...args);
+        }
+      });
     }
   },
 
   fireWithoutClone(...args) {
     for (let callback of this.callbacks) {
-      runSafeSyncWithoutClone(callback, ...args);
+      this.context.runSafeWithoutClone(callback, ...args);
     }
   },
 
   close() {
     if (this.callbacks.size) {
       this.unregister();
     }
     this.callbacks = null;
@@ -604,17 +654,25 @@ function SingletonEventManager(context, 
   this.name = name;
   this.register = register;
   this.unregister = new Map();
   context.callOnClose(this);
 }
 
 SingletonEventManager.prototype = {
   addListener(callback, ...args) {
-    let unregister = this.register(callback, ...args);
+    let wrappedCallback = (...args) => {
+      if (this.context.unloaded) {
+        dump(`${this.name} event fired after context unloaded.`);
+      } else if (this.unregister.has(callback)) {
+        return callback(...args);
+      }
+    };
+
+    let unregister = this.register(wrappedCallback, ...args);
     this.unregister.set(callback, unregister);
   },
 
   removeListener(callback) {
     if (!this.unregister.has(callback)) {
       return;
     }
 
@@ -928,17 +986,17 @@ Messenger.prototype = {
 
         receiveMessage: ({target, data: message, sender, recipient}) => {
           let {name, portId} = message;
           let mm = getMessageManager(target);
           if (this.delegate) {
             this.delegate.getSender(this.context, target, sender);
           }
           let port = new Port(this.context, mm, name, portId, sender);
-          runSafeSyncWithoutClone(callback, port.api());
+          this.context.runSafeWithoutClone(callback, port.api());
           return true;
         },
       };
 
       MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
       return () => {
         MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
       };
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -145,19 +145,36 @@ add_task(function* webnav_ordering() {
 
     let index1 = find(action1);
     let index2 = find(action2);
     ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`);
     ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`);
     ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`);
   }
 
+  // As required in the webNavigation API documentation:
+  // If a navigating frame contains subframes, its onCommitted is fired before any
+  // of its children's onBeforeNavigate; while onCompleted is fired after
+  // all of its children's onCompleted.
   checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"});
   checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"});
 
+  // As required in the webNAvigation API documentation, check the event sequence:
+  // onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted
+  let expectedEventSequence = [
+    "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted",
+  ];
+
+  for (let i = 1; i < expectedEventSequence.length; i++) {
+    let after = expectedEventSequence[i];
+    let before = expectedEventSequence[i - 1];
+    checkBefore({url: URL, event: before}, {url: URL, event: after});
+    checkBefore({url: FRAME, event: before}, {url: FRAME, event: after});
+  }
+
   yield loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; });
 
   checkRequired(FRAME2);
 
   let navigationSequence = [
     {
       action: () => { win.frames[0].document.getElementById("elt").click(); },
       waitURL: `${FRAME2}#ref`,
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -1735,25 +1735,26 @@ this.PlacesUtils = {
    *          complete.
    *
    * @return {Promise}
    * @resolves to a JS object that represents either a single item or a
    * bookmarks tree.  Each node in the tree has the following properties set:
    *  - guid (string): the item's GUID (same as aItemGuid for the top item).
    *  - [deprecated] id (number): the item's id. This is only if
    *    aOptions.includeItemIds is set.
-   *  - type (number):  the item's type.  @see PlacesUtils.TYPE_X_*
+   *  - type (string):  the item's type.  @see PlacesUtils.TYPE_X_*
    *  - title (string): the item's title. If it has no title, this property
    *    isn't set.
    *  - dateAdded (number, microseconds from the epoch): the date-added value of
    *    the item.
    *  - lastModified (number, microseconds from the epoch): the last-modified
    *    value of the item.
    *  - annos (see getAnnotationsForItem): the item's annotations.  This is not
    *    set if there are no annotations set for the item).
+   *  - index: the item's index under it's parent.
    *
    * The root object (i.e. the one for aItemGuid) also has the following
    * properties set:
    *  - parentGuid (string): the GUID of the root's parent.  This isn't set if
    *    the root item is the places root.
    *  - itemsCount (number, not enumerable): the number of items, including the
    *    root item itself, which are represented in the resolved object.
    *
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -1251,17 +1251,17 @@ Search.prototype = {
                           .getFaviconLinkForIcon(NetUtil.newURI(icon)).spec;
       }
 
       let match = {
         // We include the deviceName in the action URL so we can render it in
         // the URLBar.
         value: makeActionURL("remotetab", { url, deviceName }),
         comment: title || url,
-        style: "action",
+        style: "action remotetab",
         // we want frecency > FRECENCY_DEFAULT so it doesn't get pushed out
         // by "remote" matches.
         frecency: FRECENCY_DEFAULT + 1,
         icon,
       }
       this._addMatch(match);
     }
   },
--- a/toolkit/components/places/tests/unifiedcomplete/test_remotetabmatches.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_remotetabmatches.js
@@ -48,17 +48,17 @@ function configureEngine(clients) {
   Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
 }
 
 // Make a match object suitable for passing to check_autocomplete.
 function makeRemoteTabMatch(url, deviceName, extra = {}) {
   return {
     uri: makeActionURI("remotetab", {url, deviceName}),
     title: extra.title || url,
-    style: [ "action" ],
+    style: [ "action", "remotetab" ],
     icon: extra.icon,
   }
 }
 
 // The tests.
 add_task(function* test_nomatch() {
   // Nothing matches.
   configureEngine({
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -30,16 +30,18 @@ if (AppConstants.platform !== "gonk") {
   Cu.import("resource://gre/modules/AddonManager.jsm");
   XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                     "resource://gre/modules/LightweightThemeManager.jsm");
 }
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
                                   "resource://gre/modules/ProfileAge.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                   "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+                                  "resource://gre/modules/WindowsRegistry.jsm");
 
 const CHANGE_THROTTLE_INTERVAL_MS = 5 * 60 * 1000;
 
 // The maximum length of a string (e.g. description) in the addons section.
 const MAX_ADDON_STRING_LENGTH = 100;
 
 /**
  * This is a policy object used to override behavior for testing.
@@ -312,26 +314,27 @@ function getGfxAdapter(aSuffix = "") {
     RAM: memoryMB,
     driver: getGfxField("adapterDriver" + aSuffix, null),
     driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
     driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
   };
 }
 
 /**
- * Gets the service pack information on Windows platforms. This was copied from
- * nsUpdateService.js.
+ * Gets the service pack and build information on Windows platforms. The initial version
+ * was copied from nsUpdateService.js.
  *
- * @return An object containing the service pack major and minor versions.
+ * @return An object containing the service pack major and minor versions, along with the
+ *         build number.
  */
-function getServicePack() {
-  const UNKNOWN_SERVICE_PACK = {major: null, minor: null};
+function getWindowsVersionInfo() {
+  const UNKNOWN_VERSION_INFO = {servicePackMajor: null, servicePackMinor: null, buildNumber: null};
 
   if (AppConstants.platform !== "win") {
-    return UNKNOWN_SERVICE_PACK;
+    return UNKNOWN_VERSION_INFO;
   }
 
   const BYTE = ctypes.uint8_t;
   const WORD = ctypes.uint16_t;
   const DWORD = ctypes.uint32_t;
   const WCHAR = ctypes.char16_t;
   const BOOL = ctypes.int;
 
@@ -362,21 +365,22 @@ function getServicePack() {
     let winVer = OSVERSIONINFOEXW();
     winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
 
     if(0 === GetVersionEx(winVer.address())) {
       throw("Failure in GetVersionEx (returned 0)");
     }
 
     return {
-      major: winVer.wServicePackMajor,
-      minor: winVer.wServicePackMinor,
+      servicePackMajor: winVer.wServicePackMajor,
+      servicePackMinor: winVer.wServicePackMinor,
+      buildNumber: winVer.dwBuildNumber,
     };
   } catch (e) {
-    return UNKNOWN_SERVICE_PACK;
+    return UNKNOWN_VERSION_INFO;
   } finally {
     kernel32.close();
   }
 }
 
 /**
  * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
  * is owned by a parent environment object and is an addon listener.
@@ -1238,19 +1242,33 @@ EnvironmentCache.prototype = {
       name: getSysinfoProperty("name", null),
       version: getSysinfoProperty("version", null),
       locale: getSystemLocale(),
     };
 
     if (["gonk", "android"].includes(AppConstants.platform)) {
       data.kernelVersion = getSysinfoProperty("kernel_version", null);
     } else if (AppConstants.platform === "win") {
-      let servicePack = getServicePack();
-      data.servicePackMajor = servicePack.major;
-      data.servicePackMinor = servicePack.minor;
+      // The path to the "UBR" key, queried to get additional version details on Windows.
+      const WINDOWS_UBR_KEY_PATH = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
+
+      let versionInfo = getWindowsVersionInfo();
+      data.servicePackMajor = versionInfo.servicePackMajor;
+      data.servicePackMinor = versionInfo.servicePackMinor;
+      // We only need the build number and UBR if we're at or above Windows 10.
+      if (typeof(data.version) === 'string' &&
+          Services.vc.compare(data.version, "10") >= 0) {
+        data.windowsBuildNumber = versionInfo.buildNumber;
+        // Query the UBR key and only add it to the environment if it's available.
+        // |readRegKey| doesn't throw, but rather returns 'undefined' on error.
+        let ubr = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+                                             WINDOWS_UBR_KEY_PATH, "UBR",
+                                             Ci.nsIWindowsRegKey.WOW64_64);
+        data.windowsUBR = (ubr !== undefined) ? ubr : null;
+      }
       data.installYear = getSysinfoProperty("installYear", null);
     }
 
     return data;
   },
 
   /**
    * Get the HDD information.
--- a/toolkit/components/telemetry/docs/environment.rst
+++ b/toolkit/components/telemetry/docs/environment.rst
@@ -103,16 +103,19 @@ Structure::
           isTablet: <bool>, // null on failure
         },
         os: {
             name: <string>, // "Windows_NT" or null on failure
             version: <string>, // e.g. "6.1", null on failure
             kernelVersion: <string>, // android/b2g only or null on failure
             servicePackMajor: <number>, // windows only or null on failure
             servicePackMinor: <number>, // windows only or null on failure
+            windowsBuildNumber: <number>, // windows 10 only or null on failure
+            windowsUBR: <number>, // windows 10 only or null on failure
+            installYear: <number>, // windows only or null on failure
             locale: <string>, // "en" or null on failure
         },
         hdd: {
           profile: { // hdd where the profile folder is located
               model: <string>, // windows only or null on failure
               revision: <string>, // windows only or null on failure
           },
           binary:  { // hdd where the application binary is located
@@ -318,15 +321,33 @@ If the user is using a partner repack, t
 
 Distributions are most reliably identified by the ``distributionId`` field. Partner information can be found in the `partner repacks <https://github.com/mozilla-partners>`_ (`the old one <http://hg.mozilla.org/build/partner-repacks/>`_ is deprecated): it contains one private repository per partner.
 Important values for ``distributionId`` include:
 
 - "MozillaOnline" for the Mozilla China repack.
 - "canonical", for the `Ubuntu Firefox repack <http://bazaar.launchpad.net/~mozillateam/firefox/firefox.trusty/view/head:/debian/distribution.ini>`_.
 - "yandex", for the Firefox Build by Yandex.
 
+system
+------
+
+os
+~~
+
+This object contains operating system information.
+
+- ``name``: the name of the OS.
+- ``version``: a string representing the OS version.
+- ``kernelVersion``: an Android/B2G only string representing the kernel version.
+- ``servicePackMajor``: the Windows only major version number for the installed service pack.
+- ``servicePackMinor``: the Windows only minor version number for the installed service pack.
+- ``windowsBuildNumber``: the Windows build number, only available for Windows >= 10.
+- ``windowsUBR``: the Windows UBR number, only available for Windows >= 10. This value is incremented by Windows cumulative updates patches.
+- ``installYear``: the Windows only integer representing the year the OS was installed.
+- ``locale``: the string representing the OS locale.
+
 addons
 ------
 
 activeAddons
 ~~~~~~~~~~~~
 
 Starting from Firefox 44, the length of the following string fields: ``name``, ``description`` and ``version`` is limited to 100 characters. The same limitation applies to the same fields in ``theme`` and ``activePlugins``.
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -506,16 +506,26 @@ function checkSystemSection(data) {
   Assert.ok(checkNullOrString(osData.locale));
 
   // Service pack is only available on Windows.
   if (gIsWindows) {
     Assert.ok(Number.isFinite(osData["servicePackMajor"]),
               "ServicePackMajor must be a number.");
     Assert.ok(Number.isFinite(osData["servicePackMinor"]),
               "ServicePackMinor must be a number.");
+    if ("windowsBuildNumber" in osData) {
+      // This might not be available on all Windows platforms.
+      Assert.ok(Number.isFinite(osData["windowsBuildNumber"]),
+                "windowsBuildNumber must be a number.");
+    }
+    if ("windowsUBR" in osData) {
+      // This might not be available on all Windows platforms.
+      Assert.ok((osData["windowsUBR"] === null) || Number.isFinite(osData["windowsUBR"]),
+                "windowsUBR must be null or a number.");
+    }
   } else if (gIsAndroid || gIsGonk) {
     Assert.ok(checkNullOrString(osData.kernelVersion));
   }
 
   let check = gIsWindows ? checkString : checkNullOrString;
   for (let disk of EXPECTED_HDD_FIELDS) {
     Assert.ok(check(data.system.hdd[disk].model));
     Assert.ok(check(data.system.hdd[disk].revision));
--- a/toolkit/modules/WindowsRegistry.jsm
+++ b/toolkit/modules/WindowsRegistry.jsm
@@ -12,25 +12,29 @@ var WindowsRegistry = {
    * Safely reads a value from the registry.
    *
    * @param aRoot
    *        The root registry to use.
    * @param aPath
    *        The registry path to the key.
    * @param aKey
    *        The key name.
+   * @param [aRegistryNode=0]
+   *        Optionally set to nsIWindowsRegKey.WOW64_64 (or nsIWindowsRegKey.WOW64_32)
+   *        to access a 64-bit (32-bit) key from either a 32-bit or 64-bit application.
    * @return The key value or undefined if it doesn't exist.  If the key is
    *         a REG_MULTI_SZ, an array is returned.
    */
-  readRegKey: function(aRoot, aPath, aKey) {
+  readRegKey: function(aRoot, aPath, aKey, aRegistryNode=0) {
     const kRegMultiSz = 7;
+    const kMode = Ci.nsIWindowsRegKey.ACCESS_READ | aRegistryNode;
     let registry = Cc["@mozilla.org/windows-registry-key;1"].
                    createInstance(Ci.nsIWindowsRegKey);
     try {
-      registry.open(aRoot, aPath, Ci.nsIWindowsRegKey.ACCESS_READ);
+      registry.open(aRoot, aPath, kMode);
       if (registry.hasValue(aKey)) {
         let type = registry.getValueType(aKey);
         switch (type) {
           case kRegMultiSz:
             // nsIWindowsRegKey doesn't support REG_MULTI_SZ type out of the box.
             let str = registry.readStringValue(aKey);
             return str.split("\0").filter(v => v);
           case Ci.nsIWindowsRegKey.TYPE_STRING:
@@ -52,23 +56,27 @@ var WindowsRegistry = {
    * Safely removes a key from the registry.
    *
    * @param aRoot
    *        The root registry to use.
    * @param aPath
    *        The registry path to the key.
    * @param aKey
    *        The key name.
+   * @param [aRegistryNode=0]
+   *        Optionally set to nsIWindowsRegKey.WOW64_64 (or nsIWindowsRegKey.WOW64_32)
+   *        to access a 64-bit (32-bit) key from either a 32-bit or 64-bit application.
    */
-  removeRegKey: function(aRoot, aPath, aKey) {
+  removeRegKey: function(aRoot, aPath, aKey, aRegistryNode=0) {
     let registry = Cc["@mozilla.org/windows-registry-key;1"].
                    createInstance(Ci.nsIWindowsRegKey);
     try {
       let mode = Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE |
-                 Ci.nsIWindowsRegKey.ACCESS_SET_VALUE;
+                 Ci.nsIWindowsRegKey.ACCESS_SET_VALUE |
+                 aRegistryNode;
       registry.open(aRoot, aPath, mode);
       if (registry.hasValue(aKey)) {
         registry.removeValue(aKey);
       }
     } catch (ex) {
     } finally {
       registry.close();
     }
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -20,23 +20,25 @@ Cu.import("resource://gre/modules/Servic
 // onCreatedNavigationTarget, onHistoryStateUpdated
 
 var Manager = {
   listeners: new Map(),
 
   init() {
     Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.addMessageListener("Extension:StateChange", this);
-    Services.mm.addMessageListener("Extension:LocationChange", this);
+    Services.mm.addMessageListener("Extension:DocumentChange", this);
+    Services.mm.addMessageListener("Extension:HistoryChange", this);
     Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true);
   },
 
   uninit() {
     Services.mm.removeMessageListener("Extension:StateChange", this);
-    Services.mm.removeMessageListener("Extension:LocationChange", this);
+    Services.mm.removeMessageListener("Extension:DocumentChange", this);
+    Services.mm.removeMessageListener("Extension:HistoryChange", this);
     Services.mm.removeMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
     Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
   },
 
   addListener(type, listener) {
     if (this.listeners.size == 0) {
       this.init();
@@ -65,18 +67,22 @@ var Manager = {
   },
 
   receiveMessage({name, data, target}) {
     switch (name) {
       case "Extension:StateChange":
         this.onStateChange(target, data);
         break;
 
-      case "Extension:LocationChange":
-        this.onLocationChange(target, data);
+      case "Extension:DocumentChange":
+        this.onDocumentChange(target, data);
+        break;
+
+      case "Extension:HistoryChange":
+        this.onHistoryChange(target, data);
         break;
 
       case "Extension:DOMContentLoaded":
         this.onLoad(target, data);
         break;
     }
   },
 
@@ -92,25 +98,29 @@ var Manager = {
         } else {
           let error = `Error code ${data.status}`;
           this.fire("onErrorOccurred", browser, data, {error, url});
         }
       }
     }
   },
 
-  onLocationChange(browser, data) {
+  onDocumentChange(browser, data) {
+    let url = data.location;
+
+    this.fire("onCommitted", browser, data, {url});
+  },
+
+  onHistoryChange(browser, data) {
     let url = data.location;
 
     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});
   },
 
   fire(type, browser, data, extra) {
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -49,78 +49,119 @@ var WebProgressListener = {
       return;
     }
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
     webProgress.removeProgressListener(this);
   },
 
   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,
-    };
+    let locationURI = request.QueryInterface(Ci.nsIChannel).URI;
+
+    this.sendStateChange({webProgress, locationURI, stateFlags, status});
 
-    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.
-        this.onLocationChange(webProgress, request, request.QueryInterface(Ci.nsIChannel).URI, 0);
-      }
+    // Based on the docs of the webNavigation.onCommitted event, it should be raised when:
+    // "The document  might still be downloading, but at least part of
+    // the document has been received"
+    // and for some reason we don't fire onLocationChange for the
+    // initial navigation of a sub-frame.
+    // For the above two reasons, when the navigation event is related to
+    // a sub-frame we process the document change here and
+    // then send an "Extension:DocumentChange" message to the main process,
+    // where it will be turned into a webNavigation.onCommitted event.
+    // (see Bug 1264936 and Bug 125662 for rationale)
+    if ((webProgress.DOMWindow.top != webProgress.DOMWindow) &&
+        (stateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT)) {
+      this.sendDocumentChange({webProgress, locationURI});
     }
   },
 
   onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
-    let {DOMWindow, loadType} = webProgress;
+    let {DOMWindow} = 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|
+    // When a frame navigation doesn't change the current loaded document
+    // (which can be due to history.pushState/replaceState or to a changed hash in the url),
+    // it is reported only to the onLocationChange, for this reason
+    // we process the history change here and then we are going to send
+    // an "Extension:HistoryChange" to the main process, where it will be turned
+    // into a webNavigation.onHistoryStateUpdated/onReferenceFragmentUpdated event.
+    if (isSameDocument) {
+      this.sendHistoryChange({webProgress, previousURI, locationURI});
+    } else if (webProgress.DOMWindow.top == webProgress.DOMWindow) {
+      // We have to catch the document changes from top level frames here,
+      // where we can detect the "server redirect" transition.
+      // (see Bug 1264936 and Bug 125662 for rationale)
+      this.sendDocumentChange({webProgress, locationURI, request});
+    }
+  },
 
-      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;
-      }
-    }
+  sendStateChange({webProgress, locationURI, stateFlags, status}) {
+    let data = {
+      requestURL: locationURI.spec,
+      windowId: webProgress.DOMWindowID,
+      parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
+      status,
+      stateFlags,
+    };
 
+    sendAsyncMessage("Extension:StateChange", data);
+  },
+
+  sendDocumentChange({webProgress, locationURI}) {
     let data = {
-      isHistoryStateUpdated, isReferenceFragmentUpdated,
       location: locationURI ? locationURI.spec : "",
       windowId: webProgress.DOMWindowID,
       parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
     };
 
-    sendAsyncMessage("Extension:LocationChange", data);
+    sendAsyncMessage("Extension:DocumentChange", data);
+  },
+
+  sendHistoryChange({webProgress, previousURI, locationURI}) {
+    let {loadType} = webProgress;
+
+    let isHistoryStateUpdated = false;
+    let isReferenceFragmentUpdated = false;
+
+    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;
+    }
+
+    if (isHistoryStateUpdated || isReferenceFragmentUpdated) {
+      let data = {
+        isHistoryStateUpdated, isReferenceFragmentUpdated,
+        location: locationURI ? locationURI.spec : "",
+        windowId: webProgress.DOMWindowID,
+        parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
+      };
+
+      sendAsyncMessage("Extension:HistoryChange", data);
+    }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
 };
 
 var disabled = false;
 WebProgressListener.init();
 addEventListener("unload", () => {