Merge m-c to inbound, a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Mon, 11 Apr 2016 16:04:55 -0700
changeset 292662 968ccb3b3ed87f564a7a44036961e8d6155eb6e4
parent 292650 cc130a8f204d7cabd016c1ac2cd15ddc661a6f72 (current diff)
parent 292661 21bf1af375c1fa8565ae3bb2e89bd1a0809363d4 (diff)
child 292663 a578a48aae83831ae942758b4a71a55ec7577e6a
push id74945
push userkwierso@gmail.com
push dateMon, 11 Apr 2016 23:04:54 +0000
treeherdermozilla-inbound@968ccb3b3ed8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
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 m-c to inbound, a=merge CLOSED TREE MozReview-Commit-ID: 41faNOn7U05
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -14,18 +14,16 @@ Cu.import("resource://gre/modules/AppCon
 XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker",
   "resource:///modules/PanelWideWidgetTracker.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets",
   "resource:///modules/CustomizableWidgets.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-  "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
   const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
   return Services.strings.createBundle(kUrl);
 });
 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
   "resource://gre/modules/ShortcutUtils.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "gELS",
   "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
@@ -4106,38 +4104,36 @@ OverflowableToolbar.prototype = {
         this._onPanelHiding(aEvent);
         break;
       case "resize":
         this._onResize(aEvent);
     }
   },
 
   show: function() {
-    let deferred = Promise.defer();
     if (this._panel.state == "open") {
-      deferred.resolve();
-      return deferred.promise;
-    }
-    let doc = this._panel.ownerDocument;
-    this._panel.hidden = false;
-    let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
-    gELS.addSystemEventListener(contextMenu, 'command', this, true);
-    let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
-    this._panel.openPopup(anchor || this._chevron);
-    this._chevron.open = true;
-
-    let overflowableToolbarInstance = this;
-    this._panel.addEventListener("popupshown", function onPopupShown(aEvent) {
-      this.removeEventListener("popupshown", onPopupShown);
-      this.addEventListener("dragover", overflowableToolbarInstance);
-      this.addEventListener("dragend", overflowableToolbarInstance);
-      deferred.resolve();
+      return Promise.resolve();
+    }
+    return new Promise(resolve => {
+      let doc = this._panel.ownerDocument;
+      this._panel.hidden = false;
+      let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
+      gELS.addSystemEventListener(contextMenu, 'command', this, true);
+      let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
+      this._panel.openPopup(anchor || this._chevron);
+      this._chevron.open = true;
+
+      let overflowableToolbarInstance = this;
+      this._panel.addEventListener("popupshown", function onPopupShown(aEvent) {
+        this.removeEventListener("popupshown", onPopupShown);
+        this.addEventListener("dragover", overflowableToolbarInstance);
+        this.addEventListener("dragend", overflowableToolbarInstance);
+        resolve();
+      });
     });
-
-    return deferred.promise;
   },
 
   _onClickChevron: function(aEvent) {
     if (this._chevron.open) {
       this._panel.hidePopup();
       this._chevron.open = false;
     } else {
       this.show();
--- a/browser/components/customizableui/ScrollbarSampler.jsm
+++ b/browser/components/customizableui/ScrollbarSampler.jsm
@@ -5,66 +5,61 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ScrollbarSampler"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-                                  "resource://gre/modules/Promise.jsm");
 
 var gSystemScrollbarWidth = null;
 
 this.ScrollbarSampler = {
   getSystemScrollbarWidth: function() {
-    let deferred = Promise.defer();
-
     if (gSystemScrollbarWidth !== null) {
-      deferred.resolve(gSystemScrollbarWidth);
-      return deferred.promise;
+      return Promise.resolve(gSystemScrollbarWidth);
     }
 
-    this._sampleSystemScrollbarWidth().then(function(systemScrollbarWidth) {
-      gSystemScrollbarWidth = systemScrollbarWidth;
-      deferred.resolve(gSystemScrollbarWidth);
+    return new Promise(resolve => {
+      this._sampleSystemScrollbarWidth().then(function(systemScrollbarWidth) {
+        gSystemScrollbarWidth = systemScrollbarWidth;
+        resolve(gSystemScrollbarWidth);
+      });
     });
-    return deferred.promise;
   },
 
   resetSystemScrollbarWidth: function() {
     gSystemScrollbarWidth = null;
   },
 
   _sampleSystemScrollbarWidth: function() {
-    let deferred = Promise.defer();
     let hwin = Services.appShell.hiddenDOMWindow;
     let hdoc = hwin.document.documentElement;
     let iframe = hwin.document.createElementNS("http://www.w3.org/1999/xhtml",
                                                "html:iframe");
     iframe.setAttribute("srcdoc", '<body style="overflow-y: scroll"></body>');
     hdoc.appendChild(iframe);
 
     let cwindow = iframe.contentWindow;
     let utils = cwindow.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDOMWindowUtils);
 
-    cwindow.addEventListener("load", function onLoad(aEvent) {
-      cwindow.removeEventListener("load", onLoad);
-      let sbWidth = {};
-      try {
-        utils.getScrollbarSize(true, sbWidth, {});
-      } catch(e) {
-        Cu.reportError("Could not sample scrollbar size: " + e + " -- " +
-                       e.stack);
-        sbWidth.value = 0;
-      }
-      // Minimum width of 10 so that we have enough padding:
-      sbWidth.value = Math.max(sbWidth.value, 10);
-      deferred.resolve(sbWidth.value);
-      iframe.remove();
+    return new Promise(resolve => {
+      cwindow.addEventListener("load", function onLoad(aEvent) {
+        cwindow.removeEventListener("load", onLoad);
+        let sbWidth = {};
+        try {
+          utils.getScrollbarSize(true, sbWidth, {});
+        } catch(e) {
+          Cu.reportError("Could not sample scrollbar size: " + e + " -- " +
+                         e.stack);
+          sbWidth.value = 0;
+        }
+        // Minimum width of 10 so that we have enough padding:
+        sbWidth.value = Math.max(sbWidth.value, 10);
+        resolve(sbWidth.value);
+        iframe.remove();
+      });
     });
-
-    return deferred.promise;
   }
 };
 Object.freeze(this.ScrollbarSampler);
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -1,18 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
                                   "resource:///modules/ScrollbarSampler.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-                                  "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
                                   "resource://gre/modules/ShortcutUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 
 /**
  * Maintains the state and dispatches events for the main menu panel.
  */
@@ -122,58 +120,56 @@ const PanelUI = {
   /**
    * Opens the menu panel. If the event target has a child with the
    * toolbarbutton-icon attribute, the panel will be anchored on that child.
    * Otherwise, the panel is anchored on the event target itself.
    *
    * @param aEvent the event (if any) that triggers showing the menu.
    */
   show: function(aEvent) {
-    let deferred = Promise.defer();
-
-    this.ensureReady().then(() => {
-      if (this.panel.state == "open" ||
-          document.documentElement.hasAttribute("customizing")) {
-        deferred.resolve();
-        return;
-      }
+    return new Promise(resolve => {
+      this.ensureReady().then(() => {
+        if (this.panel.state == "open" ||
+            document.documentElement.hasAttribute("customizing")) {
+          resolve();
+          return;
+        }
 
-      let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
-      if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
-        updateEditUIVisibility();
-      }
+        let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
+        if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
+          updateEditUIVisibility();
+        }
 
-      let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
-      if (personalBookmarksPlacement &&
-          personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
-        PlacesToolbarHelper.customizeChange();
-      }
+        let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+        if (personalBookmarksPlacement &&
+            personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
+          PlacesToolbarHelper.customizeChange();
+        }
 
-      let anchor;
-      if (!aEvent ||
-          aEvent.type == "command") {
-        anchor = this.menuButton;
-      } else {
-        anchor = aEvent.target;
-      }
+        let anchor;
+        if (!aEvent ||
+            aEvent.type == "command") {
+          anchor = this.menuButton;
+        } else {
+          anchor = aEvent.target;
+        }
 
-      this.panel.addEventListener("popupshown", function onPopupShown() {
-        this.removeEventListener("popupshown", onPopupShown);
-        deferred.resolve();
-      });
+        this.panel.addEventListener("popupshown", function onPopupShown() {
+          this.removeEventListener("popupshown", onPopupShown);
+          resolve();
+        });
 
-      let iconAnchor =
-        document.getAnonymousElementByAttribute(anchor, "class",
-                                                "toolbarbutton-icon");
-      this.panel.openPopup(iconAnchor || anchor);
-    }, (reason) => {
-      console.error("Error showing the PanelUI menu", reason);
+        let iconAnchor =
+          document.getAnonymousElementByAttribute(anchor, "class",
+                                                  "toolbarbutton-icon");
+        this.panel.openPopup(iconAnchor || anchor);
+      }, (reason) => {
+        console.error("Error showing the PanelUI menu", reason);
+      });
     });
-
-    return deferred.promise;
   },
 
   /**
    * If the menu panel is being shown, hide it.
    */
   hide: function() {
     if (document.documentElement.hasAttribute("customizing")) {
       return;
@@ -226,25 +222,25 @@ const PanelUI = {
    * @return a Promise that resolves once the panel is ready to roll.
    */
   ensureReady: function(aCustomizing=false) {
     if (this._readyPromise) {
       return this._readyPromise;
     }
     this._readyPromise = Task.spawn(function*() {
       if (!this._initialized) {
-        let delayedStartupDeferred = Promise.defer();
-        let delayedStartupObserver = (aSubject, aTopic, aData) => {
-          if (aSubject == window) {
-            Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
-            delayedStartupDeferred.resolve();
-          }
-        };
-        Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
-        yield delayedStartupDeferred.promise;
+        yield new Promise(resolve => {
+          let delayedStartupObserver = (aSubject, aTopic, aData) => {
+            if (aSubject == window) {
+              Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
+              resolve();
+            }
+          };
+          Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
+        });
       }
 
       this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang",
                                    getLocale());
       if (!this._scrollWidth) {
         // In order to properly center the contents of the panel, while ensuring
         // that we have enough space on either side to show a scrollbar, we have to
         // do a bit of hackery. In particular, we calculate a new width for the
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -26,17 +26,16 @@ skip-if = os == "linux" # Intermittent f
 [browser_UITour3.js]
 skip-if = os == "linux" # Linux: Bug 986760, Bug 989101.
 [browser_UITour_availableTargets.js]
 [browser_UITour_annotation_size_attributes.js]
 [browser_UITour_defaultBrowser.js]
 [browser_UITour_detach_tab.js]
 [browser_UITour_forceReaderMode.js]
 [browser_UITour_heartbeat.js]
-skip-if = e10s # Bug 1240747 - UITour.jsm not e10s friendly.
 [browser_UITour_loop.js]
 skip-if = true # Bug 1225832 - New Loop architecture is not compatible with test.
 [browser_UITour_loop_panel.js]
 [browser_UITour_modalDialog.js]
 skip-if = os != "mac" # modal dialog disabling only working on OS X.
 [browser_UITour_observe.js]
 [browser_UITour_panel_close_annotation.js]
 skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
--- a/browser/components/uitour/test/browser_UITour_heartbeat.js
+++ b/browser/components/uitour/test/browser_UITour_heartbeat.js
@@ -2,24 +2,16 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 var gTestTab;
 var gContentAPI;
 var gContentWindow;
 
-function test() {
-  UITourTest();
-  requestLongerTimeout(2);
-  registerCleanupFunction(() => {
-    Services.prefs.clearUserPref("browser.uitour.surveyDuration");
-  });
-}
-
 function getHeartbeatNotification(aId, aChromeWindow = window) {
   let notificationBox = aChromeWindow.document.getElementById("high-priority-global-notificationbox");
   // UITour.jsm prefixes the notification box ID with "heartbeat-" to prevent collisions.
   return notificationBox.getNotificationWithValue("heartbeat-" + aId);
 }
 
 /**
  * Simulate a click on a rating element in the Heartbeat notification.
@@ -97,524 +89,622 @@ function checkTelemetry(aPayload, aFlowI
       ok(Number.isInteger(ts) && ts > 0, "Timestamp '" + field + "' must be a natural number");
     }
     extraKeys.delete(field);
   }
 
   is(extraKeys.size, 0, "No unexpected fields in the Telemetry payload");
 }
 
-var tests = [
-  /**
-   * Check that the "stars" heartbeat UI correctly shows and closes.
-   */
-  function test_heartbeat_stars_show(done) {
-    let flowId = "ui-ratefirefox-" + Math.random();
-    let engagementURL = "http://example.com";
+/**
+ * Waits for an UITour notification dispatched through |UITour.notify|. This should be
+ * done with |gContentAPI.observe|. Unfortunately, in e10s, |gContentAPI.observe| doesn't
+ * allow for multiple calls to the same callback, allowing to catch just the first
+ * notification.
+ *
+ * @param aEventName
+ *        The notification name to wait for.
+ * @return {Promise} Resolved with the data that comes with the event.
+ */
+function promiseWaitHeartbeatNotification(aEventName) {
+  return ContentTask.spawn(gTestTab.linkedBrowser, { aEventName },
+      function({ aEventName }) {
+        return new Promise(resolve => {
+          addEventListener("mozUITourNotification", function listener(event) {
+            if (event.detail.event !== aEventName) {
+              return;
+            }
+            removeEventListener("mozUITourNotification", listener, false);
+            resolve(event.detail.params);
+          }, false);
+        });
+      });
+}
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          cleanUpNotification(flowId);
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          done();
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received");
-          checkTelemetry(aData, flowId, ["offeredTS", "closedTS"]);
-          break;
-        }
-        default:
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-      }
-    });
+/**
+ * Waits for UITour notifications dispatched through |UITour.notify|. This works like
+ * |promiseWaitHeartbeatNotification|, but waits for all the passed notifications to
+ * be received before resolving. If it receives an unaccounted notification, it rejects.
+ *
+ * @param events
+ *        An array of expected notification names to wait for.
+ * @return {Promise} Resolved with the data that comes with the event. Rejects with the
+ *         name of an undesired notification if received.
+ */
+function promiseWaitExpectedNotifications(events) {
+  return ContentTask.spawn(gTestTab.linkedBrowser, { events },
+      function({ events }) {
+        let stillToReceive = events;
+        return new Promise((res, rej) => {
+          addEventListener("mozUITourNotification", function listener(event) {
+            if (stillToReceive.includes(event.detail.event)) {
+              // Filter out the received event.
+              stillToReceive = stillToReceive.filter(x => x !== event.detail.event);
+            } else {
+              removeEventListener("mozUITourNotification", listener, false);
+              rej(event.detail.event);
+            }
+            // We still need to catch some notifications. Don't do anything.
+            if (stillToReceive.length > 0) {
+              return;
+            }
+            // We don't need to listen for other notifications. Resolve the promise.
+            removeEventListener("mozUITourNotification", listener, false);
+            res();
+          }, false);
+        });
+      });
+}
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-  },
+function validateTimestamp(eventName, timestamp) {
+  info("'" + eventName + "' notification received (timestamp " + timestamp.toString() + ").");
+  ok(Number.isFinite(timestamp), "Timestamp must be a number.");
+}
 
-  /**
-   * Test that the heartbeat UI correctly works with null engagement URL.
-   */
-  function test_heartbeat_null_engagementURL(done) {
-    let flowId = "ui-ratefirefox-" + Math.random();
-    let originalTabCount = gBrowser.tabs.length;
+add_task(function* test_setup(){
+  yield setup_UITourTest();
+  requestLongerTimeout(2);
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("browser.uitour.surveyDuration");
+  });
+});
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-          simulateVote(flowId, 2);
-          break;
-        }
-        case "Heartbeat:Voted": {
-          info("'Heartbeat:Voted' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-          done();
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received.");
-          checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-          is(aData.score, 2, "Checking Telemetry payload.score");
-          break;
-        }
-        default:
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-      }
-    });
+/**
+ * Check that the "stars" heartbeat UI correctly shows and closes.
+ */
+add_UITour_task(function* test_heartbeat_stars_show() {
+  let flowId = "ui-ratefirefox-" + Math.random();
+  let engagementURL = "http://example.com";
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(
+    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
+
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
+
+  // Validate the returned timestamp.
+  let data = yield shownPromise;
+  validateTimestamp('Heartbeat:Offered', data.timestamp);
+
+  // Close the heartbeat notification.
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
+  cleanUpNotification(flowId);
+
+  data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+
+  data = yield pingSentPromise;
+  info("'Heartbeat:TelemetrySent' notification received");
+  checkTelemetry(data, flowId, ["offeredTS", "closedTS"]);
+
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
-  },
+/**
+ * Test that the heartbeat UI correctly works with null engagement URL.
+ */
+add_UITour_task(function* test_heartbeat_null_engagementURL() {
+  let flowId = "ui-ratefirefox-" + Math.random();
+  let originalTabCount = gBrowser.tabs.length;
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
+    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
+
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
 
-   /**
-   * Test that the heartbeat UI correctly works with an invalid, but non null, engagement URL.
-   */
-  function test_heartbeat_invalid_engagement_URL(done) {
-    let flowId = "ui-ratefirefox-" + Math.random();
-    let originalTabCount = gBrowser.tabs.length;
-    let invalidEngagementURL = "invalidEngagement";
+  // Validate the returned timestamp.
+  let data = yield shownPromise;
+  validateTimestamp('Heartbeat:Offered', data.timestamp);
+
+  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
+  // wait for them here.
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
+  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
+
+  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
+  simulateVote(flowId, 2);
+  data = yield votedPromise;
+  validateTimestamp('Heartbeat:Voted', data.timestamp);
+
+  // Validate the closing timestamp.
+  data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-          simulateVote(flowId, 2);
-          break;
-        }
-        case "Heartbeat:Voted": {
-          info("'Heartbeat:Voted' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-          done();
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received.");
-          checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-          is(aData.score, 2, "Checking Telemetry payload.score");
-          break;
-        }
-        default:
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-      }
-    });
+  // Validate the data we send out.
+  data = yield pingSentPromise;
+  info("'Heartbeat:TelemetrySent' notification received.");
+  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
+  is(data.score, 2, "Checking Telemetry payload.score");
+
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
+
+/**
+ * Test that the heartbeat UI correctly works with an invalid, but non null, engagement URL.
+ */
+add_UITour_task(function* test_heartbeat_invalid_engagement_URL() {
+  let flowId = "ui-ratefirefox-" + Math.random();
+  let originalTabCount = gBrowser.tabs.length;
+  let invalidEngagementURL = "invalidEngagement";
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
+    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
+
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, invalidEngagementURL);
+
+  // Validate the returned timestamp.
+  let data = yield shownPromise;
+  validateTimestamp('Heartbeat:Offered', data.timestamp);
+
+  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
+  // wait for them here.
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
+  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, invalidEngagementURL);
-  },
+  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
+  simulateVote(flowId, 2);
+  data = yield votedPromise;
+  validateTimestamp('Heartbeat:Voted', data.timestamp);
+
+  // Validate the closing timestamp.
+  data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
+
+  // Validate the data we send out.
+  data = yield pingSentPromise;
+  info("'Heartbeat:TelemetrySent' notification received.");
+  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
+  is(data.score, 2, "Checking Telemetry payload.score");
 
-  /**
-   * Test that the score is correctly reported.
-   */
-  function test_heartbeat_stars_vote(done) {
-    const expectedScore = 4;
-    let flowId = "ui-ratefirefox-" + Math.random();
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
+
+/**
+ * Test that the score is correctly reported.
+ */
+add_UITour_task(function* test_heartbeat_stars_vote() {
+  const expectedScore = 4;
+  let originalTabCount = gBrowser.tabs.length;
+  let flowId = "ui-ratefirefox-" + Math.random();
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
+    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
+
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-          simulateVote(flowId, expectedScore);
-          break;
-        }
-        case "Heartbeat:Voted": {
-          info("'Heartbeat:Voted' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          is(aData.score, expectedScore, "Should report a score of " + expectedScore);
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          done();
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received.");
-          checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-          is(aData.score, expectedScore, "Checking Telemetry payload.score");
-          break;
-        }
-        default:
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-      }
-    });
+  // Validate the returned timestamp.
+  let data = yield shownPromise;
+  validateTimestamp('Heartbeat:Offered', data.timestamp);
+
+  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
+  // wait for them here.
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
+  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
-  },
+  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
+  simulateVote(flowId, expectedScore);
+  data = yield votedPromise;
+  validateTimestamp('Heartbeat:Voted', data.timestamp);
+  is(data.score, expectedScore, "Should report a score of " + expectedScore);
 
-  /**
-   * Test that the engagement page is correctly opened when voting.
-   */
-  function test_heartbeat_engagement_tab(done) {
-    let engagementURL = "http://example.com";
-    let flowId = "ui-ratefirefox-" + Math.random();
-    let originalTabCount = gBrowser.tabs.length;
-    const expectedTabCount = originalTabCount + 1;
-    let heartbeatVoteSeen = false;
+  // Validate the closing timestamp and vote.
+  data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
+
+  // Validate the data we send out.
+  data = yield pingSentPromise;
+  info("'Heartbeat:TelemetrySent' notification received.");
+  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
+  is(data.score, expectedScore, "Checking Telemetry payload.score");
+
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-          simulateVote(flowId, 1);
-          break;
-        }
-        case "Heartbeat:Voted": {
-          info("'Heartbeat:Voted' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          heartbeatVoteSeen = true;
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          ok(heartbeatVoteSeen, "Heartbeat vote should have been received");
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
-          gBrowser.removeCurrentTab();
-          done();
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received.");
-          checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-          is(aData.score, 1, "Checking Telemetry payload.score");
-          break;
-        }
-        default:
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-      }
-    });
+/**
+ * Test that the engagement page is correctly opened when voting.
+ */
+add_UITour_task(function* test_heartbeat_engagement_tab() {
+  let engagementURL = "http://example.com";
+  let flowId = "ui-ratefirefox-" + Math.random();
+  let originalTabCount = gBrowser.tabs.length;
+  const expectedTabCount = originalTabCount + 1;
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
+    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
+
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-  },
+  // Validate the returned timestamp.
+  let data = yield shownPromise;
+  validateTimestamp('Heartbeat:Offered', data.timestamp);
+
+  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
+  // wait for them here.
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
+  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
 
-  /**
-   * Test that the engagement button opens the engagement URL.
-   */
-  function test_heartbeat_engagement_button(done) {
-    let engagementURL = "http://example.com";
-    let flowId = "ui-engagewithfirefox-" + Math.random();
-    let originalTabCount = gBrowser.tabs.length;
-    const expectedTabCount = originalTabCount + 1;
-    let heartbeatEngagedSeen = false;
+  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
+  simulateVote(flowId, 1);
+  data = yield votedPromise;
+  validateTimestamp('Heartbeat:Voted', data.timestamp);
+
+  // Validate the closing timestamp, vote and make sure the engagement page was opened.
+  data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+  is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
+  gBrowser.removeCurrentTab();
+
+  // Validate the data we send out.
+  data = yield pingSentPromise;
+  info("'Heartbeat:TelemetrySent' notification received.");
+  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
+  is(data.score, 1, "Checking Telemetry payload.score");
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          let notification = getHeartbeatNotification(flowId);
-          is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
-          // The UI was just shown. We can simulate a click on the engagement button.
-          let engagementButton = notification.querySelector(".notification-button");
-          is(engagementButton.label, "Engage Me", "Check engagement button text");
-          engagementButton.doCommand();
-          break;
-        }
-        case "Heartbeat:Engaged": {
-          info("'Heartbeat:Engaged' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          heartbeatEngagedSeen = true;
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(heartbeatEngagedSeen, "Heartbeat:Engaged should have been received");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
-          gBrowser.removeCurrentTab();
-          executeSoon(done);
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received.");
-          checkTelemetry(aData, flowId, ["offeredTS", "engagedTS", "closedTS"]);
-          break;
-        }
-        default: {
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-        }
-      }
-    });
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
+
+/**
+ * Test that the engagement button opens the engagement URL.
+ */
+add_UITour_task(function* test_heartbeat_engagement_button() {
+  let engagementURL = "http://example.com";
+  let flowId = "ui-engagewithfirefox-" + Math.random();
+  let originalTabCount = gBrowser.tabs.length;
+  const expectedTabCount = originalTabCount + 1;
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
+    "Heartbeat:NotificationClosed", "Heartbeat:Engaged", "Heartbeat:TelemetrySent"]);
+
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL, null, null, {
+    engagementButtonLabel: "Engage Me",
+  });
+
+  let data = yield shownPromise;
+  validateTimestamp('Heartbeat:Offered', data.timestamp);
+
+  // Wait an the Engaged, Closed and Telemetry Sent events. They are fired together, so
+  // wait for them here.
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let engagedPromise = promiseWaitHeartbeatNotification("Heartbeat:Engaged");
+  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
 
-    gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL, null, null, {
-      engagementButtonLabel: "Engage Me",
-    });
-  },
+  // Simulate user engagement.
+  let notification = getHeartbeatNotification(flowId);
+  is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
+  // The UI was just shown. We can simulate a click on the engagement button.
+  let engagementButton = notification.querySelector(".notification-button");
+  is(engagementButton.label, "Engage Me", "Check engagement button text");
+  engagementButton.doCommand();
+
+  data = yield engagedPromise;
+  validateTimestamp('Heartbeat:Engaged', data.timestamp);
+
+  // Validate the closing timestamp, vote and make sure the engagement page was opened.
+  data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+  is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
+  gBrowser.removeCurrentTab();
 
