Merge fx-team to central, a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Fri, 12 Aug 2016 14:37:49 -0700
changeset 309281 2ed7e61b988d2466a61528f66050596ef272ebda
parent 309280 ef3a50774180e1ccc1b6d210295db8275739b92e (current diff)
parent 309258 124669ed2f09f68f7f8dd38f4fee5f3084da0aff (diff)
child 309282 c385cb6a0838b9e5593fc2d81a6f44fc709548e1
child 309302 6e191a55c3d23e83e6a2e72e4e80c1dc21516493
child 309372 204eae54f875f73c337013c1f3b1bf9de7c5d14f
child 309373 1ba6215e84c3b34b928898331c3d444b3dd96ee3
push id31321
push userkwierso@gmail.com
push dateFri, 12 Aug 2016 23:30:18 +0000
treeherderautoland@c385cb6a0838 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone51.0a1
first release with
nightly linux32
2ed7e61b988d / 51.0a1 / 20160813030202 / files
nightly linux64
2ed7e61b988d / 51.0a1 / 20160813030202 / files
nightly mac
2ed7e61b988d / 51.0a1 / 20160813030202 / files
nightly win32
2ed7e61b988d / 51.0a1 / 20160813030202 / files
nightly win64
2ed7e61b988d / 51.0a1 / 20160813030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge CLOSED TREE
devtools/shared/webconsole/network-monitor.js
toolkit/components/telemetry/Histograms.json
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7193,16 +7193,18 @@ var gIdentityHandler = {
     // Now open the popup, anchored off the primary chrome element
     this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft");
   },
 
   onPopupShown(event) {
     if (event.target == this._identityPopup) {
       window.addEventListener("focus", this, true);
     }
+    this._identityPopupMultiView._mainView.style.height =
+      this._identityPopup.getBoundingClientRect().height + "px";
   },
 
   onPopupHidden(event) {
     if (event.target == this._identityPopup) {
       window.removeEventListener("focus", this, true);
       this._identityBox.removeAttribute("open");
     }
   },
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -147,17 +147,17 @@ const PanelUI = {
         let anchor;
         if (!aEvent ||
             aEvent.type == "command") {
           anchor = this.menuButton;
         } else {
           anchor = aEvent.target;
         }
 
-        this.panel.addEventListener("popupshown", function onPopupShown() {
+        this.panel.addEventListener("popupshown", function onPopupShown(event) {
           this.removeEventListener("popupshown", onPopupShown);
           resolve();
         });
 
         let iconAnchor =
           document.getAnonymousElementByAttribute(anchor, "class",
                                                   "toolbarbutton-icon");
         this.panel.openPopup(iconAnchor || anchor);
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -53,43 +53,26 @@
       <field name="_panel" readonly="true">
         this.parentNode;
       </field>
 
       <field name="_currentSubView">null</field>
       <field name="_anchorElement">null</field>
       <field name="_mainViewHeight">0</field>
       <field name="_subViewObserver">null</field>
-      <field name="__transitioning">false</field>
-      <field name="_ignoreMutations">false</field>
+      <field name="__transitioning">true</field>
 
       <property name="showingSubView" readonly="true"
                 onget="return this._viewStack.getAttribute('viewtype') == 'subview'"/>
       <property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/>
       <property name="_mainView" readonly="true"
                 onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/>
       <property name="showingSubViewAsMainView" readonly="true"
                 onget="return this.getAttribute('mainViewIsSubView') == 'true'"/>
 
-      <property name="ignoreMutations">
-        <getter>
-          return this._ignoreMutations;
-        </getter>
-        <setter><![CDATA[
-          this._ignoreMutations = val;
-          if (!val && this._panel.state == "open") {
-            if (this.showingSubView) {
-              this._syncContainerWithSubView();
-            } else {
-              this._syncContainerWithMainView();
-            }
-          }
-        ]]></setter>
-      </property>
-
       <property name="_transitioning">
         <getter>
           return this.__transitioning;
         </getter>
         <setter><![CDATA[
           this.__transitioning = val;
           if (val) {
             this.setAttribute("transitioning", "true");
@@ -218,17 +201,20 @@
       </method>
 
       <method name="_setViewContainerHeight">
         <parameter name="aHeight"/>
         <body><![CDATA[
           let container = this._viewContainer;
           this._transitioning = true;
 
-          let onTransitionEnd = () => {
+          let onTransitionEnd = (event) => {
+            if (event.propertyName != "transform") {
+              return;
+            }
             container.removeEventListener("transitionend", onTransitionEnd);
             this._transitioning = false;
           };
 
           container.addEventListener("transitionend", onTransitionEnd);
           container.style.height = `${aHeight}px`;
         ]]></body>
       </method>
@@ -291,35 +277,43 @@
                 if (this.showingSubView) {
                   setTimeout(this._syncContainerWithSubView.bind(this), 0);
                 } else if (!this.transitioning) {
                   setTimeout(this._syncContainerWithMainView.bind(this), 0);
                 }
               }
               break;
             case "popupshowing":
-              this.setAttribute("panelopen", "true");
               // Bug 941196 - The panel can get taller when opening a subview. Disabling
               // autoPositioning means that the panel won't jump around if an opened
               // subview causes the panel to exceed the dimensions of the screen in the
               // direction that the panel originally opened in. This property resets
               // every time the popup closes, which is why we have to set it each time.
               this._panel.autoPosition = false;
               this._syncContainerWithMainView();
 
               this._mainViewObserver.observe(this._mainView, {
                 attributes: true,
                 characterData: true,
                 childList: true,
                 subtree: true
               });
 
-              break;
-            case "popupshown":
-              this._setMaxHeight();
+              let onTransitionEnd = (event) => {
+                if (event.propertyName != "transform") {
+                  return;
+                }
+                let panel = event.target;
+                panel.removeEventListener("tranitionend", onTransitionEnd);
+                // Needed in case the panel is closed before the transition ends.
+                if (panel.state == "open") {
+                  this.setAttribute("panelopen", "true");
+                }
+              };
+              this._panel.addEventListener("transitionend", onTransitionEnd);
               break;
             case "popuphidden":
               this.removeAttribute("panelopen");
               this._mainView.style.removeProperty("height");
               this.showMainView();
               this._mainViewObserver.disconnect();
               break;
           }
@@ -333,32 +327,19 @@
       </method>
 
       <method name="_shouldSetHeight">
         <body><![CDATA[
           return this.getAttribute("nosubviews") != "true";
         ]]></body>
       </method>
 
-      <method name="_setMaxHeight">
-        <body><![CDATA[
-          if (!this._shouldSetHeight())
-            return;
-
-          // Ignore the mutation that'll fire when we set the height of
-          // the main view.
-          this.ignoreMutations = true;
-          this._mainView.style.height =
-            this.getBoundingClientRect().height + "px";
-          this.ignoreMutations = false;
-        ]]></body>
-      </method>
       <method name="_adjustContainerHeight">
         <body><![CDATA[
-          if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
+          if (!this.showingSubView && !this._transitioning) {
             let height;
             if (this.showingSubViewAsMainView) {
               height = this._heightOfSubview(this._mainView);
             } else {
               height = this._mainView.scrollHeight;
             }
             this._viewContainer.style.height = height + "px";
           }
@@ -366,17 +347,17 @@
       </method>
       <method name="_syncContainerWithSubView">
         <body><![CDATA[
           // Check that this panel is still alive:
           if (!this._panel || !this._panel.parentNode) {
             return;
           }
 
-          if (!this.ignoreMutations && this.showingSubView) {
+          if (this.showingSubView) {
             let newHeight = this._heightOfSubview(this._currentSubView, this._subViews);
             this._viewContainer.style.height = newHeight + "px";
           }
         ]]></body>
       </method>
       <method name="_syncContainerWithMainView">
         <body><![CDATA[
           // Check that this panel is still alive:
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -144,12 +144,13 @@ skip-if = os == "linux" # crashing on Li
 [browser_1087303_button_fullscreen.js]
 tags = fullscreen
 skip-if = os == "mac"
 [browser_1087303_button_preferences.js]
 [browser_1089591_still_customizable_after_reset.js]
 [browser_1096763_seen_widgets_post_reset.js]
 [browser_1161838_inserted_new_default_buttons.js]
 [browser_bootstrapped_custom_toolbar.js]
+[browser_check_tooltips_in_navbar.js]
 [browser_customizemode_contextmenu_menubuttonstate.js]
+[browser_no_mutationrecords_during_panel_opening.js]
 [browser_panel_toggle.js]
 [browser_switch_to_customize_mode.js]
-[browser_check_tooltips_in_navbar.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_no_mutationrecords_during_panel_opening.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test that we don't get unexpected mutations during the opening of the
+ * browser menu.
+ */
+
+add_task(function* test_setup() {
+  yield resetCustomization();
+  yield PanelUI.show();
+  let hiddenPromise = promisePanelHidden(window);
+  PanelUI.hide();
+  yield hiddenPromise;
+});
+
+add_task(function* no_mutation_events_during_opening() {
+  let panel = PanelUI.panel;
+  yield PanelUI.ensureReady();
+
+  let failures = 0;
+  let observer = new MutationObserver(function(mutations) {
+    for (let mutation of mutations) {
+      if (mutation.target.localName == "panel" &&
+          mutation.type == "attributes" &&
+          mutation.attributeName == "animate") {
+        // This mutation is allowed because it triggers the CSS transition.
+        continue;
+      }
+
+      if (mutation.type == "attributes" &&
+          mutation.attributeName == "panelopen") {
+        // This mutation is allowed because it is set after the panel has
+        // finished the transition.
+        continue;
+      }
+
+      let newValue = null;
+      if (mutation.type == "attributes") {
+        newValue = mutation.target.getAttribute(mutation.attributeName);
+      } else if (mutation.type == "characterData") {
+        newValue = mutation.target.textContent;
+      }
+
+      if (AppConstants.isPlatformAndVersionAtMost("win", "6.1") &&
+          mutation.target.className == "panel-arrowbox" &&
+          mutation.attributeName == "style" &&
+          newValue.startsWith("transform:")) {
+        // Windows 7 and earlier has an alignment offset on the arrowbox.
+        // This is allowed here as it is no longer used on newer platforms.
+        continue;
+      }
+
+      if (newValue == mutation.oldValue) {
+        // Mutations records are observed even when the new and old value are
+        // identical. This is unlikely to invalidate the panel, so ignore these.
+        continue;
+      }
+
+      let nodeIdentifier = `${mutation.target.localName}#${mutation.target.id}.${mutation.target.className};`;
+      ok(false, `Observed: ${mutation.type}; ${nodeIdentifier} ${mutation.attributeName}; oldValue: ${mutation.oldValue}; newValue: ${newValue}`);
+      failures++;
+    }
+  });
+  observer.observe(panel, {
+    childList: true,
+    attributes: true,
+    characterData: true,
+    subtree: true,
+    attributeOldValue: true,
+    characterDataOldValue: true,
+  });
+  let shownPromise = promisePanelShown(window);
+  PanelUI.show();
+  yield shownPromise;
+  observer.disconnect();
+
+  is(failures, 0, "There should be no unexpected mutation events during opening of the panel");
+});
+
+add_task(function* cleanup() {
+  let hiddenPromise = promisePanelHidden(window);
+  PanelUI.hide();
+  yield hiddenPromise;
+});
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
@@ -170,27 +170,33 @@ function* testPopupSize(standardsMode, b
 
   let {panel} = browserWin.PanelUI;
   let origPanelRect = panel.getBoundingClientRect();
 
   // Check that the panel is still positioned as expected.
   let checkPanelPosition = () => {
     is(panel.getAttribute("side"), arrowSide, "Panel arrow is positioned as expected");
 
+    function isGreaterThanOrWithinPixelRoundingError(a, b, message) {
+      let result = a + 1 >= b;
+      ok(result, `${a} should be greater than or within one pixel of ${b}: ${message}`);
+    }
+
     let panelRect = panel.getBoundingClientRect();
     if (arrowSide == "top") {
-      ok(panelRect.top, origPanelRect.top, "Panel has not moved downwards");
-      ok(panelRect.bottom >= origPanelRect.bottom, `Panel has not shrunk from original size (${panelRect.bottom} >= ${origPanelRect.bottom})`);
+      isGreaterThanOrWithinPixelRoundingError(panelRect.top, origPanelRect.top, "Panel has not moved downwards");
+      isGreaterThanOrWithinPixelRoundingError(panelRect.bottom, origPanelRect.bottom, "Panel has not shrunk from original size");
 
       let screenBottom = browserWin.screen.availTop + win.screen.availHeight;
       let panelBottom = browserWin.mozInnerScreenY + panelRect.bottom;
       ok(panelBottom <= screenBottom, `Bottom of popup should be on-screen. (${panelBottom} <= ${screenBottom})`);
     } else {
-      ok(panelRect.bottom, origPanelRect.bottom, "Panel has not moved upwards");
-      ok(panelRect.top <= origPanelRect.top, `Panel has not shrunk from original size (${panelRect.top} <= ${origPanelRect.top})`);
+      isGreaterThanOrWithinPixelRoundingError(panelRect.bottom, origPanelRect.bottom, "Panel has not moved upwards");
+      // The arguments here are reversed compared to the above calls due to the coordinate system.
+      isGreaterThanOrWithinPixelRoundingError(origPanelRect.top, panelRect.top, "Panel has not shrunk from original size");
 
       let panelTop = browserWin.mozInnerScreenY + panelRect.top;
       ok(panelTop >= browserWin.screen.availTop, `Top of popup should be on-screen. (${panelTop} >= ${browserWin.screen.availTop})`);
     }
   };
 
 
   let isStandards = win.document.compatMode != "BackCompat";
--- a/browser/components/search/content/searchReset.js
+++ b/browser/components/search/content/searchReset.js
@@ -7,51 +7,31 @@
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 const TELEMETRY_RESULT_ENUM = {
   RESTORED_DEFAULT: 0,
   KEPT_CURRENT: 1,
   CHANGED_ENGINE: 2,
-  CLOSED_PAGE: 3
+  CLOSED_PAGE: 3,
+  OPENED_SETTINGS: 4
 };
 
 window.onload = function() {
-  let list = document.getElementById("defaultEngine");
-  let originalDefault = Services.search.originalDefaultEngine.name;
-  Services.search.getDefaultEngines().forEach(e => {
-    let opt = document.createElement("option");
-    opt.setAttribute("value", e.name);
-    opt.engine = e;
-    opt.textContent = e.name;
-    if (e.iconURI)
-      opt.style.backgroundImage = 'url("' + e.iconURI.spec + '")';
-    if (e.name == originalDefault)
-      opt.setAttribute("selected", "true");
-    list.appendChild(opt);
-  });
-
-  let updateIcon = () => {
-    list.style.setProperty("--engine-icon-url",
-                           list.selectedOptions[0].style.backgroundImage);
-  };
-
-  list.addEventListener("change", updateIcon);
-  // When selecting using the keyboard, the 'change' event is only fired after
-  // the user presses <enter> or moves the focus elsewhere.
-  // keypress/keyup fire too late and cause flicker when updating the icon.
-  // keydown fires too early and the selected option isn't changed yet.
-  list.addEventListener("keydown", () => {
-    Services.tm.mainThread.dispatch(updateIcon, Ci.nsIThread.DISPATCH_NORMAL);
-  });
-  updateIcon();
+  let defaultEngine = document.getElementById("defaultEngine");
+  let originalDefault = Services.search.originalDefaultEngine;
+  defaultEngine.textContent = originalDefault.name;
+  defaultEngine.style.backgroundImage =
+    'url("' + originalDefault.iconURI.spec + '")';
 
   document.getElementById("searchResetChangeEngine").focus();
   window.addEventListener("unload", recordPageClosed);
+  document.getElementById("linkSettingsPage")
+          .addEventListener("click", openingSettings);
 };
 
 function doSearch() {
   let queryString = "";
   let purpose = "";
   let params = window.location.href.match(/^about:searchreset\?([^#]*)/);
   if (params) {
     params = params[1].split("&");
@@ -72,40 +52,39 @@ function doSearch() {
                   .getInterface(Ci.nsIWebNavigation)
                   .QueryInterface(Ci.nsIDocShellTreeItem)
                   .rootTreeItem
                   .QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindow);
   win.openUILinkIn(submission.uri.spec, "current", false, submission.postData);
 }
 
+function openingSettings() {
+  record(TELEMETRY_RESULT_ENUM.OPENED_SETTINGS);
+  window.removeEventListener("unload", recordPageClosed);
+}
+
 function record(result) {
   Services.telemetry.getHistogramById("SEARCH_RESET_RESULT").add(result);
 }
 
 function keepCurrentEngine() {
   // Calling the currentEngine setter will force a correct loadPathHash to be
   // written for this engine, so that we don't prompt the user again.
   Services.search.currentEngine = Services.search.currentEngine;
   record(TELEMETRY_RESULT_ENUM.KEPT_CURRENT);
   doSearch();
 }
 
 function changeSearchEngine() {
-  let list = document.getElementById("defaultEngine");
-  let engine = list.selectedOptions[0].engine;
+  let engine = Services.search.originalDefaultEngine;
   if (engine.hidden)
     engine.hidden = false;
   Services.search.currentEngine = engine;
 
-  // Record if we restored the original default or changed to another engine.
-  let originalDefault = Services.search.originalDefaultEngine.name;
-  let code = TELEMETRY_RESULT_ENUM.CHANGED_ENGINE;
-  if (Services.search.originalDefaultEngine.name == engine.name)
-    code = TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT;
-  record(code);
+  record(TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT);
 
   doSearch();
 }
 
 function recordPageClosed() {
   record(TELEMETRY_RESULT_ENUM.CLOSED_PAGE);
 }
--- a/browser/components/search/content/searchReset.xhtml
+++ b/browser/components/search/content/searchReset.xhtml
@@ -34,19 +34,17 @@
 
     <div class="container">
       <div class="title">
         <h1 class="title-text">&searchreset.pageTitle;</h1>
       </div>
 
       <div class="description">
         <p>&searchreset.pageInfo1;</p>
-        <p>&searchreset.selector.label;
-          <select id="defaultEngine"></select>
-        </p>
+        <p>&searchreset.selector.label;<span id="defaultEngine"/></p>
 
         <p>&searchreset.beforelink.pageInfo2;<a id="linkSettingsPage" href="about:preferences#search">&searchreset.link.pageInfo2;</a>&searchreset.afterlink.pageInfo2;</p>
       </div>
 
       <div class="button-container">
         <xul:button id="searchResetKeepCurrent"
                     label="&searchreset.noChangeButton;"
                     accesskey="&searchreset.noChangeButton.access;"
--- a/browser/components/search/test/browser_aboutSearchReset.js
+++ b/browser/components/search/test/browser_aboutSearchReset.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 const TELEMETRY_RESULT_ENUM = {
   RESTORED_DEFAULT: 0,
   KEPT_CURRENT: 1,
   CHANGED_ENGINE: 2,
-  CLOSED_PAGE: 3
+  CLOSED_PAGE: 3,
+  OPENED_SETTINGS: 4
 };
 
 const kSearchStr = "a search";
 const kSearchPurpose = "searchbar";
 
 const kTestEngine = "testEngine.xml";
 
 function checkTelemetryRecords(expectedValue) {
@@ -72,72 +73,49 @@ var gTests = [
   }
 },
 
 {
   desc: "Test the 'Restore Search Defaults' button.",
   run: function* () {
     let currentEngine = Services.search.currentEngine;
     let originalEngine = Services.search.originalDefaultEngine;
+    let doc = gBrowser.contentDocument;
+    let defaultEngineSpan = doc.getElementById("defaultEngine");
+    is(defaultEngineSpan.textContent, originalEngine.name,
+       "the name of the original default engine is displayed");
+
     let expectedURL = originalEngine.
                       getSubmission(kSearchStr, null, kSearchPurpose).
                       uri.spec;
-
     let loadPromise = promiseStoppedLoad(expectedURL);
-    let doc = gBrowser.contentDocument;
     let button = doc.getElementById("searchResetChangeEngine");
     is(doc.activeElement, button,
        "the 'Change Search Engine' button is focused");
     button.click();
     yield loadPromise;
 
     is(originalEngine, Services.search.currentEngine,
        "the default engine is back to the original one");
 
     checkTelemetryRecords(TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT);
     Services.search.currentEngine = currentEngine;
   }
 },
 
 {
-  desc: "Test the engine selector drop down.",
+  desc: "Click the settings link.",
   run: function* () {
-    let originalEngineName = Services.search.originalDefaultEngine.name;
-
-    let doc = gBrowser.contentDocument;
-    let list = doc.getElementById("defaultEngine");
-    is(list.value, originalEngineName,
-       "the default selection of the dropdown is the original default engine");
-
-    let defaultEngines = Services.search.getDefaultEngines();
-    is(list.childNodes.length, defaultEngines.length,
-       "the dropdown has the correct count of engines");
-
-    // Select an engine that isn't the original default one.
-    let engine;
-    for (let i = 0; i < defaultEngines.length; ++i) {
-      if (defaultEngines[i].name != originalEngineName) {
-        engine = defaultEngines[i];
-        engine.hidden = true;
-        break;
-      }
-    }
-    list.value = engine.name;
-
-    let expectedURL = engine.getSubmission(kSearchStr, null, kSearchPurpose)
-                            .uri.spec;
-    let loadPromise = promiseStoppedLoad(expectedURL);
-    doc.getElementById("searchResetChangeEngine").click();
+    let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser,
+                                                     false,
+                                                     "about:preferences#search")
+    gBrowser.contentDocument.getElementById("linkSettingsPage").click();
     yield loadPromise;
 
-    ok(!engine.hidden, "the selected engine has been unhidden");
-    is(engine, Services.search.currentEngine,
-       "the current engine is what was selected in the drop down");
-
-    checkTelemetryRecords(TELEMETRY_RESULT_ENUM.CHANGED_ENGINE);
+    checkTelemetryRecords(TELEMETRY_RESULT_ENUM.OPENED_SETTINGS);
   }
 },
 
 {
   desc: "Load another page without clicking any of the buttons.",
   run: function* () {
     yield promiseTabLoadEvent(gBrowser.selectedTab, "about:mozilla");
 
--- a/browser/themes/shared/searchReset.css
+++ b/browser/themes/shared/searchReset.css
@@ -5,40 +5,18 @@
 body {
   align-items: center;
 }
 
 .title {
   background-image: url("chrome://browser/skin/icon-search-64.svg");
 }
 
-select {
-  font: inherit;
-  padding-inline-end: 24px;
+#defaultEngine {
   padding-inline-start: 26px;
-  background-image: var(--engine-icon-url),
-                    url("chrome://global/skin/in-content/dropdown.svg#dropdown");
   background-repeat: no-repeat;
-  background-position: 8px center, calc(100% - 4px) center;
+  background-position: 5px center;
   background-size: 16px, 16px;
 }
 
-select:-moz-dir(rtl) {
-  background-position: calc(100% - 8px) center, 4px center;
-}
-
-select:-moz-focusring {
-  color: transparent;
-  text-shadow: 0 0 0 var(--in-content-text-color);
+#defaultEngine:-moz-dir(rtl) {
+  background-position: calc(100% - 5px) center;
 }
-
-option {
-  padding: 4px;
-  padding-inline-start: 30px;
-  background-repeat: no-repeat;
-  background-position: 8px center;
-  background-size: 16px;
-  background-color: var(--in-content-page-background);
-}
-
-option:-moz-dir(rtl) {
-  background-position: calc(100% - 8px) center;
-}
--- a/browser/tools/mozscreenshots/browser.ini
+++ b/browser/tools/mozscreenshots/browser.ini
@@ -1,8 +1,12 @@
 [DEFAULT]
 subsuite = screenshots
 support-files =
   head.js
   mozscreenshots/extension/lib/permissionPrompts.html
+  mozscreenshots/extension/lib/controlCenter/password.html
+  mozscreenshots/extension/lib/controlCenter/mixed.html
+  mozscreenshots/extension/lib/controlCenter/mixed_active.html
+  mozscreenshots/extension/lib/controlCenter/mixed_passive.html
   mozscreenshots/extension/lib/borderify.xpi
 
 [browser_screenshots.js]
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/controlCenter/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+  ../head.js
+
+[browser_controlCenter.js]
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/controlCenter/browser_controlCenter.js
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(function* capture() {
+  if (!shouldCapture()) {
+    return;
+  }
+  let sets = ["LightweightThemes", "ControlCenter"];
+
+  yield TestRunner.start(sets, "controlCenter");
+});
--- a/browser/tools/mozscreenshots/moz.build
+++ b/browser/tools/mozscreenshots/moz.build
@@ -3,16 +3,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += [
     # Each test is in it's own directory so it gets run in a clean profile with
     # run-by-dir.
     'browser.ini',
+    'controlCenter/browser.ini',
     'devtools/browser.ini',
     'permissionPrompts/browser.ini',
     'preferences/browser.ini',
     'primaryUI/browser.ini',
 ]
 
 TEST_DIRS += [
     'mozscreenshots/extension',
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ControlCenter"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://testing-common/BrowserTestUtils.jsm");
+Cu.import("resource:///modules/SitePermissions.jsm");
+
+let {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+const RESOURCE_PATH = "extensions/mozscreenshots/browser/chrome/mozscreenshots/lib/controlCenter";
+const HTTP_PAGE = "http://example.com/";
+const HTTPS_PAGE = "https://example.com/";
+const PERMISSIONS_PAGE = "https://test1.example.com/";
+const HTTP_PASSWORD_PAGE = `http://test2.example.org/${RESOURCE_PATH}/password.html`;
+const MIXED_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed.html`;
+const MIXED_ACTIVE_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed_active.html`;
+const MIXED_PASSIVE_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed_passive.html`;
+const TRACKING_PAGE = `http://tracking.example.org/${RESOURCE_PATH}/tracking.html`;
+
+this.ControlCenter = {
+  init(libDir) { },
+
+  configurations: {
+    about: {
+      applyConfig: Task.async(function* () {
+        yield loadPage("about:home");
+        yield openIdentityPopup();
+      }),
+    },
+
+    localFile: {
+      applyConfig: Task.async(function* () {
+        let filePath = "file:///";
+        if (Services.appinfo.OS === "WINNT") {
+          filePath += "C:/";
+        }
+        yield loadPage(filePath);
+        yield openIdentityPopup();
+      }),
+    },
+
+    http: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(HTTP_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+
+    httpSubView: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(HTTP_PAGE);
+        yield openIdentityPopup(true);
+      }),
+    },
+
+    https: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(HTTPS_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+
+    httpsSubView: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(HTTPS_PAGE);
+        yield openIdentityPopup(true);
+      }),
+    },
+
+    singlePermission: {
+      applyConfig: Task.async(function* () {
+        let uri = Services.io.newURI(PERMISSIONS_PAGE, null, null)
+        SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
+
+        yield loadPage(PERMISSIONS_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+
+    allPermissions: {
+      applyConfig: Task.async(function* () {
+        // there are 3 possible non-default permission states, so we alternate between them
+        let states = [SitePermissions.ALLOW, SitePermissions.BLOCK, SitePermissions.SESSION];
+        let uri = Services.io.newURI(PERMISSIONS_PAGE, null, null)
+        SitePermissions.listPermissions().forEach(function (permission, index) {
+          SitePermissions.set(uri, permission, states[index % 3]);
+        });
+
+        yield loadPage(PERMISSIONS_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+
+    mixed: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(MIXED_CONTENT_URL);
+        yield openIdentityPopup();
+      }),
+    },
+
+    mixedSubView: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(MIXED_CONTENT_URL);
+        yield openIdentityPopup(true);
+      }),
+    },
+
+    mixedPassive: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(MIXED_PASSIVE_CONTENT_URL);
+        yield openIdentityPopup();
+      }),
+    },
+
+    mixedPassiveSubView: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(MIXED_PASSIVE_CONTENT_URL);
+        yield openIdentityPopup(true);
+      }),
+    },
+
+    mixedActive: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(MIXED_ACTIVE_CONTENT_URL);
+        yield openIdentityPopup();
+      }),
+    },
+
+    mixedActiveSubView: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(MIXED_ACTIVE_CONTENT_URL);
+        yield openIdentityPopup(true);
+      }),
+    },
+
+    mixedActiveUnblocked: {
+      applyConfig: Task.async(function* () {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let gBrowser = browserWindow.gBrowser;
+        yield loadPage(MIXED_ACTIVE_CONTENT_URL);
+        gBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, MIXED_ACTIVE_CONTENT_URL);
+        yield openIdentityPopup();
+      }),
+    },
+
+    mixedActiveUnblockedSubView: {
+      applyConfig: Task.async(function* () {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let gBrowser = browserWindow.gBrowser;
+        yield loadPage(MIXED_ACTIVE_CONTENT_URL);
+        gBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, MIXED_ACTIVE_CONTENT_URL);
+        yield openIdentityPopup(true);
+      }),
+    },
+
+    httpPassword: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(HTTP_PASSWORD_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+
+    httpPasswordSubView: {
+      applyConfig: Task.async(function* () {
+        yield loadPage(HTTP_PASSWORD_PAGE);
+        yield openIdentityPopup(true);
+      }),
+    },
+
+    trackingProtectionNoElements: {
+      applyConfig: Task.async(function* () {
+        Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+
+        yield loadPage(HTTP_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+
+    trackingProtectionEnabled: {
+      applyConfig: Task.async(function* () {
+        Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+        Services.prefs.setIntPref("privacy.trackingprotection.introCount", 20);
+        yield UrlClassifierTestUtils.addTestTrackers();
+
+        yield loadPage(TRACKING_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+
+    trackingProtectionDisabled: {
+      applyConfig: Task.async(function* () {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let gBrowser = browserWindow.gBrowser;
+        Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+        Services.prefs.setIntPref("privacy.trackingprotection.introCount", 20);
+        yield UrlClassifierTestUtils.addTestTrackers();
+
+        yield loadPage(TRACKING_PAGE);
+        yield openIdentityPopup();
+        // unblock the page
+        gBrowser.ownerGlobal.document.querySelector("#tracking-action-unblock").click();
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, TRACKING_PAGE);
+        yield openIdentityPopup();
+      }),
+    },
+  },
+};
+
+function* loadPage(url) {
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  let gBrowser = browserWindow.gBrowser;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, url);
+}
+
+function* openIdentityPopup(expand) {
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  let gBrowser = browserWindow.gBrowser;
+  let { gIdentityHandler } = gBrowser.ownerGlobal;
+  gIdentityHandler._identityPopup.hidePopup();
+  gIdentityHandler._identityBox.querySelector("#identity-icon").click();
+  if (expand) {
+    // give some time for opening to avoid weird style issues
+    yield new Promise((c) => setTimeout(c, 500));
+    gIdentityHandler._identityPopup.querySelector("#identity-popup-security-expander").click();
+  }
+}
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm
@@ -15,16 +15,17 @@ Cu.import("resource://testing-common/Con
 Cu.import("resource://testing-common/BrowserTestUtils.jsm");
 
 const URL = "https://test1.example.com/extensions/mozscreenshots/browser/chrome/mozscreenshots/lib/permissionPrompts.html";
 let lastTab = null;
 
 this.PermissionPrompts = {
   init(libDir) {
     Services.prefs.setBoolPref("media.navigator.permission.fake", true);
+    Services.prefs.setBoolPref("media.getusermedia.screensharing.allow_on_old_platforms", true);
     Services.prefs.setCharPref("media.getusermedia.screensharing.allowed_domains",
                                "test1.example.com");
     Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false);
   },
 
   configurations: {
     shareDevices: {
       applyConfig: Task.async(function*() {
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>Mixed Content test</title>
+  </head>
+  <body>
+    <iframe style="visibility:hidden" src="http://example.com"></iframe>
+    <img style="visibility:hidden" src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_active.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>Mixed Active Content test</title>
+  </head>
+  <body>
+    <iframe style="visibility:hidden" src="http://example.com"></iframe>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>Mixed Passive Content test</title>
+  </head>
+  <body>
+    <img style="visibility:hidden" src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>HTTP Password test</title>
+  </head>
+  <body>
+    <form>
+      <input type="password" />
+      <button type="submit">Submit</button>
+    </form>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>Tracking test</title>
+  </head>
+  <body>
+    <iframe style="visibility:hidden" src="http://tracking.example.com/"></iframe>
+  </body>
+</html>
--- a/devtools/client/framework/source-map-service.js
+++ b/devtools/client/framework/source-map-service.js
@@ -14,17 +14,16 @@ const { LocationStore, serialize, deseri
  * auto-update when the source changes (from pretty printing, source maps loading, etc)
  *
  * @param {TabTarget} target
  */
 
 function SourceMapService(target) {
   this._target = target;
   this._locationStore = new LocationStore();
-  this._isInitialResolve = true;
 
   EventEmitter.decorate(this);
 
   this._onSourceUpdated = this._onSourceUpdated.bind(this);
   this._resolveLocation = this._resolveLocation.bind(this);
   this._resolveAndUpdate = this._resolveAndUpdate.bind(this);
   this.subscribe = this.subscribe.bind(this);
   this.unsubscribe = this.unsubscribe.bind(this);
@@ -36,72 +35,66 @@ function SourceMapService(target) {
   target.on("will-navigate", this.reset);
   target.on("close", this.destroy);
 }
 
 /**
  * Clears the store containing the cached resolved locations and promises
  */
 SourceMapService.prototype.reset = function () {
-  this._isInitialResolve = true;
   this._locationStore.clear();
 };
 
 SourceMapService.prototype.destroy = function () {
   this.reset();
   this._target.off("source-updated", this._onSourceUpdated);
   this._target.off("navigate", this.reset);
   this._target.off("will-navigate", this.reset);
   this._target.off("close", this.destroy);
-  this._isInitialResolve = null;
   this._target = this._locationStore = null;
 };
 
 /**
  * Sets up listener for the callback to update the FrameView and tries to resolve location
  * @param location
  * @param callback
  */
 SourceMapService.prototype.subscribe = function (location, callback) {
   this.on(serialize(location), callback);
   this._locationStore.set(location);
-  if (this._isInitialResolve) {
-    this._resolveAndUpdate(location);
-    this._isInitialResolve = false;
-  }
+  this._resolveAndUpdate(location);
 };
 
 /**
  * Removes the listener for the location and clears cached locations
  * @param location
  * @param callback
  */
 SourceMapService.prototype.unsubscribe = function (location, callback) {
   this.off(serialize(location), callback);
-  this._locationStore.clearByURL(location.url);
+  // Check to see if the store exists before attempting to clear a location
+  // Sometimes un-subscribe happens during the destruction cascades and this
+  // condition is to protect against that. Could be looked into in the future.
+  if (this._locationStore) {
+    this._locationStore.clearByURL(location.url);
+  }
 };
 
 /**
  * Tries to resolve the location and if successful,
  * emits the resolved location and caches it
  * @param location
  * @private
  */
 SourceMapService.prototype._resolveAndUpdate = function (location) {
   this._resolveLocation(location).then(resolvedLocation => {
-    // We try to source map the first console log to initiate the source-updated event from
-    // target. The isSameLocation check is to make sure we don't update the frame, if the
-    // location is not source-mapped.
-    if (resolvedLocation) {
-      if (this._isInitialResolve) {
-        if (!isSameLocation(location, resolvedLocation)) {
-          this.emit(serialize(location), location, resolvedLocation);
-          return;
-        }
-      }
+    // We try to source map the first console log to initiate the source-updated
+    // event from target. The isSameLocation check is to make sure we don't update
+    // the frame, if the location is not source-mapped.
+    if (resolvedLocation && !isSameLocation(location, resolvedLocation)) {
       this.emit(serialize(location), location, resolvedLocation);
     }
   });
 };
 
 /**
  * Validates the location model,
  * checks if there is existing promise to resolve location, if so returns cached promise
@@ -177,24 +170,23 @@ function resolveLocation(target, locatio
       url: location.url,
       line: location.line,
       column: location.column || Infinity
     });
     // Source or mapping not found, so don't do anything
     if (newLocation.error) {
       return null;
     }
-
     return newLocation;
   });
 }
 
 /**
  * Returns if the original location and resolved location are the same
  * @param location
  * @param resolvedLocation
  * @returns {boolean}
  */
 function isSameLocation(location, resolvedLocation) {
   return location.url === resolvedLocation.url &&
     location.line === resolvedLocation.line &&
     location.column === resolvedLocation.column;
-};
\ No newline at end of file
+}
--- a/devtools/client/framework/test/browser_source_map-01.js
+++ b/devtools/client/framework/test/browser_source_map-01.js
@@ -14,49 +14,48 @@ thisTestLeaksUncaughtRejectionsAndShould
  */
 
 const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
 // Empty page
 const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
 const JS_URL = `${URL_ROOT}code_binary_search.js`;
 const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`;
 const { SourceMapService } = require("devtools/client/framework/source-map-service");
+const { serialize } = require("devtools/client/framework/location-store");
 
 add_task(function* () {
   const toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
-
   const service = new SourceMapService(toolbox.target);
-
-  const aggregator = [];
+  let aggregator = new Map();
 
   function onUpdate(e, oldLoc, newLoc) {
     if (oldLoc.line === 6) {
       checkLoc1(oldLoc, newLoc);
     } else if (oldLoc.line === 8) {
       checkLoc2(oldLoc, newLoc);
-    } else if (oldLoc.line === 2) {
-      checkLoc3(oldLoc, newLoc);
     } else {
       throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
     }
-    aggregator.push(newLoc);
+    aggregator.set(serialize(oldLoc), newLoc);
   }
 
   let loc1 = { url: JS_URL, line: 6 };
   let loc2 = { url: JS_URL, line: 8, column: 3 };
 
   service.subscribe(loc1, onUpdate);
   service.subscribe(loc2, onUpdate);
 
   // Inject JS script
   let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_binary_search");
   yield createScript(JS_URL);
   yield sourceShown;
 
-  yield waitUntil(() => aggregator.length === 2);
+  yield waitUntil(() => aggregator.size === 2);
+
+  aggregator = Array.from(aggregator.values());
 
   ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
   ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
 
   yield toolbox.destroy();
   gBrowser.removeCurrentTab();
   finish();
 });
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -586,17 +586,22 @@ Toolbox.prototype = {
   _addReloadKeys: function (shortcuts) {
     [
       ["reload", false],
       ["reload2", false],
       ["forceReload", true],
       ["forceReload2", true]
     ].forEach(([id, force]) => {
       let key = toolboxStrings("toolbox." + id + ".key");
-      shortcuts.on(key, this.reloadTarget.bind(this, force));
+      shortcuts.on(key, (name, event) => {
+        this.reloadTarget(force);
+
+        // Prevent Firefox shortcuts from reloading the page
+        event.preventDefault();
+      });
     });
   },
 
   _addHostListeners: function (shortcuts) {
     shortcuts.on(toolboxStrings("toolbox.nextTool.key"),
                  this.selectNextTool.bind(this));
     shortcuts.on(toolboxStrings("toolbox.previousTool.key"),
                  this.selectPreviousTool.bind(this));
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -12,17 +12,19 @@ const {Cc, Ci} = require("chrome");
 const {Task} = require("devtools/shared/task");
 const {InplaceEditor, editableItem} =
       require("devtools/client/shared/inplace-editor");
 const {ReflowFront} = require("devtools/shared/fronts/layout");
 const {LocalizationHelper} = require("devtools/client/shared/l10n");
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 
 const STRINGS_URI = "chrome://devtools/locale/shared.properties";
+const STRINGS_INSPECTOR = "chrome://devtools-shared/locale/styleinspector.properties";
 const SHARED_L10N = new LocalizationHelper(STRINGS_URI);
+const INSPECTOR_L10N = new LocalizationHelper(STRINGS_INSPECTOR);
 const NUMERIC = /^-?[\d\.]+$/;
 const LONG_TEXT_ROTATE_LIMIT = 3;
 
 /**
  * An instance of EditingSession tracks changes that have been made during the
  * modification of box model values. All of these changes can be reverted by
  * calling revert. The main parameter is the LayoutView that created it.
  *
@@ -729,18 +731,24 @@ LayoutView.prototype = {
       }
     }
 
     let title = property;
     if (sourceRule && sourceRule.selectors) {
       title += "\n" + sourceRule.selectors.join(", ");
     }
     if (sourceRule && sourceRule.parentStyleSheet) {
-      title += "\n" + sourceRule.parentStyleSheet.href + ":" + sourceRule.line;
+      if (sourceRule.parentStyleSheet.href) {
+        title += "\n" + sourceRule.parentStyleSheet.href + ":" + sourceRule.line;
+      } else {
+        title += "\n" + INSPECTOR_L10N.getStr("rule.sourceInline") +
+          ":" + sourceRule.line;
+      }
     }
+
     el.setAttribute("title", title);
   },
 
   /**
    * Show the box-model highlighter on the currently selected element
    * @param {Object} options Options passed to the highlighter actor
    */
   showBoxModel: function (options = {}) {
--- a/devtools/client/inspector/layout/test/browser_layout_tooltips.js
+++ b/devtools/client/inspector/layout/test/browser_layout_tooltips.js
@@ -23,55 +23,55 @@ const TEST_URI = "<style>" +
 //   - name: the name of the property that is set by the css rule
 //   - ruleSelector: the selector of the rule
 //   - styleSheetLocation: the fileName:lineNumber
 const VALUES_TEST_DATA = [{
   selector: "#div1",
   values: [{
     name: "margin-top",
     ruleSelector: "#div1",
-    styleSheetLocation: "null:1"
+    styleSheetLocation: "inline:1"
   }, {
     name: "margin-right",
     ruleSelector: "#div1",
-    styleSheetLocation: "null:1"
+    styleSheetLocation: "inline:1"
   }, {
     name: "margin-bottom",
     ruleSelector: "#div1",
-    styleSheetLocation: "null:1"
+    styleSheetLocation: "inline:1"
   }, {
     name: "margin-left",
     ruleSelector: "#div1",
-    styleSheetLocation: "null:1"
+    styleSheetLocation: "inline:1"
   }]
 }, {
   selector: "#div2",
   values: [{
     name: "border-bottom-width",
     ruleSelector: "#div2",
-    styleSheetLocation: "null:2"
+    styleSheetLocation: "inline:2"
   }]
 }, {
   selector: "#div3",
   values: [{
     name: "padding-top",
     ruleSelector: "html, body, #div3",
-    styleSheetLocation: "null:3"
+    styleSheetLocation: "inline:3"
   }, {
     name: "padding-right",
     ruleSelector: "html, body, #div3",
-    styleSheetLocation: "null:3"
+    styleSheetLocation: "inline:3"
   }, {
     name: "padding-bottom",
     ruleSelector: "html, body, #div3",
-    styleSheetLocation: "null:3"
+    styleSheetLocation: "inline:3"
   }, {
     name: "padding-left",
     ruleSelector: "html, body, #div3",
-    styleSheetLocation: "null:3"
+    styleSheetLocation: "inline:3"
   }]
 }];
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openLayoutView();
 
   info("Checking the regions tooltips");
--- a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
@@ -9,28 +9,27 @@ const HIGHLIGHTER_TYPE = "EyeDropper";
 const ID = "eye-dropper-";
 const TEST_URI = "data:text/html;charset=utf-8,<style>html{background:red}</style>";
 
 add_task(function* () {
   let helper = yield openInspectorForURL(TEST_URI)
                .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
   helper.prefix = ID;
 
-  let {show, synthesizeKey, finalize} = helper;
+  let {show, finalize} = helper;
 
   info("Show the eyedropper with the copyOnSelect option");
   yield show("html", {copyOnSelect: true});
 
   info("Make sure to wait until the eyedropper is done taking a screenshot of the page");
   yield waitForElementAttributeSet("root", "drawn", helper);
 
   yield waitForClipboard(() => {
     info("Activate the eyedropper so the background color is copied");
-    let generateKey = synthesizeKey({key: "VK_RETURN", options: {}});
-    generateKey.next();
+    EventUtils.synthesizeKey("VK_RETURN", {});
   }, "#FF0000");
 
   ok(true, "The clipboard contains the right value");
 
   yield waitForElementAttributeRemoved("root", "drawn", helper);
   yield waitForElementAttributeSet("root", "hidden", helper);
   ok(true, "The eyedropper is now hidden");
 
--- a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js
@@ -30,42 +30,42 @@ add_task(function* () {
   yield respondsToMoveEvents(helper);
   yield respondsToReturnAndEscape(helper);
 
   helper.finalize();
 });
 
 function* respondsToMoveEvents(helper) {
   info("Checking that the eyedropper responds to events from the mouse and keyboard");
-  let {mouse, synthesizeKey} = helper;
+  let {mouse} = helper;
 
   for (let {type, x, y, key, shift, expected} of MOVE_EVENTS_DATA) {
     info(`Simulating a ${type} event to move to ${expected.x} ${expected.y}`);
     if (type === "mouse") {
       yield mouse.move(x, y);
     } else if (type === "keyboard") {
       let options = shift ? {shiftKey: true} : {};
-      yield synthesizeKey({key, options});
+      yield EventUtils.synthesizeKey(key, options);
     }
     yield checkPosition(expected, helper);
   }
 }
 
 function* checkPosition({x, y}, {getElementAttribute}) {
   let style = yield getElementAttribute("root", "style");
   is(style, `top:${y}px;left:${x}px;`,
      `The eyedropper is at the expected ${x} ${y} position`);
 }
 
-function* respondsToReturnAndEscape({synthesizeKey, isElementHidden, show}) {
+function* respondsToReturnAndEscape({isElementHidden, show}) {
   info("Simulating return to select the color and hide the eyedropper");
 
-  yield synthesizeKey({key: "VK_RETURN", options: {}});
+  yield EventUtils.synthesizeKey("VK_RETURN", {});
   let hidden = yield isElementHidden("root");
   ok(hidden, "The eyedropper has been hidden");
 
   info("Showing the eyedropper again and simulating escape to hide it");
 
   yield show("html");
-  yield synthesizeKey({key: "VK_ESCAPE", options: {}});
+  yield EventUtils.synthesizeKey("VK_ESCAPE", {});
   hidden = yield isElementHidden("root");
   ok(hidden, "The eyedropper has been hidden again");
 }
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -460,20 +460,16 @@ const getHighlighterHelperFor = (type) =
           prefix + id, name, highlighter);
       },
 
       synthesizeMouse: function* (options) {
         options = Object.assign({selector: ":root"}, options);
         yield testActor.synthesizeMouse(options);
       },
 
-      synthesizeKey: function* (options) {
-        yield testActor.synthesizeKey(options);
-      },
-
       // This object will synthesize any "mouse" prefixed event to the
       // `testActor`, using the name of method called as suffix for the
       // event's name.
       // If no x, y coords are given, the previous ones are used.
       //
       // For example:
       //   mouse.down(10, 20); // synthesize "mousedown" at 10,20
       //   mouse.move(20, 30); // synthesize "mousemove" at 20,30
--- a/devtools/client/memory/test/browser/head.js
+++ b/devtools/client/memory/test/browser/head.js
@@ -51,16 +51,20 @@ this.closeMemoryPanel = Task.async(funct
  *     this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
  *         // Your tests go here...
  *     });
  */
 function makeMemoryTest(url, generator) {
   return Task.async(function* () {
     waitForExplicitFinish();
 
+    // It can take a long time to save a snapshot to disk, read the snapshots
+    // back from disk, and finally perform analyses on them.
+    requestLongerTimeout(2);
+
     const tab = yield addTab(url);
     const results = yield openMemoryPanel(tab);
 
     try {
       yield* generator(results);
     } catch (err) {
       ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
     }
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -107,17 +107,16 @@ skip-if = (os == 'linux' && e10s && debu
 [browser_net_post-data-03.js]
 [browser_net_prefs-and-l10n.js]
 [browser_net_prefs-reload.js]
 [browser_net_raw_headers.js]
 [browser_net_reload-button.js]
 [browser_net_reload-markers.js]
 [browser_net_req-resp-bodies.js]
 [browser_net_resend.js]
-skip-if = e10s # Bug 1091612
 [browser_net_security-details.js]
 [browser_net_security-error.js]
 [browser_net_security-icon-click.js]
 [browser_net_security-redirect.js]
 [browser_net_security-state.js]
 [browser_net_security-tab-deselect.js]
 [browser_net_security-tab-visibility.js]
 [browser_net_security-warnings.js]
--- a/devtools/client/responsive.html/browser/tunnel.js
+++ b/devtools/client/responsive.html/browser/tunnel.js
@@ -175,16 +175,26 @@ function tunnelToInnerBrowser(outer, inn
         enumerable: true,
       });
 
       // Wants to access the content's `frameLoader`, so we'll redirect it to
       // inner browser.
       outer.setDocShellIsActiveAndForeground = value => {
         inner.frameLoader.tabParent.setDocShellIsActiveAndForeground(value);
       };
+
+      // Make the PopupNotifications object available on the iframe's owner
+      // This is used for permission doorhangers
+      Object.defineProperty(inner.ownerGlobal, "PopupNotifications", {
+        get() {
+          return outer.ownerGlobal.PopupNotifications;
+        },
+        configurable: true,
+        enumerable: true,
+      });
     }),
 
     stop() {
       let tab = gBrowser.getTabForBrowser(outer);
       let filteredProgressListener = gBrowser._tabFilters.get(tab);
       browserWindow = null;
       gBrowser = null;
 
@@ -205,16 +215,19 @@ function tunnelToInnerBrowser(outer, inn
       // Reset overridden XBL properties and methods.  Deleting the override
       // means it will fallback to the original XBL binding definitions which
       // are on the prototype.
       delete outer.isRemoteBrowser;
       delete outer.hasContentOpener;
       delete outer.docShellIsActive;
       delete outer.setDocShellIsActiveAndForeground;
 
+      // Delete the PopupNotifications getter added for permission doorhangers
+      delete inner.ownerGlobal.PopupNotifications;
+
       mmTunnel.destroy();
       mmTunnel = null;
 
       // Invalidate outer's permanentKey so that SessionStore stops associating
       // things that happen to the outer browser with the content inside in the
       // inner browser.
       outer.permanentKey = { id: "zombie" };
     },
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 # !e10s: RDM only works for remote tabs
 skip-if = !e10s
 support-files =
   devices.json
   doc_page_state.html
+  geolocation.html
   head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/framework/test/shared-redux-head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
@@ -20,16 +21,17 @@ support-files =
 [browser_device_width.js]
 [browser_exit_button.js]
 [browser_frame_script_active.js]
 [browser_menu_item_01.js]
 [browser_menu_item_02.js]
 [browser_mouse_resize.js]
 [browser_navigation.js]
 [browser_page_state.js]
+[browser_permission_doorhanger.js]
 [browser_resize_cmd.js]
 skip-if = true # GCLI target confused after swap, will fix in bug 1240912
 [browser_screenshot_button.js]
 [browser_shutdown_close_sync.js]
 [browser_toolbox_computed_view.js]
 [browser_toolbox_rule_view.js]
 [browser_touch_simulation.js]
 [browser_viewport_basics.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that permission popups asking for user approval still appear in RDM
+const DUMMY_URL = "http://example.com/";
+const TEST_URL = `${URL_ROOT}geolocation.html`;
+
+function waitForGeolocationPrompt(win, browser) {
+  return new Promise(resolve => {
+    win.PopupNotifications.panel.addEventListener("popupshown", function popupShown() {
+      let notification = win.PopupNotifications.getNotification("geolocation", browser);
+      if (notification) {
+        win.PopupNotifications.panel.removeEventListener("popupshown", popupShown);
+        resolve();
+      }
+    });
+  });
+}
+
+add_task(function* () {
+  let tab = yield addTab(DUMMY_URL);
+  let browser = tab.linkedBrowser;
+  let win = browser.ownerGlobal;
+
+  let waitPromptPromise = waitForGeolocationPrompt(win, browser);
+
+  // Checks if a geolocation permission doorhanger appears when openning a page
+  // requesting geolocation
+  yield load(browser, TEST_URL);
+  yield waitPromptPromise;
+
+  ok(true, "Permission doorhanger appeared without RDM enabled");
+
+  // Lets switch back to the dummy website and enable RDM
+  yield load(browser, DUMMY_URL);
+  let { ui } = yield openRDM(tab);
+  let newBrowser = ui.getViewportBrowser();
+
+  waitPromptPromise = waitForGeolocationPrompt(win, newBrowser);
+
+  // Checks if the doorhanger appeared again when reloading the geolocation
+  // page inside RDM
+  yield load(browser, TEST_URL);
+  yield waitPromptPromise;
+
+  ok(true, "Permission doorhanger appeared inside RDM");
+
+  yield closeRDM(tab);
+  yield removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/geolocation.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Geolocation permission test</title>
+  </head>
+  <body>
+    <script type="text/javascript">
+      "use strict";
+      navigator.geolocation.getCurrentPosition(function (pos) {});
+    </script>
+  </body>
+</html>
\ No newline at end of file
--- a/devtools/client/themes/images/close.svg
+++ b/devtools/client/themes/images/close.svg
@@ -1,6 +1,6 @@
 <!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
-  <path d="M6.7 8l3.6-3.6c.2-.2.2-.5 0-.7-.2-.2-.5-.2-.7 0L6 7.3 2.4 3.7c-.2-.2-.5-.2-.7 0-.2.2-.2.5 0 .7L5.3 8l-3.6 3.6c-.2.2-.2.5 0 .7.2.2.5.2.7 0L6 8.7l3.6 3.6c.2.2.5.2.7 0 .2-.2.2-.5 0-.7L6.7 8z"/>
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
+  <path d="M8.707 8l4.23 4.23a.5.5 0 1 1-.707.707L8 8.707l-4.23 4.23a.5.5 0 1 1-.707-.707L7.293 8l-4.23-4.23a.5.5 0 1 1 .707-.707L8 7.293l4.23-4.23a.5.5 0 0 1 .707.707L8.707 8z" fill-rule="evenodd"/>
 </svg>
--- a/devtools/server/actors/highlighters/eye-dropper.js
+++ b/devtools/server/actors/highlighters/eye-dropper.js
@@ -153,17 +153,17 @@ EyeDropper.prototype = {
     this.ctx.mozImageSmoothingEnabled = false;
 
     this.magnifiedArea = {width: MAGNIFIER_WIDTH, height: MAGNIFIER_HEIGHT,
                           x: DEFAULT_START_POS_X, y: DEFAULT_START_POS_Y};
 
     this.moveTo(DEFAULT_START_POS_X, DEFAULT_START_POS_Y);
 
     // Focus the content so the keyboard can be used.
-    this.win.document.documentElement.focus();
+    this.win.focus();
 
     return true;
   },
 
   /**
    * Hide the eye-dropper highlighter.
    */
   hide() {
@@ -355,64 +355,65 @@ EyeDropper.prototype = {
     onColorSelected.then(() => this.hide(), e => console.error(e));
   },
 
   /**
    * Handler for the keydown event. Either select the color or move the panel in a
    * direction depending on the key pressed.
    */
   handleKeyDown(e) {
+    // Bail out early if any unsupported modifier is used, so that we let
+    // keyboard shortcuts through.
+    if (e.metaKey || e.ctrlKey || e.altKey) {
+      return;
+    }
+
     if (e.keyCode === e.DOM_VK_RETURN) {
       this.selectColor();
+      e.preventDefault();
       return;
     }
 
     if (e.keyCode === e.DOM_VK_ESCAPE) {
       this.emit("canceled");
       this.hide();
+      e.preventDefault();
       return;
     }
 
     let offsetX = 0;
     let offsetY = 0;
     let modifier = 1;
 
     if (e.keyCode === e.DOM_VK_LEFT) {
       offsetX = -1;
-    }
-    if (e.keyCode === e.DOM_VK_RIGHT) {
+    } else if (e.keyCode === e.DOM_VK_RIGHT) {
       offsetX = 1;
-    }
-    if (e.keyCode === e.DOM_VK_UP) {
+    } else if (e.keyCode === e.DOM_VK_UP) {
       offsetY = -1;
-    }
-    if (e.keyCode === e.DOM_VK_DOWN) {
+    } else if (e.keyCode === e.DOM_VK_DOWN) {
       offsetY = 1;
     }
+
     if (e.shiftKey) {
       modifier = 10;
     }
 
     offsetY *= modifier;
     offsetX *= modifier;
 
     if (offsetX !== 0 || offsetY !== 0) {
       this.magnifiedArea.x += offsetX;
       this.magnifiedArea.y += offsetY;
 
       this.draw();
 
       this.moveTo(this.magnifiedArea.x / this.pageZoom,
                   this.magnifiedArea.y / this.pageZoom);
-    }
 
-    // Prevent all keyboard interaction with the page, except if a modifier is used to let
-    // keyboard shortcuts through.
-    let hasModifier = e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
-    if (!hasModifier) {
       e.preventDefault();
     }
   },
 
   /**
    * Copy the currently inspected color to the clipboard.
    * @return {Promise} Resolves when the copy has been done (after a delay that is used to
    * let users know that something was copied).
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1506,57 +1506,54 @@ WebConsoleActor.prototype =
    * Handler for network events. This method is invoked when a new network event
    * is about to be recorded.
    *
    * @see NetworkEventActor
    * @see NetworkMonitor from webconsole/utils.js
    *
    * @param object aEvent
    *        The initial network request event information.
-   * @param nsIHttpChannel aChannel
-   *        The network request nsIHttpChannel object.
    * @return object
    *         A new NetworkEventActor is returned. This is used for tracking the
    *         network request and response.
    */
-  onNetworkEvent: function WCA_onNetworkEvent(aEvent, aChannel)
+  onNetworkEvent: function WCA_onNetworkEvent(aEvent)
   {
-    let actor = this.getNetworkEventActor(aChannel);
+    let actor = this.getNetworkEventActor(aEvent.channelId);
     actor.init(aEvent);
 
     let packet = {
       from: this.actorID,
       type: "networkEvent",
       eventActor: actor.grip()
     };
 
     this.conn.send(packet);
 
     return actor;
   },
 
   /**
-   * Get the NetworkEventActor for a nsIChannel, if it exists,
+   * Get the NetworkEventActor for a nsIHttpChannel, if it exists,
    * otherwise create a new one.
    *
-   * @param nsIHttpChannel aChannel
-   *        The channel for the network event.
+   * @param string channelId
+   *        The id of the channel for the network event.
    * @return object
    *         The NetworkEventActor for the given channel.
    */
-  getNetworkEventActor: function WCA_getNetworkEventActor(aChannel) {
-    let actor = this._netEvents.get(aChannel);
+  getNetworkEventActor: function WCA_getNetworkEventActor(channelId) {
+    let actor = this._netEvents.get(channelId);
     if (actor) {
       // delete from map as we should only need to do this check once
-      this._netEvents.delete(aChannel);
-      actor.channel = null;
+      this._netEvents.delete(channelId);
       return actor;
     }
 
-    actor = new NetworkEventActor(aChannel, this);
+    actor = new NetworkEventActor(this);
     this._actorPool.addActor(actor);
     return actor;
   },
 
   /**
    * Send a new HTTP request from the target's window.
    *
    * @param object aMessage
@@ -1570,20 +1567,21 @@ WebConsoleActor.prototype =
     let request = new this.window.XMLHttpRequest();
     request.open(details.method, details.url, true);
 
     for (let {name, value} of details.headers) {
       request.setRequestHeader(name, value);
     }
     request.send(details.body);
 
-    let actor = this.getNetworkEventActor(request.channel);
+    let channel = request.channel.QueryInterface(Ci.nsIHttpChannel);
+    let actor = this.getNetworkEventActor(channel.channelId);
 
     // map channel to actor so we can associate future events with it
-    this._netEvents.set(request.channel, actor);
+    this._netEvents.set(channel.channelId, actor);
 
     return {
       from: this.actorID,
       eventActor: actor.grip()
     };
   },
 
   /**
@@ -1797,26 +1795,22 @@ WebConsoleActor.prototype.requestTypes =
 };
 
 exports.WebConsoleActor = WebConsoleActor;
 
 /**
  * Creates an actor for a network event.
  *
  * @constructor
- * @param object aChannel
- *        The nsIChannel associated with this event.
- * @param object aWebConsoleActor
+ * @param object webConsoleActor
  *        The parent WebConsoleActor instance for this object.
  */
-function NetworkEventActor(aChannel, aWebConsoleActor)
-{
-  this.parent = aWebConsoleActor;
+function NetworkEventActor(webConsoleActor) {
+  this.parent = webConsoleActor;
   this.conn = this.parent.conn;
-  this.channel = aChannel;
 
   this._request = {
     method: null,
     url: null,
     httpVersion: null,
     headers: [],
     cookies: [],
     headersSize: null,
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -717,25 +717,24 @@ NetworkResponseListener.prototype = {
  *          window object.
  *        - appId (number): filter requests by the appId.
  *        - topFrame (nsIDOMElement): filter requests by their topFrameElement.
  *        Filters are optional. If any of these filters match the request is
  *        logged (OR is applied). If no filter is provided then all requests are
  *        logged.
  * @param object owner
  *        The network monitor owner. This object needs to hold:
- *        - onNetworkEvent(requestInfo, channel, networkMonitor).
+ *        - onNetworkEvent(requestInfo)
  *          This method is invoked once for every new network request and it is
- *          given the following arguments: the initial network request
- *          information, and the channel. The third argument is the NetworkMonitor
- *          instance. onNetworkEvent() must return an object which holds several add*()
- *          methods which are used to add further network request/response
- *          information.
- *        - stackTraceCollector If the owner has this optional property, it will
- *          be used as a StackTraceCollector by the NetworkMonitor.
+ *          given the initial network request information as an argument.
+ *          onNetworkEvent() must return an object which holds several add*()
+ *          methods which are used to add further network request/response information.
+ *        - stackTraceCollector
+ *          If the owner has this optional property, it will be used as a
+ *          StackTraceCollector by the NetworkMonitor.
  */
 function NetworkMonitor(filters, owner) {
   this.filters = filters;
   this.owner = owner;
   this.openRequests = {};
   this.openResponses = {};
   this._httpResponseExaminer =
     DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
@@ -1084,17 +1083,17 @@ NetworkMonitor.prototype = {
         headers.push({ name: name, value: value });
       }
     });
 
     if (cookieHeader) {
       cookies = NetworkHelper.parseCookieHeader(cookieHeader);
     }
 
-    httpActivity.owner = this.owner.onNetworkEvent(event, channel);
+    httpActivity.owner = this.owner.onNetworkEvent(event);
 
     this._setupResponseListener(httpActivity);
 
     httpActivity.owner.addRequestHeaders(headers, extraStringData);
     httpActivity.owner.addRequestCookies(cookies);
 
     this.openRequests[httpActivity.id] = httpActivity;
     return httpActivity;
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5377,17 +5377,17 @@
   },
   "SEARCH_RESET_RESULT": {
     "alert_emails": ["fqueze@mozilla.com"],
     "bug_numbers": [1203168],
     "expires_in_version": "53",
     "kind": "enumerated",
     "n_values": 5,
     "releaseChannelCollection": "opt-out",
-    "description": "Result of showing the search reset prompt to the user. 0=restored original default, 1=kept current engine, 2=changed engine, 3=closed the page"
+    "description": "Result of showing the search reset prompt to the user. 0=restored original default, 1=kept current engine, 2=changed engine, 3=closed the page, 4=opened search settings"
   },
   "SEARCH_SERVICE_INIT_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 1000,
     "n_buckets": 15,
     "description": "Time (ms) it takes to initialize the search service"
   },
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -283,17 +283,19 @@ function getHistogram(histogramId) {
 function getSnapshot(histogramId) {
   return Telemetry.getHistogramById(histogramId).snapshot();
 }
 
 // Helper for setting an empty list of Environment preferences to watch.
 function setEmptyPrefWatchlist() {
   let TelemetryEnvironment =
     Cu.import("resource://gre/modules/TelemetryEnvironment.jsm").TelemetryEnvironment;
-  TelemetryEnvironment.testWatchPreferences(new Map());
+  return TelemetryEnvironment.onInitialized().then(() => {
+    TelemetryEnvironment.testWatchPreferences(new Map());
+  });
 }
 
 if (runningInParent) {
   // Set logging preferences for all the tests.
   Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
   // Telemetry archiving should be on.
   Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
   // Telemetry xpcshell tests cannot show the infobar.
--- a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
+++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
@@ -68,17 +68,17 @@ add_task(function*() {
 
   // Setup.
   do_get_profile(true);
   loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   yield TelemetryController.testSetup();
   if (runningInParent) {
     // Make sure we don't generate unexpected pings due to pref changes.
-    setEmptyPrefWatchlist();
+    yield setEmptyPrefWatchlist();
   }
 
   // Run test in child, don't wait for it to finish.
   let childPromise = run_test_in_child("test_ChildHistograms.js");
   yield do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
 
   // Gather payload from child.
   dump("... requesting child payloads\n");
--- a/toolkit/components/telemetry/tests/unit/test_PingAPI.js
+++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
@@ -60,23 +60,22 @@ var getArchivedPingsInfo = Task.async(fu
     }
   }
 
   // Sort the list by creation date and then return it.
   archivedPings.sort((a, b) => b.timestamp - a.timestamp);
   return archivedPings;
 });
 
-function run_test() {
+add_task(function* test_setup() {
   do_get_profile(true);
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
-  run_next_test();
-}
+});
 
 add_task(function* test_archivedPings() {
   // TelemetryController should not be fully initialized at this point.
   // Submitting pings should still work fine.
 
   const PINGS = [
     {
       type: "test-ping-api-1",
--- a/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -79,29 +79,27 @@ var promiseValidateArchivedPings = Task.
       expectedSubsessionCounter = 1;
       expectedPreviousSessionId = currentInfo.sessionId;
     } else {
       expectedSubsessionCounter++;
     }
   }
 });
 
-function run_test() {
+add_task(function* test_setup() {
   do_test_pending();
 
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
   Preferences.set(PREF_TELEMETRY_ENABLED, true);
-
-  run_next_test();
-}
+});
 
 add_task(function* test_subsessionsChaining() {
   if (gIsAndroid) {
     // We don't support subsessions yet on Android, so skip the next checks.
     return;
   }
 
   const PREF_TEST = PREF_BRANCH + "test.pref1";
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -85,30 +85,29 @@ function checkPingFormat(aPing, aType, a
   Assert.ok("channel" in aPing.application,
             "The application section must have a channel field.");
 
   // Check the clientId and environment fields, as needed.
   Assert.equal("clientId" in aPing, aHasClientId);
   Assert.equal("environment" in aPing, aHasEnvironment);
 }
 
-function run_test() {
-  do_test_pending();
-
+add_task(function* test_setup() {
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
-  Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(run_next_test));
-}
+  yield new Promise(resolve =>
+    Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve)));
+});
 
 add_task(function* asyncSetup() {
   yield TelemetryController.testSetup();
 });
 
 // Ensure that not overwriting an existing file fails silently
 add_task(function* test_overwritePing() {
   let ping = {id: "foo"};
@@ -501,10 +500,9 @@ add_task(function* test_telemetryCleanFH
   yield TelemetryStorage.removeFHRDatabase();
   for (let dbFilePath of DEFAULT_DB_PATHS) {
     Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath);
   }
 });
 
 add_task(function* stopServer() {
   yield PingServer.stop();
-  do_test_finished();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
@@ -21,28 +21,26 @@ function contentHandler(metadata, respon
 {
   dump("contentHandler called for path: " + metadata._path + "\n");
   // We intentionally don't finish writing the response here to let the
   // client time out.
   response.processAsync();
   response.setHeader("Content-Type", "text/plain");
 }
 
-function run_test() {
+add_task(function* test_setup() {
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
-
-  run_next_test();
-}
+});
 
 /**
  * Ensures that TelemetryController does not hang processing shutdown
  * phases. Assumes that Telemetry shutdown routines do not take longer than
  * CRASH_TIMEOUT_MS to complete.
  */
 add_task(function* test_sendTelemetryShutsDownWithinReasonableTimeout() {
   const CRASH_TIMEOUT_MS = 5 * 1000;
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
@@ -11,32 +11,29 @@ Cu.import("resource://gre/modules/Teleme
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
 
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 
 var gHttpServer = null;
 
-function run_test() {
-  do_test_pending();
+add_task(function* test_setup() {
   do_get_profile();
 
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   // Start the webserver to check if the pending ping correctly arrives.
   gHttpServer = new HttpServer();
   gHttpServer.start(-1);
-
-  run_next_test();
-}
+});
 
 add_task(function* testSendPendingOnIdleDaily() {
   // Create a valid pending ping.
   const PENDING_PING = {
     id: "2133234d-4ea1-44f4-909e-ce8c6c41e0fc",
     type: "test-ping",
     version: 4,
     application: {},
@@ -67,10 +64,10 @@ add_task(function* testSendPendingOnIdle
   module.TelemetrySendImpl.observe(null, "idle-daily", null);
   let request = yield pendingPromise;
   let ping = decodeRequestPayload(request);
 
   // Validate the ping data.
   Assert.equal(ping.id, PENDING_PING.id);
   Assert.equal(ping.type, PENDING_PING.type);
 
-  gHttpServer.stop(do_test_finished);
+  yield new Promise(resolve => gHttpServer.stop(resolve));
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
@@ -51,32 +51,30 @@ function setMinimumPolicyVersion(aNewPol
     Preferences.set(CHANNEL_DEPENDENT_PREF, aNewPolicyVersion);
     return;
   }
 
   // We don't have a channel specific minimu, so set the common one.
   Preferences.set(PREF_MINIMUM_POLICY_VERSION, aNewPolicyVersion);
 }
 
-function run_test() {
+add_task(function* test_setup() {
   // Addon manager needs a profile directory
   do_get_profile(true);
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   // Don't bypass the notifications in this test, we'll fake it.
   Services.prefs.setBoolPref(PREF_BYPASS_NOTIFICATION, false);
 
   TelemetryReportingPolicy.setup();
-
-  run_next_test();
-}
+});
 
 add_task(function* test_firstRun() {
   const PREF_FIRST_RUN = "toolkit.telemetry.reportingpolicy.firstRun";
   const FIRST_RUN_TIMEOUT_MSEC = 60 * 1000; // 60s
   const OTHER_RUNS_TIMEOUT_MSEC = 10 * 1000; // 10s
 
   Preferences.reset(PREF_FIRST_RUN);
 
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
@@ -64,24 +64,23 @@ var checkPingsSaved = Task.async(functio
       dump("checkPingsSaved - failed to find ping: " + path + "\n");
       allFound = false;
     }
   }
 
   return allFound;
 });
 
-function run_test() {
+add_task(function* test_setup() {
   // Trigger a proper telemetry init.
   do_get_profile(true);
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
-  run_next_test();
-}
+});
 
 // Test the ping sending logic.
 add_task(function* test_sendPendingPings() {
   const TYPE_PREFIX = "test-sendPendingPings-";
   const TEST_TYPE_A = TYPE_PREFIX + "A";
   const TEST_TYPE_B = TYPE_PREFIX + "B";
 
   const TYPE_A_COUNT = 20;
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -139,29 +139,28 @@ var assertNotSaved = Task.async(function
  * receives and decodes a Telemetry payload.
  *
  * @param aRequest the HTTP request sent from HttpServer.
  */
 function pingHandler(aRequest) {
   gSeenPings++;
 }
 
-function run_test() {
+add_task(function* test_setup() {
   PingServer.start();
   PingServer.registerPingHandler(pingHandler);
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setCharPref(TelemetryController.Constants.PREF_SERVER,
                              "http://localhost:" + PingServer.port);
-  run_next_test();
-}
+});
 
 /**
  * Setup the tests by making sure the ping storage directory is available, otherwise
  * |TelemetryController.testSaveDirectoryToFile| could fail.
  */
 add_task(function* setupEnvironment() {
   // The following tests assume this pref to be true by default.
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true);
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -429,24 +429,22 @@ function write_fake_shutdown_file() {
 function write_fake_failedprofilelocks_file() {
   let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
   let file = profileDirectory.clone();
   file.append("Telemetry.FailedProfileLocks.txt");
   let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
   writeStringToFile(file, contents);
 }
 
-function run_test() {
-  do_test_pending();
-
+add_task(function* test_setup() {
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   // Make it look like we've previously failed to lock a profile a couple times.
   write_fake_failedprofilelocks_file();
 
   // Make it look like we've shutdown before.
@@ -469,18 +467,19 @@ function run_test() {
   do_check_true(Telemetry.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched);
 
   do_register_cleanup(function() {
     threads.forEach(function(thread) {
       thread.shutdown();
     });
   });
 
-  Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(run_next_test));
-}
+  yield new Promise(resolve =>
+    Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve)));
+});
 
 add_task(function* asyncSetup() {
   yield TelemetryController.testSetup();
   // Load the client ID from the client ID provider to check for pings sanity.
   gClientID = yield ClientID.getClientID();
 });
 
 // Ensures that expired histograms are not part of the payload.
@@ -1858,10 +1857,9 @@ add_task(function* test_userIdleAndSched
   checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
   Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
 
   yield TelemetryController.testShutdown();
 });
 
 add_task(function* stopServer() {
   yield PingServer.stop();
-  do_test_finished();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
@@ -26,36 +26,31 @@ function loadAddonManager() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   startupManager();
 }
 
 function getSimpleMeasurementsFromTelemetryController() {
   return TelemetrySession.getPayload().simpleMeasurements;
 }
 
-function initialiseTelemetry() {
-  return TelemetryController.testSetup();
-}
-
-function run_test() {
+add_task(function* test_setup() {
   // Telemetry needs the AddonManager.
   loadAddonManager();
   // Make profile available for |TelemetryController.testShutdown()|.
   do_get_profile();
 
   // Make sure we don't generate unexpected pings due to pref changes.
-  setEmptyPrefWatchlist();
+  yield setEmptyPrefWatchlist();
 
-  do_test_pending();
-  const Telemetry = Services.telemetry;
-  Telemetry.asyncFetchTelemetryData(run_next_test);
-}
+  yield new Promise(resolve =>
+    Services.telemetry.asyncFetchTelemetryData(resolve));
+});
 
 add_task(function* actualTest() {
-  yield initialiseTelemetry();
+  yield TelemetryController.testSetup();
 
   // Test the module logic
   let tmp = {};
   Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
   let TelemetryTimestamps = tmp.TelemetryTimestamps;
   let now = Date.now();
   TelemetryTimestamps.add("foo");
   do_check_true(TelemetryTimestamps.get().foo != null); // foo was added
@@ -84,11 +79,9 @@ add_task(function* actualTest() {
   // Test that the data gets added to the telemetry ping properly
   let simpleMeasurements = getSimpleMeasurementsFromTelemetryController();
   do_check_true(simpleMeasurements != null); // got simple measurements from ping data
   do_check_true(simpleMeasurements.foo > 1); // foo was included
   do_check_true(simpleMeasurements.bar > 1); // bar was included
   do_check_eq(undefined, simpleMeasurements.baz); // baz wasn't included since it wasn't added
 
   yield TelemetryController.testShutdown();
-
-  do_test_finished();
 });
--- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
+++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
@@ -224,18 +224,26 @@ const BackgroundPageThumbs = {
       this._destroyBrowser();
       let curCapture = this._captureQueue.length ? this._captureQueue[0] : null;
       // we could retry the pending capture, but it's possible the crash
       // was due directly to it, so trying again might just crash again.
       // We could keep a flag to indicate if it previously crashed, but
       // "resetting" the capture requires more work - so for now, we just
       // discard it.
       if (curCapture && curCapture.pending) {
-        curCapture._done(null, TEL_CAPTURE_DONE_CRASHED);
-        // _done automatically continues queue processing.
+        // Continue queue processing by calling curCapture._done().  Do it after
+        // this crashed listener returns, though.  A new browser will be created
+        // immediately (on the same stack as the _done call stack) if there are
+        // any more queued-up captures, and that seems to mess up the new
+        // browser's message manager if it happens on the same stack as the
+        // listener.  Trying to send a message to the manager in that case
+        // throws NS_ERROR_NOT_INITIALIZED.
+        Services.tm.currentThread.dispatch(() => {
+          curCapture._done(null, TEL_CAPTURE_DONE_CRASHED);
+        }, Ci.nsIEventTarget.DISPATCH_NORMAL);
       }
       // else: we must have been idle and not currently doing a capture (eg,
       // maybe a GC or similar crashed) - so there's no need to attempt a
       // queue restart - the next capture request will set everything up.
     });
 
     browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
     this._thumbBrowser = browser;
--- a/toolkit/components/thumbnails/test/browser.ini
+++ b/toolkit/components/thumbnails/test/browser.ini
@@ -7,17 +7,17 @@ support-files =
   head.js
   privacy_cache_control.sjs
   thumbnails_background.sjs
   thumbnails_crash_content_helper.js
   thumbnails_update.sjs
 
 [browser_thumbnails_bg_bad_url.js]
 [browser_thumbnails_bg_crash_during_capture.js]
-skip-if = buildapp == 'mulet' || !crashreporter || e10s # crashing the remote thumbnailer crashes the remote test tab
+skip-if = buildapp == 'mulet' || !crashreporter
 [browser_thumbnails_bg_crash_while_idle.js]
 skip-if = buildapp == 'mulet' || !crashreporter
 [browser_thumbnails_bg_basic.js]
 [browser_thumbnails_bg_queueing.js]
 [browser_thumbnails_bg_timeout.js]
 [browser_thumbnails_bg_redirect.js]
 [browser_thumbnails_bg_destroy_browser.js]
 [browser_thumbnails_bg_no_cookies_sent.js]
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -420,17 +420,19 @@
         if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
           container.orient = "horizontal";
           arrowbox.orient = "vertical";
           if (position.indexOf("_after") > 0) {
             arrowbox.pack = "end";
           } else {
             arrowbox.pack = "start";
           }
-          arrowbox.style.transform = "translate(0, " + -offset + "px)";
+          if (offset != "0") {
+            arrowbox.style.transform = "translate(0, " + -offset + "px)";
+          }
 
           // The assigned side stays the same regardless of direction.
           var isRTL = (window.getComputedStyle(this).direction == "rtl");
 
           if (position.indexOf("start_") == 0) {
             container.dir = "reverse";
             this.setAttribute("side", isRTL ? "left" : "right");
           }
@@ -442,17 +444,19 @@
         else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
           container.orient = "";
           arrowbox.orient = "";
           if (position.indexOf("_end") > 0) {
             arrowbox.pack = "end";
           } else {
             arrowbox.pack = "start";
           }
-          arrowbox.style.transform = "translate(" + -offset + "px, 0)";
+          if (offset != "0") {
+            arrowbox.style.transform = "translate(" + -offset + "px, 0)";
+          }
 
           if (position.indexOf("before_") == 0) {
             container.dir = "reverse";
             this.setAttribute("side", "bottom");
           }
           else {
             container.dir = "";
             this.setAttribute("side", "top");
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -197,28 +197,19 @@ function loadView(aViewId) {
 
     gViewController.loadInitialView(aViewId);
   } else {
     gViewController.loadView(aViewId);
   }
 }
 
 function isCorrectlySigned(aAddon) {
-  // Temporary add-ons do not require signing.
-  if (aAddon.scope == AddonManager.SCOPE_TEMPORARY)
-      return true;
-  // On UNIX platforms except OSX, an additional location for system add-ons
-  // exists in /usr/{lib,share}/mozilla/extensions. Add-ons installed there
-  // do not require signing either.
-  if (aAddon.scope == AddonManager.SCOPE_SYSTEM &&
-      Services.appinfo.OS != "Darwin")
-    return true;
-  if (aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING)
-    return false;
-  return true;
+  // Add-ons without an "isCorrectlySigned" property are correctly signed as
+  // they aren't the correct type for signing.
+  return aAddon.isCorrectlySigned !== false;
 }
 
 function isDiscoverEnabled() {
   if (Services.prefs.getPrefType(PREF_DISCOVERURL) == Services.prefs.PREF_INVALID)
     return false;
 
   try {
     if (!Services.prefs.getBoolPref(PREF_DISCOVER_ENABLED))
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -711,35 +711,20 @@ function canRunInSafeMode(aAddon) {
  *         The add-on to check
  * @return true if the add-on should not be appDisabled
  */
 function isUsableAddon(aAddon) {
   // Hack to ensure the default theme is always usable
   if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
     return true;
 
-  if (aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS &&
-      aAddon.signedState != AddonManager.SIGNEDSTATE_SYSTEM) {
-    logger.warn(`System add-on update ${aAddon.id} not signed with the system key.`);
+  if (mustSign(aAddon.type) && !aAddon.isCorrectlySigned) {
+    logger.warn(`Add-on ${aAddon.id} is not correctly signed.`);
     return false;
   }
-  // Temporary and built-in system add-ons do not require signing.
-  // On UNIX platforms except OSX, an additional location for system add-ons
-  // exists in /usr/{lib,share}/mozilla/extensions. Add-ons installed there
-  // do not require signing either.
-  if (((aAddon._installLocation.scope != AddonManager.SCOPE_SYSTEM ||
-        Services.appinfo.OS == "Darwin") &&
-       aAddon._installLocation.name != KEY_APP_SYSTEM_DEFAULTS &&
-       aAddon._installLocation.name != KEY_APP_TEMPORARY) &&
-       mustSign(aAddon.type)) {
-    if (aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
-      logger.warn(`Add-on ${aAddon.id} not signed.`);
-      return false;
-    }
-  }
 
   if (aAddon.blocklistState == Blocklist.STATE_BLOCKED) {
     logger.warn(`Add-on ${aAddon.id} is blocklisted.`);
     return false;
   }
 
   // Experiments are installed through an external mechanism that
   // limits target audience to compatible clients. We trust it knows what
@@ -6942,16 +6927,42 @@ AddonInternal.prototype = {
     return this._selectedLocale;
   },
 
   get providesUpdatesSecurely() {
     return !!(this.updateKey || !this.updateURL ||
               this.updateURL.substring(0, 6) == "https:");
   },
 
+  get isCorrectlySigned() {
+    switch (this._installLocation.name) {
+      case KEY_APP_SYSTEM_ADDONS:
+        // System add-ons must be signed by the system key.
+        return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM
+
+      case KEY_APP_SYSTEM_DEFAULTS:
+      case KEY_APP_TEMPORARY:
+        // Temporary and built-in system add-ons do not require signing.
+        return true;
+
+      case KEY_APP_SYSTEM_SHARE:
+      case KEY_APP_SYSTEM_LOCAL:
+        // On UNIX platforms except OSX, an additional location for system
+        // add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons
+        // installed there do not require signing.
+        if (Services.appinfo.OS != "Darwin")
+          return true;
+        break;
+    }
+
+    if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED)
+      return true;
+    return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
+  },
+
   get isCompatible() {
     return this.isCompatibleWith();
   },
 
   get disabled() {
     return (this.userDisabled || this.appDisabled || this.softDisabled);
   },
 
@@ -7768,17 +7779,18 @@ function defineAddonWrapperProperty(name
     enumerable: true,
   });
 }
 
 ["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible",
  "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
  "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
  "strictCompatibility", "compatibilityOverrides", "updateURL", "dependencies",
- "getDataDirectory", "multiprocessCompatible", "signedState"].forEach(function(aProp) {
+ "getDataDirectory", "multiprocessCompatible", "signedState",
+ "isCorrectlySigned"].forEach(function(aProp) {
    defineAddonWrapperProperty(aProp, function() {
      let addon = addonFor(this);
      return (aProp in addon) ? addon[aProp] : undefined;
    });
 });
 
 ["fullDescription", "developerComments", "eula", "supportURL",
  "contributionURL", "contributionAmount", "averageRating", "reviewCount",
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -1147,16 +1147,22 @@ function MockAddon(aId, aName, aType, aO
     aOperationsRequiringRestart :
     (AddonManager.OP_NEEDS_RESTART_INSTALL |
      AddonManager.OP_NEEDS_RESTART_UNINSTALL |
      AddonManager.OP_NEEDS_RESTART_ENABLE |
      AddonManager.OP_NEEDS_RESTART_DISABLE);
 }
 
 MockAddon.prototype = {
+  get isCorrectlySigned() {
+    if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED)
+      return true;
+    return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
+  },
+
   get shouldBeActive() {
     return !this.appDisabled && !this._userDisabled &&
            !(this.pendingOperations & AddonManager.PENDING_UNINSTALL);
   },
 
   get appDisabled() {
     return this._appDisabled;
   },