-  /**
-   * Test that the learn more link is displayed and that the page is correctly opened when
-   * clicking on it.
-   */
-  function test_heartbeat_learnmore(done) {
-    let dummyURL = "http://example.com";
-    let flowId = "ui-ratefirefox-" + Math.random();
-    let originalTabCount = gBrowser.tabs.length;
-    const expectedTabCount = originalTabCount + 1;
+  // Validate the data we send out.
+  data = yield pingSentPromise;
+  info("'Heartbeat:TelemetrySent' notification received.");
+  checkTelemetry(data, flowId, ["offeredTS", "engagedTS", "closedTS"]);
+
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
+
+/**
+ * Test that the learn more link is displayed and that the page is correctly opened when
+ * clicking on it.
+ */
+add_UITour_task(function* test_heartbeat_learnmore() {
+  let dummyURL = "http://example.com";
+  let flowId = "ui-ratefirefox-" + Math.random();
+  let originalTabCount = gBrowser.tabs.length;
+  const expectedTabCount = originalTabCount + 1;
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          // The UI was just shown. Simulate a click on the learn more link.
-          clickLearnMore(flowId);
-          break;
-        }
-        case "Heartbeat:LearnMore": {
-          info("'Heartbeat:LearnMore' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          cleanUpNotification(flowId);
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
-          is(gBrowser.tabs.length, expectedTabCount, "Learn more URL should open in a new tab.");
-          gBrowser.removeCurrentTab();
-          done();
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received.");
-          checkTelemetry(aData, flowId, ["offeredTS", "learnMoreTS", "closedTS"]);
-          break;
-        }
-        default:
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-      }
-    });
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
+    "Heartbeat:NotificationClosed", "Heartbeat:LearnMore", "Heartbeat:TelemetrySent"]);
+
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, dummyURL,
+                            "What is this?", dummyURL);
+
+  let data = yield shownPromise;
+  validateTimestamp('Heartbeat:Offered', data.timestamp);
+
+  // Wait an the LearnMore, Closed and Telemetry Sent events. They are fired together, so
+  // wait for them here.
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let learnMorePromise = promiseWaitHeartbeatNotification("Heartbeat:LearnMore");
+  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, dummyURL,
-                              "What is this?", dummyURL);
-  },
+  // The UI was just shown. Simulate a click on the learn more link.
+  clickLearnMore(flowId);
+
+  data = yield learnMorePromise;
+  validateTimestamp('Heartbeat:LearnMore', data.timestamp);
+  cleanUpNotification(flowId);
 
-  taskify(function* test_invalidEngagementButtonLabel() {
-    let engagementURL = "http://example.com";
-    let flowId = "invalidEngagementButtonLabel-" + Math.random();
+  // The notification was closed.
+  data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+  is(gBrowser.tabs.length, expectedTabCount, "Learn more URL should open in a new tab.");
+  gBrowser.removeCurrentTab();
+
+  // Validate the data we send out.
+  data = yield pingSentPromise;
+  info("'Heartbeat:TelemetrySent' notification received.");
+  checkTelemetry(data, flowId, ["offeredTS", "learnMoreTS", "closedTS"]);
+
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
 
-    let eventPromise = promisePageEvent();
+add_UITour_task(function* test_invalidEngagementButtonLabel() {
+  let engagementURL = "http://example.com";
+  let flowId = "invalidEngagementButtonLabel-" + Math.random();
 
-    gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                              null, null, {
-                                engagementButtonLabel: 42,
-                              });
+  let eventPromise = promisePageEvent();
 
-    yield eventPromise;
-    ok(!isTourBrowser(gBrowser.selectedBrowser),
-       "Invalid engagementButtonLabel should prevent init");
-
-  }),
+  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
+                            null, null, {
+                              engagementButtonLabel: 42,
+                            });
 
-  taskify(function* test_privateWindowsOnly_noneOpen() {
-    let engagementURL = "http://example.com";
-    let flowId = "privateWindowsOnly_noneOpen-" + Math.random();
+  yield eventPromise;
+  ok(!isTourBrowser(gBrowser.selectedBrowser),
+     "Invalid engagementButtonLabel should prevent init");
 
-    let eventPromise = promisePageEvent();
+})
 
-    gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                              null, null, {
-                                engagementButtonLabel: "Yes!",
-                                privateWindowsOnly: true,
-                              });
+add_UITour_task(function* test_privateWindowsOnly_noneOpen() {
+  let engagementURL = "http://example.com";
+  let flowId = "privateWindowsOnly_noneOpen-" + Math.random();
+
+  let eventPromise = promisePageEvent();
 
-    yield eventPromise;
-    ok(!isTourBrowser(gBrowser.selectedBrowser),
-       "If there are no private windows opened, tour init should be prevented");
-  }),
+  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
+                            null, null, {
+                              engagementButtonLabel: "Yes!",
+                              privateWindowsOnly: true,
+                            });
 
-  taskify(function* test_privateWindowsOnly_notMostRecent() {
-    let engagementURL = "http://example.com";
-    let flowId = "notMostRecent-" + Math.random();
-
-    let privateWin = yield BrowserTestUtils.openNewBrowserWindow({ private: true });
-    let mostRecentWin = yield BrowserTestUtils.openNewBrowserWindow();
+  yield eventPromise;
+  ok(!isTourBrowser(gBrowser.selectedBrowser),
+     "If there are no private windows opened, tour init should be prevented");
+})
 
-    let eventPromise = promisePageEvent();
+add_UITour_task(function* test_privateWindowsOnly_notMostRecent() {
+  let engagementURL = "http://example.com";
+  let flowId = "notMostRecent-" + Math.random();
 
-    gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                              null, null, {
-                                engagementButtonLabel: "Yes!",
-                                privateWindowsOnly: true,
-                              });
+  let privateWin = yield BrowserTestUtils.openNewBrowserWindow({ private: true });
+  let mostRecentWin = yield BrowserTestUtils.openNewBrowserWindow();
 
-    yield eventPromise;
-    is(getHeartbeatNotification(flowId, window), null,
-       "Heartbeat shouldn't appear in the default window");
-    is(!!getHeartbeatNotification(flowId, privateWin), true,
-       "Heartbeat should appear in the most recent private window");
-    is(getHeartbeatNotification(flowId, mostRecentWin), null,
-       "Heartbeat shouldn't appear in the most recent non-private window");
+  let eventPromise = promisePageEvent();
+
+  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
+                            null, null, {
+                              engagementButtonLabel: "Yes!",
+                              privateWindowsOnly: true,
+                            });
 
-    yield BrowserTestUtils.closeWindow(mostRecentWin);
-    yield BrowserTestUtils.closeWindow(privateWin);
-  }),
-
-  taskify(function* test_privateWindowsOnly() {
-    let engagementURL = "http://example.com";
-    let learnMoreURL = "http://example.org/learnmore/";
-    let flowId = "ui-privateWindowsOnly-" + Math.random();
+  yield eventPromise;
+  is(getHeartbeatNotification(flowId, window), null,
+     "Heartbeat shouldn't appear in the default window");
+  is(!!getHeartbeatNotification(flowId, privateWin), true,
+     "Heartbeat should appear in the most recent private window");
+  is(getHeartbeatNotification(flowId, mostRecentWin), null,
+     "Heartbeat shouldn't appear in the most recent non-private window");
 
-    let privateWin = yield BrowserTestUtils.openNewBrowserWindow({ private: true });
+  yield BrowserTestUtils.closeWindow(mostRecentWin);
+  yield BrowserTestUtils.closeWindow(privateWin);
+})
 
-    yield new Promise((resolve) => {
-      gContentAPI.observe(function(aEventName, aData) {
-        info(aEventName + " notification received: " + JSON.stringify(aData, null, 2));
-        ok(false, "No heartbeat notifications should arrive for privateWindowsOnly");
-      }, resolve);
-    });
+add_UITour_task(function* test_privateWindowsOnly() {
+  let engagementURL = "http://example.com";
+  let learnMoreURL = "http://example.org/learnmore/";
+  let flowId = "ui-privateWindowsOnly-" + Math.random();
 
-    gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                              "Learn More", learnMoreURL, {
-                                engagementButtonLabel: "Yes!",
-                                privateWindowsOnly: true,
-                              });
+  let privateWin = yield BrowserTestUtils.openNewBrowserWindow({ private: true });
 
-    yield promisePageEvent();
-
-    ok(isTourBrowser(gBrowser.selectedBrowser), "UITour should have been init for the browser");
+  yield new Promise((resolve) => {
+    gContentAPI.observe(function(aEventName, aData) {
+      info(aEventName + " notification received: " + JSON.stringify(aData, null, 2));
+      ok(false, "No heartbeat notifications should arrive for privateWindowsOnly");
+    }, resolve);
+  });
 
-    let notification = getHeartbeatNotification(flowId, privateWin);
-
-    is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
+  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
+                            "Learn More", learnMoreURL, {
+                              engagementButtonLabel: "Yes!",
+                              privateWindowsOnly: true,
+                            });
 
-    info("Test the learn more link.");
-    let learnMoreLink = notification.querySelector(".text-link");
-    is(learnMoreLink.value, "Learn More", "Check learn more label");
-    let learnMoreTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
-    learnMoreLink.click();
-    let learnMoreTab = yield learnMoreTabPromise;
-    is(learnMoreTab.linkedBrowser.currentURI.host, "example.org", "Check learn more site opened");
-    ok(PrivateBrowsingUtils.isBrowserPrivate(learnMoreTab.linkedBrowser), "Ensure the learn more tab is private");
-    yield BrowserTestUtils.removeTab(learnMoreTab);
+  yield promisePageEvent();
+
+  ok(isTourBrowser(gBrowser.selectedBrowser), "UITour should have been init for the browser");
+
+  let notification = getHeartbeatNotification(flowId, privateWin);
+
+  is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
 
-    info("Test the engagement button's new tab.");
-    let engagementButton = notification.querySelector(".notification-button");
-    is(engagementButton.label, "Yes!", "Check engagement button text");
-    let engagementTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
-    engagementButton.doCommand();
-    let engagementTab = yield engagementTabPromise;
-    is(engagementTab.linkedBrowser.currentURI.host, "example.com", "Check enagement site opened");
-    ok(PrivateBrowsingUtils.isBrowserPrivate(engagementTab.linkedBrowser), "Ensure the engagement tab is private");
-    yield BrowserTestUtils.removeTab(engagementTab);
+  info("Test the learn more link.");
+  let learnMoreLink = notification.querySelector(".text-link");
+  is(learnMoreLink.value, "Learn More", "Check learn more label");
+  let learnMoreTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
+  learnMoreLink.click();
+  let learnMoreTab = yield learnMoreTabPromise;
+  is(learnMoreTab.linkedBrowser.currentURI.host, "example.org", "Check learn more site opened");
+  ok(PrivateBrowsingUtils.isBrowserPrivate(learnMoreTab.linkedBrowser), "Ensure the learn more tab is private");
+  yield BrowserTestUtils.removeTab(learnMoreTab);
 
-    yield BrowserTestUtils.closeWindow(privateWin);
-  }),
+  info("Test the engagement button's new tab.");
+  let engagementButton = notification.querySelector(".notification-button");
+  is(engagementButton.label, "Yes!", "Check engagement button text");
+  let engagementTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
+  engagementButton.doCommand();
+  let engagementTab = yield engagementTabPromise;
+  is(engagementTab.linkedBrowser.currentURI.host, "example.com", "Check enagement site opened");
+  ok(PrivateBrowsingUtils.isBrowserPrivate(engagementTab.linkedBrowser), "Ensure the engagement tab is private");
+  yield BrowserTestUtils.removeTab(engagementTab);
 
-  /**
-   * Test that the survey closes itself after a while and submits Telemetry
-   */
-  taskify(function* test_telemetry_surveyExpired() {
-    let flowId = "survey-expired-" + Math.random();
-    let engagementURL = "http://example.com";
-    let surveyDuration = 1; // 1 second (pref is in seconds)
-    Services.prefs.setIntPref("browser.uitour.surveyDuration", surveyDuration);
+  yield BrowserTestUtils.closeWindow(privateWin);
+})
 
-    let telemetryPromise = new Promise((resolve, reject) => {
-        gContentAPI.observe(function (aEventName, aData) {
-          switch (aEventName) {
-            case "Heartbeat:NotificationOffered":
-              info("'Heartbeat:NotificationOffered' notification received");
-              break;
-            case "Heartbeat:SurveyExpired":
-              info("'Heartbeat:SurveyExpired' notification received");
-              ok(true, "Survey should end on its own after a time out");
-            case "Heartbeat:NotificationClosed":
-              info("'Heartbeat:NotificationClosed' notification received");
-              break;
-            case "Heartbeat:TelemetrySent": {
-              info("'Heartbeat:TelemetrySent' notification received");
-              checkTelemetry(aData, flowId, ["offeredTS", "expiredTS", "closedTS"]);
-              resolve();
-              break;
-            }
-            default:
-              // not expecting other states for this test
-              ok(false, "Unexpected notification received: " + aEventName);
-              reject();
-          }
-        });
-    });
+/**
+ * Test that the survey closes itself after a while and submits Telemetry
+ */
+add_UITour_task(function* test_telemetry_surveyExpired() {
+  let flowId = "survey-expired-" + Math.random();
+  let engagementURL = "http://example.com";
+  let surveyDuration = 1; // 1 second (pref is in seconds)
+  Services.prefs.setIntPref("browser.uitour.surveyDuration", surveyDuration);
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
+    "Heartbeat:NotificationClosed", "Heartbeat:SurveyExpired", "Heartbeat:TelemetrySent"]);
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-    yield telemetryPromise;
-    Services.prefs.clearUserPref("browser.uitour.surveyDuration");
-  }),
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
+
+  let expiredPromise = promiseWaitHeartbeatNotification("Heartbeat:SurveyExpired");
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let pingPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
+
+  yield Promise.all([shownPromise, expiredPromise, closedPromise]);
+  // Validate the ping data.
+  let data = yield pingPromise;
+  checkTelemetry(data, flowId, ["offeredTS", "expiredTS", "closedTS"]);
+
+  Services.prefs.clearUserPref("browser.uitour.surveyDuration");
+
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
 
-  /**
-   * Check that certain whitelisted experiment parameters get reflected in the
-   * Telemetry ping
-   */
-  function test_telemetry_params(done) {
-    let flowId = "telemetry-params-" + Math.random();
-    let engagementURL = "http://example.com";
-    let extraParams = {
-      "surveyId": "foo",
-      "surveyVersion": 1.5,
-      "testing": true,
-      "notWhitelisted": 123,
-    };
-    let expectedFields = ["surveyId", "surveyVersion", "testing"];
+/**
+ * Check that certain whitelisted experiment parameters get reflected in the
+ * Telemetry ping
+ */
+add_UITour_task(function* test_telemetry_params() {
+  let flowId = "telemetry-params-" + Math.random();
+  let engagementURL = "http://example.com";
+  let extraParams = {
+    "surveyId": "foo",
+    "surveyVersion": 1.5,
+    "testing": true,
+    "notWhitelisted": 123,
+  };
+  let expectedFields = ["surveyId", "surveyVersion", "testing"];
+
+  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
+  // in UITour-lib.js, otherwise no message will get propagated.
+  gContentAPI.observe(() => {});
+
+  let receivedExpectedPromise = promiseWaitExpectedNotifications(
+    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
 
-    gContentAPI.observe(function (aEventName, aData) {
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered": {
-          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
-          cleanUpNotification(flowId);
-          break;
-        }
-        case "Heartbeat:NotificationClosed": {
-          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
-          break;
-        }
-        case "Heartbeat:TelemetrySent": {
-          info("'Heartbeat:TelemetrySent' notification received");
-          checkTelemetry(aData, flowId, ["offeredTS", "closedTS"].concat(expectedFields));
-          for (let param of expectedFields) {
-            is(aData[param], extraParams[param],
-               "Whitelisted experiment configs should be copied into Telemetry pings");
-          }
-          done();
-          break;
-        }
-        default:
-          // We are not expecting other states for this test.
-          ok(false, "Unexpected notification received: " + aEventName);
-      }
-    });
+  // Show the Heartbeat notification and wait for it to be displayed.
+  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
+  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!",
+                            flowId, engagementURL, null, null, extraParams);
+  yield shownPromise;
+
+  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
+  let pingPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
+  cleanUpNotification(flowId);
 
-    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!",
-                              flowId, engagementURL, null, null, extraParams);
-  },
-];
+  // The notification was closed.
+  let data = yield closedPromise;
+  validateTimestamp('Heartbeat:NotificationClosed', data.timestamp);
+
+  // Validate the data we send out.
+  data = yield pingPromise;
+  info("'Heartbeat:TelemetrySent' notification received.");
+  checkTelemetry(data, flowId, ["offeredTS", "closedTS"].concat(expectedFields));
+  for (let param of expectedFields) {
+    is(data[param], extraParams[param],
+       "Whitelisted experiment configs should be copied into Telemetry pings");
+  }
+
+  // This rejects whenever an unexpected notification is received.
+  yield receivedExpectedPromise;
+})
--- a/browser/components/uitour/test/head.js
+++ b/browser/components/uitour/test/head.js
@@ -283,16 +283,19 @@ function loadUITourTestPage(callback, ho
               // forwarded functions. We'll construct a function on the content-side
               // that forwards all its arguments to a message, and we'll listen for
               // those messages on our side and call the corresponding function with
               // the arguments we got from the content side.
               if (typeof arg == "function") {
                 callbackMap.set(index, arg);
                 fnIndices.push(index);
                 let handler = function(msg) {
+                  // Please note that this handler assumes that the callback is used only once.
+                  // That means that a single gContentAPI.observer() call can't be used to observe
+                  // multiple events.
                   browser.messageManager.removeMessageListener(proxyFunctionName + index, handler);
                   callbackMap.get(index).apply(null, msg.data);
                 };
                 browser.messageManager.addMessageListener(proxyFunctionName + index, handler);
                 return "";
               }
               return arg;
             });
--- a/devtools/client/animationinspector/animation-inspector.xhtml
+++ b/devtools/client/animationinspector/animation-inspector.xhtml
@@ -17,17 +17,17 @@
     <div id="global-toolbar" class="theme-toolbar">
       <span class="label">&allAnimations;</span>
       <button id="toggle-all" standalone="true" class="devtools-button pause-button"></button>
     </div>
     <div id="timeline-toolbar" class="theme-toolbar">
       <button id="rewind-timeline" standalone="true" class="devtools-button"></button>
       <button id="pause-resume-timeline" standalone="true" class="devtools-button pause-button paused"></button>
       <span id="timeline-rate" standalone="true" class="devtools-button"></span>
-      <span id="timeline-current-time" class="label devtools-toolbarbutton"></span>
+      <span id="timeline-current-time" class="label"></span>
     </div>
     <div id="players"></div>
     <div id="error-message">
       <p id="error-type"></p>
       <p>&selectElement;</p>
       <button id="element-picker" standalone="true" class="devtools-button"></button>
     </div>
     <script type="application/javascript;version=1.8" src="animation-controller.js"></script>
--- a/devtools/client/locales/en-US/storage.dtd
+++ b/devtools/client/locales/en-US/storage.dtd
@@ -1,8 +1,11 @@
 <!-- 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/. -->
 
 <!-- LOCALIZATION NOTE : This file contains the Storage Inspector strings. -->
 
 <!-- LOCALIZATION NOTE : Placeholder for the searchbox that allows you to filter the table items. -->
 <!ENTITY searchBox.placeholder         "Filter items">
+
+<!-- LOCALIZATION NOTE : Label of popup menu action to delete all storage items. -->
+<!ENTITY storage.popupMenu.deleteAllLabel "Delete All">
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -114,8 +114,12 @@ storage.data.label=Data
 
 # LOCALIZATION NOTE (storage.parsedValue.label):
 # This is the heading displayed over the item parsed value in the sidebar
 storage.parsedValue.label=Parsed Value
 
 # LOCALIZATION NOTE (storage.popupMenu.deleteLabel):
 # Label of popup menu action to delete storage item.
 storage.popupMenu.deleteLabel=Delete “%S”
+
+# LOCALIZATION NOTE (storage.popupMenu.deleteAllLabel):
+# Label of popup menu action to delete all storage items.
+storage.popupMenu.deleteAllFromLabel=Delete All From “%S”
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -89,16 +89,17 @@ html, body {
 
 #global-toolbar > .title {
   border-right: 1px solid var(--theme-splitter-color);
   padding: 1px 6px 0 2px;
 }
 
 #global-toolbar .toolbar-button {
   margin: 0 0 0 5px;
+  padding: 0;
 }
 
 #global-toolbar .toolbar-button,
 #global-toolbar .toolbar-button::before {
   width: 12px;
   height: 12px;
 }
 
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -798,16 +798,19 @@ TableWidget.prototype = {
 
   /**
    * Removes the row associated with the `item` object.
    */
   remove: function(item) {
     if (typeof item == "string") {
       item = this.items.get(item);
     }
+    if (!item) {
+      return;
+    }
     let removed = this.items.delete(item[this.uniqueId]);
 
     if (!removed) {
       return;
     }
     for (let column of this.columns.values()) {
       column.remove(item);
       column.updateZebra();
--- a/devtools/client/shared/widgets/TreeWidget.js
+++ b/devtools/client/shared/widgets/TreeWidget.js
@@ -12,30 +12,33 @@ const EventEmitter = require("devtools/s
 /**
  * A tree widget with keyboard navigation and collapsable structure.
  *
  * @param {nsIDOMNode} node
  *        The container element for the tree widget.
  * @param {Object} options
  *        - emptyText {string}: text to display when no entries in the table.
  *        - defaultType {string}: The default type of the tree items. For ex.
- *        'js'
+ *          'js'
  *        - sorted {boolean}: Defaults to true. If true, tree items are kept in
- *        lexical order. If false, items will be kept in insertion order.
+ *          lexical order. If false, items will be kept in insertion order.
+ *        - contextMenuId {string}: ID of context menu to be displayed on
+ *          tree items.
  */
 function TreeWidget(node, options = {}) {
   EventEmitter.decorate(this);
 
   this.document = node.ownerDocument;
   this.window = this.document.defaultView;
   this._parent = node;
 
   this.emptyText = options.emptyText || "";
   this.defaultType = options.defaultType;
   this.sorted = options.sorted !== false;
+  this.contextMenuId = options.contextMenuId;
 
   this.setupRoot();
 
   this.placeholder = this.document.createElementNS(HTML_NS, "label");
   this.placeholder.className = "tree-widget-empty-text";
   this._parent.appendChild(this.placeholder);
 
   if (this.emptyText) {
@@ -48,40 +51,41 @@ function TreeWidget(node, options = {}) 
 TreeWidget.prototype = {
 
   _selectedLabel: null,
   _selectedItem: null,
 
   /**
    * Select any node in the tree.
    *
-   * @param {array} id
+   * @param {array} ids
    *        An array of ids leading upto the selected item
    */
-  set selectedItem(id) {
+  set selectedItem(ids) {
     if (this._selectedLabel) {
       this._selectedLabel.classList.remove("theme-selected");
     }
     let currentSelected = this._selectedLabel;
-    if (id == -1) {
+    if (ids == -1) {
       this._selectedLabel = this._selectedItem = null;
       return;
     }
-    if (!Array.isArray(id)) {
+    if (!Array.isArray(ids)) {
       return;
     }
-    this._selectedLabel = this.root.setSelectedItem(id);
+    this._selectedLabel = this.root.setSelectedItem(ids);
     if (!this._selectedLabel) {
       this._selectedItem = null;
     } else {
       if (currentSelected != this._selectedLabel) {
         this.ensureSelectedVisible();
       }
-      this._selectedItem =
-      JSON.parse(this._selectedLabel.parentNode.getAttribute("data-id"));
+      this._selectedItem = ids;
+      this.emit("select", this._selectedItem,
+        this.attachments.get(JSON.stringify(ids)));
     }
   },
 
   /**
    * Gets the selected item in the tree.
    *
    * @return {array}
    *        An array of ids leading upto the selected item
@@ -115,19 +119,26 @@ TreeWidget.prototype = {
     this.root = null;
   },
 
   /**
    * Sets up the root container of the TreeWidget.
    */
   setupRoot: function() {
     this.root = new TreeItem(this.document);
+    if (this.contextMenuId) {
+      this.root.children.addEventListener("contextmenu", (event) => {
+        let menu = this.document.getElementById(this.contextMenuId);
+        menu.openPopupAtScreen(event.screenX, event.screenY, true);
+      });
+    }
+
     this._parent.appendChild(this.root.children);
 
-    this.root.children.addEventListener("click", e => this.onClick(e));
+    this.root.children.addEventListener("mousedown", e => this.onClick(e));
     this.root.children.addEventListener("keypress", e => this.onKeypress(e));
   },
 
   /**
    * Sets the text to be shown when no node is present in the tree
    */
   setPlaceholderText: function(text) {
     this.placeholder.textContent = text;
@@ -310,39 +321,34 @@ TreeWidget.prototype = {
       if (target == this.root.children) {
         return;
       }
       target = target.parentNode;
     }
     if (!target) {
       return;
     }
+
     if (target.hasAttribute("expanded")) {
       target.removeAttribute("expanded");
     } else {
       target.setAttribute("expanded", "true");
     }
-    if (this._selectedLabel) {
-      this._selectedLabel.classList.remove("theme-selected");
-    }
+
     if (this._selectedLabel != target) {
       let ids = target.parentNode.getAttribute("data-id");
-      this._selectedItem = JSON.parse(ids);
-      this.emit("select", this._selectedItem, this.attachments.get(ids));
-      this._selectedLabel = target;
+      this.selectedItem = JSON.parse(ids);
     }
-    target.classList.add("theme-selected");
   },
 
   /**
    * Keypress handler for this tree. Used to select next and previous visible
    * items, as well as collapsing and expanding any item.
    */
   onKeypress: function(event) {
-    let currentSelected = this._selectedLabel;
     switch (event.keyCode) {
       case event.DOM_VK_UP:
         this.selectPreviousItem();
         break;
 
       case event.DOM_VK_DOWN:
         this.selectNextItem();
         break;
@@ -362,21 +368,16 @@ TreeWidget.prototype = {
         } else {
           this.selectPreviousItem();
         }
         break;
 
       default: return;
     }
     event.preventDefault();
-    if (this._selectedLabel != currentSelected) {
-      let ids = JSON.stringify(this._selectedItem);
-      this.emit("select", this._selectedItem, this.attachments.get(ids));
-      this.ensureSelectedVisible();
-    }
   },
 
   /**
    * Scrolls the viewport of the tree so that the selected item is always
    * visible.
    */
   ensureSelectedVisible: function() {
     let {top, bottom} = this._selectedLabel.getBoundingClientRect();
--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -18,18 +18,25 @@
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"/>
   <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
 
   <commandset id="editMenuCommands"/>
 
   <popupset id="storagePopupSet">
+    <menupopup id="storage-tree-popup">
+      <menuitem id="storage-tree-popup-delete-all"
+                label="&storage.popupMenu.deleteAllLabel;"/>
+    </menupopup>
     <menupopup id="storage-table-popup">
       <menuitem id="storage-table-popup-delete"/>
+      <menuitem id="storage-table-popup-delete-all-from"/>
+      <menuitem id="storage-table-popup-delete-all"
+                label="&storage.popupMenu.deleteAllLabel;"/>
     </menupopup>
   </popupset>
 
   <box flex="1" class="devtools-responsive-container theme-body">
     <vbox id="storage-tree"/>
     <splitter class="devtools-side-splitter"/>
     <vbox flex="1">
       <hbox id="storage-toolbar" class="devtools-toolbar">
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -1,29 +1,34 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
+  storage-cache-error.html
   storage-complex-values.html
   storage-cookies.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_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_cache_error.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+// Test handling errors in CacheStorage
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cache-error.html");
+
+  const cacheItemId = ["Cache", "javascript:parent.frameContent"];
+
+  gUI.tree.selectedItem = cacheItemId;
+  ok(gUI.tree.isSelected(cacheItemId),
+    `The item ${cacheItemId.join(" > ")} is present in the tree`);
+
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+// Test deleting all cookies
+
+function* performDelete(store, rowName, deleteAll) {
+  let contextMenu = gPanelWindow.document.getElementById(
+    "storage-table-popup");
+  let menuDeleteAllItem = contextMenu.querySelector(
+    "#storage-table-popup-delete-all");
+  let menuDeleteAllFromItem = contextMenu.querySelector(
+    "#storage-table-popup-delete-all-from");
+
+  let storeName = store.join(" > ");
+
+  yield selectTreeItem(store);
+
+  let eventWait = gUI.once("store-objects-updated");
+
+  let cells = getRowCells(rowName);
+  yield waitForContextMenu(contextMenu, cells.name, () => {
+    info(`Opened context menu in ${storeName}, row '${rowName}'`);
+    if (deleteAll) {
+      menuDeleteAllItem.click();
+    } else {
+      menuDeleteAllFromItem.click();
+      let hostName = cells.host.value;
+      ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName),
+        `Context menu item label contains '${hostName}'`);
+    }
+  });
+
+  yield eventWait;
+}
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+  info("test state before delete");
+  yield checkState([
+    [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
+    [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+  ]);
+
+  info("delete all from domain");
+  // delete only cookies that match the host exactly
+  yield performDelete(["cookies", "test1.example.org"], "c1", false);
+
+  info("test state after delete all from domain");
+  yield checkState([
+    // Domain cookies (.example.org) must not be deleted.
+    [["cookies", "test1.example.org"], ["cs2", "uc1"]],
+    [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+  ]);
+
+  info("delete all");
+  // delete all cookies for host, including domain cookies
+  yield performDelete(["cookies", "sectest1.example.org"], "uc1", true);
+
+  info("test state after delete all");
+  yield checkState([
+    // Domain cookies (.example.org) are deleted too, so deleting in sectest1
+    // also removes stuff from test1.
+    [["cookies", "test1.example.org"], []],
+    [["cookies", "sectest1.example.org"], []],
+  ]);
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/browser_storage_delete.js
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -23,27 +23,28 @@ add_task(function* () {
   let contextMenu = gPanelWindow.document.getElementById("storage-table-popup");
   let menuDeleteItem = contextMenu.querySelector("#storage-table-popup-delete");
 
   for (let [ [store, host], rowName, cellToClick] of TEST_CASES) {
     info(`Selecting tree item ${store} > ${host}`);
     yield selectTreeItem([store, host]);
 
     let row = getRowCells(rowName);
-
     ok(gUI.table.items.has(rowName),
       `There is a row '${rowName}' in ${store} > ${host}`);
 
+    let eventWait = gUI.once("store-objects-updated");
+
     yield waitForContextMenu(contextMenu, row[cellToClick], () => {
       info(`Opened context menu in ${store} > ${host}, row '${rowName}'`);
       menuDeleteItem.click();
       ok(menuDeleteItem.getAttribute("label").includes(rowName),
         `Context menu item label contains '${rowName}'`);
     });
 
-    yield gUI.once("store-objects-updated");
+    yield eventWait;
 
     ok(!gUI.table.items.has(rowName),
       `There is no row '${rowName}' in ${store} > ${host} after deletion`);
   }
 
   yield finishTests();
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -0,0 +1,79 @@
+/* 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/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+// Test deleting all storage items
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+  let contextMenu = gPanelWindow.document.getElementById("storage-table-popup");
+  let menuDeleteAllItem = contextMenu.querySelector(
+    "#storage-table-popup-delete-all");
+
+  info("test state before delete");
+  const beforeState = [
+    [["localStorage", "http://test1.example.org"],
+      ["ls1", "ls2"]],
+    [["localStorage", "http://sectest1.example.org"],
+      ["iframe-u-ls1"]],
+    [["localStorage", "https://sectest1.example.org"],
+      ["iframe-s-ls1"]],
+    [["sessionStorage", "http://test1.example.org"],
+      ["ss1"]],
+    [["sessionStorage", "http://sectest1.example.org"],
+      ["iframe-u-ss1", "iframe-u-ss2"]],
+    [["sessionStorage", "https://sectest1.example.org"],
+      ["iframe-s-ss1"]],
+  ];
+
+  yield checkState(beforeState);
+
+  info("do the delete");
+  const deleteHosts = [
+    [["localStorage", "https://sectest1.example.org"], "iframe-s-ls1"],
+    [["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1"],
+  ];
+
+  for (let [store, rowName] of deleteHosts) {
+    let storeName = store.join(" > ");
+
+    yield selectTreeItem(store);
+
+    let eventWait = gUI.once("store-objects-cleared");
+
+    let cell = getRowCells(rowName).name;
+    yield waitForContextMenu(contextMenu, cell, () => {
+      info(`Opened context menu in ${storeName}, row '${rowName}'`);
+      menuDeleteAllItem.click();
+    });
+
+    yield eventWait;
+  }
+
+  info("test state after delete");
+  const afterState = [
+    // iframes from the same host, one secure, one unsecure, are independent
+    // from each other. Delete all in one doesn't touch the other one.
+    [["localStorage", "http://test1.example.org"],
+      ["ls1", "ls2"]],
+    [["localStorage", "http://sectest1.example.org"],
+      ["iframe-u-ls1"]],
+    [["localStorage", "https://sectest1.example.org"],
+      []],
+    [["sessionStorage", "http://test1.example.org"],
+      ["ss1"]],
+    [["sessionStorage", "http://sectest1.example.org"],
+      ["iframe-u-ss1", "iframe-u-ss2"]],
+    [["sessionStorage", "https://sectest1.example.org"],
+      []],
+  ];
+
+  yield checkState(afterState);
+
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+// Test deleting all storage items from the tree.
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+  let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup");
+  let menuDeleteAllItem = contextMenu.querySelector(
+    "#storage-tree-popup-delete-all");
+
+  info("test state before delete");
+  yield checkState([
+    [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
+    [["localStorage", "http://test1.example.org"], ["ls1", "ls2"]],
+    [["sessionStorage", "http://test1.example.org"], ["ss1"]],
+  ]);
+
+  info("do the delete");
+  const deleteHosts = [
+    ["cookies", "test1.example.org"],
+    ["localStorage", "http://test1.example.org"],
+    ["sessionStorage", "http://test1.example.org"],
+  ];
+
+  for (let store of deleteHosts) {
+    let storeName = store.join(" > ");
+
+    yield selectTreeItem(store);
+
+    let eventName = "store-objects-" +
+      (store[0] == "cookies" ? "updated" : "cleared");
+    let eventWait = gUI.once(eventName);
+
+    let selector = `[data-id='${JSON.stringify(store)}'] > .tree-widget-item`;
+    let target = gPanelWindow.document.querySelector(selector);
+    ok(target, `tree item found in ${storeName}`);
+    yield waitForContextMenu(contextMenu, target, () => {
+      info(`Opened tree context menu in ${storeName}`);
+      menuDeleteAllItem.click();
+    });
+
+    yield eventWait;
+  }
+
+  info("test state after delete");
+  yield checkState([
+    [["cookies", "test1.example.org"], []],
+    [["localStorage", "http://test1.example.org"], []],
+    [["sessionStorage", "http://test1.example.org"], []],
+  ]);
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -504,27 +504,23 @@ function matchVariablesViewProperty(prop
 
 /**
  * Click selects a row in the table.
  *
  * @param {[String]} ids
  *        The array id of the item in the tree
  */
 function* selectTreeItem(ids) {
-  // Expand tree as some/all items could be collapsed leading to click on an
-  // incorrect tree item
-  gUI.tree.expandAll();
-
-  let selector = "[data-id='" + JSON.stringify(ids) + "'] > .tree-widget-item";
-  let target = gPanelWindow.document.querySelector(selector);
-  ok(target, "tree item found with ids " + JSON.stringify(ids));
+  /* If this item is already selected, return */
+  if (gUI.tree.isSelected(ids)) {
+    return;
+  }
 
   let updated = gUI.once("store-objects-updated");
-
-  yield click(target);
+  gUI.tree.selectedItem = ids;
   yield updated;
 }
 
 /**
  * Click selects a row in the table.
  *
  * @param {String} id
  *        The id of the row in the table widget
@@ -840,13 +836,40 @@ function waitForContextMenu(popup, butto
     onHidden && onHidden();
 
     deferred.resolve(popup);
   }
 
   popup.addEventListener("popupshown", onPopupShown);
 
   info("wait for the context menu to open");
+  button.scrollIntoView();
   let eventDetails = {type: "contextmenu", button: 2};
   EventUtils.synthesizeMouse(button, 2, 2, eventDetails,
                              button.ownerDocument.defaultView);
   return deferred.promise;
 }
+
+/**
+ * Verify the storage inspector state: check that given type/host exists
+ * in the tree, and that the table contains rows with specified names.
+ *
+ * @param {Array} state Array of state specifications. For example,
+ *        [["cookies", "example.com"], ["c1", "c2"]] means to select the
+ *        "example.com" host in cookies and then verify there are "c1" and "c2"
+ *        cookies (and no other ones).
+ */
+function* checkState(state) {
+  for (let [store, names] of state) {
+    let storeName = store.join(" > ");
+    info(`Selecting tree item ${storeName}`);
+    yield selectTreeItem(store);
+
+    let items = gUI.table.items;
+
+    is(items.size, names.length,
+      `There is correct number of rows in ${storeName}`);
+    for (let name of names) {
+      ok(items.has(name),
+        `There is item with name '${name}' in ${storeName}`);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/storage-cache-error.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Storage inspector test for handling errors in CacheStorage</title>
+</head>
+<body>
+<script type="application/javascript;version=1.7">
+"use strict";
+
+// Create an iframe with a javascript: source URL. Such iframes are
+// considered untrusted by the CacheStorage.
+let frameEl = document.createElement("iframe");
+document.body.appendChild(frameEl);
+
+window.frameContent = 'Hello World';
+frameEl.contentWindow.location.href = "javascript:parent.frameContent";
+</script>
+</body>
+</html>
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -65,17 +65,20 @@ var StorageUI = this.StorageUI = functio
   EventEmitter.decorate(this);
 
   this._target = target;
   this._window = panelWin;
   this._panelDoc = panelWin.document;
   this.front = front;
 
   let treeNode = this._panelDoc.getElementById("storage-tree");
-  this.tree = new TreeWidget(treeNode, {defaultType: "dir"});
+  this.tree = new TreeWidget(treeNode, {
+    defaultType: "dir",
+    contextMenuId: "storage-tree-popup"
+  });
   this.onHostSelect = this.onHostSelect.bind(this);
   this.tree.on("select", this.onHostSelect);
 
   let tableNode = this._panelDoc.getElementById("storage-table");
   this.table = new TableWidget(tableNode, {
     emptyText: L10N.getStr("table.emptyText"),
     highlightUpdated: true,
     cellContextMenuId: "storage-table-popup"
@@ -106,32 +109,51 @@ var StorageUI = this.StorageUI = functio
   this.onUpdate = this.onUpdate.bind(this);
   this.front.on("stores-update", this.onUpdate);
   this.onCleared = this.onCleared.bind(this);
   this.front.on("stores-cleared", this.onCleared);
 
   this.handleKeypress = this.handleKeypress.bind(this);
   this._panelDoc.addEventListener("keypress", this.handleKeypress);
 
-  this.onPopupShowing = this.onPopupShowing.bind(this);
+  this.onTreePopupShowing = this.onTreePopupShowing.bind(this);
+  this._treePopup = this._panelDoc.getElementById("storage-tree-popup");
+  this._treePopup.addEventListener("popupshowing", this.onTreePopupShowing);
+
+  this.onTablePopupShowing = this.onTablePopupShowing.bind(this);
   this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
-  this._tablePopup.addEventListener("popupshowing", this.onPopupShowing, false);
+  this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing);
 
   this.onRemoveItem = this.onRemoveItem.bind(this);
+  this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this);
+  this.onRemoveAll = this.onRemoveAll.bind(this);
+
   this._tablePopupDelete = this._panelDoc.getElementById(
     "storage-table-popup-delete");
-  this._tablePopupDelete.addEventListener("command", this.onRemoveItem, false);
+  this._tablePopupDelete.addEventListener("command", this.onRemoveItem);
+
+  this._tablePopupDeleteAllFrom = this._panelDoc.getElementById(
+    "storage-table-popup-delete-all-from");
+  this._tablePopupDeleteAllFrom.addEventListener("command",
+    this.onRemoveAllFrom);
+
+  this._tablePopupDeleteAll = this._panelDoc.getElementById(
+    "storage-table-popup-delete-all");
+  this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll);
+
+  this._treePopupDeleteAll = this._panelDoc.getElementById(
+    "storage-tree-popup-delete-all");
+  this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll);
 };
 
 exports.StorageUI = StorageUI;
 
 StorageUI.prototype = {
 
   storageTypes: null,
-  shouldResetColumns: true,
   shouldLoadMoreItems: true,
 
   set animationsEnabled(value) {
     this._panelDoc.documentElement.classList.toggle("no-animate", !value);
   },
 
   destroy: function() {
     this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
@@ -140,18 +162,26 @@ StorageUI.prototype = {
     this.table.destroy();
 
     this.front.off("stores-update", this.onUpdate);
     this.front.off("stores-cleared", this.onCleared);
     this._panelDoc.removeEventListener("keypress", this.handleKeypress);
     this.searchBox.removeEventListener("input", this.filterItems);
     this.searchBox = null;
 
-    this._tablePopup.removeEventListener("popupshowing", this.onPopupShowing);
+    this._treePopup.removeEventListener("popupshowing",
+      this.onTreePopupShowing);
+    this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
+
+    this._tablePopup.removeEventListener("popupshowing",
+      this.onTablePopupShowing);
     this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
+    this._tablePopupDeleteAllFrom.removeEventListener("command",
+      this.onRemoveAllFrom);
+    this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
   },
 
   /**
    * Empties and hides the object viewer sidebar
    */
   hideSidebar: function() {
     this.view.empty();
     this.sidebar.hidden = true;
@@ -386,20 +416,20 @@ StorageUI.prototype = {
       throw new Error("Invalid reason specified");
     }
 
     storageType.getStoreObjects(host, names, fetchOpts).then(({data}) => {
       if (!data.length) {
         this.emit("store-objects-updated");
         return;
       }
-      if (this.shouldResetColumns) {
+      if (reason === REASON.POPULATE) {
         this.resetColumns(data[0], type);
+        this.table.host = host;
       }
-      this.table.host = host;
       this.populateTable(data, reason);
       this.emit("store-objects-updated");
 
       this.makeFieldsEditable();
     }, Cu.reportError);
   },
 
   /**
@@ -432,25 +462,23 @@ StorageUI.prototype = {
       for (let host in storageTypes[type].hosts) {
         this.tree.add([type, {id: host, type: "url"}]);
         for (let name of storageTypes[type].hosts[host]) {
           try {
             let names = JSON.parse(name);
             this.tree.add([type, host, ...names]);
             if (!this.tree.selectedItem) {
               this.tree.selectedItem = [type, host, names[0], names[1]];
-              this.fetchStorageObjects(type, host, [name], REASON.POPULATE);
             }
           } catch (ex) {
             // Do Nothing
           }
         }
         if (!this.tree.selectedItem) {
           this.tree.selectedItem = [type, host];
-          this.fetchStorageObjects(type, host, null, REASON.POPULATE);
         }
       }
     }
   },
 
   /**
    * Populates the selected entry from teh table in the sidebar for a more
    * detailed view.
@@ -620,17 +648,16 @@ StorageUI.prototype = {
     let [type, host] = item;
     let names = null;
     if (!host) {
       return;
     }
     if (item.length > 2) {
       names = [JSON.stringify(item.slice(2))];
     }
-    this.shouldResetColumns = true;
     this.fetchStorageObjects(type, host, names, REASON.POPULATE);
     this.itemOffset = 0;
   },
 
   /**
    * Resets the column headers in the storage table with the pased object `data`
    *
    * @param {object} data
@@ -652,17 +679,16 @@ StorageUI.prototype = {
         columns[key] = L10N.getStr("table.headers." + type + "." + key);
       } catch (e) {
         console.error("Unable to localize table header type:" + type +
                       " key:" + key);
       }
     }
     this.table.setColumns(columns, null, HIDDEN_COLUMNS);
     this.table.datatype = type;
-    this.shouldResetColumns = false;
     this.hideSidebar();
   },
 
   /**
    * Populates or updates the rows in the storage table.
    *
    * @param {array[object]} data
    *        Array of objects to be populated in the storage table
@@ -752,39 +778,96 @@ StorageUI.prototype = {
     this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS);
   },
 
   /**
    * Fires before a cell context menu with the "Delete" action is shown.
    * If the current storage actor doesn't support removing items, prevent
    * showing the menu.
    */
-  onPopupShowing: function(event) {
+  onTablePopupShowing: function(event) {
     if (!this.getCurrentActor().removeItem) {
       event.preventDefault();
       return;
     }
 
+    const maxLen = ITEM_NAME_MAX_LENGTH;
+    let [type] = this.tree.selectedItem;
     let rowId = this.table.contextMenuRowId;
     let data = this.table.items.get(rowId);
     let name = data[this.table.uniqueId];
 
-    const maxLen = ITEM_NAME_MAX_LENGTH;
     if (name.length > maxLen) {
       name = name.substr(0, maxLen) + L10N.ellipsis;
     }
 
     this._tablePopupDelete.setAttribute("label",
       L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
+
+    if (type === "cookies") {
+      let host = data.host;
+      if (host.length > maxLen) {
+        host = host.substr(0, maxLen) + L10N.ellipsis;
+      }
+
+      this._tablePopupDeleteAllFrom.hidden = false;
+      this._tablePopupDeleteAllFrom.setAttribute("label",
+        L10N.getFormatStr("storage.popupMenu.deleteAllFromLabel", host));
+    } else {
+      this._tablePopupDeleteAllFrom.hidden = true;
+    }
+  },
+
+  onTreePopupShowing: function(event) {
+    let showMenu = false;
+    let selectedItem = this.tree.selectedItem;
+    // Never show menu on the 1st level item
+    if (selectedItem && selectedItem.length > 1) {
+      // this.currentActor() would return wrong value here
+      let actor = this.storageTypes[selectedItem[0]];
+      if (actor.removeAll) {
+        showMenu = true;
+      }
+    }
+
+    if (!showMenu) {
+      event.preventDefault();
+    }
   },
 
   /**
    * Handles removing an item from the storage
    */
   onRemoveItem: function() {
     let [, host] = this.tree.selectedItem;
     let actor = this.getCurrentActor();
     let rowId = this.table.contextMenuRowId;
     let data = this.table.items.get(rowId);
 
     actor.removeItem(host, data[this.table.uniqueId]);
   },
+
+  /**
+   * Handles removing all items from the storage
+   */
+  onRemoveAll: function() {
+    // Cannot use this.currentActor() if the handler is called from the
+    // tree context menu: it returns correct value only after the table
+    // data from server are successfully fetched (and that's async).
+    let [type, host] = this.tree.selectedItem;
+    let actor = this.storageTypes[type];
+
+    actor.removeAll(host);
+  },
+
+  /**
+   * Handles removing all cookies with exactly the same domain as the
+   * cookie in the selected row.
+   */
+  onRemoveAllFrom: function() {
+    let [, host] = this.tree.selectedItem;
+    let actor = this.getCurrentActor();
+    let rowId = this.table.contextMenuRowId;
+    let data = this.table.items.get(rowId);
+
+    actor.removeAll(host, data.host);
+  },
 };
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -128,16 +128,22 @@ body {
 }
 
 #global-toolbar .label,
 #timeline-toolbar .label {
   padding: 0 5px;
   border-style: solid;
 }
 
+#global-toolbar .devtools-button,
+#timeline-toolbar .devtools-button {
+  margin: 0;
+  padding: 0;
+}
+
 #timeline-toolbar .devtools-button,
 #timeline-toolbar .label {
   border-width: 0 1px 0 0;
 }
 
 #element-picker::before {
   background-image: url("chrome://devtools/skin/images/command-pick.svg");
 }
@@ -163,17 +169,17 @@ body {
     background-image: url("images/debugger-play@2x.png");
   }
 
   #rewind-timeline::before {
     background-image: url("images/rewind@2x.png");
   }
 }
 
-#timeline-rate select {
+#timeline-rate select.devtools-button {
   -moz-appearance: none;
   text-align: center;
   font-family: inherit;
   color: var(--theme-body-color);
   font-size: 1em;
   position: absolute;
   top: 0;
   left: 0;
--- a/devtools/client/themes/inspector.css
+++ b/devtools/client/themes/inspector.css
@@ -1,22 +1,13 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-
-:root[platform="mac"] #inspector-toolbar:-moz-locale-dir(ltr) {
-  padding-left: 2px;
-}
-
-:root[platform="mac"] #inspector-toolbar:-moz-locale-dir(rtl) {
-  padding-left: 4px;
-}
-
 #inspector-searchlabel {
   overflow: hidden;
 }
 
 #inspector-breadcrumbs-toolbar {
   padding: 0px;
   border-bottom-width: 0px;
   border-top-width: 1px;
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -110,27 +110,18 @@ html, body, #app, #memory-tool {
 #clear-snapshots::before {
   background-image: url(chrome://devtools/skin/images/clear.svg);
 }
 
 #diff-snapshots::before {
   background-image: url(chrome://devtools/skin/images/diff.svg);
 }
 
-/**
- * Due to toolbar styles of `.devtools-toolbarbutton:not([label])` which overrides
- * .devtools-toolbarbutton's min-width of 78px, reset the min-width.
- */
-#import-snapshot,
-#clear-snapshots {
-  -moz-box-align: center;
+#import-snapshot {
   flex-grow: 1;
-  padding: 1px;
-  margin: 2px 1px;
-  min-width: unset;
 }
 
 .spacer {
   flex: 1;
 }
 
 #filter {
   align-self: stretch;
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -68,36 +68,37 @@
   transition: background 0.05s ease-in-out;
 }
 
 .devtools-menulist,
 .devtools-toolbarbutton {
   -moz-box-align: center;
   min-width: 78px;
   padding: 1px;
-  margin: 2px 3px;
+  margin: 2px 1px;
 }
 
 .devtools-menulist:-moz-focusring,
-.devtools-toolbarbutton:-moz-focusring {
+.devtools-toolbarbutton:-moz-focusring,
+.devtools-button:-moz-focusring {
   outline: 1px dotted hsla(210,30%,85%,0.7);
-  outline-offset: -4px;
+  outline-offset: -1px;
 }
 
 .devtools-toolbarbutton:not([label]) > .toolbarbutton-icon,
 .devtools-button::before {
   width: 16px;
   height: 16px;
   transition: opacity 0.05s ease-in-out;
 }
 
 /* HTML buttons */
 .devtools-button {
-  margin: 0;
-  padding: 0;
+  margin: 2px 1px;
+  padding: 1px;
   min-width: 32px;
   /* The icon is absolutely positioned in the button using ::before */
   position: relative;
 }
 
 .devtools-button::before {
   content: "";
   display: block;
@@ -293,24 +294,16 @@
 .devtools-toolbarbutton.devtools-clear-icon {
   list-style-image: var(--clear-icon-url);
 }
 
 .devtools-option-toolbarbutton {
   list-style-image: url("chrome://devtools/skin/images/tool-options.svg");
 }
 
-/* Toolbar button groups */
-.devtools-toolbarbutton-group > .devtools-toolbarbutton {
-  margin-left: 1px;
-  margin-right: 1px;
-  outline-offset: -3px;
-  box-shadow: none;
-}
-
 .devtools-toolbarbutton-group > .devtools-toolbarbutton:last-child {
   -moz-margin-end: 0;
 }
 
 .devtools-toolbarbutton-group + .devtools-toolbarbutton {
   -moz-margin-start: 3px;
 }
 
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -8,17 +8,17 @@
 
 const { Cc, Ci } = require("chrome");
 const { BreakpointActor, setBreakpointAtEntryPoints } = require("devtools/server/actors/breakpoint");
 const { OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
 const { createValueGrip } = require("devtools/server/actors/object");
 const { ActorClass, Arg, RetVal, method } = require("devtools/server/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, fetch } = DevToolsUtils;
-const { dirname, joinURI } = require("devtools/shared/path");
+const { joinURI } = require("devtools/shared/path");
 const promise = require("promise");
 const { defer, resolve, reject, all } = promise;
 
 loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
 loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
 loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
 
 function isEvalSource(source) {
@@ -47,18 +47,17 @@ function getSourceURL(source, window) {
         if (window) {
           // If this is a named eval script created from the console, make it
           // relative to the current page. window is only available
           // when we care about this.
           return joinURI(window.location.href, source.displayURL);
         }
       }
       else {
-        return joinURI(dirname(source.introductionScript.source.url),
-                       source.displayURL);
+        return joinURI(source.introductionScript.source.url, source.displayURL);
       }
     }
 
     return source.displayURL;
   }
   else if (source.url === 'debugger eval code') {
     // Treat code evaluated by the console as unnamed eval scripts
     return null;
@@ -856,9 +855,8 @@ let SourceActor = ActorClass({
       return false;
     }
     setBreakpointAtEntryPoints(actor, entryPoints);
     return true;
   }
 });
 
 exports.SourceActor = SourceActor;
-
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -347,17 +347,19 @@ StorageActors.defaults = function(typeNa
         data: []
       };
 
       let principal = null;
       if (this.typeName === "indexedDB") {
         // We only acquire principal when the type of the storage is indexedDB
         // because the principal only matters the indexedDB.
         let win = this.storageActor.getWindowFromHost(host);
-        principal = win.document.nodePrincipal;
+        if (win) {
+          principal = win.document.nodePrincipal;
+        }
       }
 
       if (names) {
         for (let name of names) {
           let values =
             yield this.getValuesForHost(host, name, options, this.hostVsStores, principal);
 
           let {result, objectStores} = values;
@@ -681,31 +683,48 @@ StorageActors.createActor({
     },
     response: {}
   }),
 
   removeItem: method(Task.async(function*(host, name) {
     this.removeCookie(host, name);
   }), {
     request: {
-      host: Arg(0),
-      name: Arg(1),
+      host: Arg(0, "string"),
+      name: Arg(1, "string"),
+    },
+    response: {}
+  }),
+
+  removeAll: method(Task.async(function*(host, domain) {
+    this.removeAllCookies(host, domain);
+  }), {
+    request: {
+      host: Arg(0, "string"),
+      domain: Arg(1, "nullable:string")
     },
     response: {}
   }),
 
   maybeSetupChildProcess: function() {
     cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this);
 
     if (!DebuggerServer.isInChildProcess) {
-      this.getCookiesFromHost = cookieHelpers.getCookiesFromHost;
-      this.addCookieObservers = cookieHelpers.addCookieObservers;
-      this.removeCookieObservers = cookieHelpers.removeCookieObservers;
-      this.editCookie = cookieHelpers.editCookie;
-      this.removeCookie = cookieHelpers.removeCookie;
+      this.getCookiesFromHost =
+        cookieHelpers.getCookiesFromHost.bind(cookieHelpers);
+      this.addCookieObservers =
+        cookieHelpers.addCookieObservers.bind(cookieHelpers);
+      this.removeCookieObservers =
+        cookieHelpers.removeCookieObservers.bind(cookieHelpers);
+      this.editCookie =
+        cookieHelpers.editCookie.bind(cookieHelpers);
+      this.removeCookie =
+        cookieHelpers.removeCookie.bind(cookieHelpers);
+      this.removeAllCookies =
+        cookieHelpers.removeAllCookies.bind(cookieHelpers);
       return;
     }
 
     const { sendSyncMessage, addMessageListener } =
       this.conn.parentMessageManager;
 
     this.conn.setupInParent({
       module: "devtools/server/actors/storage",
@@ -717,16 +736,18 @@ StorageActors.createActor({
     this.addCookieObservers =
       callParentProcess.bind(null, "addCookieObservers");
     this.removeCookieObservers =
       callParentProcess.bind(null, "removeCookieObservers");
     this.editCookie =
       callParentProcess.bind(null, "editCookie");
     this.removeCookie =
       callParentProcess.bind(null, "removeCookie");
+    this.removeAllCookies =
+      callParentProcess.bind(null, "removeAllCookies");
 
     addMessageListener("storage:storage-cookie-request-child",
                        cookieHelpers.handleParentRequest);
 
     function callParentProcess(methodName, ...args) {
       let reply = sendSyncMessage("storage:storage-cookie-request-parent", {
         method: methodName,
         args: args
@@ -870,42 +891,54 @@ var cookieHelpers = {
       cookie.value,
       cookie.isSecure,
       cookie.isHttpOnly,
       cookie.isSession,
       cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires
     );
   },
 
-  removeCookie: function(host, name) {
+  _removeCookies: function(host, opts = {}) {
     function hostMatches(cookieHost, matchHost) {
       if (cookieHost == null) {
         return matchHost == null;
       }
       if (cookieHost.startsWith(".")) {
         return matchHost.endsWith(cookieHost);
       }
       return cookieHost == host;
     }
 
     let enumerator = Services.cookies.getCookiesFromHost(host);
     while (enumerator.hasMoreElements()) {
       let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
-      if (hostMatches(cookie.host, host) && cookie.name === name) {
+      if (hostMatches(cookie.host, host) &&
+          (!opts.name || cookie.name === opts.name) &&
+          (!opts.domain || cookie.host === opts.domain)) {
         Services.cookies.remove(
           cookie.host,
           cookie.name,
           cookie.path,
           false,
           cookie.originAttributes
         );
       }
     }
   },
 
+  removeCookie: function(host, name) {
+    if (name !== undefined) {
+      this._removeCookies(host, { name });
+    }
+  },
+
+  removeAllCookies: function(host, domain) {
+    this._removeCookies(host, { domain });
+  },
+
   addCookieObservers: function() {
     Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
     return null;
   },
 
   removeCookieObservers: function() {
     Services.obs.removeObserver(cookieHelpers, "cookie-changed", false);
     return null;
@@ -964,16 +997,21 @@ var cookieHelpers = {
         let rowdata = msg.data.args[0];
         return cookieHelpers.editCookie(rowdata);
       }
       case "removeCookie": {
         let host = msg.data.args[0];
         let name = msg.data.args[1];
         return cookieHelpers.removeCookie(host, name);
       }
+      case "removeAllCookies": {
+        let host = msg.data.args[0];
+        let domain = msg.data.args[1];
+        return cookieHelpers.removeAllCookies(host, domain);
+      }
       default:
         console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
         throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
     }
   },
 };
 
 /**
@@ -1175,16 +1213,26 @@ function getObjectForLocalOrSessionStora
       storage.removeItem(name);
     }), {
       request: {
         host: Arg(0),
         name: Arg(1),
       },
       response: {}
     }),
+
+    removeAll: method(Task.async(function*(host) {
+      let storage = this.hostVsStores.get(host);
+      storage.clear();
+    }), {
+      request: {
+        host: Arg(0)
+      },
+      response: {}
+    }),
   };
 }
 
 /**
  * The Local Storage actor and front.
  */
 StorageActors.createActor({
   typeName: "localStorage",
@@ -1294,18 +1342,22 @@ StorageActors.createActor({
       return location.href;
     }
     return location.protocol + "//" + location.host;
   },
 
   populateStoresForHost: Task.async(function*(host) {
     let storeMap = new Map();
     let caches = yield this.getCachesForHost(host);
-    for (let name of (yield caches.keys())) {
-      storeMap.set(name, (yield caches.open(name)));
+    try {
+      for (let name of (yield caches.keys())) {
+        storeMap.set(name, (yield caches.open(name)));
+      }
+    } catch (ex) {
+      console.error(`Failed to enumerate CacheStorage for host ${host}:`, ex);
     }
     this.hostVsStores.set(host, storeMap);
   }),
 
   /**
    * This method is overriden and left blank as for Cache Storage, this
    * operation cannot be performed synchronously. Thus, the preListStores
    * method exists to do the same task asynchronously.
@@ -1555,23 +1607,25 @@ StorageActors.createActor({
       yield this.populateStoresForHost(host);
     }
   }),
 
   populateStoresForHost: Task.async(function*(host) {
     let storeMap = new Map();
     let {names} = yield this.getDBNamesForHost(host);
     let win = this.storageActor.getWindowFromHost(host);
-    let principal = win.document.nodePrincipal;
+    if (win) {
+      let principal = win.document.nodePrincipal;
 
-    for (let name of names) {
-      let metadata = yield this.getDBMetaData(host, principal, name);
+      for (let name of names) {
+        let metadata = yield this.getDBMetaData(host, principal, name);
 
-      metadata = indexedDBHelpers.patchMetadataMapsAndProtos(metadata);
-      storeMap.set(name, metadata);
+        metadata = indexedDBHelpers.patchMetadataMapsAndProtos(metadata);
+        storeMap.set(name, metadata);
+      }
     }
 
     this.hostVsStores.set(host, storeMap);
   }),
 
   /**
    * Returns the over-the-wire implementation of the indexed db entity.
    */
@@ -2073,17 +2127,17 @@ var StorageActor = exports.StorageActor 
       type: "storesUpdate",
       data: Arg(0, "storeUpdateObject")
     },
     "stores-cleared": {
       type: "storesCleared",
       data: Arg(0, "json")
     },
     "stores-reloaded": {
-      type: "storesRelaoded",
+      type: "storesReloaded",
       data: Arg(0, "json")
     }
   },
 
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, null);
 
     this.conn = conn;
@@ -2305,21 +2359,23 @@ var StorageActor = exports.StorageActor 
     }
     if (!this.boundUpdate[action]) {
       this.boundUpdate[action] = {};
     }
     if (!this.boundUpdate[action][storeType]) {
       this.boundUpdate[action][storeType] = {};
     }
     for (let host in data) {
-      if (!this.boundUpdate[action][storeType][host] || action == "deleted") {
-        this.boundUpdate[action][storeType][host] = data[host];
-      } else {
-        this.boundUpdate[action][storeType][host] =
-        this.boundUpdate[action][storeType][host].concat(data[host]);
+      if (!this.boundUpdate[action][storeType][host]) {
+        this.boundUpdate[action][storeType][host] = [];
+      }
+      for (let name of data[host]) {
+        if (!this.boundUpdate[action][storeType][host].includes(name)) {
+          this.boundUpdate[action][storeType][host].push(name);
+        }
       }
     }
     if (action == "added") {
       // If the same store name was previously deleted or changed, but now is
       // added somehow, dont send the deleted or changed update.
       this.removeNamesFromUpdateList("deleted", storeType, data);
       this.removeNamesFromUpdateList("changed", storeType, data);
     } else if (action == "changed" && this.boundUpdate.added &&
--- a/devtools/server/actors/utils/TabSources.js
+++ b/devtools/server/actors/utils/TabSources.js
@@ -5,16 +5,17 @@
 "use strict";
 
 const { Ci, Cu } = require("chrome");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, fetch } = DevToolsUtils;
 const EventEmitter = require("devtools/shared/event-emitter");
 const { OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
 const { resolve } = require("promise");
+const { joinURI } = require("devtools/shared/path");
 const URL = require("URL");
 
 loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/source", true);
 loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/source", true);
 loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
 loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
 
 /**
@@ -400,17 +401,17 @@ TabSources.prototype = {
       return this._sourceMaps.get(aSource);
     }
     else if (!aSource || !aSource.sourceMapURL) {
       return resolve(null);
     }
 
     let sourceMapURL = aSource.sourceMapURL;
     if (aSource.url) {
-      sourceMapURL = this._normalize(sourceMapURL, aSource.url);
+      sourceMapURL = joinURI(aSource.url, sourceMapURL);
     }
     let result = this._fetchSourceMap(sourceMapURL, aSource.url);
 
     // The promises in `_sourceMaps` must be the exact same instances
     // as returned by `_fetchSourceMap` for `clearSourceMapCache` to
     // work.
     this._sourceMaps.set(aSource, result);
     return result;
@@ -480,17 +481,17 @@ TabSources.prototype = {
       return;
     }
 
     const base = this._dirname(
       aAbsSourceMapURL.indexOf("data:") === 0
         ? aScriptURL
         : aAbsSourceMapURL);
     aSourceMap.sourceRoot = aSourceMap.sourceRoot
-      ? this._normalize(aSourceMap.sourceRoot, base)
+      ? joinURI(base, aSourceMap.sourceRoot)
       : base;
   },
 
   _dirname: function (aPath) {
     let url = new URL(aPath);
     let href = url.href;
     return href.slice(0, href.lastIndexOf("/"));
   },
@@ -782,29 +783,16 @@ TabSources.prototype = {
    *
    * @param aURL String
    *        The URL of the source that is no longer pretty printed.
    */
   disablePrettyPrint: function (aURL) {
     this.prettyPrintedSources.delete(aURL);
   },
 
-  /**
-   * Normalize multiple relative paths towards the base paths on the right.
-   */
-  _normalize: function (...aURLs) {
-    assert(aURLs.length > 1, "Should have more than 1 URL");
-    let base = new URL(aURLs.pop());
-    let url;
-    while ((url = aURLs.pop())) {
-      base = new URL(url, base);
-    }
-    return base.href;
-  },
-
   iter: function () {
     let actors = Object.keys(this._sourceMappedSourceActors).map(k => {
       return this._sourceMappedSourceActors[k];
     });
     for (let actor of this._sourceActors.values()) {
       if (!this._sourceMaps.has(actor.source)) {
         actors.push(actor);
       }
--- a/devtools/shared/path.js
+++ b/devtools/shared/path.js
@@ -2,23 +2,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const URL = require("URL");
 
 /*
- * Returns the directory name of the path
- */
-exports.dirname = path => {
-  return new URL(".", new URL(path)).href;
-}
-
-/*
  * Join all the arguments together and normalize the resulting URI.
  * The initial path must be an full URI with a protocol (i.e. http://).
  */
 exports.joinURI = (initialPath, ...paths) => {
   let url;
 
   try {
     url = new URL(initialPath);