Merge mozilla-central to inbound. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 15 May 2019 12:31:11 +0300
changeset 535936 946206b6d05bf1c42683f57249043e1eec87ec4f
parent 535935 037e2534c8bd12a8a06ae7d128d01d11768ffe2a (current diff)
parent 535783 76bbedc1ec1ae367906390c01a8ca008d7944cac (diff)
child 535937 4a51777a5592e666753a6b0c84a4900f5cfe1913
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound. a=merge CLOSED TREE
mobile/android/base/java/org/mozilla/gecko/advertising/AdvertisingUtil.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryActivationPingDelegate.java
mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryActivationPingBuilder.java
services/common/tests/unit/test_blocklist_signatures.js
services/common/tests/unit/test_blocklist_signatures/collection_signing_ee.pem.certspec
services/common/tests/unit/test_blocklist_signatures/collection_signing_int.pem.certspec
services/common/tests/unit/test_blocklist_signatures/collection_signing_root.pem.certspec
services/common/tests/unit/test_blocklist_signatures/moz.build
toolkit/modules/addons/.eslintrc.js
toolkit/modules/addons/MatchURLFilters.jsm
toolkit/modules/addons/SecurityInfo.jsm
toolkit/modules/addons/WebNavigation.jsm
toolkit/modules/addons/WebNavigationContent.js
toolkit/modules/addons/WebNavigationFrames.jsm
toolkit/modules/addons/WebRequest.jsm
toolkit/modules/addons/WebRequestCommon.jsm
toolkit/modules/addons/WebRequestContent.js
toolkit/modules/addons/WebRequestUpload.jsm
toolkit/modules/tests/browser/WebRequest_dynamic.sjs
toolkit/modules/tests/browser/WebRequest_redirection.sjs
toolkit/modules/tests/browser/browser_WebNavigation.js
toolkit/modules/tests/browser/browser_WebRequest.js
toolkit/modules/tests/browser/browser_WebRequest_ancestors.js
toolkit/modules/tests/browser/browser_WebRequest_cookies.js
toolkit/modules/tests/browser/browser_WebRequest_filtering.js
toolkit/modules/tests/browser/dummy_page.html
toolkit/modules/tests/browser/file_WebNavigation_page1.html
toolkit/modules/tests/browser/file_WebNavigation_page2.html
toolkit/modules/tests/browser/file_WebNavigation_page3.html
toolkit/modules/tests/browser/file_WebRequest_page1.html
toolkit/modules/tests/browser/file_WebRequest_page2.html
toolkit/modules/tests/browser/file_image_bad.png
toolkit/modules/tests/browser/file_image_good.png
toolkit/modules/tests/browser/file_image_redirect.png
toolkit/modules/tests/browser/file_script_bad.js
toolkit/modules/tests/browser/file_script_good.js
toolkit/modules/tests/browser/file_script_redirect.js
toolkit/modules/tests/browser/file_script_xhr.js
toolkit/modules/tests/browser/file_style_bad.css
toolkit/modules/tests/browser/file_style_good.css
toolkit/modules/tests/browser/file_style_redirect.css
--- a/accessible/base/FocusManager.cpp
+++ b/accessible/base/FocusManager.cpp
@@ -94,16 +94,20 @@ FocusManager::FocusDisposition FocusMana
     if (child == focus) return eContainedByFocus;
 
     child = child->Parent();
   }
 
   return eNone;
 }
 
+bool FocusManager::WasLastFocused(const Accessible* aAccessible) const {
+  return mLastFocus == aAccessible;
+}
+
 void FocusManager::NotifyOfDOMFocus(nsISupports* aTarget) {
 #ifdef A11Y_LOG
   if (logging::IsEnabled(logging::eFocus))
     logging::FocusNotificationTarget("DOM focus", "Target", aTarget);
 #endif
 
   mActiveItem = nullptr;
 
@@ -335,16 +339,17 @@ void FocusManager::ProcessFocusEvent(Acc
   // Reset cached caret value. The cache will be updated upon processing the
   // next caret move event. This ensures that we will return the correct caret
   // offset before the caret move event is handled.
   SelectionMgr()->ResetCaretOffset();
 
   RefPtr<AccEvent> focusEvent = new AccEvent(nsIAccessibleEvent::EVENT_FOCUS,
                                              target, aEvent->FromUserInput());
   nsEventShell::FireEvent(focusEvent);
+  mLastFocus = target;
 
   // Fire scrolling_start event when the document receives the focus if it has
   // an anchor jump. If an accessible within the document receive the focus
   // then null out the anchor jump because it no longer applies.
   DocAccessible* targetDocument = target->Document();
   Accessible* anchorJump = targetDocument->AnchorJump();
   if (anchorJump) {
     if (target == targetDocument) {
--- a/accessible/base/FocusManager.h
+++ b/accessible/base/FocusManager.h
@@ -65,16 +65,28 @@ class FocusManager {
 
   /**
    * Return whether the given accessible is focused or contains the focus or
    * contained by focused accessible.
    */
   enum FocusDisposition { eNone, eFocused, eContainsFocus, eContainedByFocus };
   FocusDisposition IsInOrContainsFocus(const Accessible* aAccessible) const;
 
+  /**
+   * Return true if the given accessible was the last accessible focused.
+   * This is useful to detect the case where the last focused accessible was
+   * removed before something else was focused. This can happen in one of two
+   * ways:
+   * 1. The DOM focus was removed. DOM doesn't fire a blur event when this
+   *    happens; see bug 559561.
+   * 2. The accessibility focus was an active item (e.g. aria-activedescendant)
+   *    and that item was removed.
+   */
+  bool WasLastFocused(const Accessible* aAccessible) const;
+
   //////////////////////////////////////////////////////////////////////////////
   // Focus notifications and processing (don't use until you know what you do).
 
   /**
    * Called when DOM focus event is fired.
    */
   void NotifyOfDOMFocus(nsISupports* aTarget);
 
@@ -119,15 +131,16 @@ class FocusManager {
 
   /**
    * Return DOM document having DOM focus.
    */
   dom::Document* FocusedDOMDocument() const;
 
  private:
   RefPtr<Accessible> mActiveItem;
+  RefPtr<Accessible> mLastFocus;
   RefPtr<Accessible> mActiveARIAMenubar;
 };
 
 }  // namespace a11y
 }  // namespace mozilla
 
 #endif
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -1211,19 +1211,19 @@ void DocAccessible::BindToDocument(Acces
     }
   }
 }
 
 void DocAccessible::UnbindFromDocument(Accessible* aAccessible) {
   NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
                "Unbinding the unbound accessible!");
 
-  // Fire focus event on accessible having DOM focus if active item was removed
+  // Fire focus event on accessible having DOM focus if last focus was removed
   // from the tree.
-  if (FocusMgr()->IsActiveItem(aAccessible)) {
+  if (FocusMgr()->WasLastFocused(aAccessible)) {
     FocusMgr()->ActiveItemChanged(nullptr);
 #ifdef A11Y_LOG
     if (logging::IsEnabled(logging::eFocus))
       logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
 #endif
   }
 
   // Remove an accessible from node-to-accessible map if it exists there.
--- a/accessible/tests/mochitest/events/a11y.ini
+++ b/accessible/tests/mochitest/events/a11y.ini
@@ -29,16 +29,17 @@ skip-if = os == 'win' || os == 'linux'
 [test_focus_contextmenu.xul]
 [test_focus_controls.html]
 [test_focus_doc.html]
 [test_focus_general.html]
 [test_focus_general.xul]
 [test_focus_listcontrols.xul]
 [test_focus_menu.xul]
 [test_focus_name.html]
+[test_focus_removal.html]
 [test_focus_selects.html]
 [test_focus_tabbox.xul]
 skip-if = webrender
 [test_focus_tree.xul]
 [test_fromUserInput.html]
 [test_label.xul]
 [test_menu.xul]
 [test_mutation.html]
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_removal.html
@@ -0,0 +1,82 @@
+<html>
+
+<head>
+  <title>Test removal of focused accessible</title>
+
+  <link rel="stylesheet" type="text/css"
+        href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+  <script type="application/javascript"
+          src="../common.js"></script>
+  <script type="application/javascript"
+          src="../events.js"></script>
+
+  <script type="application/javascript">
+    async function setFocus(aNodeToFocus, aExpectedFocus) {
+      let expected = aExpectedFocus || aNodeToFocus;
+      let focused = waitForEventPromise(EVENT_FOCUS, expected);
+      info("Focusing " + aNodeToFocus.id);
+      aNodeToFocus.focus();
+      await focused;
+      ok(true, expected.id + " focused after " +
+        aNodeToFocus.id + ".focus()");
+    }
+
+    async function expectFocusAfterRemove(aNodeToRemove, aExpectedFocus) {
+      let focused = waitForEventPromise(EVENT_FOCUS, aExpectedFocus);
+      info("Removing " + aNodeToRemove.id);
+      aNodeToRemove.remove();
+      await focused;
+      let friendlyExpected = aExpectedFocus == document ?
+        "document" : aExpectedFocus.id;
+      ok(true, friendlyExpected + " focused after " +
+        aNodeToRemove.id + " removed");
+    }
+
+    async function doTests() {
+      info("Testing removal of focused node itself");
+      let button = getNode("button");
+      await setFocus(button);
+      await expectFocusAfterRemove(button, document);
+
+      info("Testing removal of focused node's parent");
+      let dialog = getNode("dialog");
+      let dialogButton = getNode("dialogButton");
+      await setFocus(dialogButton);
+      await expectFocusAfterRemove(dialog, document);
+
+      info("Testing removal of aria-activedescendant target");
+      let listbox = getNode("listbox");
+      let option = getNode("option");
+      await setFocus(listbox, option);
+      await expectFocusAfterRemove(option, listbox);
+
+      SimpleTest.finish();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTests);
+  </script>
+</head>
+
+<body>
+
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+
+  <button id="button"></button>
+
+  <div role="dialog" id="dialog">
+    <button id="dialogButton"></button>
+  </div>
+
+  <div role="listbox" id="listbox" tabindex="0" aria-activedescendant="option">
+    <div role="option" id="option"></div>
+  </div>
+
+</body>
+</html>
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -575,16 +575,26 @@ var gXPInstallObserver = {
         } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
           error += "Blocklisted";
           args = [install.name];
         } else {
           error += "Incompatible";
           args = [brandShortName, Services.appinfo.version, install.name];
         }
 
+        if (install.addon && !Services.policies.mayInstallAddon(install.addon)) {
+          error = "addonInstallBlockedByPolicy";
+          let extensionSettings = Services.policies.getExtensionSettings(install.addon.id);
+          let message = "";
+          if (extensionSettings && "blocked_install_message" in extensionSettings) {
+            message = " " + extensionSettings.blocked_install_message;
+          }
+          args = [install.name, install.addon.id, message];
+        }
+
         // Add Learn More link when refusing to install an unsigned add-on
         if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
           options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
         }
 
         messageString = gNavigatorBundle.getFormattedString(error, args);
 
         PopupNotifications.show(browser, notificationID, messageString, anchorID,
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -31,16 +31,17 @@ let ACTORS = {
 
 let LEGACY_ACTORS = {
   AboutLogins: {
     child: {
       matches: ["about:logins"],
       module: "resource:///actors/AboutLoginsChild.jsm",
       events: {
         "AboutLoginsDeleteLogin": {wantUntrusted: true},
+        "AboutLoginsOpenSite": {wantUntrusted: true},
         "AboutLoginsUpdateLogin": {wantUntrusted: true},
         "AboutLoginsInit": {wantUntrusted: true},
       },
       messages: [
         "AboutLogins:AllLogins",
         "AboutLogins:LoginAdded",
         "AboutLogins:LoginModified",
         "AboutLogins:LoginRemoved",
@@ -541,18 +542,19 @@ const listeners = {
     // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN AsyncPrefs.init
 
     "webrtc:UpdateGlobalIndicators": ["webrtcUI"],
     "webrtc:UpdatingIndicators": ["webrtcUI"],
   },
 
   mm: {
     "AboutLogins:DeleteLogin": ["AboutLoginsParent"],
+    "AboutLogins:OpenSite": ["AboutLoginsParent"],
+    "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "AboutLogins:UpdateLogin": ["AboutLoginsParent"],
-    "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "Content:Click": ["ContentClick"],
     "ContentSearch": ["ContentSearch"],
     "FormValidation:ShowPopup": ["FormValidationHandler"],
     "FormValidation:HidePopup": ["FormValidationHandler"],
     "PictureInPicture:Request": ["PictureInPicture"],
     "PictureInPicture:Close": ["PictureInPicture"],
     "PictureInPicture:Playing": ["PictureInPicture"],
     "PictureInPicture:Paused": ["PictureInPicture"],
--- a/browser/components/aboutlogins/AboutLoginsChild.jsm
+++ b/browser/components/aboutlogins/AboutLoginsChild.jsm
@@ -25,16 +25,20 @@ class AboutLoginsChild extends ActorChil
           cloneFunctions: true,
         });
         break;
       }
       case "AboutLoginsDeleteLogin": {
         this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {login: event.detail});
         break;
       }
+      case "AboutLoginsOpenSite": {
+        this.mm.sendAsyncMessage("AboutLogins:OpenSite", {login: event.detail});
+        break;
+      }
       case "AboutLoginsUpdateLogin": {
         this.mm.sendAsyncMessage("AboutLogins:UpdateLogin", {login: event.detail});
         break;
       }
     }
   }
 
   receiveMessage(message) {
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -56,16 +56,27 @@ var AboutLoginsParent = {
     }
 
     switch (message.name) {
       case "AboutLogins:DeleteLogin": {
         let login = LoginHelper.vanillaObjectToLogin(message.data.login);
         Services.logins.removeLogin(login);
         break;
       }
+      case "AboutLogins:OpenSite": {
+        let guid = message.data.login.guid;
+        let logins = LoginHelper.searchLoginsWithObject({guid});
+        if (!logins || logins.length != 1) {
+          log.warn(`AboutLogins:OpenSite: expected to find a login for guid: ${guid} but found ${(logins || []).length}`);
+          return;
+        }
+
+        message.target.ownerGlobal.openWebLinkIn(logins[0].hostname, "tab", {relatedToCurrent: true});
+        break;
+      }
       case "AboutLogins:Subscribe": {
         if (!ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers).length) {
           Services.obs.addObserver(this, "passwordmgr-storage-changed");
         }
         this._subscribers.add(message.target);
 
         let messageManager = message.target.messageManager;
         messageManager.sendAsyncMessage("AboutLogins:AllLogins", this.getAllLogins());
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/aboutLogins.css
@@ -0,0 +1,33 @@
+/* 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/. */
+
+body {
+  display: grid;
+  grid-template-columns: 280px 1fr;
+  grid-template-rows: 75px 1fr;
+  grid-template-areas: "header header"
+                       "logins login";
+  height: 100vh;
+}
+
+header {
+  display: flex;
+  grid-area: header;
+  background-color: var(--in-content-box-background);
+  border-bottom: 1px solid var(--in-content-box-border-color);
+}
+
+login-filter {
+  flex: auto;
+  align-self: center;
+}
+
+login-list {
+  grid-area: logins;
+}
+
+login-item {
+  grid-area: login;
+  max-width: 800px;
+}
--- a/browser/components/aboutlogins/content/aboutLogins.ftl
+++ b/browser/components/aboutlogins/content/aboutLogins.ftl
@@ -7,21 +7,31 @@
 ### and strings are likely to change often.
 
 ### Fluent isn't translating elements in the shadow DOM so the translated strings
 ### need to be applied to the composed node where they can be moved to the proper
 ### descendant after translation.
 
 about-logins-page-title = Login Manager
 
+login-filter =
+  .placeholder = Search Logins
+
 login-list =
-  .login-list-header = Logins
+  .count =
+    { $count ->
+        [one] { $count } entry
+       *[other] { $count } entries
+    }
 
 login-item =
   .cancel-button = Cancel
   .delete-button = Delete
+  .edit-button = Edit
   .hostname-label = Website Address
+  .modal-input-reveal-button = Toggle password visibility
+  .open-site-button = Launch
   .password-label = Password
   .save-changes-button = Save Changes
   .time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
   .time-changed = Last changed: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
   .time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
   .username-label = Username
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -4,66 +4,107 @@
 
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';"/>
     <title data-l10n-id="about-logins-page-title"></title>
     <link rel="localization" href="browser/aboutLogins.ftl">
+    <script defer="defer" src="chrome://browser/content/aboutlogins/components/reflected-fluent-element.js"></script>
+    <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-filter.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-item.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list-item.js"></script>
+    <script defer="defer" src="chrome://browser/content/aboutlogins/components/modal-input.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/aboutLogins.js"></script>
     <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+    <link rel="stylesheet" type="text/css" href="chrome://browser/content/aboutlogins/aboutLogins.css">
   </head>
   <body>
+    <header>
+      <login-filter data-l10n-id="login-filter"
+                    data-l10n-attrs="placeholder"></login-filter>
+    </header>
     <login-list data-l10n-id="login-list"
-                data-l10n-attrs="login-list-header"></login-list>
+                data-l10n-attrs="count"
+                data-l10n-args='{"count": 0}'></login-list>
     <login-item data-l10n-id="login-item"
                 data-l10n-args='{"timeCreated": 0, "timeChanged": 0, "timeUsed": 0}'
                 data-l10n-attrs="cancel-button,
                                  delete-button,
+                                 edit-button,
                                  hostname-label,
+                                 modal-input-reveal-button,
+                                 open-site-button,
                                  password-label,
                                  save-changes-button,
                                  time-created,
                                  time-changed,
                                  time-used,
                                  username-label"></login-item>
 
     <template id="login-list-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list.css">
-      <h2></h2>
+      <div class="meta">
+        <span class="count"></span>
+      </div>
       <ol>
       </ol>
     </template>
 
     <template id="login-list-item-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list-item.css">
       <span class="hostname"></span>
       <span class="username"></span>
     </template>
 
     <template id="login-item-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-item.css">
-      <h2 class="header"></h2>
-      <button class="delete-button"></button>
-      <label>
-        <span class="hostname-label field-label"></span>
-        <span class="hostname"/>
-      </label>
-      <label>
-        <span class="username-label field-label"></span>
-        <input name="username"/>
-      </label>
-      <label>
-        <span class="password-label field-label"></span>
-        <input type="password" name="password"/>
-      </label>
+      <div class="header">
+        <h2 class="title"></h2>
+        <button class="edit-button"></button>
+        <button class="delete-button"></button>
+      </div>
+      <div class="detail-row">
+        <label>
+          <span class="hostname-label field-label"></span>
+          <span class="hostname"/>
+        </label>
+        <button class="open-site-button"></button>
+      </div>
+      <div class="detail-row">
+        <label>
+          <span class="username-label field-label"></span>
+          <modal-input name="username"/>
+        </label>
+      </div>
+      <div class="detail-row">
+        <label>
+          <span class="password-label field-label"></span>
+          <modal-input type="password" name="password"/>
+        </label>
+      </div>
       <p class="time-created meta-info"></p>
       <p class="time-changed meta-info"></p>
       <p class="time-used meta-info"></p>
       <button class="save-changes-button"></button>
       <button class="cancel-button"></button>
     </template>
+
+    <template id="login-filter-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+      <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-filter.css">
+      <input type="text"/>
+    </template>
+
+    <template id="modal-input-template">
+      <link rel="stylesheet" type="text/css" href="chrome://global/skin/in-content/common.css">
+      <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/modal-input.css">
+      <span class="locked-value"></span>
+      <input type="text" class="unlocked-value"/>
+      <button class="reveal-button"/>
+    </template>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/login-filter.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+:host {
+  display: flex;
+}
+
+input {
+  margin: 18px;
+  flex: auto;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/login-filter.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+/* globals ReflectedFluentElement */
+
+class LoginFilter extends ReflectedFluentElement {
+  connectedCallback() {
+    if (this.children.length) {
+      return;
+    }
+
+    let loginFilterTemplate = document.querySelector("#login-filter-template");
+    this.attachShadow({mode: "open"})
+        .appendChild(loginFilterTemplate.content.cloneNode(true));
+
+    this.reflectFluentStrings();
+
+    this.addEventListener("input", this);
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "input": {
+        this.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+          bubbles: true,
+          composed: true,
+          detail: event.originalTarget.value,
+        }));
+        break;
+      }
+    }
+  }
+
+  static get reflectedFluentIDs() {
+    return ["placeholder"];
+  }
+
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
+  }
+
+  handleSpecialCaseFluentString(attrName) {
+    if (attrName != "placeholder") {
+      return false;
+    }
+
+    this.shadowRoot.querySelector("input").placeholder = this.getAttribute(attrName);
+    return true;
+  }
+}
+customElements.define("login-filter", LoginFilter);
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -1,27 +1,61 @@
 /* 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/. */
 
-h2 {
-  border-bottom: 1px solid var(--grey-30);
+:host {
+  padding-top: 36px;
+  padding-left: 40px;
+  padding-right: 40px;
+}
+
+:host(:not([editing])) .save-changes-button,
+:host(:not([editing])) .cancel-button {
+  display: none;
+}
+
+.header {
+  display: flex;
+  border-bottom: 1px solid var(--in-content-box-border-color);
+}
+
+.title {
+  margin-top: 0;
+  margin-bottom: 0;
+  flex: auto;
+}
+
+.detail-row {
+  display: flex;
+  margin-bottom: 20px;
+}
+
+.detail-row > label {
+  flex: auto;
+}
+
+.detail-row > button {
+  align-self: end;
 }
 
 .field-label {
   display: block;
+  font-size: smaller;
+  opacity: .7;
+  margin-bottom: 5px;
 }
 
 .meta-info {
   font-size: smaller;
 }
 
 .meta-info:not(:first-of-type) {
   margin-top: 0;
 }
 
 .meta-info:not(:last-of-type) {
   margin-bottom: 0;
 }
 
 .meta-info:first-of-type {
-  border-top: 1px solid var(--grey-30);
+  border-top: 1px solid var(--in-content-box-border-color);
 }
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -1,117 +1,139 @@
 /* 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/. */
 
-class LoginItem extends HTMLElement {
+/* globals ReflectedFluentElement */
+
+class LoginItem extends ReflectedFluentElement {
   constructor() {
     super();
     this._login = {};
   }
 
   connectedCallback() {
     if (this.children.length) {
       this.render();
       return;
     }
 
     let loginItemTemplate = document.querySelector("#login-item-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginItemTemplate.content.cloneNode(true));
 
+    this.reflectFluentStrings();
+
     for (let selector of [
       ".delete-button",
+      ".edit-button",
+      ".open-site-button",
       ".save-changes-button",
       ".cancel-button",
     ]) {
       let button = this.shadowRoot.querySelector(selector);
       button.addEventListener("click", this);
     }
 
     window.addEventListener("AboutLoginsLoginSelected", this);
 
     this.render();
   }
 
-  static get observedAttributes() {
+  static get reflectedFluentIDs() {
     return [
       "cancel-button",
       "delete-button",
+      "edit-button",
       "hostname-label",
+      "modal-input-reveal-button",
+      "open-site-button",
       "password-label",
       "save-changes-button",
       "time-created",
       "time-changed",
       "time-used",
       "username-label",
     ];
   }
 
-  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
-     need to get reflected in to their targeted element. */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
+  }
+
+  handleSpecialCaseFluentString(attrName) {
+    if (attrName != "modal-input-reveal-button") {
+      return false;
     }
 
-    // Strings that are reflected to their shadowed element are assigned
-    // to an attribute name that matches a className on the element.
-    let shadowedElement = this.shadowRoot.querySelector("." + attr);
-    shadowedElement.textContent = newValue;
+    this.shadowRoot.querySelector("modal-input[name='password']")
+                   .setAttribute("reveal-button", this.getAttribute(attrName));
+    return true;
   }
 
   render() {
     let l10nArgs = {
       timeCreated: this._login.timeCreated || "",
       timeChanged: this._login.timePasswordChanged || "",
       timeUsed: this._login.timeLastUsed || "",
     };
     document.l10n.setAttributes(this, "login-item", l10nArgs);
     let hostnameNoScheme = this._login.hostname && new URL(this._login.hostname).hostname;
-    this.shadowRoot.querySelector(".header").textContent = hostnameNoScheme || "";
+    this.shadowRoot.querySelector(".title").textContent = hostnameNoScheme || "";
     this.shadowRoot.querySelector(".hostname").textContent = this._login.hostname || "";
-    this.shadowRoot.querySelector("input[name='username']").value = this._login.username || "";
-    this.shadowRoot.querySelector("input[name='password']").value = this._login.password || "";
+    this.shadowRoot.querySelector("modal-input[name='username']").setAttribute("value", this._login.username || "");
+    this.shadowRoot.querySelector("modal-input[name='password']").setAttribute("value", this._login.password || "");
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "AboutLoginsLoginSelected": {
         this.setLogin(event.detail);
         break;
       }
       case "click": {
+        if (event.target.classList.contains("cancel-button")) {
+          this.toggleEditing();
+          this.render();
+          return;
+        }
         if (event.target.classList.contains("delete-button")) {
           document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
             bubbles: true,
             detail: this._login,
           }));
           return;
         }
+        if (event.target.classList.contains("edit-button")) {
+          this.toggleEditing();
+          return;
+        }
+        if (event.target.classList.contains("open-site-button")) {
+          document.dispatchEvent(new CustomEvent("AboutLoginsOpenSite", {
+            bubbles: true,
+            detail: this._login,
+          }));
+          return;
+        }
         if (event.target.classList.contains("save-changes-button")) {
           let loginUpdates = {
             guid: this._login.guid,
           };
-          let formUsername = this.shadowRoot.querySelector("input[name='username']").value.trim();
+          let formUsername = this.shadowRoot.querySelector("modal-input[name='username']").value.trim();
           if (formUsername != this._login.username) {
             loginUpdates.username = formUsername;
           }
-          let formPassword = this.shadowRoot.querySelector("input[name='password']").value.trim();
+          let formPassword = this.shadowRoot.querySelector("modal-input[name='password']").value.trim();
           if (formPassword != this._login.password) {
             loginUpdates.password = formPassword;
           }
           document.dispatchEvent(new CustomEvent("AboutLoginsUpdateLogin", {
             bubbles: true,
             detail: loginUpdates,
           }));
-          return;
-        }
-        if (event.target.classList.contains("cancel-button")) {
-          this.render();
         }
         break;
       }
     }
   }
 
   setLogin(login) {
     this._login = login;
@@ -119,20 +141,29 @@ class LoginItem extends HTMLElement {
   }
 
   loginModified(login) {
     if (login.guid != this._login.guid) {
       return;
     }
 
     this._login = login;
+    this.toggleEditing(false);
     this.render();
   }
 
   loginRemoved(login) {
     if (login.guid != this._login.guid) {
       return;
     }
     this._login = {};
     this.render();
   }
+
+  toggleEditing(force) {
+    let shouldEdit = force !== undefined ? force : !this.hasAttribute("editing");
+    this.shadowRoot.querySelector(".edit-button").disabled = shouldEdit;
+    this.shadowRoot.querySelectorAll("modal-input")
+                   .forEach(el => el.toggleAttribute("editing", shouldEdit));
+    this.toggleAttribute("editing", shouldEdit);
+  }
 }
 customElements.define("login-item", LoginItem);
--- a/browser/components/aboutlogins/content/components/login-list-item.css
+++ b/browser/components/aboutlogins/content/components/login-list-item.css
@@ -1,37 +1,35 @@
 /* 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/. */
 
 :host {
-  border-inline-start: 4px solid transparent;
-  border-bottom: 1px solid var(--grey-30);
   display: block;
   padding: 10px;
+  border-bottom: 1px solid var(--in-content-box-border-color);
+  border-inline-start: 4px solid transparent;
+}
+
+/* [hidden] isn't applying to elements in Shadow DOM. */
+:host([hidden]) {
+  display: none;
 }
 
 :host(:hover) {
-  background-color: var(--grey-90-a10);
+  background-color: var(--in-content-box-background-hover);
 }
 
 :host(:hover:active) {
-  background-color: var(--grey-90-a20);
+  background-color: var(--in-content-box-background-active);
 }
 
 :host(.selected) {
-  border-inline-start-color: var(--blue-40);
-  background-color: var(--grey-20);
-}
-
-:host(.selected:hover) {
-}
-
-:host(.selected:hover:active) {
-  background-color: var(--grey-30);
+  border-inline-start-color: var(--in-content-border-highlight);
+  background-color: var(--in-content-box-background-hover);
 }
 
 .hostname {
   font-weight: bold;
 }
 
 .hostname,
 .username {
--- a/browser/components/aboutlogins/content/components/login-list.css
+++ b/browser/components/aboutlogins/content/components/login-list.css
@@ -1,7 +1,27 @@
 /* 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/. */
 
+:host {
+  border-inline-end: 1px solid var(--in-content-box-border-color);
+  background-color: var(--in-content-box-background);
+}
+
+.meta {
+  display: flex;
+  padding: 10px;
+  border-bottom: 1px solid var(--in-content-box-border-color);
+  background-color: var(--in-content-box-info-background);
+}
+
+.count {
+  flex: auto;
+  text-align: end;
+  font-size: smaller;
+}
+
 ol {
+  margin-top: 0;
+  margin-bottom: 0;
   padding-inline-start: 0;
 }
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -1,72 +1,90 @@
 /* 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/. */
 
-/* globals LoginListItem */
+/* globals ReflectedFluentElement, LoginListItem */
 
-class LoginList extends HTMLElement {
+class LoginList extends ReflectedFluentElement {
   constructor() {
     super();
     this._logins = [];
     this._selectedItem = null;
   }
 
   connectedCallback() {
     if (this.children.length) {
       return;
     }
     let loginListTemplate = document.querySelector("#login-list-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginListTemplate.content.cloneNode(true));
+
+    this.reflectFluentStrings();
+
     this.render();
 
     window.addEventListener("AboutLoginsLoginSelected", this);
+    window.addEventListener("AboutLoginsFilterLogins", this);
   }
 
   render() {
     let list = this.shadowRoot.querySelector("ol");
     for (let login of this._logins) {
       list.append(new LoginListItem(login));
     }
+    document.l10n.setAttributes(this, "login-list", {count: this._logins.length});
   }
 
   handleEvent(event) {
     switch (event.type) {
+      case "AboutLoginsFilterLogins": {
+        let query = event.detail.toLocaleLowerCase();
+        let matchingLoginGuids;
+        if (query) {
+          matchingLoginGuids = this._logins.filter(login => {
+            return login.hostname.toLocaleLowerCase().includes(query) ||
+                   login.username.toLocaleLowerCase().includes(query);
+          }).map(login => login.guid);
+        } else {
+          matchingLoginGuids = this._logins.map(login => login.guid);
+        }
+        for (let listItem of this.shadowRoot.querySelectorAll("login-list-item")) {
+          if (matchingLoginGuids.includes(listItem.getAttribute("guid"))) {
+            if (listItem.hidden) {
+              listItem.hidden = false;
+            }
+          } else if (!listItem.hidden) {
+            listItem.hidden = true;
+          }
+        }
+        document.l10n.setAttributes(this, "login-list", {count: matchingLoginGuids.length});
+        break;
+      }
       case "AboutLoginsLoginSelected": {
         if (this._selectedItem) {
           if (this._selectedItem.getAttribute("guid") == event.detail.guid) {
             return;
           }
           this._selectedItem.classList.toggle("selected", false);
         }
         this._selectedItem = this.shadowRoot.querySelector(`login-list-item[guid="${event.detail.guid}"]`);
         this._selectedItem.classList.toggle("selected", true);
         break;
       }
     }
   }
 
-  static get observedAttributes() {
-    return ["login-list-header"];
+  static get reflectedFluentIDs() {
+    return ["count"];
   }
 
-  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
-     need to get reflected in to their targeted element. */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
-    }
-
-    switch (attr) {
-      case "login-list-header":
-        this.shadowRoot.querySelector("h2").textContent = newValue;
-        break;
-    }
+  static get observedAttributes() {
+    return this.reflectedFluentIDs;
   }
 
   setLogins(logins) {
     let list = this.shadowRoot.querySelector("ol");
     list.textContent = "";
     this._logins = logins;
     this.render();
   }
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/modal-input.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+:host([editing]) .locked-value,
+:host(:not([editing])) .unlocked-value {
+  display: none;
+}
+
+:host(:not([type="password"])) .reveal-button {
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/modal-input.js
@@ -0,0 +1,116 @@
+/* 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/. */
+
+/* globals ReflectedFluentElement */
+
+class ModalInput extends ReflectedFluentElement {
+  static get LOCKED_PASSWORD_DISPLAY() {
+    return "••••••••";
+  }
+
+  connectedCallback() {
+    if (this.children.length) {
+      return;
+    }
+
+    let modalInputTemplate = document.querySelector("#modal-input-template");
+    this.attachShadow({mode: "open"})
+        .appendChild(modalInputTemplate.content.cloneNode(true));
+
+    if (this.hasAttribute("value")) {
+      this.value = this.getAttribute("value");
+    }
+
+    if (this.getAttribute("type") == "password") {
+      let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+      unlockedValue.setAttribute("type", "password");
+    }
+
+    this.shadowRoot.querySelector(".reveal-button").addEventListener("click", this);
+  }
+
+  static get reflectedFluentIDs() {
+    return ["reveal-button"];
+  }
+
+  static get observedAttributes() {
+    return ["editing", "type", "value"].concat(ModalInput.reflectedFluentIDs);
+  }
+
+  attributeChangedCallback(attr, oldValue, newValue) {
+    super.attributeChangedCallback(attr, oldValue, newValue);
+
+    if (!this.shadowRoot) {
+      return;
+    }
+
+    let lockedValue = this.shadowRoot.querySelector(".locked-value");
+    let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+
+    switch (attr) {
+      case "editing": {
+        let isEditing = newValue !== null;
+        if (!isEditing) {
+          this.setAttribute("value", unlockedValue.value);
+        }
+        break;
+      }
+      case "type": {
+        if (newValue == "password") {
+          lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+          unlockedValue.setAttribute("type", "password");
+        } else {
+          lockedValue.textContent = this.getAttribute("value");
+          unlockedValue.setAttribute("type", "text");
+        }
+        break;
+      }
+      case "value": {
+        this.value = newValue;
+        break;
+      }
+    }
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "click": {
+        if (event.target.classList.contains("reveal-button")) {
+          let lockedValue = this.shadowRoot.querySelector(".locked-value");
+          let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
+          let editing = this.hasAttribute("editing");
+          if ((editing && unlockedValue.getAttribute("type") == "password") ||
+              (!editing && lockedValue.textContent == this.constructor.LOCKED_PASSWORD_DISPLAY)) {
+            lockedValue.textContent = this.value;
+            unlockedValue.setAttribute("type", "text");
+          } else {
+            lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+            unlockedValue.setAttribute("type", "password");
+          }
+        }
+        break;
+      }
+    }
+  }
+
+  get value() {
+    return this.hasAttribute("editing") ? this.shadowRoot.querySelector(".unlocked-value").value.trim()
+                                        : this.getAttribute("value") || "";
+  }
+
+  set value(val) {
+    if (this.getAttribute("value") != val) {
+      this.setAttribute("value", val);
+      return;
+    }
+    this.shadowRoot.querySelector(".unlocked-value").value = val;
+    let lockedValue = this.shadowRoot.querySelector(".locked-value");
+    if (this.getAttribute("type") == "password" && val && val.length) {
+      lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
+    } else {
+      lockedValue.textContent = val;
+    }
+  }
+}
+customElements.define("modal-input", ModalInput);
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/reflected-fluent-element.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+class ReflectedFluentElement extends HTMLElement {
+  _isReflectedAttributePresent(attr) {
+    return this.constructor.reflectedFluentIDs.includes(attr.name);
+  }
+
+  /* This function should be called to apply any localized strings that Fluent
+     may have applied to the element before the custom element was defined. */
+  reflectFluentStrings() {
+    for (let reflectedFluentID of this.constructor.reflectedFluentIDs) {
+      if (this.hasAttribute(reflectedFluentID)) {
+        if (this.handleSpecialCaseFluentString &&
+            this.handleSpecialCaseFluentString(reflectedFluentID)) {
+          continue;
+        }
+
+        let attrValue = this.getAttribute(reflectedFluentID);
+        // Strings that are reflected to their shadowed element are assigned
+        // to an attribute name that matches a className on the element.
+        let shadowedElement = this.shadowRoot.querySelector("." + reflectedFluentID);
+        shadowedElement.textContent = attrValue;
+      }
+    }
+  }
+
+  /* Fluent doesn't handle localizing into Shadow DOM yet so strings
+     need to get reflected in to their targeted element. */
+  attributeChangedCallback(attr, oldValue, newValue) {
+    if (!this.shadowRoot) {
+      return;
+    }
+
+    // Don't respond to attribute changes that aren't related to locale text.
+    if (!this.constructor.reflectedFluentIDs.includes(attr)) {
+      return;
+    }
+
+    if (this.handleSpecialCaseFluentString &&
+        this.handleSpecialCaseFluentString(attr)) {
+      return;
+    }
+
+    // Strings that are reflected to their shadowed element are assigned
+    // to an attribute name that matches a className on the element.
+    let shadowedElement = this.shadowRoot.querySelector("." + attr);
+    shadowedElement.textContent = newValue;
+  }
+}
+customElements.define("reflected-fluent-element", ReflectedFluentElement);
--- a/browser/components/aboutlogins/jar.mn
+++ b/browser/components/aboutlogins/jar.mn
@@ -1,13 +1,19 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 browser.jar:
+  content/browser/aboutlogins/components/login-filter.css      (content/components/login-filter.css)
+  content/browser/aboutlogins/components/login-filter.js       (content/components/login-filter.js)
   content/browser/aboutlogins/components/login-item.css        (content/components/login-item.css)
   content/browser/aboutlogins/components/login-item.js         (content/components/login-item.js)
   content/browser/aboutlogins/components/login-list.css        (content/components/login-list.css)
   content/browser/aboutlogins/components/login-list.js         (content/components/login-list.js)
   content/browser/aboutlogins/components/login-list-item.css   (content/components/login-list-item.css)
   content/browser/aboutlogins/components/login-list-item.js    (content/components/login-list-item.js)
+  content/browser/aboutlogins/components/modal-input.css       (content/components/modal-input.css)
+  content/browser/aboutlogins/components/modal-input.js        (content/components/modal-input.js)
+  content/browser/aboutlogins/components/reflected-fluent-element.js  (content/components/reflected-fluent-element.js)
+  content/browser/aboutlogins/aboutLogins.css   (content/aboutLogins.css)
   content/browser/aboutlogins/aboutLogins.js    (content/aboutLogins.js)
   content/browser/aboutlogins/aboutLogins.html  (content/aboutLogins.html)
--- a/browser/components/aboutlogins/tests/browser/browser.ini
+++ b/browser/components/aboutlogins/tests/browser/browser.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 prefs =
   signon.management.page.enabled=true
 
 [browser_deleteLogin.js]
 [browser_loginListChanges.js]
+[browser_openSite.js]
 [browser_updateLogin.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openSite.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+                                             Ci.nsILoginInfo, "init");
+const LOGIN_URL = "https://example.com/";
+let TEST_LOGIN1 = new nsLoginInfo(LOGIN_URL, LOGIN_URL, null, "user1", "pass1", "username", "password");
+
+add_task(async function setup() {
+  let storageChangedPromised = TestUtils.topicObserved("passwordmgr-storage-changed",
+                                                       (_, data) => data == "addLogin");
+  TEST_LOGIN1 = Services.logins.addLogin(TEST_LOGIN1);
+  await storageChangedPromised;
+  await BrowserTestUtils.openNewForegroundTab({gBrowser, url: "about:logins"});
+  registerCleanupFunction(() => {
+    BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    Services.logins.removeAllLogins();
+  });
+});
+
+add_task(async function test_launch_login_item() {
+  let promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser, LOGIN_URL);
+
+  let browser = gBrowser.selectedBrowser;
+  await ContentTask.spawn(browser, LoginHelper.loginToVanillaObject(TEST_LOGIN1), async (login) => {
+    let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+    loginItem.setLogin(login);
+    let openSiteButton = loginItem.shadowRoot.querySelector(".open-site-button");
+    openSiteButton.click();
+  });
+
+  info("waiting for new tab to get opened");
+  let newTab = await promiseNewTab;
+  ok(true, "New tab opened to " + LOGIN_URL);
+
+  BrowserTestUtils.removeTab(newTab);
+});
--- a/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
@@ -39,28 +39,35 @@ add_task(async function test_login_item(
 
     let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
     let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
       return loginItem._login.guid == loginListItem.getAttribute("guid") &&
              loginItem._login.guid == login.guid;
     }, "Waiting for login item to get populated");
     ok(loginItemPopulated, "The login item should get populated");
 
-    let usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
-    let passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
+    let usernameInput = loginItem.shadowRoot.querySelector("modal-input[name='username']");
+    let passwordInput = loginItem.shadowRoot.querySelector("modal-input[name='password']");
+
+    let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+    editButton.click();
+    await Promise.resolve();
 
     usernameInput.value += "-undome";
     passwordInput.value += "-undome";
 
     let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
     cancelButton.click();
     await Promise.resolve();
     is(usernameInput.value, login.username, "Username change should be reverted");
     is(passwordInput.value, login.password, "Password change should be reverted");
 
+    editButton.click();
+    await Promise.resolve();
+
     usernameInput.value += "-saveme";
     passwordInput.value += "-saveme";
 
     let saveChangesButton = loginItem.shadowRoot.querySelector(".save-changes-button");
     saveChangesButton.click();
 
     await ContentTaskUtils.waitForCondition(() => {
       return loginListItem._login.username == usernameInput.value &&
--- a/browser/components/aboutlogins/tests/mochitest/mochitest.ini
+++ b/browser/components/aboutlogins/tests/mochitest/mochitest.ini
@@ -1,10 +1,16 @@
 [DEFAULT]
 support-files =
    ../../content/aboutLogins.html
+   ../../content/components/login-filter.js
    ../../content/components/login-item.js
    ../../content/components/login-list.js
    ../../content/components/login-list-item.js
+   ../../content/components/modal-input.js
+   ../../content/components/reflected-fluent-element.js
    aboutlogins_common.js
 
+[test_login_filter.html]
 [test_login_item.html]
 [test_login_list.html]
+[test_modal_input.html]
+[test_reflected_fluent_element.html]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_filter.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the login-filter component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the login-filter component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="/tests/SimpleTest/EventUtils.js"></script>
+  <script src="reflected-fluent-element.js"></script>
+  <script src="login-filter.js"></script>
+  <script src="login-list-item.js"></script>
+  <script src="login-list.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the login-filter component **/
+
+let gLoginFilter;
+let gLoginList;
+add_task(async function setup() {
+  stubFluentL10n({
+    "count": "count",
+  });
+
+  let templateFrame = document.getElementById("templateFrame");
+  let displayEl = document.getElementById("display");
+  importDependencies(templateFrame, displayEl);
+
+  gLoginFilter = document.createElement("login-filter");
+  displayEl.appendChild(gLoginFilter);
+
+  gLoginList = document.createElement("login-list");
+  displayEl.appendChild(gLoginList);
+});
+
+add_task(async function test_empty_filter() {
+  ok(gLoginFilter, "loginFilter exists");
+  is(gLoginFilter.shadowRoot.querySelector("input").value, "", "Initially empty");
+});
+
+add_task(async function test_input_events() {
+  let filterEvent = null;
+  window.addEventListener("AboutLoginsFilterLogins", event => filterEvent = event);
+  let input = SpecialPowers.wrap(gLoginFilter.shadowRoot.querySelector("input"));
+  input.setUserInput("test");
+  ok(filterEvent, "Filter event received");
+  is(filterEvent.detail, "test", "Event includes input value");
+});
+
+add_task(async function test_list_filtered() {
+  const LOGINS = [{
+    guid: "123456789",
+    hostname: "https://example.com",
+    username: "user1",
+    password: "pass1",
+  }, {
+    guid: "987654321",
+    hostname: "https://example.com",
+    username: "user2",
+    password: "pass2",
+  }];
+  gLoginList.setLogins(LOGINS);
+
+  let tests = [
+    ["", 2],
+    [LOGINS[0].username, 1],
+    [LOGINS[0].username + "-notfound", 0],
+    [LOGINS[0].username.substr(2, 3), 1],
+    ["", 2],
+    // The password is not used for search.
+    [LOGINS[0].password, 0],
+    [LOGINS[0].password + "-notfound", 0],
+    [LOGINS[0].password.substr(2, 3), 0],
+    ["", 2],
+    [LOGINS[0].hostname, 2],
+    [LOGINS[0].hostname + "-notfound", 0],
+    [LOGINS[0].hostname.substr(2, 3), 2],
+    ["", 2],
+    // The guid is not used for search.
+    [LOGINS[0].guid, 0],
+    [LOGINS[0].guid + "-notfound", 0],
+    [LOGINS[0].guid.substr(0, 2), 0],
+    ["", 2],
+  ];
+
+  let loginFilterInput = gLoginFilter.shadowRoot.querySelector("input");
+  loginFilterInput.focus();
+
+  for (let i = 0; i < tests.length; i++) {
+    info("Testcase: " + i);
+
+    let testObj = {
+      testCase: i,
+      query: tests[i][0],
+      resultExpectedCount: tests[i][1],
+    };
+
+    let filterLength = loginFilterInput.value.length;
+    while (filterLength-- > 0) {
+      sendKey("BACK_SPACE");
+    }
+    sendString(testObj.query);
+
+    await SimpleTest.promiseWaitForCondition(() => {
+      return gLoginList.hasAttribute("count") &&
+             +gLoginList.getAttribute("count") == testObj.resultExpectedCount;
+    }, `Waiting for the search result count to update to ${testObj.resultExpectedCount} (tc#${testObj.testCase})`);
+    let count = +gLoginList.getAttribute("count");
+    is(count, testObj.resultExpectedCount,
+       `The login list count should match the expected result (tc#${testObj.testCase})`);
+  }
+});
+</script>
+
+</body>
+</html>
--- a/browser/components/aboutlogins/tests/mochitest/test_login_item.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_item.html
@@ -2,17 +2,19 @@
 <html>
 <!--
 Test the login-item component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-item component</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
   <script src="login-item.js"></script>
+  <script src="modal-input.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
   </p>
 <div id="content" style="display: none">
@@ -48,81 +50,81 @@ add_task(async function setup() {
 
   gLoginItem = document.createElement("login-item");
   displayEl.appendChild(gLoginItem);
 });
 
 add_task(async function test_empty_item() {
   ok(gLoginItem, "loginItem exists");
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be blank");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be blank");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, "", "username should be blank");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, "", "password should be blank");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank");
 });
 
 add_task(async function test_set_login() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be populated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
 });
 
 add_task(async function test_different_login_modified() {
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginModified(otherLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_different_login_removed() {
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginRemoved(otherLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_login_modified() {
   let modifiedLogin = Object.assign({}, TEST_LOGIN_1, {username: "updateduser"});
   gLoginItem.loginModified(modifiedLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, modifiedLogin.hostname, "hostname should be updated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, modifiedLogin.password, "password should be updated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, modifiedLogin.username, "username should be updated");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, modifiedLogin.password, "password should be updated");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, modifiedLogin.timeCreated, "time-created should be updated");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, modifiedLogin.timePasswordChanged, "time-changed should be updated");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, modifiedLogin.timeLastUsed, "time-used should be updated");
 });
 
 add_task(async function test_login_removed() {
   gLoginItem.loginRemoved(TEST_LOGIN_1);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be cleared");
-  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared");
-  is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be cleared");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, "", "username should be cleared");
+  is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, "", "password should be cleared");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be cleared");
   is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be cleared");
   is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be cleared");
 });
 
 </script>
 
 </body>
--- a/browser/components/aboutlogins/tests/mochitest/test_login_list.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_list.html
@@ -2,16 +2,17 @@
 <html>
 <!--
 Test the login-list component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-list component</title>
   <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
   <script src="login-list-item.js"></script>
   <script src="login-list.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
@@ -35,16 +36,20 @@ const TEST_LOGIN_1 = {
 const TEST_LOGIN_2 = {
   guid: "987654321",
   hostname: "https://example.com",
   username: "user2",
   password: "pass2",
 };
 
 add_task(async function setup() {
+  stubFluentL10n({
+    "count": "count",
+  });
+
   let templateFrame = document.getElementById("templateFrame");
   let displayEl = document.getElementById("display");
   importDependencies(templateFrame, displayEl);
 
   gLoginList = document.createElement("login-list");
   displayEl.appendChild(gLoginList);
 });
 
@@ -66,16 +71,64 @@ add_task(async function test_populated_l
 
   ok(!loginListItems[0].classList.contains("selected"), "The first item should not be selected by default");
   ok(!loginListItems[1].classList.contains("selected"), "The second item should not be selected by default");
   loginListItems[0].click();
   ok(loginListItems[0].classList.contains("selected"), "The first item should be selected");
   ok(!loginListItems[1].classList.contains("selected"), "The second item should still not be selected");
 });
 
+add_task(async function test_filtered_list() {
+  is(gLoginList.shadowRoot.querySelectorAll("login-list-item:not([hidden])").length, 2, "Both logins should be visible");
+  let countSpan = gLoginList.shadowRoot.querySelector(".count");
+  is(countSpan.textContent, "2", "Count should match full list length");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "user1",
+  }));
+  is(countSpan.textContent, "1", "Count should match result amount");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
+  is(loginListItems[0].shadowRoot.querySelector(".username").textContent, "user1", "user1 is expected first");
+  ok(!loginListItems[0].hidden, "user1 should remain visible");
+  ok(loginListItems[1].hidden, "user2 should be hidden");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "user2",
+  }));
+  is(countSpan.textContent, "1", "Count should match result amount");
+  ok(loginListItems[0].hidden, "user1 should be hidden");
+  ok(!loginListItems[1].hidden, "user2 should be visible");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "user",
+  }));
+  is(countSpan.textContent, "2", "Count should match result amount");
+  ok(!loginListItems[0].hidden, "user1 should be visible");
+  ok(!loginListItems[1].hidden, "user2 should be visible");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "foo",
+  }));
+  is(countSpan.textContent, "0", "Count should match result amount");
+  ok(loginListItems[0].hidden, "user1 should be hidden");
+  ok(loginListItems[1].hidden, "user2 should be hidden");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    composed: true,
+    detail: "",
+  }));
+  is(countSpan.textContent, "2", "Count should be reset to full list length");
+  ok(!loginListItems[0].hidden, "user1 should be visible");
+  ok(!loginListItems[1].hidden, "user2 should be visible");
+});
+
 add_task(async function test_login_modified() {
   let modifiedLogin = Object.assign(TEST_LOGIN_1, {username: "user11"});
   gLoginList.loginModified(modifiedLogin);
   await asyncElementRendered();
   let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   is(loginListItems.length, 2, "Both logins should be displayed");
   is(loginListItems[0].getAttribute("guid"), TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
   is(loginListItems[0].shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname,
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_modal_input.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the modal-input component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the modal-input component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
+  <script src="modal-input.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the modal-input component **/
+
+let gModalInput;
+const TEST_INPUT_VALUE = "fakeValue";
+add_task(async function setup() {
+  let templateFrame = document.getElementById("templateFrame");
+  let displayEl = document.getElementById("display");
+  importDependencies(templateFrame, displayEl);
+
+  gModalInput = document.createElement("modal-input");
+  gModalInput.setAttribute("value", TEST_INPUT_VALUE);
+  displayEl.appendChild(gModalInput);
+});
+
+add_task(async function test_initial_state() {
+  ok(gModalInput, "modalInput exists");
+  is(gModalInput.shadowRoot.querySelector(".locked-value").textContent, TEST_INPUT_VALUE, "Values are set initially");
+  is(gModalInput.shadowRoot.querySelector(".unlocked-value").value, TEST_INPUT_VALUE, "Values are set initially");
+  is(getComputedStyle(gModalInput.shadowRoot.querySelector(".locked-value")).display, "inline", ".locked-value is visible by default");
+  is(getComputedStyle(gModalInput.shadowRoot.querySelector(".unlocked-value")).display, "none", ".unlocked-value is hidden by default");
+});
+
+add_task(async function test_editing_set_unset() {
+  let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
+  let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
+  gModalInput.setAttribute("editing", "");
+  is(getComputedStyle(lockedValue).display, "none", ".locked-value is hidden when editing");
+  is(getComputedStyle(unlockedValue).display, "inline", ".unlocked-value is visible when editing");
+
+  const NEW_VALUE = "editedValue";
+  SpecialPowers.wrap(unlockedValue).setUserInput(NEW_VALUE);
+  gModalInput.removeAttribute("editing");
+
+  is(lockedValue.textContent, NEW_VALUE, "Values are updated from edit");
+  is(unlockedValue.value, NEW_VALUE, "Values are updated from edit");
+  is(gModalInput.getAttribute("value"), NEW_VALUE, "The value attribute on the host element is updated from edit");
+  is(getComputedStyle(lockedValue).display, "inline", ".locked-value is visible when not editing");
+  is(getComputedStyle(unlockedValue).display, "none", ".unlocked-value is hidden when not editing");
+});
+
+add_task(async function test_password() {
+  gModalInput.setAttribute("type", "password");
+  let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
+  let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
+
+  is(lockedValue.textContent, gModalInput.constructor.LOCKED_PASSWORD_DISPLAY,
+     "type=password should display masked characters when locked");
+  is(unlockedValue.value, gModalInput.getAttribute("value"), "type=password should have actual value in .unlocked-value");
+  is(unlockedValue.getAttribute("type"), "password", "input[type=password] should be used for .unlocked-value with type=password");
+
+  gModalInput.removeAttribute("value");
+  is(lockedValue.textContent, "",
+     "type=password should display nothing when locked without a value (.locked-value)");
+  is(unlockedValue.value, "",
+     "type=password should display nothing when locked without a value (.unlocked-value)");
+});
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/mochitest/test_reflected_fluent_element.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the reflected-fluent-element component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the reflected-fluent-element component</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="reflected-fluent-element.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the reflected-fluent-element component **/
+
+const TEST_STRINGS = {
+  loginFilter: {
+    placeholder: "Sample placeholder",
+  },
+  loginItem: {
+    "cancel-button": "Cancel",
+    "delete-button": "Delete",
+    "hostname-label": "Website Address",
+    "password-label": "Password",
+    "save-changes-button": "Save Changes",
+    // See stubFluentL10n for the following three
+    "time-created": "",
+    "time-changed": "",
+    "time-used": "",
+    "username-label": "Username",
+  },
+};
+
+let gLoginFilter;
+let gLoginItem;
+add_task(async function setup() {
+  stubFluentL10n({
+    "time-created": "timeCreated",
+    "time-changed": "timeChanged",
+    "time-used": "timeUsed",
+  });
+
+  let displayEl = document.getElementById("display");
+
+  // Create and append the login-filter element before its template
+  // is cloned the custom element defined.
+  gLoginFilter = document.createElement("login-filter");
+  gLoginFilter.setAttribute("placeholder", TEST_STRINGS.loginFilter.placeholder);
+  displayEl.appendChild(gLoginFilter);
+
+  // ... and do the same with the login-item.
+  gLoginItem = document.createElement("login-item");
+  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
+    gLoginItem.setAttribute(attrKey, TEST_STRINGS.loginItem[attrKey]);
+  }
+  displayEl.appendChild(gLoginItem);
+
+  let templateFrame = document.getElementById("templateFrame");
+  importDependencies(templateFrame, displayEl);
+
+  // The script needs to be inserted after the element and template are appended
+  // to match the environment of the locale text being applied before the custom
+  // element is defined.
+  for (let scriptSrc of ["login-filter.js", "login-item.js", "login-list.js"]) {
+    let scriptEl = document.createElement("script");
+    scriptEl.setAttribute("src", scriptSrc);
+    document.head.appendChild(scriptEl);
+  }
+});
+
+add_task(async function test_placeholder_on_login_filter() {
+  ok(gLoginFilter, "loginFilter exists");
+  await SimpleTest.promiseWaitForCondition(() => !!gLoginFilter.shadowRoot, "Wait for shadowRoot");
+  is(gLoginFilter.shadowRoot.querySelector("input").placeholder,
+     TEST_STRINGS.loginFilter.placeholder,
+     "Placeholder text should be present when set before the element is defined");
+});
+
+add_task(async function test_login_item() {
+  ok(gLoginItem, "loginItem exists");
+  await SimpleTest.promiseWaitForCondition(() => !!gLoginItem.shadowRoot, "Wait for shadowRoot");
+
+  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
+    let selector = "." + attrKey;
+    is(gLoginItem.shadowRoot.querySelector(selector).textContent,
+       TEST_STRINGS.loginItem[attrKey],
+       selector + " textContent should be present when set before the element is defined");
+  }
+});
+
+add_task(async function test_attribute_changed_callback() {
+  let displayEl = document.getElementById("display");
+  let loginList = document.createElement("login-list");
+  displayEl.appendChild(loginList);
+  await SimpleTest.promiseWaitForCondition(() => !!loginList.shadowRoot, "Wait for element to get templated");
+
+  loginList.setAttribute("count", "1234");
+  await SimpleTest.promiseWaitForCondition(() => loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
+                                           "Wait for text to get localized");
+  ok(loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
+     "The count attribute should be inherited by the .count span");
+});
+</script>
+
+</body>
+</html>
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -537,89 +537,129 @@ var Policies = {
           Services.prefs.clearUserPref("browser.policies.runOncePerModification.extensionsInstall");
           let addons = await AddonManager.getAddonsByIDs(param.Uninstall);
           for (let addon of addons) {
             if (addon) {
               try {
                 await addon.uninstall();
               } catch (e) {
                 // This can fail for add-ons that can't be uninstalled.
-                // Just ignore.
+                log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`);
               }
             }
           }
         });
       }
       if ("Install" in param) {
         runOncePerModification("extensionsInstall", JSON.stringify(param.Install), async () => {
           await uninstallingPromise;
           for (let location of param.Install) {
-            let url;
-            if (location.includes("://")) {
-              // Assume location is an URI
-              url = location;
-            } else {
+            let uri;
+            try {
+              uri = Services.io.newURI(location);
+            } catch (e) {
+              // If it's not a URL, it's probably a file path.
               // Assume location is a file path
-              let xpiFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+              // This is done for legacy support (old API)
               try {
-                xpiFile.initWithPath(location);
-              } catch (e) {
+                let xpiFile = new FileUtils.File(location);
+                uri = Services.io.newFileURI(xpiFile);
+              } catch (ex) {
                 log.error(`Invalid extension path location - ${location}`);
-                continue;
-              }
-              url = Services.io.newFileURI(xpiFile).spec;
-            }
-            AddonManager.getInstallForURL(url, {
-              telemetryInfo: {source: "enterprise-policy"},
-            }).then(install => {
-              if (install.addon && install.addon.appDisabled) {
-                log.error(`Incompatible add-on - ${location}`);
-                install.cancel();
                 return;
               }
-              let listener = {
-              /* eslint-disable-next-line no-shadow */
-                onDownloadEnded: (install) => {
-                  if (install.addon && install.addon.appDisabled) {
-                    log.error(`Incompatible add-on - ${location}`);
-                    install.removeListener(listener);
-                    install.cancel();
-                  }
-                },
-                onDownloadFailed: () => {
-                  install.removeListener(listener);
-                  log.error(`Download failed - ${location}`);
-                  clearRunOnceModification("extensionsInstall");
-                },
-                onInstallFailed: () => {
-                  install.removeListener(listener);
-                  log.error(`Installation failed - ${location}`);
-                },
-                onInstallEnded: () => {
-                  install.removeListener(listener);
-                  log.debug(`Installation succeeded - ${location}`);
-                },
-              };
-              install.addListener(listener);
-              install.install();
-            });
+            }
+            installAddonFromURL(uri.spec);
           }
         });
       }
       if ("Locked" in param) {
         for (let ID of param.Locked) {
-          manager.disallowFeature(`modify-extension:${ID}`);
+          manager.disallowFeature(`uninstall-extension:${ID}`);
+          manager.disallowFeature(`disable-extension:${ID}`);
         }
       }
     },
   },
 
   "ExtensionSettings": {
     onBeforeAddons(manager, param) {
-      manager.setExtensionSettings(param);
+      try {
+        manager.setExtensionSettings(param);
+      } catch (e) {
+       log.error("Invalid ExtensionSettings");
+      }
+    },
+    async onBeforeUIStartup(manager, param) {
+      let extensionSettings = param;
+      let blockAllExtensions = false;
+      if ("*" in extensionSettings) {
+        if ("installation_mode" in extensionSettings["*"] &&
+            extensionSettings["*"].installation_mode == "blocked") {
+          blockAllExtensions = true;
+          // Turn off discovery pane in about:addons
+          setAndLockPref("extensions.getAddons.showPane", false);
+          // Block about:debugging
+          blockAboutPage(manager, "about:debugging");
+        }
+      }
+      let {addons} = await AddonManager.getActiveAddons();
+      let allowedExtensions = [];
+      for (let extensionID in extensionSettings) {
+        if (extensionID == "*") {
+          // Ignore global settings
+          continue;
+        }
+        if ("installation_mode" in extensionSettings[extensionID]) {
+          if (extensionSettings[extensionID].installation_mode == "force_installed" ||
+              extensionSettings[extensionID].installation_mode == "normal_installed") {
+            if (!extensionSettings[extensionID].install_url) {
+              throw new Error(`Missing install_url for ${extensionID}`);
+            }
+            if (!addons.find(addon => addon.id == extensionID)) {
+              installAddonFromURL(extensionSettings[extensionID].install_url, extensionID);
+            }
+            manager.disallowFeature(`uninstall-extension:${extensionID}`);
+            if (extensionSettings[extensionID].installation_mode == "force_installed") {
+              manager.disallowFeature(`disable-extension:${extensionID}`);
+            }
+            allowedExtensions.push(extensionID);
+          } else if (extensionSettings[extensionID].installation_mode == "allowed") {
+            allowedExtensions.push(extensionID);
+          } else if (extensionSettings[extensionID].installation_mode == "blocked") {
+            if (addons.find(addon => addon.id == extensionID)) {
+              // Can't use the addon from getActiveAddons since it doesn't have uninstall.
+              let addon = await AddonManager.getAddonByID(extensionID);
+              try {
+                await addon.uninstall();
+              } catch (e) {
+                // This can fail for add-ons that can't be uninstalled.
+                log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`);
+              }
+            }
+          }
+        }
+      }
+      if (blockAllExtensions) {
+        for (let addon of addons) {
+          if (addon.isSystem || addon.isBuiltin) {
+            continue;
+          }
+          if (!allowedExtensions.includes(addon.id)) {
+            try {
+              // Can't use the addon from getActiveAddons since it doesn't have uninstall.
+              let addonToUninstall = await AddonManager.getAddonByID(addon.id);
+              await addonToUninstall.uninstall();
+            } catch (e) {
+              // This can fail for add-ons that can't be uninstalled.
+              log.debug(`Add-on ID (${addon.id}) couldn't be uninstalled.`);
+            }
+          }
+        }
+      }
     },
   },
 
   "ExtensionUpdate": {
     onBeforeAddons(manager, param) {
       if (!param) {
         setAndLockPref("extensions.update.enabled", param);
       }
@@ -1299,16 +1339,64 @@ function clearRunOnceModification(action
 
 function replacePathVariables(path) {
   if (path.includes("${home}")) {
     return path.replace("${home}", FileUtils.getFile("Home", []).path);
   }
   return path;
 }
 
+/**
+ * installAddonFromURL
+ *
+ * Helper function that installs an addon from a URL
+ * and verifies that the addon ID matches.
+*/
+function installAddonFromURL(url, extensionID) {
+  AddonManager.getInstallForURL(url, {
+    telemetryInfo: {source: "enterprise-policy"},
+  }).then(install => {
+    if (install.addon && install.addon.appDisabled) {
+      log.error(`Incompatible add-on - ${location}`);
+      install.cancel();
+      return;
+    }
+    let listener = {
+    /* eslint-disable-next-line no-shadow */
+      onDownloadEnded: (install) => {
+        if (extensionID && install.addon.id != extensionID) {
+          log.error(`Add-on downloaded from ${url} had unexpected id (got ${install.addon.id} expected ${extensionID})`);
+          install.removeListener(listener);
+          install.cancel();
+        }
+        if (install.addon && install.addon.appDisabled) {
+          log.error(`Incompatible add-on - ${url}`);
+          install.removeListener(listener);
+          install.cancel();
+        }
+      },
+      onDownloadFailed: () => {
+        install.removeListener(listener);
+        log.error(`Download failed - ${url}`);
+        clearRunOnceModification("extensionsInstall");
+      },
+      onInstallFailed: () => {
+        install.removeListener(listener);
+        log.error(`Installation failed - ${url}`);
+      },
+      onInstallEnded: () => {
+        install.removeListener(listener);
+        log.debug(`Installation succeeded - ${url}`);
+      },
+    };
+    install.addListener(listener);
+    install.install();
+  });
+}
+
 let gChromeURLSBlocked = false;
 
 // If any about page is blocked, we block the loading of all
 // chrome:// URLs in the browser window.
 function blockAboutPage(manager, feature, neededOnContentProcess = false) {
   manager.disallowFeature(feature, neededOnContentProcess);
   if (!gChromeURLSBlocked) {
     blockAllChromeURLs();
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -318,20 +318,54 @@
             "type": "string"
           }
         }
       }
     },
 
     "ExtensionSettings": {
       "type": "object",
+      "properties": {
+        "*": {
+          "type": "object",
+          "properties": {
+            "installation_mode": {
+              "type": "string",
+              "enum": ["allowed", "blocked"]
+            },
+            "allowed_types": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "enum": ["extension", "dictionary", "locale", "theme"]
+              }
+            },
+            "blocked_install_message": {
+              "type": "string"
+            },
+            "install_sources": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          }
+        }
+      },
       "patternProperties": {
         "^.*$": {
           "type": "object",
           "properties": {
+            "installation_mode": {
+              "type": "string",
+              "enum": ["allowed", "blocked", "force_installed", "normal_installed"]
+            },
+            "install_url": {
+              "type": "string"
+            },
             "blocked_install_message": {
               "type": "string"
             }
           }
         }
       }
     },
 
@@ -606,33 +640,78 @@
           "type": "boolean"
         }
       }
     },
 
     "Preferences": {
       "type": "object",
       "properties": {
-        "network.IDN_show_punycode": {
+        "app.update.auto ": {
+          "type": "boolean"
+        },
+        "browser.cache.disk.enable": {
           "type": "boolean"
         },
         "browser.fixup.dns_first_for_single_words": {
           "type": "boolean"
         },
+        "browser.search.update": {
+          "type": "boolean"
+        },
+        "browser.tabs.warnOnClose": {
+          "type": "boolean"
+        },
         "browser.cache.disk.parent_directory": {
           "type": "string"
         },
         "browser.urlbar.suggest.openpage": {
           "type": "boolean"
         },
         "browser.urlbar.suggest.history": {
           "type": "boolean"
         },
         "browser.urlbar.suggest.bookmark": {
           "type": "boolean"
+        },
+        "dom.disable_window_flip": {
+          "type": "boolean"
+        },
+        "dom.disable_window_move_resize": {
+          "type": "boolean"
+        },
+        "dom.event.contextmenu.enabled": {
+          "type": "boolean"
+        },
+        "extensions.getAddons.showPane": {
+          "type": "boolean"
+        },
+        "media.gmp-gmpopenh264.enabled": {
+          "type": "boolean"
+        },
+        "media.gmp-widevinecdm.enabled": {
+          "type": "boolean"
+        },
+        "network.dns.disableIPv6": {
+          "type": "boolean"
+        },
+        "network.IDN_show_punycode": {
+          "type": "boolean"
+        },
+        "places.history.enabled": {
+          "type": "boolean"
+        },
+        "security.default_personal_cert": {
+          "type": "string"
+        },
+        "security.ssl.errorReporting.enabled": {
+          "type": "boolean"
+        },
+        "ui.key.menuAccessKeyFocuses": {
+          "type": "boolean"
         }
       }
     },
 
     "PromptForDownloadLocation": {
       "type": "boolean"
     },
 
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -4,16 +4,17 @@ support-files =
   opensearch.html
   opensearchEngine.xml
   policytest_v0.1.xpi
   policytest_v0.2.xpi
   policy_websitefilter_block.html
   policy_websitefilter_exception.html
   ../../../../../toolkit/components/antitracking/test/browser/page.html
   ../../../../../toolkit/components/antitracking/test/browser/subResources.sjs
+  extensionsettings.html
 
 [browser_policies_getActivePolicies.js]
 skip-if = os != 'mac'
 [browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_setAndLockPref_API.js]
 [browser_policy_app_update.js]
 [browser_policy_block_about_addons.js]
 [browser_policy_block_about_config.js]
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js
@@ -1,23 +1,207 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-add_task(async function test_extensionsettings() {
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser/";
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ *        The name of the notification to wait for.
+ *
+ * @returns {Promise}
+ *          Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name) {
+  return new Promise(resolve => {
+    function popupshown() {
+      let notification = PopupNotifications.getNotification(name);
+      if (!notification) { return; }
+
+      ok(notification, `${name} notification shown`);
+      ok(PopupNotifications.isPanelOpen, "notification panel open");
+
+      PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+      resolve(PopupNotifications.panel.firstElementChild);
+    }
+
+    PopupNotifications.panel.addEventListener("popupshown", popupshown);
+  });
+}
+
+add_task(async function test_install_source_blocked_link() {
   await setupPolicyEngineWithJson({
     "policies": {
       "ExtensionSettings": {
-        "extension1@mozilla.com": {
-          "blocked_install_message": "Extension1 error message.",
+        "*": {
+          "install_sources": ["http://blocks.other.install.sources/*"],
         },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_blocked_installtrigger() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
         "*": {
-          "blocked_install_message": "Generic error message.",
+          "install_sources": ["http://blocks.other.install.sources/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_installtrigger").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_blocked_otherdomain() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
         },
       },
     },
   });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
 
-  let extensionSettings =  Services.policies.getExtensionSettings("extension1@mozilla.com");
-  is(extensionSettings.blocked_install_message, "Extension1 error message.", "Should have extension specific message.");
-  extensionSettings =  Services.policies.getExtensionSettings("extension2@mozilla.com");
-  is(extensionSettings.blocked_install_message, "Generic error message.", "Should have generic message.");
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_otherdomain").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_blocked_direct() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://blocks.other.install.sources/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-install-origin-blocked");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {baseUrl: BASE_URL}, async function({baseUrl}) {
+    content.document.location.href = baseUrl + "policytest_v0.1.xpi";
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_allowed_link() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
 });
+
+add_task(async function test_install_source_allowed_installtrigger() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_installtrigger").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_allowed_otherdomain() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*", "http://example.org/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, () => {
+    content.document.getElementById("policytest_otherdomain").click();
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_install_source_allowed_direct() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "*": {
+          "install_sources": ["http://mochi.test/*"],
+        },
+      },
+    },
+  });
+  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+    opening: BASE_URL + "extensionsettings.html",
+    waitForStateStop: true});
+
+  await ContentTask.spawn(tab.linkedBrowser, {baseUrl: BASE_URL}, async function({baseUrl}) {
+    content.document.location.href = baseUrl + "policytest_v0.1.xpi";
+  });
+  await popupPromise;
+  BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/extensionsettings.html
@@ -0,0 +1,23 @@
+
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script type="text/javascript">
+function installTrigger(url) {
+  InstallTrigger.install({extension: url});
+}
+</script>
+</head>
+<body>
+<p>
+<a id="policytest" href="policytest_v0.1.xpi">policytest@mozilla.com</a>
+</p>
+<p>
+<a id="policytest_installtrigger" onclick="installTrigger(this.href);return false;" href="policytest_v0.1.xpi">policytest@mozilla.com</a>
+</p>
+<p>
+<a id="policytest_otherdomain" href="http://example.org:80/browser/browser/components/enterprisepolicies/tests/browser/policytest_v0.1.xpi">policytest@mozilla.com</a>
+</p>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
+const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "48", "48");
+
+const server = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
+const BASE_URL = `http://example.com/data`;
+
+let addonID = "policytest2@mozilla.com";
+
+add_task(async function setup() {
+  await AddonTestUtils.promiseStartupManager();
+
+  let webExtensionFile = AddonTestUtils.createTempWebExtensionFile({
+    manifest: {
+      applications: {
+        gecko: {
+          id: addonID,
+        },
+      },
+    },
+  });
+
+  server.registerFile("/data/policy_test.xpi", webExtensionFile);
+});
+
+add_task(async function test_extensionsettings() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "extension1@mozilla.com": {
+          "blocked_install_message": "Extension1 error message.",
+        },
+        "*": {
+          "blocked_install_message": "Generic error message.",
+        },
+      },
+    },
+  });
+
+  let extensionSettings =  Services.policies.getExtensionSettings("extension1@mozilla.com");
+  equal(extensionSettings.blocked_install_message, "Extension1 error message.", "Should have extension specific message.");
+  extensionSettings =  Services.policies.getExtensionSettings("extension2@mozilla.com");
+  equal(extensionSettings.blocked_install_message, "Generic error message.", "Should have generic message.");
+});
+
+add_task(async function test_addon_blocked() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "policytest2@mozilla.com": {
+          "installation_mode": "blocked",
+        },
+      },
+    },
+  });
+
+  let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi");
+  await install.install();
+  notEqual(install.addon, null, "Addon should not be null");
+  equal(install.addon.appDisabled, true, "Addon should be disabled");
+  await install.addon.uninstall();
+});
+
+add_task(async function test_addon_allowed() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "policytest2@mozilla.com": {
+          "installation_mode": "allowed",
+        },
+        "*": {
+          "installation_mode": "blocked",
+        },
+      },
+    },
+  });
+
+  let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi");
+  await install.install();
+  notEqual(install.addon, null, "Addon should not be null");
+  equal(install.addon.appDisabled, false, "Addon should not be disabled");
+  await install.addon.uninstall();
+});
+
+add_task(async function test_addon_uninstalled() {
+  let install = await AddonManager.getInstallForURL(BASE_URL + "/policy_test.xpi");
+  await install.install();
+  notEqual(install.addon, null, "Addon should not be null");
+
+  await Promise.all([
+    AddonTestUtils.promiseAddonEvent("onUninstalled"),
+    setupPolicyEngineWithJson({
+      "policies": {
+        "ExtensionSettings": {
+          "*": {
+            "installation_mode": "blocked",
+          },
+        },
+      },
+    }),
+  ]);
+  let addon = await AddonManager.getAddonByID(addonID);
+  equal(addon, null, "Addon should be null");
+});
+
+add_task(async function test_addon_forceinstalled() {
+  await Promise.all([
+    AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+    setupPolicyEngineWithJson({
+      "policies": {
+        "ExtensionSettings": {
+          "policytest2@mozilla.com": {
+            "installation_mode": "force_installed",
+            "install_url": BASE_URL + "/policy_test.xpi",
+          },
+        },
+      },
+    }),
+  ]);
+  let addon = await AddonManager.getAddonByID(addonID);
+  notEqual(addon, null, "Addon should not be null");
+  equal(addon.appDisabled, false, "Addon should not be disabled");
+  equal(addon.permissions & AddonManager.PERM_CAN_UNINSTALL, 0, "Addon should not be able to be uninstalled.");
+  equal(addon.permissions & AddonManager.PERM_CAN_DISABLE, 0, "Addon should not be able to be disabled.");
+  await addon.uninstall();
+});
+
+add_task(async function test_addon_normalinstalled() {
+  await Promise.all([
+    AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+    setupPolicyEngineWithJson({
+      "policies": {
+        "ExtensionSettings": {
+          "policytest2@mozilla.com": {
+            "installation_mode": "normal_installed",
+            "install_url": BASE_URL + "/policy_test.xpi",
+          },
+        },
+      },
+    }),
+  ]);
+  let addon = await AddonManager.getAddonByID(addonID);
+  notEqual(addon, null, "Addon should not be null");
+  equal(addon.appDisabled, false, "Addon should not be disabled");
+  equal(addon.permissions & AddonManager.PERM_CAN_UNINSTALL, 0, "Addon should not be able to be uninstalled.");
+  notEqual(addon.permissions & AddonManager.PERM_CAN_DISABLE, 0, "Addon should be able to be disabled.");
+  await addon.uninstall();
+});
--- a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini
+++ b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 firefox-appdir = browser
 head = head.js
 
 [test_3rdparty.js]
 [test_appupdateurl.js]
 [test_clear_blocked_cookies.js]
 [test_defaultbrowsercheck.js]
+[test_extensionsettings.js]
 [test_macosparser_unflatten.js]
 skip-if = os != 'mac'
 [test_permissions.js]
 [test_popups_cookies_addons_flash.js]
 support-files = config_popups_cookies_addons_flash.json
 [test_preferences.js]
 [test_proxy.js]
 [test_requestedlocales.js]
--- a/browser/locales/en-US/browser/policies/policies-descriptions.ftl
+++ b/browser/locales/en-US/browser/policies/policies-descriptions.ftl
@@ -85,16 +85,18 @@ policy-DownloadDirectory = Set and lock 
 # “lock” means that the user won’t be able to change this setting
 policy-EnableTrackingProtection = Enable or disable Content Blocking and optionally lock it.
 
 # A “locked” extension can’t be disabled or removed by the user. This policy
 # takes 3 keys (“Install”, ”Uninstall”, ”Locked”), you can either keep them in
 # English or translate them as verbs.
 policy-Extensions = Install, uninstall or lock extensions. The Install option takes URLs or paths as parameters. The Uninstall and Locked options take extension IDs.
 
+policy-ExtensionSettings = Manage all aspects of extension installation.
+
 policy-ExtensionUpdate = Enable or disable automatic extension updates.
 
 policy-FirefoxHome = Configure Firefox Home.
 
 policy-FlashPlugin = Allow or deny usage of the Flash plugin.
 
 policy-HardwareAcceleration = If false, turn off hardware acceleration.
 
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -36,16 +36,23 @@ xpinstallPromptMessage.dontAllow.accessk
 xpinstallPromptMessage.install=Continue to Installation
 xpinstallPromptMessage.install.accesskey=C
 
 xpinstallDisabledMessageLocked=Software installation has been disabled by your system administrator.
 xpinstallDisabledMessage=Software installation is currently disabled. Click Enable and try again.
 xpinstallDisabledButton=Enable
 xpinstallDisabledButton.accesskey=n
 
+# LOCALIZATION NOTE (addonInstallBlockedByPolicy)
+# This message is shown when the installation of an add-on is blocked by
+# enterprise policy. %1$S is replaced by the name of the add-on.
+# %2$S is replaced by the ID of add-on. %3$S is a custom message that
+# the administration can add to the message.
+addonInstallBlockedByPolicy=%1$S (%2$S) is blocked by your system administrator.%3$S
+
 # LOCALIZATION NOTE (webextPerms.header)
 # This string is used as a header in the webextension permissions dialog,
 # %S is replaced with the localized name of the extension being installed.
 # See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612
 # for an example of the full dialog.
 # Note, this string will be used as raw markup. Avoid characters like <, >, &
 webextPerms.header=Add %S?
 
--- a/devtools/client/accessibility/accessibility.css
+++ b/devtools/client/accessibility/accessibility.css
@@ -358,25 +358,48 @@ body {
   background: var(--theme-toolbar-background);
   font: message-box;
   font-size: var(--accessibility-font-size);
   height: var(--accessibility-toolbar-height);
   color: var(--theme-toolbar-color);
 }
 
 .badge {
+  display: inline-block;
   font: message-box;
+  font-size: var(--theme-body-font-size);
+  line-height: calc(14 / 11);
   border-radius: 3px;
-  padding: 0px 2px;
+  padding: 0px 3px;
   margin-inline-start: 5px;
   color: var(--accessible-label-color);
   background-color: var(--accessible-label-background-color);
   border: 1px solid var(--accessible-label-border-color);
 }
 
+.badge.audit-badge::before {
+  content: "";
+  display: inline-block;
+  vertical-align: -1px;
+  width: 10px;
+  height: 10px;
+  margin-inline-end: 1px;
+  background: url("chrome://devtools/skin/images/alert-tiny.svg") no-repeat;
+  -moz-context-properties: fill;
+  fill: currentColor;
+  opacity: 0.9;
+}
+
+/* improve alignment in high res (where we can use half pixels) */
+@media (min-resolution: 1.5x) {
+  .badge.audit-badge::before {
+    vertical-align: -1.5px;
+  }
+}
+
 .badge.toggle-button {
   color: var(--theme-body-color);
   background-color: var(--badge-interactive-background-color);
   border-color: transparent;
 }
 
 .devtools-toolbar .badge.toggle-button:focus {
   outline: 2px solid var(--accessibility-toolbar-focus);
--- a/devtools/client/accessibility/components/AccessibilityTreeFilter.js
+++ b/devtools/client/accessibility/components/AccessibilityTreeFilter.js
@@ -70,17 +70,17 @@ class AccessibilityTreeFilter extends Co
 
     this.toggleFilter(filterKey);
   }
 
   render() {
     const { auditing, filters } = this.props;
     const filterButtons = Object.entries(filters).map(([filterKey, active]) =>
       ToggleButton({
-        className: "audit-badge badge",
+        className: "badge",
         key: filterKey,
         active,
         label: L10N.getStr(FILTER_LABELS[filterKey]),
         onClick: this.onClick.bind(this, filterKey),
         onKeyDown: this.onKeyDown.bind(this, filterKey),
         busy: auditing === filterKey,
       }));
 
--- a/devtools/client/accessibility/components/Badge.js
+++ b/devtools/client/accessibility/components/Badge.js
@@ -7,25 +7,28 @@
 const { Component } = require("devtools/client/shared/vendor/react");
 const { span } = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 class Badge extends Component {
   static get propTypes() {
     return {
       label: PropTypes.string.isRequired,
+      ariaLabel: PropTypes.string,
       tooltip: PropTypes.string,
     };
   }
 
   render() {
-    const { label, tooltip } = this.props;
+    const { label, ariaLabel, tooltip } = this.props;
 
-    return span({
-      className: "audit-badge badge",
-      title: tooltip,
-      "aria-label": label,
-    },
-      label);
+    return span(
+      {
+        className: "audit-badge badge",
+        title: tooltip,
+        "aria-label": ariaLabel || label,
+      },
+      label
+    );
   }
 }
 
 module.exports = Badge;
--- a/devtools/client/accessibility/components/ContrastBadge.js
+++ b/devtools/client/accessibility/components/ContrastBadge.js
@@ -33,14 +33,15 @@ class ContrastBadge extends Component {
     }
 
     if (score !== SCORES.FAIL) {
       return null;
     }
 
     return Badge({
       label: L10N.getStr("accessibility.badge.contrast"),
+      ariaLabel: L10N.getStr("accessibility.badge.contrast.warning"),
       tooltip: L10N.getStr("accessibility.badge.contrast.tooltip"),
     });
   }
 }
 
 module.exports = ContrastBadge;
--- a/devtools/client/accessibility/test/jest/components/__snapshots__/accessibility-tree-filter.test.js.snap
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/accessibility-tree-filter.test.js.snap
@@ -1,11 +1,11 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`AccessibilityTreeFilter component: audit filter filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button checked\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"false\\" class=\\"badge toggle-button checked\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: audit filter filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"true\\" class=\\"audit-badge badge toggle-button checked devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"true\\" class=\\"badge toggle-button checked devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: audit filter not filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter not filtered 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: audit filter not filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"true\\" class=\\"audit-badge badge toggle-button devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: audit filter not filtered auditing 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"true\\" class=\\"badge toggle-button devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
 
-exports[`AccessibilityTreeFilter component: toggle filter 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
+exports[`AccessibilityTreeFilter component: toggle filter 1`] = `"<div role=\\"toolbar\\" class=\\"accessibility-tree-filters\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
--- a/devtools/client/accessibility/test/jest/components/__snapshots__/badges.test.js.snap
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/badges.test.js.snap
@@ -1,13 +1,13 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Badges component: contrast ratio fail range render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast\\">accessibility.badge.contrast</span></span>"`;
+exports[`Badges component: contrast ratio fail range render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast.warning\\">accessibility.badge.contrast</span></span>"`;
 
-exports[`Badges component: contrast ratio fail render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast\\">accessibility.badge.contrast</span></span>"`;
+exports[`Badges component: contrast ratio fail render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"><span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast.warning\\">accessibility.badge.contrast</span></span>"`;
 
 exports[`Badges component: contrast ratio success render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"></span>"`;
 
 exports[`Badges component: empty checks render 1`] = `null`;
 
 exports[`Badges component: no props render 1`] = `null`;
 
 exports[`Badges component: null checks render 1`] = `null`;
--- a/devtools/client/accessibility/test/jest/components/__snapshots__/contrast-badge.test.js.snap
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/contrast-badge.test.js.snap
@@ -1,11 +1,11 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ContrastBadge component: error render 1`] = `null`;
 
-exports[`ContrastBadge component: fail render 1`] = `"<span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast\\">accessibility.badge.contrast</span>"`;
+exports[`ContrastBadge component: fail render 1`] = `"<span class=\\"audit-badge badge\\" title=\\"accessibility.badge.contrast.tooltip\\" aria-label=\\"accessibility.badge.contrast.warning\\">accessibility.badge.contrast</span>"`;
 
 exports[`ContrastBadge component: success large text render 1`] = `null`;
 
 exports[`ContrastBadge component: success range render 1`] = `null`;
 
 exports[`ContrastBadge component: success render 1`] = `null`;
--- a/devtools/client/accessibility/test/jest/components/accessibility-tree-filter.test.js
+++ b/devtools/client/accessibility/test/jest/components/accessibility-tree-filter.test.js
@@ -33,19 +33,19 @@ describe("AccessibilityTreeFilter compon
     expect(toolbar.is("div")).toBe(true);
     expect(toolbar.prop("role")).toBe("toolbar");
 
     const filterButtons = filters.find(ToggleButton);
     expect(filterButtons.length).toBe(1);
 
     const button = filterButtons.at(0).childAt(0);
     expect(button.is("button")).toBe(true);
-    expect(button.hasClass("audit-badge")).toBe(true);
     expect(button.hasClass("badge")).toBe(true);
     expect(button.hasClass("toggle-button")).toBe(true);
+    expect(button.hasClass("audit-badge")).toBe(false);
     expect(button.prop("aria-pressed")).toBe(false);
     expect(button.text()).toBe("accessibility.badge.contrast");
   });
 
   it("audit filter filtered", () => {
     const store = setupStore({
       preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
     });
@@ -100,18 +100,18 @@ describe("AccessibilityTreeFilter compon
 
   it("toggle filter", () => {
     const store = setupStore();
     const wrapper = mount(Provider({store}, AccessibilityTreeFilter()));
     expect(wrapper.html()).toMatchSnapshot();
 
     const filterInstance = wrapper.find(AccessibilityTreeFilterClass).instance();
     filterInstance.toggleFilter = jest.fn();
-    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: " " });
+    wrapper.find("button.toggle-button.badge").simulate("keydown", { key: " " });
     expect(filterInstance.toggleFilter.mock.calls.length).toBe(1);
 
-    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: "Enter" });
+    wrapper.find("button.toggle-button.badge").simulate("keydown", { key: "Enter" });
     expect(filterInstance.toggleFilter.mock.calls.length).toBe(2);
 
-    wrapper.find("button.audit-badge.badge").simulate("click", { clientX: 1 });
+    wrapper.find("button.toggle-button.badge").simulate("click", { clientX: 1 });
     expect(filterInstance.toggleFilter.mock.calls.length).toBe(3);
   });
 });
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/expand.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/expand.js
@@ -49,17 +49,18 @@ function generateDefaults(overrides) {
   };
 }
 const LongStringClientMock = require("../__mocks__/long-string-client");
 
 function mount(props, { initialState } = {}) {
   const client = {
     createObjectClient: grip =>
       ObjectClient(grip, {
-        getPrototype: () => Promise.resolve(protoStub)
+        getPrototype: () => Promise.resolve(protoStub),
+        getProxySlots: () => Promise.resolve(gripRepStubs.get("testProxySlots"))
       }),
 
     createLongStringClient: grip =>
       LongStringClientMock(grip, {
         substring: function(initiaLength, length, cb) {
           cb({
             substring: "<<<<"
           });
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/proxy.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/proxy.js
@@ -1,17 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 /* global jest */
 const { mountObjectInspector } = require("../test-utils");
 
 const { MODE } = require("../../../reps/constants");
-const stub = require("../../../reps/stubs/grip").get("testProxy");
+const gripStubs = require("../../../reps/stubs/grip");
+const stub = gripStubs.get("testProxy");
+const proxySlots = gripStubs.get("testProxySlots");
 const { formatObjectInspector } = require("../test-utils");
 
 const ObjectClient = require("../__mocks__/object-client");
 function generateDefaults(overrides) {
   return {
     roots: [
       {
         path: "root",
@@ -27,63 +29,72 @@ function generateDefaults(overrides) {
 function getEnumPropertiesMock() {
   return jest.fn(() => ({
     iterator: {
       slice: () => ({})
     }
   }));
 }
 
+function getProxySlotsMock() {
+  return jest.fn(() => proxySlots);
+}
+
 function mount(props, { initialState } = {}) {
   const enumProperties = getEnumPropertiesMock();
+  const getProxySlots = getProxySlotsMock();
 
   const client = {
-    createObjectClient: grip => ObjectClient(grip, { enumProperties })
+    createObjectClient: grip =>
+      ObjectClient(grip, { enumProperties, getProxySlots })
   };
 
   const obj = mountObjectInspector({
     client,
     props: generateDefaults(props),
     initialState
   });
 
-  return { ...obj, enumProperties };
+  return { ...obj, enumProperties, getProxySlots };
 }
 
 describe("ObjectInspector - Proxy", () => {
   it("renders Proxy as expected", () => {
-    const { wrapper, enumProperties } = mount(
+    const { wrapper, enumProperties, getProxySlots } = mount(
       {},
       {
         initialState: {
           objectInspector: {
             // Have the prototype already loaded so the component does not call
             // enumProperties for the root's properties.
-            loadedProperties: new Map([["root", { prototype: {} }]]),
+            loadedProperties: new Map([["root", proxySlots]]),
             evaluations: new Map()
           }
         }
       }
     );
 
     expect(formatObjectInspector(wrapper)).toMatchSnapshot();
 
     // enumProperties should not have been called.
     expect(enumProperties.mock.calls).toHaveLength(0);
+
+    // getProxySlots should not have been called.
+    expect(getProxySlots.mock.calls).toHaveLength(0);
   });
 
   it("calls enumProperties on <target> and <handler> clicks", () => {
     const { wrapper, enumProperties } = mount(
       {},
       {
         initialState: {
           objectInspector: {
             // Have the prototype already loaded so the component does not call
             // enumProperties for the root's properties.
-            loadedProperties: new Map([["root", { prototype: {} }]]),
+            loadedProperties: new Map([["root", proxySlots]]),
             evaluations: new Map()
           }
         }
       }
     );
 
     const nodes = wrapper.find(".node");
 
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/get-children.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/get-children.js
@@ -70,24 +70,25 @@ describe("getChildren", () => {
     expect(paths).toEqual([
       "Symbol(rootpath/x)",
       "Symbol(rootpath/<get x()>)",
       "Symbol(rootpath/<set x()>)"
     ]);
   });
 
   it("returns the expected nodes for Proxy", () => {
-    const nodes = getChildren({
-      item: createNode({
-        name: "root",
-        path: "rootpath",
-        contents: { value: gripStubs.get("testProxy") }
-      })
+    const proxyNode = createNode({
+      name: "root",
+      path: "rootpath",
+      contents: { value: gripStubs.get("testProxy") }
     });
-
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const nodes = getChildren({ item: proxyNode, loadedProperties });
     const names = nodes.map(n => n.name);
     const paths = nodes.map(n => n.path.toString());
 
     expect(names).toEqual(["<target>", "<handler>"]);
     expect(paths).toEqual([
       "Symbol(rootpath/<target>)",
       "Symbol(rootpath/<handler>)"
     ]);
@@ -243,17 +244,17 @@ describe("getChildren", () => {
     const cachedNodes = new Map();
     const node = createNode({
       name: "root",
       contents: { value: gripStubs.get("testProxy") }
     });
     const children = getChildren({
       cachedNodes,
       item: node,
-      loadedProperties: new Map([[node.path, { prototype: {} }]])
+      loadedProperties: new Map([[node.path, gripStubs.get("testProxySlots")]])
     });
     expect(cachedNodes.get(node.path)).toBe(children);
   });
 
   it("doesn't cache children on node with buckets and no loaded props", () => {
     const cachedNodes = new Map();
     const node = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-indexed-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-indexed-properties.js
@@ -205,17 +205,20 @@ describe("shouldLoadItemIndexedPropertie
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemIndexedProperties(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-non-indexed-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-non-indexed-properties.js
@@ -168,17 +168,20 @@ describe("shouldLoadItemNonIndexedProper
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemNonIndexedProperties(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-prototype.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-prototype.js
@@ -147,34 +147,37 @@ describe("shouldLoadItemPrototype", () =
     expect(shouldLoadItemPrototype(defaultPropertiesNode)).toBeFalsy();
   });
 
   it("returns false for a MapEntry node", () => {
     const node = GripMapEntryRep.createGripMapEntry("key", "value");
     expect(shouldLoadItemPrototype(node)).toBeFalsy();
   });
 
-  it("returns true for a Proxy node", () => {
+  it("returns false for a Proxy node", () => {
     const node = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    expect(shouldLoadItemPrototype(node)).toBeTruthy();
+    expect(shouldLoadItemPrototype(node)).toBeFalsy();
   });
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemPrototype(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-symbols.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-symbols.js
@@ -164,17 +164,20 @@ describe("shouldLoadItemSymbols", () => 
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemSymbols(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/types.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/types.js
@@ -68,17 +68,18 @@ export type PropertiesIterator = {
   count: number,
   slice: (start: number, count: number) => Promise<GripProperties>
 };
 
 export type ObjectClient = {
   enumEntries: () => Promise<PropertiesIterator>,
   enumProperties: (options: Object) => Promise<PropertiesIterator>,
   enumSymbols: () => Promise<PropertiesIterator>,
-  getPrototype: () => Promise<{ prototype: Object }>
+  getPrototype: () => Promise<{ prototype: Object }>,
+  getProxySlots: () => Promise<{ proxyTarget: Object, proxyHandler: Object }>
 };
 
 export type LongStringClient = {
   substring: (
     start: number,
     end: number,
     response: {
       substring?: string,
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/client.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/client.js
@@ -112,16 +112,22 @@ async function getFullText(
 
       resolve({
         fullText: initial + response.substring
       });
     });
   });
 }
 
+async function getProxySlots(
+  objectClient: ObjectClient
+): Promise<{ proxyTarget?: Object, proxyHandler?: Object }> {
+  return objectClient.getProxySlots();
+}
+
 function iteratorSlice(
   iterator: PropertiesIterator,
   start: ?number,
   end: ?number
 ): Promise<GripProperties> {
   start = start || 0;
   const count = end ? end - start + 1 : iterator.count;
 
@@ -132,10 +138,11 @@ function iteratorSlice(
 }
 
 module.exports = {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   enumSymbols,
   getPrototype,
-  getFullText
+  getFullText,
+  getProxySlots
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/load-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/load-properties.js
@@ -3,17 +3,18 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   getPrototype,
   enumSymbols,
-  getFullText
+  getFullText,
+  getProxySlots
 } = require("./client");
 
 const {
   getClosestGripNode,
   getClosestNonBucketNode,
   getValue,
   nodeHasAccessors,
   nodeHasAllEntriesInPreview,
@@ -72,16 +73,20 @@ function loadItemProperties(
   if (shouldLoadItemSymbols(item, loadedProperties)) {
     promises.push(enumSymbols(getObjectClient(), start, end));
   }
 
   if (shouldLoadItemFullText(item, loadedProperties)) {
     promises.push(getFullText(createLongStringClient(value), item));
   }
 
+  if (shouldLoadItemProxySlots(item, loadedProperties)) {
+    promises.push(getProxySlots(getObjectClient()));
+  }
+
   return Promise.all(promises).then(mergeResponses);
 }
 
 function mergeResponses(responses: Array<Object>): Object {
   const data = {};
 
   for (const response of responses) {
     if (response.hasOwnProperty("ownProperties")) {
@@ -94,16 +99,21 @@ function mergeResponses(responses: Array
 
     if (response.prototype) {
       data.prototype = response.prototype;
     }
 
     if (response.fullText) {
       data.fullText = response.fullText;
     }
+
+    if (response.proxyTarget && response.proxyHandler) {
+      data.proxyTarget = response.proxyTarget;
+      data.proxyHandler = response.proxyHandler;
+    }
   }
 
   return data;
 }
 
 function shouldLoadItemIndexedProperties(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
@@ -168,17 +178,18 @@ function shouldLoadItemPrototype(
     value &&
     !loadedProperties.has(item.path) &&
     !nodeIsBucket(item) &&
     !nodeIsMapEntry(item) &&
     !nodeIsEntries(item) &&
     !nodeIsDefaultProperties(item) &&
     !nodeHasAccessors(item) &&
     !nodeIsPrimitive(item) &&
-    !nodeIsLongString(item)
+    !nodeIsLongString(item) &&
+    !nodeIsProxy(item)
   );
 }
 
 function shouldLoadItemSymbols(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
 ): boolean {
   const value = getValue(item);
@@ -199,18 +210,26 @@ function shouldLoadItemSymbols(
 
 function shouldLoadItemFullText(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
 ) {
   return !loadedProperties.has(item.path) && nodeIsLongString(item);
 }
 
+function shouldLoadItemProxySlots(
+  item: Node,
+  loadedProperties: LoadedProperties = new Map()
+): boolean {
+  return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
 module.exports = {
   loadItemProperties,
   mergeResponses,
   shouldLoadItemEntries,
   shouldLoadItemIndexedProperties,
   shouldLoadItemNonIndexedProperties,
   shouldLoadItemPrototype,
   shouldLoadItemSymbols,
-  shouldLoadItemFullText
+  shouldLoadItemFullText,
+  shouldLoadItemProxySlots
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js
@@ -341,18 +341,21 @@ function makeNodesForPromiseProperties(i
         type: NODE_TYPES.PROMISE_VALUE
       })
     );
   }
 
   return properties;
 }
 
-function makeNodesForProxyProperties(item: Node): Array<Node> {
-  const { proxyHandler, proxyTarget } = getValue(item);
+function makeNodesForProxyProperties(
+  loadedProps: GripProperties,
+  item: Node
+): Array<Node> {
+  const { proxyHandler, proxyTarget } = loadedProps;
 
   return [
     createNode({
       parent: item,
       name: "<target>",
       contents: { value: proxyTarget },
       type: NODE_TYPES.PROXY_TARGET
     }),
@@ -787,18 +790,18 @@ function getChildren(options: {
   if (nodeHasChildren(item)) {
     return addToCache(item.contents);
   }
 
   if (nodeIsMapEntry(item)) {
     return addToCache(makeNodesForMapEntry(item));
   }
 
-  if (nodeIsProxy(item)) {
-    return addToCache(makeNodesForProxyProperties(item));
+  if (nodeIsProxy(item) && hasLoadedProps) {
+    return addToCache(makeNodesForProxyProperties(loadedProps, item));
   }
 
   if (nodeIsLongString(item) && hasLoadedProps) {
     // Set longString object's fullText to fetched one.
     return addToCache(setNodeFullText(loadedProps, item));
   }
 
   if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/stubs/grip.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/stubs/grip.js
@@ -298,32 +298,16 @@ stubs.set("testStringObject", {
     safeGetterValues: {},
     wrappedValue: "foo"
   }
 });
 stubs.set("testProxy", {
   type: "object",
   actor: "server1.conn1.child1/obj47",
   class: "Proxy",
-  proxyTarget: {
-    type: "object",
-    actor: "server1.conn1.child1/obj48",
-    class: "Object",
-    ownPropertyLength: 1
-  },
-  proxyHandler: {
-    type: "object",
-    actor: "server1.conn1.child1/obj49",
-    class: "Array",
-    ownPropertyLength: 4,
-    preview: {
-      kind: "ArrayLike",
-      length: 3
-    }
-  },
   preview: {
     kind: "Object",
     ownProperties: {
       "<target>": {
         value: {
           type: "object",
           actor: "server1.conn1.child1/obj48",
           class: "Object",
@@ -341,16 +325,34 @@ stubs.set("testProxy", {
             length: 3
           }
         }
       }
     },
     ownPropertiesLength: 2
   }
 });
+stubs.set("testProxySlots", {
+  proxyTarget: {
+    type: "object",
+    actor: "server1.conn1.child1/obj48",
+    class: "Object",
+    ownPropertyLength: 1
+  },
+  proxyHandler: {
+    type: "object",
+    actor: "server1.conn1.child1/obj49",
+    class: "Array",
+    ownPropertyLength: 4,
+    preview: {
+      kind: "ArrayLike",
+      length: 3
+    }
+  }
+});
 stubs.set("testArrayBuffer", {
   type: "object",
   actor: "server1.conn1.child1/obj170",
   class: "ArrayBuffer",
   extensible: true,
   frozen: false,
   sealed: false,
   ownPropertyLength: 0,
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/tests/grip.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/tests/grip.js
@@ -119,17 +119,18 @@ describe("Grip - Proxy", () => {
   const object = stubs.get("testProxy");
 
   it("correctly selects Grip Rep", () => {
     expect(getRep(object)).toBe(Grip.rep);
   });
 
   it("renders as expected", () => {
     const renderRep = props => shallowRenderRep(object, props);
-    const handlerLength = getGripLengthBubbleText(object.proxyHandler, {
+    const handler = object.preview.ownProperties["<handler>"].value;
+    const handlerLength = getGripLengthBubbleText(handler, {
       mode: MODE.TINY
     });
     const out = `Proxy { <target>: {…}, <handler>: ${handlerLength} […] }`;
 
     expect(renderRep({ mode: undefined }).text()).toBe(out);
     expect(renderRep({ mode: MODE.TINY }).text()).toBe("Proxy");
     expect(renderRep({ mode: MODE.SHORT }).text()).toBe(out);
     expect(renderRep({ mode: MODE.LONG }).text()).toBe(out);
--- a/devtools/client/debugger/src/components/App.js
+++ b/devtools/client/debugger/src/components/App.js
@@ -265,24 +265,22 @@ class App extends Component<Props, State
       editorPane.dispatchEvent(new Event("resizeend"));
     }
   }
 
   renderLayout = () => {
     const { startPanelCollapsed, endPanelCollapsed } = this.props;
     const horizontal = this.isHorizontal();
 
-    const maxSize = horizontal ? "70%" : "95%";
-
     return (
       <SplitBox
         style={{ width: "100vw" }}
         initialSize={prefs.endPanelSize}
         minSize={30}
-        maxSize={maxSize}
+        maxSize="70%"
         splitterSize={1}
         vert={horizontal}
         onResizeEnd={num => {
           prefs.endPanelSize = num;
           this.triggerEditorPaneResize();
         }}
         startPanel={
           <SplitBox
--- a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap
+++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap
@@ -24,17 +24,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -160,17 +159,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -244,17 +242,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
       <span
         className="suffix"
       >
         (mapped)
       </span>
     </span>
@@ -389,17 +386,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
             <span
               className="suffix"
             >
               (mapped)
             </span>
           </span>
@@ -480,17 +476,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       root
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -620,17 +615,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             root
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -694,17 +688,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       http://mdn.com
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -802,17 +795,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             http://mdn.com
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -866,17 +858,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       http://mdn.com
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -976,17 +967,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             http://mdn.com
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1041,17 +1031,16 @@ Object {
       className="arrow expanded"
     />
     <AccessibleImage
       className="globe-small"
     />
     <span
       className="label"
     >
-       
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1129,17 +1118,16 @@ Object {
             className="arrow expanded"
           />
           <AccessibleImage
             className="globe-small"
           />
           <span
             className="label"
           >
-             
             folder
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1184,17 +1172,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="angular"
     />
     <span
       className="label"
     >
-       
       Angular
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1290,17 +1277,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="angular"
           />
           <span
             className="label"
           >
-             
             Angular
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1353,17 +1339,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1437,17 +1422,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             folder
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1490,17 +1474,16 @@ Object {
       className="arrow expanded"
     />
     <AccessibleImage
       className="globe-small"
     />
     <span
       className="label"
     >
-       
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1578,17 +1561,16 @@ Object {
             className="arrow expanded"
           />
           <AccessibleImage
             className="globe-small"
           />
           <span
             className="label"
           >
-             
             folder
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1633,17 +1615,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="folder"
     />
     <span
       className="label"
     >
-       
       moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1741,17 +1722,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="folder"
           />
           <span
             className="label"
           >
-             
             moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1805,17 +1785,16 @@ Object {
       className="arrow"
     />
     <AccessibleImage
       className="webpack"
     />
     <span
       className="label"
     >
-       
       Webpack
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -1911,17 +1890,16 @@ Object {
             className="arrow"
           />
           <AccessibleImage
             className="webpack"
           />
           <span
             className="label"
           >
-             
             Webpack
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -1986,17 +1964,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -2124,17 +2101,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -2209,17 +2185,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -2349,17 +2324,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             one.js
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
@@ -2435,17 +2409,16 @@ Object {
           "relativeUrl": "http://mdn.com/one.js",
           "url": "http://mdn.com/one.js",
         }
       }
     />
     <span
       className="label"
     >
-       
       external file
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
     "addCollapseExpandAllOptions": [Function],
     "context": Object {},
@@ -2573,17 +2546,16 @@ Object {
                 "relativeUrl": "http://mdn.com/one.js",
                 "url": "http://mdn.com/one.js",
               }
             }
           />
           <span
             className="label"
           >
-             
             external file
              
           </span>
         </div>,
         "_rendering": false,
         "_updater": [Circular],
       },
     },
--- a/devtools/client/inspector/computed/test/browser.ini
+++ b/devtools/client/inspector/computed/test/browser.ini
@@ -29,20 +29,20 @@ support-files =
 [browser_computed_media-queries.js]
 [browser_computed_no-results-placeholder.js]
 [browser_computed_original-source-link.js]
 [browser_computed_pseudo-element_01.js]
 [browser_computed_refresh-on-style-change_01.js]
 [browser_computed_search-filter.js]
 [browser_computed_search-filter_clear.js]
 [browser_computed_search-filter_context-menu.js]
-subsuite = clipboard
+tags = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_computed_search-filter_escape-keypress.js]
 [browser_computed_search-filter_noproperties.js]
 [browser_computed_select-and-copy-styles-01.js]
-subsuite = clipboard
+tags = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_computed_select-and-copy-styles-02.js]
-subsuite = clipboard
+tags = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_computed_style-editor-link.js]
 skip-if = true # bug 1307846
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -60,16 +60,17 @@ devtools.jar:
     skin/dark-theme.css (themes/dark-theme.css)
     skin/light-theme.css (themes/light-theme.css)
     skin/toolbars.css (themes/toolbars.css)
     skin/toolbox.css (themes/toolbox.css)
     skin/tooltips.css (themes/tooltips.css)
     skin/images/accessibility.svg (themes/images/accessibility.svg)
     skin/images/add.svg (themes/images/add.svg)
     skin/images/alert.svg (themes/images/alert.svg)
+    skin/images/alert-tiny.svg (themes/images/alert-tiny.svg)
     skin/images/arrow.svg (themes/images/arrow.svg)
     skin/images/arrow-big.svg (themes/images/arrow-big.svg)
     skin/images/arrowhead-left.svg (themes/images/arrowhead-left.svg)
     skin/images/arrowhead-right.svg (themes/images/arrowhead-right.svg)
     skin/images/arrowhead-down.svg (themes/images/arrowhead-down.svg)
     skin/images/arrowhead-up.svg (themes/images/arrowhead-up.svg)
     skin/images/breadcrumbs-divider.svg (themes/images/breadcrumbs-divider.svg)
     skin/images/checkbox.svg (themes/images/checkbox.svg)
--- a/devtools/client/locales/en-US/accessibility.properties
+++ b/devtools/client/locales/en-US/accessibility.properties
@@ -158,16 +158,22 @@ accessibility.contrast.annotation.fail=D
 accessibility.badges=Accessibility checks
 
 # LOCALIZATION NOTE (accessibility.badge.contrast): A title text for the badge
 # that is rendered within the accessible row in the accessibility tree for a
 # given accessible object that does not satisfy the WCAG guideline for colour
 # contrast.
 accessibility.badge.contrast=contrast
 
+# LOCALIZATION NOTE (accessibility.badge.contrast.warning): A label for the
+# badge and attached warning icon that is rendered within the accessible row in
+# the accessibility tree for a given accessible object that does not satisfy the
+# WCAG guideline for colour contrast.
+accessibility.badge.contrast.warning=contrast warning
+
 # LOCALIZATION NOTE (accessibility.badge.contrast.tooltip): A title text for the
 # badge tooltip that is rendered on mouse hover over the badge in the accessible
 # row in the accessibility tree for a given accessible object that does not
 # satisfy the WCAG guideline for colour contrast.
 accessibility.badge.contrast.tooltip=Does not meet WCAG standards for accessible text.
 
 # LOCALIZATION NOTE (accessibility.tree.filters): A title text for the toolbar
 # within the main accessibility panel that contains a list of filters to be for
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -1707,18 +1707,18 @@ function makeNodesForPromiseProperties(i
       contents: { value: value },
       type: NODE_TYPES.PROMISE_VALUE
     }));
   }
 
   return properties;
 }
 
-function makeNodesForProxyProperties(item) {
-  const { proxyHandler, proxyTarget } = getValue(item);
+function makeNodesForProxyProperties(loadedProps, item) {
+  const { proxyHandler, proxyTarget } = loadedProps;
 
   return [createNode({
     parent: item,
     name: "<target>",
     contents: { value: proxyTarget },
     type: NODE_TYPES.PROXY_TARGET
   }), createNode({
     parent: item,
@@ -2101,18 +2101,18 @@ function getChildren(options) {
   if (nodeHasChildren(item)) {
     return addToCache(item.contents);
   }
 
   if (nodeIsMapEntry(item)) {
     return addToCache(makeNodesForMapEntry(item));
   }
 
-  if (nodeIsProxy(item)) {
-    return addToCache(makeNodesForProxyProperties(item));
+  if (nodeIsProxy(item) && hasLoadedProps) {
+    return addToCache(makeNodesForProxyProperties(loadedProps, item));
   }
 
   if (nodeIsLongString(item) && hasLoadedProps) {
     // Set longString object's fullText to fetched one.
     return addToCache(setNodeFullText(loadedProps, item));
   }
 
   if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
@@ -3476,17 +3476,18 @@ module.exports = {
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   getPrototype,
   enumSymbols,
-  getFullText
+  getFullText,
+  getProxySlots
 } = __webpack_require__(197);
 
 const {
   getClosestGripNode,
   getClosestNonBucketNode,
   getValue,
   nodeHasAccessors,
   nodeHasAllEntriesInPreview,
@@ -3530,16 +3531,20 @@ function loadItemProperties(item, create
   if (shouldLoadItemSymbols(item, loadedProperties)) {
     promises.push(enumSymbols(getObjectClient(), start, end));
   }
 
   if (shouldLoadItemFullText(item, loadedProperties)) {
     promises.push(getFullText(createLongStringClient(value), item));
   }
 
+  if (shouldLoadItemProxySlots(item, loadedProperties)) {
+    promises.push(getProxySlots(getObjectClient()));
+  }
+
   return Promise.all(promises).then(mergeResponses);
 }
 
 function mergeResponses(responses) {
   const data = {};
 
   for (const response of responses) {
     if (response.hasOwnProperty("ownProperties")) {
@@ -3552,16 +3557,21 @@ function mergeResponses(responses) {
 
     if (response.prototype) {
       data.prototype = response.prototype;
     }
 
     if (response.fullText) {
       data.fullText = response.fullText;
     }
+
+    if (response.proxyTarget && response.proxyHandler) {
+      data.proxyTarget = response.proxyTarget;
+      data.proxyHandler = response.proxyHandler;
+    }
   }
 
   return data;
 }
 
 function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
   const gripItem = getClosestGripNode(item);
   const value = getValue(gripItem);
@@ -3585,38 +3595,43 @@ function shouldLoadItemEntries(item, loa
   const value = getValue(gripItem);
 
   return value && nodeIsEntries(getClosestNonBucketNode(item)) && !nodeHasAllEntriesInPreview(gripItem) && !loadedProperties.has(item.path) && !nodeNeedsNumericalBuckets(item);
 }
 
 function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
-  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item);
+  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
 }
 
 function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
   return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
 }
 
 function shouldLoadItemFullText(item, loadedProperties = new Map()) {
   return !loadedProperties.has(item.path) && nodeIsLongString(item);
 }
 
+function shouldLoadItemProxySlots(item, loadedProperties = new Map()) {
+  return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
 module.exports = {
   loadItemProperties,
   mergeResponses,
   shouldLoadItemEntries,
   shouldLoadItemIndexedProperties,
   shouldLoadItemNonIndexedProperties,
   shouldLoadItemPrototype,
   shouldLoadItemSymbols,
-  shouldLoadItemFullText
+  shouldLoadItemFullText,
+  shouldLoadItemProxySlots
 };
 
 /***/ }),
 
 /***/ 197:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
@@ -3701,33 +3716,38 @@ async function getFullText(longStringCli
 
       resolve({
         fullText: initial + response.substring
       });
     });
   });
 }
 
+async function getProxySlots(objectClient) {
+  return objectClient.getProxySlots();
+}
+
 function iteratorSlice(iterator, start, end) {
   start = start || 0;
   const count = end ? end - start + 1 : iterator.count;
 
   if (count === 0) {
     return Promise.resolve({});
   }
   return iterator.slice(start, count);
 }
 
 module.exports = {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   enumSymbols,
   getPrototype,
-  getFullText
+  getFullText,
+  getProxySlots
 };
 
 /***/ }),
 
 /***/ 2:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -313,26 +313,28 @@ VariablesViewController.prototype = {
    *
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The grip to use to populate the target.
    */
   _populateFromObject: function(aTarget, aGrip) {
     if (aGrip.class === "Proxy") {
-      this.addExpander(
-        aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
-        aGrip.proxyTarget);
-      this.addExpander(
-        aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
-        aGrip.proxyHandler);
-
-      // Refuse to play the proxy's stupid game and return immediately
+      // Refuse to play the proxy's stupid game and just expose the target and handler.
       const deferred = defer();
-      deferred.resolve();
+      const objectClient = this._getObjectClient(aGrip);
+      objectClient.getProxySlots(aResponse => {
+        const target = aTarget.addItem("<target>", { value: aResponse.proxyTarget },
+          { internalItem: true });
+        this.addExpander(target, aResponse.proxyTarget);
+        const handler = aTarget.addItem("<handler>", { value: aResponse.proxyHandler },
+          { internalItem: true });
+        this.addExpander(handler, aResponse.proxyHandler);
+        deferred.resolve();
+      });
       return deferred.promise;
     }
 
     if (aGrip.class === "Promise" && aGrip.promiseState) {
       const { state, value, reason } = aGrip.promiseState;
       aTarget.addItem("<state>", { value: state }, { internalItem: true });
       if (state === "fulfilled") {
         this.addExpander(
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/alert-tiny.svg
@@ -0,0 +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" viewBox="0 0 10 10" width="10" height="10">
+  <path fill="context-fill" fill-rule="evenodd" d="M4.94.76a.5.5 0 0 0-.88 0l-4 7.5A.5.5 0 0 0 .5 9h8a.5.5 0 0 0 .44-.74l-4-7.5zM4.5 2.8a.7.7 0 0 0-.7.7v1.8a.7.7 0 1 0 1.4 0V3.5a.7.7 0 0 0-.7-.7zm0 5.4a.7.7 0 1 0 0-1.4.7.7 0 0 0 0 1.4z"/>
+</svg>
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -353,16 +353,17 @@ skip-if = true  # Bug 1438979
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_object_inspector__proto__.js]
 [browser_webconsole_object_inspector_entries.js]
 [browser_webconsole_object_inspector_getters.js]
 [browser_webconsole_object_inspector_getters_prototype.js]
 [browser_webconsole_object_inspector_getters_shadowed.js]
 [browser_webconsole_object_inspector_key_sorting.js]
 [browser_webconsole_object_inspector_local_session_storage.js]
+[browser_webconsole_object_inspector_nested_proxy.js]
 [browser_webconsole_object_inspector_selected_text.js]
 [browser_webconsole_object_inspector_scroll.js]
 [browser_webconsole_object_inspector_while_debugging_and_inspecting.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_optimized_out_vars.js]
 [browser_webconsole_output_copy.js]
 tags = clipboard
 [browser_webconsole_output_copy_newlines.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_nested_proxy.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check evaluating and expanding getters in the console.
+const TEST_URI = "data:text/html;charset=utf8,"
+ + "<h1>Object Inspector on deeply nested proxies</h1>";
+
+add_task(async function() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    let proxy = new Proxy({}, {});
+    for (let i = 0; i < 1e5; ++i) {
+      proxy = new Proxy(proxy, proxy);
+    }
+    content.wrappedJSObject.console.log("oi-test", proxy);
+  });
+
+  const node = await waitFor(() => findMessage(hud, "oi-test"));
+  const oi = node.querySelector(".tree");
+  const [proxyNode] = getObjectInspectorNodes(oi);
+
+  expandObjectInspectorNode(proxyNode);
+  await waitFor(() => getObjectInspectorNodes(oi).length > 1);
+  checkChildren(proxyNode, [`<target>`, `<handler>`]);
+
+  const targetNode = findObjectInspectorNode(oi, "<target>");
+  expandObjectInspectorNode(targetNode);
+  await waitFor(() => getObjectInspectorChildrenNodes(targetNode).length > 0);
+  checkChildren(targetNode, [`<target>`, `<handler>`]);
+
+  const handlerNode = findObjectInspectorNode(oi, "<handler>");
+  expandObjectInspectorNode(handlerNode);
+  await waitFor(() => getObjectInspectorChildrenNodes(handlerNode).length > 0);
+  checkChildren(handlerNode, [`<target>`, `<handler>`]);
+});
+
+function checkChildren(node, expectedChildren) {
+  const children = getObjectInspectorChildrenNodes(node);
+  is(children.length, expectedChildren.length,
+    "There is the expected number of children");
+  children.forEach((child, index) => {
+    ok(child.textContent.includes(expectedChildren[index]),
+      `Expected "${expectedChildren[index]}" child`);
+  });
+}
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -861,16 +861,34 @@ const proto = {
       source,
       line: stack.line,
       column: stack.column,
       functionDisplayName: stack.functionDisplayName,
     };
   },
 
   /**
+   * Handle a protocol request to get the target and handler internal slots of a proxy.
+   */
+  proxySlots: function() {
+    // There could be transparent security wrappers, unwrap to check if it's a proxy.
+    // However, retrieve proxyTarget and proxyHandler from `this.obj` to avoid exposing
+    // the unwrapped target and handler.
+    const unwrapped = DevToolsUtils.unwrap(this.obj);
+    if (!unwrapped || !unwrapped.isProxy) {
+      return this.throwError("objectNotProxy",
+        "'proxySlots' request is only valid for grips with a 'Proxy' class.");
+    }
+    return {
+      proxyTarget: this.hooks.createValueGrip(this.obj.proxyTarget),
+      proxyHandler: this.hooks.createValueGrip(this.obj.proxyHandler),
+    };
+  },
+
+  /**
    * Release the actor, when it isn't needed anymore.
    * Protocol.js uses this release method to call the destroy method.
    */
   release: function() {},
 };
 
 exports.ObjectActor = protocol.ActorClassWithSpec(objectSpec, proto);
 exports.ObjectActorProto = proto;
--- a/devtools/server/actors/object/previewers.js
+++ b/devtools/server/actors/object/previewers.js
@@ -283,37 +283,37 @@ const previewers = {
         break;
       }
     }
 
     return true;
   }],
 
   Proxy: [function({obj, hooks}, grip, rawObj) {
+    // Only preview top-level proxies, avoiding recursion. Otherwise, since both the
+    // target and handler can also be proxies, we could get an exponential behavior.
+    if (hooks.getGripDepth() > 1) {
+      return true;
+    }
+
     // The `isProxy` getter of the debuggee object only detects proxies without
     // security wrappers. If false, the target and handler are not available.
     const hasTargetAndHandler = obj.isProxy;
-    if (hasTargetAndHandler) {
-      grip.proxyTarget = hooks.createValueGrip(obj.proxyTarget);
-      grip.proxyHandler = hooks.createValueGrip(obj.proxyHandler);
-    }
 
     grip.preview = {
       kind: "Object",
       ownProperties: Object.create(null),
       ownPropertiesLength: 2 * hasTargetAndHandler,
     };
 
-    if (hooks.getGripDepth() > 1) {
-      return true;
-    }
-
     if (hasTargetAndHandler) {
-      grip.preview.ownProperties["<target>"] = {value: grip.proxyTarget};
-      grip.preview.ownProperties["<handler>"] = {value: grip.proxyHandler};
+      Object.assign(grip.preview.ownProperties, {
+        "<target>": {value: hooks.createValueGrip(obj.proxyTarget)},
+        "<handler>": {value: hooks.createValueGrip(obj.proxyHandler)},
+      });
     }
 
     return true;
   }],
 };
 
 /**
  * Generic previewer for classes wrapping primitives, like String,
--- a/devtools/server/tests/unit/test_objectgrips-17.js
+++ b/devtools/server/tests/unit/test_objectgrips-17.js
@@ -52,18 +52,22 @@ function test({ threadClient, debuggee }
   return new Promise(function(resolve) {
     threadClient.addOneTimeListener("paused", async function(event, packet) {
       // Get the grips.
       const [proxyGrip, inheritsProxyGrip, inheritsProxy2Grip] = packet.frame.arguments;
 
       // Check the grip of the proxy object.
       check_proxy_grip(debuggee, testOptions, proxyGrip);
 
+      // Check the target and handler slots of the proxy object.
+      const proxyClient = threadClient.pauseGrip(proxyGrip);
+      const proxySlots = await proxyClient.getProxySlots();
+      check_proxy_slots(debuggee, testOptions, proxyGrip, proxySlots);
+
       // Check the prototype and properties of the proxy object.
-      const proxyClient = threadClient.pauseGrip(proxyGrip);
       const proxyResponse = await proxyClient.getPrototypeAndProperties();
       check_properties(testOptions, proxyResponse.ownProperties, true, false);
       check_prototype(debuggee, testOptions, proxyResponse.prototype, true, false);
 
       // Check the prototype and properties of the object which inherits from the proxy.
       const inheritsProxyClient = threadClient.pauseGrip(inheritsProxyGrip);
       const inheritsProxyResponse = await inheritsProxyClient.getPrototypeAndProperties();
       check_properties(testOptions, inheritsProxyResponse.ownProperties, false, false);
@@ -112,47 +116,58 @@ function test({ threadClient, debuggee }
 
 function check_proxy_grip(debuggee, testOptions, grip) {
   const { global, isOpaque, subsumes, globalIsInvisible } = testOptions;
   const {preview} = grip;
 
   if (global === debuggee) {
     // The proxy has no security wrappers.
     strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
-    ok(grip.proxyTarget, "There is a [[ProxyTarget]] grip.");
-    ok(grip.proxyHandler, "There is a [[ProxyHandler]] grip.");
     strictEqual(preview.ownPropertiesLength, 2, "The preview has 2 properties.");
-    const target = preview.ownProperties["<target>"].value;
-    strictEqual(target, grip.proxyTarget, "<target> contains the [[ProxyTarget]].");
-    const handler = preview.ownProperties["<handler>"].value;
-    strictEqual(handler, grip.proxyHandler, "<handler> contains the [[ProxyHandler]].");
+    const props = preview.ownProperties;
+    ok(props["<target>"].value, "<target> contains the [[ProxyTarget]].");
+    ok(props["<handler>"].value, "<handler> contains the [[ProxyHandler]].");
   } else if (isOpaque) {
     // The proxy has opaque security wrappers.
     strictEqual(grip.class, "Opaque", "The grip has an Opaque class.");
     strictEqual(grip.ownPropertyLength, 0, "The grip has no properties.");
   } else if (!subsumes) {
     // The proxy belongs to compartment not subsumed by the debuggee.
-    strictEqual(grip.class, "Restricted", "The grip has an Restricted class.");
+    strictEqual(grip.class, "Restricted", "The grip has a Restricted class.");
     ok(!("ownPropertyLength" in grip), "The grip doesn't know the number of properties.");
   } else if (globalIsInvisible) {
     // The proxy belongs to an invisible-to-debugger compartment.
     strictEqual(grip.class, "InvisibleToDebugger: Object",
                 "The grip has an InvisibleToDebugger class.");
     ok(!("ownPropertyLength" in grip), "The grip doesn't know the number of properties.");
   } else {
     // The proxy has non-opaque security wrappers.
     strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
-    ok(!("proxyTarget" in grip), "There is no [[ProxyTarget]] grip.");
-    ok(!("proxyHandler" in grip), "There is no [[ProxyHandler]] grip.");
     strictEqual(preview.ownPropertiesLength, 0, "The preview has no properties.");
     ok(!("<target>" in preview), "The preview has no <target> property.");
     ok(!("<handler>" in preview), "The preview has no <handler> property.");
   }
 }
 
+function check_proxy_slots(debuggee, testOptions, grip, proxySlots) {
+  const { global } = testOptions;
+
+  if (grip.class !== "Proxy") {
+    strictEqual(proxySlots, undefined, "Slots can only be retrived for Proxy grips.");
+  } else if (global === debuggee) {
+    const { proxyTarget, proxyHandler } = proxySlots;
+    strictEqual(proxyTarget.type, "object", "There is a [[ProxyTarget]] grip.");
+    strictEqual(proxyHandler.type, "object", "There is a [[ProxyHandler]] grip.");
+  } else {
+    const { proxyTarget, proxyHandler } = proxySlots;
+    strictEqual(proxyTarget.type, "undefined", "There is no [[ProxyTarget]] grip.");
+    strictEqual(proxyHandler.type, "undefined", "There is no [[ProxyHandler]] grip.");
+  }
+}
+
 function check_properties(testOptions, props, isProxy, createdInDebuggee) {
   const { subsumes, globalIsInvisible } = testOptions;
   const ownPropertiesLength = Reflect.ownKeys(props).length;
 
   if (createdInDebuggee || !isProxy && subsumes && !globalIsInvisible) {
     // The debuggee can access the properties.
     strictEqual(ownPropertiesLength, 1, "1 own property was retrieved.");
     strictEqual(props.x.value, 1, "The property has the right value.");
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-nested-proxy.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
+  await new Promise(function(resolve) {
+    threadClient.addOneTimeListener("paused", async function(event, packet) {
+      const [grip] = packet.frame.arguments;
+      const objClient = threadClient.pauseGrip(grip);
+      const {proxyTarget, proxyHandler} = await objClient.getProxySlots();
+
+      strictEqual(grip.class, "Proxy", "Its a proxy grip.");
+      strictEqual(proxyTarget.class, "Proxy", "The target is also a proxy.");
+      strictEqual(proxyHandler.class, "Proxy", "The handler is also a proxy.");
+
+      await threadClient.resume();
+      resolve();
+    });
+    debuggee.eval(function stopMe(arg) {
+      debugger;
+    }.toString());
+    debuggee.eval(`
+      var proxy = new Proxy({}, {});
+      for (let i = 0; i < 1e5; ++i)
+        proxy = new Proxy(proxy, proxy);
+      stopMe(proxy);
+    `);
+  });
+}));
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -165,16 +165,17 @@ skip-if = true # breakpoint sliding is n
 [test_objectgrips-22.js]
 [test_objectgrips-array-like-object.js]
 [test_objectgrips-property-value-01.js]
 [test_objectgrips-property-value-02.js]
 [test_objectgrips-property-value-03.js]
 [test_objectgrips-fn-apply-01.js]
 [test_objectgrips-fn-apply-02.js]
 [test_objectgrips-fn-apply-03.js]
+[test_objectgrips-nested-proxy.js]
 [test_promise_state-01.js]
 [test_promise_state-02.js]
 [test_promise_state-03.js]
 [test_interrupt.js]
 [test_stepping-01.js]
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
--- a/devtools/shared/client/debugger-client.js
+++ b/devtools/shared/client/debugger-client.js
@@ -354,20 +354,21 @@ DebuggerClient.prototype = {
     request.format = "json";
     request.stack = getStack();
 
     // Implement a Promise like API on the returned object
     // that resolves/rejects on request response
     const deferred = promise.defer();
     function listenerJson(resp) {
       removeRequestListeners();
+      resp = safeOnResponse(resp);
       if (resp.error) {
-        deferred.reject(safeOnResponse(resp));
+        deferred.reject(resp);
       } else {
-        deferred.resolve(safeOnResponse(resp));
+        deferred.resolve(resp);
       }
     }
     function listenerBulk(resp) {
       removeRequestListeners();
       deferred.resolve(safeOnResponse(resp));
     }
 
     const removeRequestListeners = () => {
--- a/devtools/shared/client/object-client.js
+++ b/devtools/shared/client/object-client.js
@@ -298,11 +298,34 @@ ObjectClient.prototype = {
     before: function(packet) {
       if (this._grip.class !== "Promise") {
         throw new Error("getPromiseRejectionStack is only valid for " +
           "promise grips.");
       }
       return packet;
     },
   }),
+
+  /**
+   * Request the target and handler internal slots of a proxy.
+   */
+  getProxySlots: DebuggerClient.requester({
+    type: "proxySlots",
+  }, {
+    before: function(packet) {
+      if (this._grip.class !== "Proxy") {
+        throw new Error("getProxySlots is only valid for proxy grips.");
+      }
+      return packet;
+    },
+    after: function(response) {
+      // Before Firefox 68 (bug 1392760), the proxySlots request didn't exist.
+      // The proxy target and handler were directly included in the grip.
+      if (response.error === "unrecognizedPacketType") {
+        const {proxyTarget, proxyHandler} = this._grip;
+        return {proxyTarget, proxyHandler};
+      }
+      return response;
+    },
+  }),
 };
 
 module.exports = ObjectClient;
--- a/devtools/shared/specs/object.js
+++ b/devtools/shared/specs/object.js
@@ -97,16 +97,21 @@ types.addDictType("object.dependentPromi
 
 types.addDictType("object.originalSourceLocation", {
   source: "source",
   line: "number",
   column: "number",
   functionDisplayName: "string",
 });
 
+types.addDictType("object.proxySlots", {
+  proxyTarget: "object.descriptor",
+  proxyHandler: "object.descriptor",
+});
+
 const objectSpec = generateActorSpec({
   typeName: "obj",
 
   methods: {
     allocationStack: {
       request: {},
       response: {
         allocationStack: RetVal("array:object.originalSourceLocation"),
@@ -193,16 +198,20 @@ const objectSpec = generateActorSpec({
       response: RetVal("object.apply"),
     },
     rejectionStack: {
       request: {},
       response: {
         rejectionStack: RetVal("array:object.originalSourceLocation"),
       },
     },
+    proxySlots: {
+      request: {},
+      response: RetVal("object.proxySlots"),
+    },
     release: { release: true },
     scope: {
       request: {},
       response: RetVal("object.scope"),
     },
     // Needed for the PauseScopedObjectActor which extends the ObjectActor.
     threadGrip: {
       request: {},
--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
+++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
@@ -167,18 +167,17 @@ interface nsIServiceWorkerManager : nsIS
                                   in AString aBody,
                                   in AString aTag,
                                   in AString aIcon,
                                   in AString aData,
                                   in AString aBehavior);
 
   [optional_argc] void sendPushEvent(in ACString aOriginAttributes,
                                      in ACString aScope,
-                                     [optional] in uint32_t aDataLength,
-                                     [optional, array, size_is(aDataLength)] in uint8_t aDataBytes);
+                                     [optional] in Array<uint8_t> aDataBytes);
   void sendPushSubscriptionChangeEvent(in ACString aOriginAttributes,
                                        in ACString scope);
 
   void addListener(in nsIServiceWorkerManagerListener aListener);
 
   void removeListener(in nsIServiceWorkerManagerListener aListener);
 
   bool isParentInterceptEnabled();
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -1952,21 +1952,17 @@ MediaManager* MediaManager::Get() {
     sSingleton = new MediaManager();
 
 #ifdef XP_WIN
     sSingleton->mMediaThread = new MTAThread("MediaManager");
 #else
     sSingleton->mMediaThread = new base::Thread("MediaManager");
 #endif
     base::Thread::Options options;
-#if defined(_WIN32)
-    options.message_loop_type = MessageLoop::TYPE_MOZILLA_NONMAINUITHREAD;
-#else
     options.message_loop_type = MessageLoop::TYPE_MOZILLA_NONMAINTHREAD;
-#endif
     if (!sSingleton->mMediaThread->StartWithOptions(options)) {
       MOZ_CRASH();
     }
 
     LOG("New Media thread for gum");
 
     nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
     if (obs) {
--- a/dom/media/test/crashtests/crashtests.list
+++ b/dom/media/test/crashtests/crashtests.list
@@ -112,15 +112,15 @@ load oscillator-ended-1.html
 load oscillator-ended-2.html
 skip-if(Android&&AndroidVersion=='22') load video-replay-after-audio-end.html # bug 1315125, bug 1358876
 # This needs to run at the end to avoid leaking busted state into other tests.
 skip-if(Android) load 691096-1.html # Bug 1365451
 load 1236639.html
 test-pref(media.navigator.permission.disabled,true) test-pref(media.getusermedia.insecure.enabled,true) test-pref(media.getusermedia.insecure.enabled,true) load 1388372.html
 load 1494073.html
 skip-if(Android) load 1526044.html # Bug 1528391
-load encrypted-track-with-bad-sample-description-index.mp4 # Bug 1533211
+skip-if(Android&&AndroidVersion<21) load encrypted-track-with-bad-sample-description-index.mp4 # Bug 1533211, unkip after bug 1550912
 load encrypted-track-without-tenc.mp4 # Bug 1533215
 load 1533909.html
 load 1538727.html
 load empty-samples.webm # Bug 1540580
 test-pref(media.autoplay.block-webaudio,false) load 1545133.html
 load track-with-zero-dimensions.mp4 # Bug 1542539
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -2171,20 +2171,19 @@ var setupIceServerConfig = useIceServer 
   return enableHttpProxy(false)
     .then(spawnIceServer)
     .then(iceServersStr => { iceServersArray = JSON.parse(iceServersStr); })
     .then(addTurnsSelfsignedCerts);
 };
 
 async function runNetworkTest(testFunction, fixtureOptions = {}) {
 
-  let version = SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].
-    getService(SpecialPowers.Ci.nsIXULAppInfo).version;
-  let isNightly = version.endsWith("a1");
-  let isAndroid = !!navigator.userAgent.includes("Android");
+  let {AppConstants} = SpecialPowers.Cu.import("resource://gre/modules/AppConstants.jsm", {});
+  let isNightly = AppConstants.NIGHTLY_BUILD;
+  let isAndroid = AppConstants.platform == "android";
 
   await scriptsReady;
   await runTestWhenReady(async options =>
     {
       await startNetworkAndTest();
       await setupIceServerConfig(fixtureOptions.useIceServer);
 
       // currently we set android hardware encoder default enabled in nightly.
--- a/dom/media/tests/mochitest/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html
@@ -31,28 +31,34 @@
       );
     }
 
     test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [
       async function PC_REMOTE_CHECK_RTP_HEADER_EXTS_AGAINST_SDP() {
 
         const sdpExtmapIds = sdputils.findExtmapIds(test.originalAnswer.sdp);
 
-        let pc = SpecialPowers.wrap(test.pcRemote._pc);
-        let [level, type, sending, data] =  await getRtpPacket(pc);
-        let extensions = ParseRtpPacket(data).header.extensions;
-
+        const pc = SpecialPowers.wrap(test.pcRemote._pc);
+        const [level, type, sending, data] =  await getRtpPacket(pc);
+        const packet = ParseRtpPacket(data);
+        const extIds = packet.header.extensions.map(e => `${e.id}`);
         // make sure we got the same number of rtp header extensions in
         // the received packet as were negotiated in the sdp.  Then
         // check to make sure each of the received extension ids were in
         // the sdp.
-        is(sdpExtmapIds.length, extensions.length, "number of received ids match sdp ids");
+        is(sdpExtmapIds.length, extIds.length,
+           `number of sdp ids match received ids ` +
+           `${JSON.stringify(sdpExtmapIds)} == ${JSON.stringify(extIds)}\n` +
+           `sdp = ${test.originalAnswer.sdp}\n` +
+           `packet = ${JSON.stringify(packet, null, 2)}`);
         // note, we are comparing a number (from the parsed rtp packet)
         // and a string (from the answer sdp)
-        ok(extensions.every((ext) => sdpExtmapIds.includes(""+ext.id)), "extension id arrays equivalent");
+        ok(extIds.every(id => sdpExtmapIds.includes(id)) &&
+           sdpExtmapIds.every(id => extIds.includes(id)),
+          `extension id arrays equivalent`);
       }
     ]);
 
     test.run();
   });
 </script>
 </pre>
 </body>
--- a/dom/serviceworkers/ServiceWorkerManager.cpp
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -946,24 +946,28 @@ RefPtr<ServiceWorkerRegistrationPromise>
   MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable));
 
   return runnable->Promise();
 }
 
 NS_IMETHODIMP
 ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes,
                                     const nsACString& aScope,
-                                    uint32_t aDataLength, uint8_t* aDataBytes,
+                                    const nsTArray<uint8_t>& aDataBytes,
                                     uint8_t optional_argc) {
-  if (optional_argc == 2) {
-    nsTArray<uint8_t> data;
-    if (!data.InsertElementsAt(0, aDataBytes, aDataLength, fallible)) {
-      return NS_ERROR_OUT_OF_MEMORY;
-    }
-    return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Some(data));
+  if (optional_argc == 1) {
+    // This does one copy here (while constructing the Maybe) and another when
+    // we end up copying into the SendPushEventRunnable.  We could fix that to
+    // only do one copy by making things between here and there take
+    // Maybe<nsTArray<uint8_t>>&&, but then we'd need to copy before we know
+    // whether we really need to in PushMessageDispatcher::NotifyWorkers.  Since
+    // in practice this only affects JS callers that pass data, and we don't
+    // have any right now, let's not worry about it.
+    return SendPushEvent(aOriginAttributes, aScope, EmptyString(),
+                         Some(aDataBytes));
   }
   MOZ_ASSERT(optional_argc == 0);
   return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Nothing());
 }
 
 nsresult ServiceWorkerManager::SendPushEvent(
     const nsACString& aOriginAttributes, const nsACString& aScope,
     const nsAString& aMessageId, const Maybe<nsTArray<uint8_t>>& aData) {
--- a/dom/serviceworkers/test/test_serviceworker_interfaces.html
+++ b/dom/serviceworkers/test/test_serviceworker_interfaces.html
@@ -37,30 +37,31 @@
         });
         worker.postMessage({
           type: 'returnPrefs',
           prefs: event.data.prefs,
           result: result
         });
 
       } else if (event.data.type == 'getHelperData') {
-        const version = SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].getService(SpecialPowers.Ci.nsIXULAppInfo).version;
-        const isNightly = version.endsWith("a1");
-        const isEarlyBetaOrEarlier = SpecialPowers.EARLY_BETA_OR_EARLIER;
-        const isRelease = !version.includes("a");
+        const {AppConstants} = SpecialPowers.Cu.import("resource://gre/modules/AppConstants.jsm", {});
+        const isNightly = AppConstants.NIGHTLY_BUILD;
+        const isEarlyBetaOrEarlier = AppConstants.EARLY_BETA_OR_EARLIER;
+        const isRelease = AppConstants.RELEASE_OR_BETA;
         const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
-        const isMac = /Mac OS/.test(navigator.oscpu);
-        const isWindows = /Windows/.test(navigator.oscpu);
-        const isAndroid = navigator.userAgent.includes("Android");
-        const isLinux = /Linux/.test(navigator.oscpu) && !isAndroid;
+        const isMac = AppConstants.platform == "macosx";
+        const isWindows = AppConstants.platform == "win";
+        const isAndroid = AppConstants.platform == "android";
+        const isLinux = AppConstants.platform == "linux";
         const isInsecureContext = !window.isSecureContext;
+        // Currently, MOZ_APP_NAME is always "fennec" for all mobile builds, so we can't use AppConstants for this
         const isFennec = isAndroid && SpecialPowers.Cc["@mozilla.org/android/bridge;1"].getService(SpecialPowers.Ci.nsIAndroidBridge).isFennec;
 
         const result = {
-          version, isNightly, isEarlyBetaOrEarlier, isRelease, isDesktop, isMac,
+          isNightly, isEarlyBetaOrEarlier, isRelease, isDesktop, isMac,
           isWindows, isAndroid, isLinux, isInsecureContext, isFennec
         };
 
         worker.postMessage({
           type: 'returnHelperData', result
         });
       }
     }
--- a/dom/serviceworkers/test/test_serviceworker_interfaces.js
+++ b/dom/serviceworkers/test/test_serviceworker_interfaces.js
@@ -237,17 +237,17 @@ var interfaceNamesInGlobalScope =
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "WorkerLocation",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "WorkerNavigator",
 // IMPORTANT: Do not change this list without review from a DOM peer!
   ];
 // IMPORTANT: Do not change the list above without review from a DOM peer!
 
-function createInterfaceMap({ version, isNightly, isRelease, isDesktop, isAndroid, isInsecureContext, isFennec }) {
+function createInterfaceMap({ isNightly, isRelease, isDesktop, isAndroid, isInsecureContext, isFennec }) {
   var interfaceMap = {};
 
   function addInterfaces(interfaces)
   {
     for (var entry of interfaces) {
       if (typeof(entry) === "string") {
         interfaceMap[entry] = true;
       } else {
--- a/dom/simpledb/SDBResults.cpp
+++ b/dom/simpledb/SDBResults.cpp
@@ -11,34 +11,24 @@
 namespace mozilla {
 namespace dom {
 
 SDBResult::SDBResult(const nsACString& aData) : mData(aData) {}
 
 NS_IMPL_ISUPPORTS(SDBResult, nsISDBResult)
 
 NS_IMETHODIMP
-SDBResult::GetAsArray(uint32_t* aDataLen, uint8_t** aData) {
-  MOZ_ASSERT(aDataLen);
-  MOZ_ASSERT(aData);
+SDBResult::GetAsArray(nsTArray<uint8_t>& aData) {
+  uint32_t length = mData.Length();
+  aData.SetLength(length);
 
-  if (mData.IsEmpty()) {
-    *aDataLen = 0;
-    *aData = nullptr;
-    return NS_OK;
+  if (length != 0) {
+    memcpy(aData.Elements(), mData.BeginReading(), length * sizeof(uint8_t));
   }
 
-  uint32_t length = mData.Length();
-
-  uint8_t* data = static_cast<uint8_t*>(moz_xmalloc(length * sizeof(uint8_t)));
-
-  memcpy(data, mData.BeginReading(), length * sizeof(uint8_t));
-
-  *aDataLen = length;
-  *aData = data;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 SDBResult::GetAsArrayBuffer(JSContext* aCx, JS::MutableHandleValue _retval) {
   JS::Rooted<JSObject*> arrayBuffer(aCx);
   nsresult rv =
       nsContentUtils::CreateArrayBuffer(aCx, mData, arrayBuffer.address());
--- a/dom/simpledb/nsISDBResults.idl
+++ b/dom/simpledb/nsISDBResults.idl
@@ -4,15 +4,14 @@
  * 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/. */
 
 #include "nsISupports.idl"
 
 [scriptable, uuid(bca19e01-b34e-4a48-8875-2f4cb871febf)]
 interface nsISDBResult : nsISupports
 {
-  [must_use] void
-  getAsArray([optional] out uint32_t dataLen,
-             [array, retval, size_is(dataLen)] out uint8_t data);
+  [must_use] Array<uint8_t>
+  getAsArray();
 
   [must_use, implicit_jscontext] jsval
   getAsArrayBuffer();
 };
--- a/dom/tests/mochitest/general/test_interfaces.js
+++ b/dom/tests/mochitest/general/test_interfaces.js
@@ -20,26 +20,28 @@
 //
 // See createInterfaceMap() below for a complete list of properties.
 //
 // The values of the properties need to be either literal true/false
 // (e.g. indicating whether something is enabled on a particular
 // channel/OS) or one of the is* constants below (in cases when
 // exposure is affected by channel or OS in a nontrivial way).
 
-const version = SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].getService(SpecialPowers.Ci.nsIXULAppInfo).version;
-const isNightly = version.endsWith("a1");
-const isEarlyBetaOrEarlier = SpecialPowers.EARLY_BETA_OR_EARLIER;
-const isRelease = !version.includes("a");
+const {AppConstants} = SpecialPowers.Cu.import("resource://gre/modules/AppConstants.jsm", {});
+
+const isNightly = AppConstants.NIGHTLY_BUILD;
+const isEarlyBetaOrEarlier = AppConstants.EARLY_BETA_OR_EARLIER;
+const isRelease = AppConstants.RELEASE_OR_BETA;
 const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
-const isMac = /Mac OS/.test(navigator.oscpu);
-const isWindows = /Windows/.test(navigator.oscpu);
-const isAndroid = navigator.userAgent.includes("Android");
-const isLinux = /Linux/.test(navigator.oscpu) && !isAndroid;
+const isMac = AppConstants.platform == "macosx";
+const isWindows = AppConstants.platform == "win";
+const isAndroid = AppConstants.platform == "android";
+const isLinux = AppConstants.platform == "linux";
 const isInsecureContext = !window.isSecureContext;
+// Currently, MOZ_APP_NAME is always "fennec" for all mobile builds, so we can't use AppConstants for this
 const isFennec = isAndroid && SpecialPowers.Cc["@mozilla.org/android/bridge;1"].getService(SpecialPowers.Ci.nsIAndroidBridge).isFennec;
 
 // IMPORTANT: Do not change this list without review from
 //            a JavaScript Engine peer!
 var ecmaGlobals =
   [
     {name: "Array", insecureContext: true},
     {name: "ArrayBuffer", insecureContext: true},
--- a/dom/workers/test/test_navigator_iframe.js
+++ b/dom/workers/test/test_navigator_iframe.js
@@ -41,13 +41,13 @@ worker.onmessage = function(event) {
      "Mismatched navigator string for " + args.name + "!");
 };
 
 worker.onerror = function(event) {
   ok(false, "Worker had an error: " + event.message);
   SimpleTest.finish();
 }
 
-var version = SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].getService(SpecialPowers.Ci.nsIXULAppInfo).version;
-var isNightly = version.endsWith("a1");
-var isRelease = !version.includes("a");
+var {AppConstants} = SpecialPowers.Cu.import("resource://gre/modules/AppConstants.jsm", {});
+var isNightly = AppConstants.NIGHTLY_BUILD;
+var isRelease = AppConstants.RELEASE_OR_BETA;
 
 worker.postMessage({ isNightly, isRelease });
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -257,17 +257,17 @@ var interfaceNamesInGlobalScope =
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "WorkerLocation", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "WorkerNavigator", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
   ];
 // IMPORTANT: Do not change the list above without review from a DOM peer!
 
-function createInterfaceMap({ version, isNightly, isRelease, isDesktop, isAndroid, isInsecureContext, isFennec }) {
+function createInterfaceMap({ isNightly, isRelease, isDesktop, isAndroid, isInsecureContext, isFennec }) {
   var interfaceMap = {};
 
   function addInterfaces(interfaces)
   {
     for (var entry of interfaces) {
       if (typeof(entry) === "string") {
         interfaceMap[entry] = !isInsecureContext;
       } else {
--- a/dom/workers/test/worker_driver.js
+++ b/dom/workers/test/worker_driver.js
@@ -31,30 +31,31 @@ function workerTestExec(script) {
   worker.onmessage = function(event) {
     if (event.data.type == 'finish') {
       SimpleTest.finish();
 
     } else if (event.data.type == 'status') {
       ok(event.data.status, event.data.msg);
 
     } else if (event.data.type == 'getHelperData') {
-      const version = SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].getService(SpecialPowers.Ci.nsIXULAppInfo).version;
-      const isNightly = version.endsWith("a1");
-      const isEarlyBetaOrEarlier = SpecialPowers.EARLY_BETA_OR_EARLIER;
-      const isRelease = !version.includes("a");
+      const {AppConstants} = SpecialPowers.Cu.import("resource://gre/modules/AppConstants.jsm", {});
+      const isNightly = AppConstants.NIGHTLY_BUILD;
+      const isEarlyBetaOrEarlier = AppConstants.EARLY_BETA_OR_EARLIER;
+      const isRelease = AppConstants.RELEASE_OR_BETA;
       const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
-      const isMac = /Mac OS/.test(navigator.oscpu);
-      const isWindows = /Windows/.test(navigator.oscpu);
-      const isAndroid = navigator.userAgent.includes("Android");
-      const isLinux = /Linux/.test(navigator.oscpu) && !isAndroid;
+      const isMac = AppConstants.platform == "macosx";
+      const isWindows = AppConstants.platform == "win";
+      const isAndroid = AppConstants.platform == "android";
+      const isLinux = AppConstants.platform == "linux";
       const isInsecureContext = !window.isSecureContext;
+      // Currently, MOZ_APP_NAME is always "fennec" for all mobile builds, so we can't use AppConstants for this
       const isFennec = isAndroid && SpecialPowers.Cc["@mozilla.org/android/bridge;1"].getService(SpecialPowers.Ci.nsIAndroidBridge).isFennec;
 
       const result = {
-        version, isNightly, isEarlyBetaOrEarlier, isRelease, isDesktop, isMac,
+        isNightly, isEarlyBetaOrEarlier, isRelease, isDesktop, isMac,
         isWindows, isAndroid, isLinux, isInsecureContext, isFennec
       };
 
       worker.postMessage({
         type: 'returnHelperData', result
       });
     }
   }
--- a/gfx/wr/webrender/src/device/gl.rs
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -2417,17 +2417,17 @@ impl Device {
                 self.gl.map_buffer(gl::PIXEL_PACK_BUFFER, gl::READ_ONLY)
             }
 
             gl::GlType::Gles => {
                 self.gl.map_buffer_range(
                     gl::PIXEL_PACK_BUFFER,
                     0,
                     pbo.reserved_size as _,
-                    gl::READ_ONLY)
+                    gl::MAP_READ_BIT)
             }
         };
 
         if buf_ptr.is_null() {
             return None;
         }
 
         let buffer = unsafe { slice::from_raw_parts(buf_ptr as *const u8, pbo.reserved_size) };
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -1004,23 +1004,22 @@ NS_IMETHODIMP
 RasterImage::Undefine(const char* prop) {
   if (!mProperties) {
     return NS_ERROR_FAILURE;
   }
   return mProperties->Undefine(prop);
 }
 
 NS_IMETHODIMP
-RasterImage::GetKeys(uint32_t* count, char*** keys) {
+RasterImage::GetKeys(nsTArray<nsCString>& keys) {
   if (!mProperties) {
-    *count = 0;
-    *keys = nullptr;
+    keys.Clear();
     return NS_OK;
   }
-  return mProperties->GetKeys(count, keys);
+  return mProperties->GetKeys(keys);
 }
 
 void RasterImage::Discard() {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(CanDiscard(), "Asked to discard but can't");
   MOZ_ASSERT(!mAnimationState || gfxPrefs::ImageMemAnimatedDiscardable(),
              "Asked to discard for animated image");
 
--- a/js/xpconnect/tests/chrome/test_xrayToJS.xul
+++ b/js/xpconnect/tests/chrome/test_xrayToJS.xul
@@ -165,19 +165,19 @@ https://bugzilla.mozilla.org/show_bug.cg
     SimpleTest.finish();
   }
 
   // Maintain a static list of the properties that are available on each standard
   // prototype, so that we make sure to audit any new ones to make sure they're
   // Xray-safe.
   //
   // DO NOT CHANGE WTIHOUT REVIEW FROM AN XPCONNECT PEER.
-  var version = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).version;
-  var isNightlyBuild = version.endsWith("a1");
-  var isReleaseOrBeta = !version.includes("a");
+  var {AppConstants} = SpecialPowers.Cu.import("resource://gre/modules/AppConstants.jsm", {});
+  var isNightlyBuild = AppConstants.NIGHTLY_BUILD;
+  var isReleaseOrBeta = AppConstants.RELEASE_OR_BETA;
   var gPrototypeProperties = {};
   var gConstructorProperties = {};
   function constructorProps(arr) {
     // Some props live on all constructors
     return arr.concat(["prototype", "length", "name"]);
   }
   gPrototypeProperties['Date'] =
     ["getTime", "getTimezoneOffset", "getYear", "getFullYear", "getUTCFullYear",
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -4702,16 +4702,23 @@ nsIFrame* nsLayoutUtils::GetParentOrPlac
 }
 
 nsIFrame* nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(nsIFrame* aFrame) {
   nsIFrame* f = GetParentOrPlaceholderFor(aFrame);
   if (f) return f;
   return GetCrossDocParentFrame(aFrame);
 }
 
+nsIFrame* nsLayoutUtils::GetDisplayListParent(nsIFrame* aFrame) {
+  if (aFrame->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
+    return aFrame->GetParent();
+  }
+  return nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(aFrame);
+}
+
 nsIFrame* nsLayoutUtils::GetNextContinuationOrIBSplitSibling(nsIFrame* aFrame) {
   nsIFrame* result = aFrame->GetNextContinuation();
   if (result) return result;
 
   if ((aFrame->GetStateBits() & NS_FRAME_PART_OF_IBSPLIT) != 0) {
     // We only store the ib-split sibling annotation with the first
     // frame in the continuation chain. Walk back to find that frame now.
     aFrame = aFrame->FirstContinuation();
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -1392,16 +1392,25 @@ class nsLayoutUtils {
 
   /**
    * If aFrame is an out of flow frame, return its placeholder, otherwise
    * return its (possibly cross-doc) parent.
    */
   static nsIFrame* GetParentOrPlaceholderForCrossDoc(nsIFrame* aFrame);
 
   /**
+   * Returns the frame that would act as the parent of aFrame when
+   * descending through the frame tree in display list building.
+   * Usually the same as GetParentOrPlaceholderForCrossDoc, except
+   * that pushed floats are treated as children of their containing
+   * block.
+   */
+  static nsIFrame* GetDisplayListParent(nsIFrame* aFrame);
+
+  /**
    * Get a frame's next-in-flow, or, if it doesn't have one, its
    * block-in-inline-split sibling.
    */
   static nsIFrame* GetNextContinuationOrIBSplitSibling(nsIFrame* aFrame);
 
   /**
    * Get the first frame in the continuation-plus-ib-split-sibling chain
    * containing aFrame.
--- a/layout/painting/RetainedDisplayListBuilder.cpp
+++ b/layout/painting/RetainedDisplayListBuilder.cpp
@@ -292,21 +292,17 @@ bool AnyContentAncestorModified(nsIFrame
     if (f->IsFrameModified()) {
       return true;
     }
 
     if (aStopAtFrame && f == aStopAtFrame) {
       break;
     }
 
-    if (f->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
-      f = f->GetParent();
-    } else {
-      f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f);
-    }
+    f = nsLayoutUtils::GetDisplayListParent(f);
   }
 
   return false;
 }
 
 static Maybe<const ActiveScrolledRoot*> SelectContainerASR(
     const DisplayItemClipChain* aClipChain, const ActiveScrolledRoot* aItemASR,
     Maybe<const ActiveScrolledRoot*>& aContainerASR) {
@@ -1201,17 +1197,17 @@ static void AddFramesForContainingBlock(
 // descendants of a modified frame (us, or another frame we'll get to soon).
 // This is combined with the work required for MarkFrameForDisplayIfVisible,
 // so that we can avoid an extra ancestor walk, and we can reuse the flag
 // to detect when we've already visited an ancestor (and thus all further
 // ancestors must also be visited).
 static void FindContainingBlocks(nsIFrame* aFrame,
                                  nsTArray<nsIFrame*>& aExtraFrames) {
   for (nsIFrame* f = aFrame; f;
-       f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) {
+       f = nsLayoutUtils::GetDisplayListParent(f)) {
     if (f->ForceDescendIntoIfVisible()) return;
     f->SetForceDescendIntoIfVisible(true);
     CRR_LOG("Considering OOFs for %p\n", f);
 
     AddFramesForContainingBlock(f, f->GetChildList(nsIFrame::kFloatList),
                                 aExtraFrames);
     AddFramesForContainingBlock(f, f->GetChildList(f->GetAbsoluteListID()),
                                 aExtraFrames);
new file mode 100644
--- /dev/null
+++ b/layout/painting/crashtests/1549909.html
@@ -0,0 +1,9 @@
+<style>
+* {
+  -webkit-column-break-after: always;
+  float: left;
+  column-width: 0px;
+}
+</style>
+}
+<video controls="controls">
--- a/layout/painting/crashtests/crashtests.list
+++ b/layout/painting/crashtests/crashtests.list
@@ -13,9 +13,10 @@ load 1454105-1.html
 load 1455944-1.html
 load 1465305-1.html
 load 1468124-1.html
 load 1469472.html
 load 1477831-1.html
 load 1504033.html
 load 1514544-1.html
 load 1547420-1.html
+load 1549909.html
 
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -1250,17 +1250,17 @@ void nsDisplayListBuilder::MarkFrameForD
 void nsDisplayListBuilder::AddFrameMarkedForDisplayIfVisible(nsIFrame* aFrame) {
   mFramesMarkedForDisplayIfVisible.AppendElement(aFrame);
 }
 
 void nsDisplayListBuilder::MarkFrameForDisplayIfVisible(
     nsIFrame* aFrame, nsIFrame* aStopAtFrame) {
   AddFrameMarkedForDisplayIfVisible(aFrame);
   for (nsIFrame* f = aFrame; f;
-       f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) {
+       f = nsLayoutUtils::GetDisplayListParent(f)) {
     if (f->ForceDescendIntoIfVisible()) return;
     f->SetForceDescendIntoIfVisible(true);
     if (f == aStopAtFrame) {
       // we've reached a frame that we know will be painted, so we can stop.
       break;
     }
   }
 }
@@ -1380,17 +1380,17 @@ static void UnmarkFrameForDisplay(nsIFra
       // we've reached a frame that we know will be painted, so we can stop.
       break;
     }
   }
 }
 
 static void UnmarkFrameForDisplayIfVisible(nsIFrame* aFrame) {
   for (nsIFrame* f = aFrame; f;
-       f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) {
+       f = nsLayoutUtils::GetDisplayListParent(f)) {
     if (!f->ForceDescendIntoIfVisible()) return;
     f->SetForceDescendIntoIfVisible(false);
   }
 }
 
 nsDisplayListBuilder::~nsDisplayListBuilder() {
   NS_ASSERTION(mFramesMarkedForDisplay.Length() == 0,
                "All frames should have been unmarked");
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -172,17 +172,16 @@ android {
         test {
             java {
                 // Bug 1229149 tracks pushing this into a :services Gradle project.
                 srcDir "${topsrcdir}/mobile/android/services/src/test/java"
 
                 if (!mozconfig.substs.MOZ_ANDROID_GCM) {
                     exclude 'org/mozilla/gecko/gcm/**/*.java'
                     exclude 'org/mozilla/gecko/push/**/*.java'
-                    exclude 'org/mozilla/gecko/advertising/**'
                 }
             }
             resources {
                 // Bug 1229149 tracks pushing this into a :services Gradle project.
                 srcDir "${topsrcdir}/mobile/android/services/src/test/resources"
             }
         }
 
@@ -240,18 +239,16 @@ dependencies {
         implementation "com.google.android.gms:play-services-ads-identifier:$google_play_services_version"
         implementation "com.google.android.gms:play-services-basement:$google_play_services_version"
     }
 
     if (mozconfig.substs.MOZ_ANDROID_GCM) {
         implementation "com.google.android.gms:play-services-basement:$google_play_services_version"
         implementation "com.google.android.gms:play-services-base:$google_play_services_version"
         implementation "com.google.android.gms:play-services-gcm:$google_play_services_version"
-        implementation "com.google.android.gms:play-services-ads-identifier:$google_play_services_version"
-        implementation "org.mindrot:jbcrypt:0.4"
     }
 
     if (mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
         implementation "com.google.android.gms:play-services-fido:$google_play_services_fido_version"
     }
 
     // Include LeakCanary in local builds, but not in official builds.
     if (mozconfig.substs.MOZILLA_OFFICIAL) {
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -133,17 +133,16 @@ import org.mozilla.gecko.tabqueue.TabQue
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
 import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate;
 import org.mozilla.gecko.telemetry.TelemetryUploadService;
 import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
-import org.mozilla.gecko.telemetry.TelemetryActivationPingDelegate;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.CommitEventSource;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.PwaConfirm;
 import org.mozilla.gecko.updater.PostUpdateHandler;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
@@ -313,25 +312,23 @@ public class BrowserApp extends GeckoApp
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
     // race by determining if the web content should be hidden at the animation's end.
     private boolean mHideWebContentOnAnimationEnd;
 
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
     private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate();
-    private final TelemetryActivationPingDelegate mTelemetryActivationPingDelegate = new TelemetryActivationPingDelegate();
 
     private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
             new ScreenshotDelegate(),
             new BookmarkStateChangeDelegate(),
             new ReaderViewBookmarkPromotion(),
             new PostUpdateHandler(),
             mTelemetryCorePingDelegate,
-            mTelemetryActivationPingDelegate,
             new OfflineTabStatusDelegate(),
             new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
     private OnboardingHelper mOnboardingHelper;       // Contains reference to Context - DO NOT LEAK!
 
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/advertising/AdvertisingUtil.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.mozilla.gecko.advertising;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.google.android.gms.ads.identifier.AdvertisingIdClient;
-
-
-import org.mindrot.jbcrypt.BCrypt;
-import org.mozilla.gecko.annotation.ReflectionTarget;
-
-@ReflectionTarget
-public class AdvertisingUtil {
-    private static final String LOG_TAG = AdvertisingUtil.class.getCanonicalName();
-
-    /* Use the same SALT for all BCrypt hashings. We want the SALT to be stable for all Fennec users but it should differ from the one from Fenix.
-     * Generated using Bcrypt.gensalt(). */
-    private static final String BCRYPT_SALT = "$2a$10$ZfglUfcbmTyaBbAQ7SL9OO";
-
-    /**
-     * Retrieves the advertising ID hashed with BCrypt. Requires Google Play Services. Note: This method must not run on
-     * the main thread.
-     */
-    @ReflectionTarget
-    public static String getAdvertisingId(Context caller) {
-        try {
-            AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(caller);
-            String advertisingId = info.getId();
-            return advertisingId != null ? BCrypt.hashpw(advertisingId, BCRYPT_SALT) : null;
-        } catch (Throwable t) {
-            Log.e(LOG_TAG, "Error retrieving advertising ID. " + t.getMessage());
-        }
-        return null;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryActivationPingDelegate.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package org.mozilla.gecko.telemetry;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.support.annotation.WorkerThread;
-import android.util.Log;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.GeckoThread;
-import org.mozilla.gecko.delegates.BrowserAppDelegate;
-import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder;
-import org.mozilla.gecko.util.StringUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.IOException;
-import java.lang.reflect.Method;
-
-/**
- * An activity-lifecycle delegate for uploading the activation ping.
- */
-public class TelemetryActivationPingDelegate extends BrowserAppDelegate {
-    private static final String LOGTAG = StringUtils.safeSubstring(
-            "Gecko" + TelemetryActivationPingDelegate.class.getSimpleName(), 0, 23);
-
-    private TelemetryDispatcher telemetryDispatcher; // lazy
-
-
-    @Override
-    public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
-        super.onCreate(browserApp, savedInstanceState);
-        uploadActivationPing(browserApp);
-    }
-
-    private void uploadActivationPing(final BrowserApp activity) {
-        if (!AppConstants.MOZ_ANDROID_GCM) {
-            return;
-        }
-
-        if (TelemetryActivationPingBuilder.activationPingAlreadySent(activity)) {
-            return;
-        }
-
-        ThreadUtils.postToBackgroundThread(() -> {
-            if (activity == null) {
-                return;
-            }
-
-            if (!TelemetryUploadService.isUploadEnabledByAppConfig(activity)) {
-                Log.d(LOGTAG, "Activation ping upload disabled by app config. Returning.");
-                return;
-            }
-
-            String identifier = null;
-
-            try {
-                final Class<?> clazz = Class.forName("org.mozilla.gecko.advertising.AdvertisingUtil");
-                final Method getAdvertisingId = clazz.getMethod("getAdvertisingId", Context.class);
-                identifier = (String) getAdvertisingId.invoke(null, activity);
-            } catch (Exception e) {
-                Log.w(LOGTAG, "Unable to get identifier: " + e);
-            }
-
-            final GeckoProfile profile = GeckoThread.getActiveProfile();
-            String clientID = null;
-            try {
-                clientID = profile.getClientId();
-            } catch (final IOException e) {
-                Log.w(LOGTAG, "Unable to get client ID: " + e);
-                if (identifier == null) {
-                    //Activation ping is mandatory to be sent with either the identifier or the clientID.
-                    Log.d(LOGTAG, "Activation ping failed to send - both identifier and clientID were unable to be retrieved.");
-                    return;
-                }
-            }
-
-            final TelemetryActivationPingBuilder pingBuilder = new TelemetryActivationPingBuilder(activity);
-            if (identifier != null) {
-                pingBuilder.setIdentifier(identifier);
-            } else {
-                pingBuilder.setClientID(clientID);
-            }
-
-            getTelemetryDispatcher().queuePingForUpload(activity, pingBuilder);
-        });
-    }
-
-    @WorkerThread // via constructor
-    private TelemetryDispatcher getTelemetryDispatcher() {
-        if (telemetryDispatcher == null) {
-            final GeckoProfile profile = GeckoThread.getActiveProfile();
-            final String profilePath = profile.getDir().getAbsolutePath();
-            final String profileName = profile.getName();
-            telemetryDispatcher = new TelemetryDispatcher(profilePath, profileName);
-        }
-        return telemetryDispatcher;
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java
@@ -16,21 +16,21 @@ import android.view.accessibility.Access
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.adjust.AttributionHelperListener;
+import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements;
 import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.search.SearchEngineManager;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements;
 import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
 import org.mozilla.gecko.telemetry.measurements.SessionMeasurements;
 import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
 import java.util.List;
@@ -134,53 +134,57 @@ public class TelemetryCorePingDelegate e
         if (getBrowserApp() == null) {
             return;
         }
 
         // The containing method can be called from onStart: queue this work so that
         // the first launch of the activity doesn't trigger profile init too early.
         //
         // Additionally, getAndIncrementSequenceNumber must be called from a worker thread.
-        ThreadUtils.postToBackgroundThread(() -> {
-            final BrowserApp activity = getBrowserApp();
-            if (activity == null) {
-                return;
-            }
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @WorkerThread
+            @Override
+            public void run() {
+                final BrowserApp activity = getBrowserApp();
+                if (activity == null) {
+                    return;
+                }
 
-            final GeckoProfile profile = GeckoThread.getActiveProfile();
-            if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) {
-                Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning.");
-                return;
-            }
+                final GeckoProfile profile = GeckoThread.getActiveProfile();
+                if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) {
+                    Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning.");
+                    return;
+                }
 
-            final String clientID;
-            final boolean hadCanaryClientId;
-            try {
-                clientID = profile.getClientId();
-                hadCanaryClientId = profile.getIfHadCanaryClientId();
-            } catch (final IOException e) {
-                Log.w(LOGTAG, "Unable to get client ID properties to generate core ping: " + e);
-                return;
-            }
+                final String clientID;
+                final boolean hadCanaryClientId;
+                try {
+                    clientID = profile.getClientId();
+                    hadCanaryClientId = profile.getIfHadCanaryClientId();
+                } catch (final IOException e) {
+                    Log.w(LOGTAG, "Unable to get client ID properties to generate core ping: " + e);
+                    return;
+                }
 
-            // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
-            final SharedPreferences sharedPrefs = getSharedPreferences(activity);
-            final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer =
-                    sessionMeasurements.getAndResetSessionMeasurements(activity);
-            final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity)
-                    .setClientID(clientID)
-                    .setHadCanaryClientId(hadCanaryClientId)
-                    .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine))
-                    .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile))
-                    .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs))
-                    .setSessionCount(sessionMeasurementsContainer.sessionCount)
-                    .setSessionDuration(sessionMeasurementsContainer.elapsedSeconds);
-            maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder);
+                // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
+                final SharedPreferences sharedPrefs = getSharedPreferences(activity);
+                final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer =
+                        sessionMeasurements.getAndResetSessionMeasurements(activity);
+                final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity)
+                        .setClientID(clientID)
+                        .setHadCanaryClientId(hadCanaryClientId)
+                        .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine))
+                        .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile))
+                        .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs))
+                        .setSessionCount(sessionMeasurementsContainer.sessionCount)
+                        .setSessionDuration(sessionMeasurementsContainer.elapsedSeconds);
+                maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder);
 
-            getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder);
+                getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder);
+            }
         });
     }
 
     private void maybeSetOptionalMeasurements(final Context context, final SharedPreferences sharedPrefs,
                                               final TelemetryCorePingBuilder pingBuilder) {
         final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
         if (distributionId != null) {
             pingBuilder.setOptDistributionID(distributionId);
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
@@ -4,18 +4,16 @@
  * file, you can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 package org.mozilla.gecko.telemetry;
 
 import android.content.Context;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
-
-import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder;
 import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
 import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCrashPingBuilder;
 import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler;
 import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
 import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore;
 import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
 import org.mozilla.gecko.util.ThreadUtils;
 
@@ -88,24 +86,16 @@ public class TelemetryDispatcher {
      * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
      */
     public void queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder) {
         final TelemetryOutgoingPing ping = pingBuilder.build();
         queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
     }
 
     /**
-     * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
-     */
-    public void queuePingForUpload(final Context context, final TelemetryActivationPingBuilder pingBuilder) {
-        final TelemetryOutgoingPing ping = pingBuilder.build();
-        queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
-    }
-
-    /**
      * Queues the given crash ping for upload and potentially schedules upload. This method can be called from any thread.
      */
     public void queuePingForUpload(final Context context, final TelemetryCrashPingBuilder pingBuilder) {
         final TelemetryOutgoingPing ping = pingBuilder.build();
         queuePingForUpload(context, ping, crashStore, uploadAllPingsImmediatelyScheduler);
     }
 
     /* package-private */ static class QueuePingRunnable implements Runnable {
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -14,17 +14,16 @@ import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.JobIdsConstants;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
-import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder;
 import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
 import org.mozilla.gecko.util.DateUtil;
 import org.mozilla.gecko.util.NetworkUtils;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
@@ -121,46 +120,27 @@ public class TelemetryUploadService exte
             delegate.setDocID(ping.getDocID());
             final String url = serverSchemeHostPort + "/" + ping.getURLPath();
             uploadPayload(url, ping.getPayload(), delegate);
 
             // There are minimal gains in trying to upload if we already failed one attempt.
             if (delegate.hadConnectionError()) {
                 break;
             }
-
-            checkPingsPersistence(context, ping.getDocID());
         }
 
         final boolean wereAllUploadsSuccessful = !delegate.hadConnectionError();
         if (wereAllUploadsSuccessful) {
             // We don't log individual successful uploads to avoid log spam.
             Log.d(LOGTAG, "Telemetry upload success!");
         }
         store.onUploadAttemptComplete(successfulUploadIDs);
         return wereAllUploadsSuccessful;
     }
 
-    /**
-     * Check if we have any pings that need to persist their succesful upload status in order to prevent further attempts.
-     * E.g. {@link TelemetryActivationPingBuilder}
-     * @param context
-     */
-    private static void checkPingsPersistence(Context context, String successfulUploadID) {
-        final String activationID = TelemetryActivationPingBuilder.getActivationPingId(context);
-
-        if (activationID == null) {
-            return;
-        }
-
-        if (activationID.equals(successfulUploadID)) {
-            TelemetryActivationPingBuilder.setActivationPingSent(context, true);
-        }
-    }
-
     private static void uploadPayload(final String url, final ExtendedJSONObject payload, final ResultDelegate delegate) {
         final BaseResource resource;
         try {
             resource = new BaseResource(url);
         } catch (final URISyntaxException e) {
             Log.w(LOGTAG, "URISyntaxException for server URL when creating BaseResource: returning.");
             return;
         }
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryActivationPingBuilder.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.mozilla.gecko.telemetry.pingbuilders;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.support.annotation.NonNull;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.distribution.DistributionStoreCallback;
-import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
-import org.mozilla.gecko.util.DateUtil;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Locale;
-
-/**
- * Builds a {@link TelemetryOutgoingPing} representing a activation ping.
- *
- */
-public class TelemetryActivationPingBuilder extends TelemetryPingBuilder {
-    private static final String LOGTAG = StringUtils.safeSubstring(TelemetryActivationPingBuilder.class.getSimpleName(), 0, 23);
-
-    //Using MOZ_APP_BASENAME would be more elegant but according to the server side schema we need to be sure that we always send the "Fennec" value.
-    private static final String APP_NAME_VALUE = "Fennec";
-
-    private static final String PREFS_ACTIVATION_ID = "activation_ping_id";
-    private static final String PREFS_ACTIVATION_SENT = "activation_ping_sent";
-
-    private static final String NAME = "activation";
-    private static final int VERSION_VALUE = 1;
-
-    private static final String IDENTIFIER = "identifier";
-    private static final String CLIENT_ID = "clientId";
-    private static final String MANUFACTURER = "manufacturer";
-    private static final String MODEL = "model";
-    private static final String DISTRIBUTION_ID = "distribution_id";
-    private static final String LOCALE = "locale";
-    private static final String OS_ATTR = "os";
-    private static final String OS_VERSION = "osversion";
-    private static final String PING_CREATION_DATE = "created";
-    private static final String TIMEZONE_OFFSET = "tz";
-    private static final String APP_NAME = "app_name";
-    private static final String CHANNEL = "channel";
-
-    public TelemetryActivationPingBuilder(final Context context) {
-        super(VERSION_VALUE, true);
-        initPayloadConstants(context);
-    }
-
-    private void initPayloadConstants(final Context context) {
-        payload.put(MANUFACTURER, Build.MANUFACTURER);
-        payload.put(MODEL, Build.MODEL);
-        payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault()));
-        payload.put(OS_ATTR, TelemetryPingBuilder.OS_NAME);
-        payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
-
-        final Calendar nowCalendar = Calendar.getInstance();
-        final DateFormat pingCreationDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
-        payload.put(PING_CREATION_DATE, pingCreationDateFormat.format(nowCalendar.getTime()));
-        payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(nowCalendar));
-        payload.put(APP_NAME, APP_NAME_VALUE);
-        payload.put(CHANNEL, AppConstants.ANDROID_PACKAGE_NAME);
-
-        SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
-        final String distributionId = prefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
-        if (distributionId != null) {
-            payload.put(DISTRIBUTION_ID, distributionId);
-        }
-
-        prefs.edit().putString(PREFS_ACTIVATION_ID, docID).apply();
-    }
-
-    public static boolean activationPingAlreadySent(Context context) {
-        return GeckoSharedPrefs.forApp(context).getBoolean(PREFS_ACTIVATION_SENT, false);
-    }
-
-    public static void setActivationPingSent(Context context, boolean value) {
-        SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
-        prefs.edit().putBoolean(PREFS_ACTIVATION_SENT, value).apply();
-        prefs.edit().remove(PREFS_ACTIVATION_ID).apply();
-    }
-
-    public static String getActivationPingId(Context context) {
-        return GeckoSharedPrefs.forApp(context).getString(PREFS_ACTIVATION_ID, null);
-    }
-
-    @Override
-    public String getDocType() {
-        return NAME;
-    }
-
-    @Override
-    public String[] getMandatoryFields() {
-        return new String[] {
-                MANUFACTURER,
-                MODEL,
-                LOCALE,
-                OS_ATTR,
-                OS_VERSION,
-                PING_CREATION_DATE,
-                TIMEZONE_OFFSET,
-                APP_NAME,
-                CHANNEL
-        };
-    }
-
-    public TelemetryActivationPingBuilder setIdentifier(@NonNull final String identifier) {
-        if (identifier == null) {
-            throw new IllegalArgumentException("Expected non-null identifier");
-        }
-
-        payload.put(IDENTIFIER, identifier);
-        return this;
-    }
-
-    public TelemetryActivationPingBuilder setClientID(@NonNull final String clientID) {
-        if (clientID == null) {
-            throw new IllegalArgumentException("Expected non-null clientID");
-        }
-        payload.put(CLIENT_ID, clientID);
-        return this;
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java
@@ -20,19 +20,16 @@ import java.util.UUID;
  * This base class handles the common ping operations under the hood:
  *   * Validating mandatory fields
  *   * Forming the server url
  */
 abstract class TelemetryPingBuilder {
     // In the server url, the initial path directly after the "scheme://host:port/"
     private static final String SERVER_INITIAL_PATH = "submit/telemetry";
 
-    // Modern pings now use a structured ingestion where we capture the schema version as one of the URI parameters.
-    private static final String SERVER_INITIAL_PATH_MODERN = "submit/mobile";
-
     // By default Fennec ping's use the old telemetry version, this can be overridden
     private static final int DEFAULT_TELEMETRY_VERSION = 1;
 
     // Unified telemetry is version 4
     public static final int UNIFIED_TELEMETRY_VERSION = 4;
 
     // We deliberately call the OS/platform Android to avoid confusion with desktop Linux
     public static final String OS_NAME = "Android";
@@ -46,22 +43,16 @@ abstract class TelemetryPingBuilder {
     }
 
     public TelemetryPingBuilder(int version) {
         docID = UUID.randomUUID().toString();
         serverPath = getTelemetryServerPath(getDocType(), docID, version);
         payload = new ExtendedJSONObject();
     }
 
-    public TelemetryPingBuilder(int version, boolean modernPing) {
-        docID = UUID.randomUUID().toString();
-        serverPath = modernPing ? getModernTelemetryServerPath(getDocType(), docID, version) : getTelemetryServerPath(getDocType(), docID, version);
-        payload = new ExtendedJSONObject();
-    }
-
     /**
      * @return the name of the ping (e.g. "core")
      */
     public abstract String getDocType();
 
     /**
      * @return the fields that are mandatory for the resultant ping to be uploaded to
      *         the server. These will be validated before the ping is built.
@@ -104,27 +95,9 @@ abstract class TelemetryPingBuilder {
                 docID + '/' +
                 docType + '/' +
                 appName + '/' +
                 appVersion + '/' +
                 appUpdateChannel + '/' +
                 appBuildId +
                 (version == UNIFIED_TELEMETRY_VERSION ? "?v=4" : "");
     }
-
-    /**
-     * Returns a url of the format:
-     *   http://hostname/submit/mobile/docType/appVersion/docId/
-     *
-     *   User for modern structured ingestion.
-     *
-     * @param docType The name of the ping (e.g. "main")
-     * @param docID A UUID that identifies the ping
-     * @param version The ping format version
-     * @return a url at which to POST the telemetry data to
-     */
-    private static String getModernTelemetryServerPath(final String docType, final String docID, int version) {
-        return SERVER_INITIAL_PATH_MODERN + '/' +
-                docType + '/' +
-                version + '/' +
-                docID;
-    }
 }
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2720,19 +2720,17 @@ pref("security.view-source.reachable-fro
 pref("security.strict_security_checks.enabled", true);
 #else
 pref("security.strict_security_checks.enabled", false);
 #endif
 
 // Remote settings preferences
 pref("services.settings.poll_interval", 86400); // 24H
 pref("services.settings.server", "https://firefox.settings.services.mozilla.com/v1");
-pref("services.settings.changes.path", "/buckets/monitor/collections/changes/records");
 pref("services.settings.default_bucket", "main");
-pref("services.settings.default_signer", "remote-settings.content-signature.mozilla.org");
 
 // The percentage of clients who will report uptake telemetry as
 // events instead of just a histogram. This only applies on Release;
 // other channels always report events.
 pref("services.common.uptake.sampleRate", 1);   // 1%
 
 // Security state OneCRL.
 pref("services.settings.security.onecrl.bucket", "security-state");
--- a/remote/RemoteAgent.jsm
+++ b/remote/RemoteAgent.jsm
@@ -95,17 +95,17 @@ class RemoteAgentClass {
     await this.tabs.start();
 
     try {
       // Immediatly instantiate the main process target in order
       // to be accessible via HTTP endpoint on startup
       const mainTarget = this.targets.getMainProcessTarget();
 
       this.server._start(port, host);
-      dump(`DevTools listening on ${mainTarget.wsDebuggerURL}`);
+      dump(`DevTools listening on ${mainTarget.wsDebuggerURL}\n`);
     } catch (e) {
       throw new Error(`Unable to start remote agent: ${e.message}`, e);
     }
 
     Preferences.set(RecommendedPreferences);
   }
 
   async close() {
--- a/remote/domains/ContentProcessDomain.jsm
+++ b/remote/domains/ContentProcessDomain.jsm
@@ -3,23 +3,41 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["ContentProcessDomain"];
 
 const {Domain} = ChromeUtils.import("chrome://remote/content/domains/Domain.jsm");
 
+ChromeUtils.defineModuleGetter(this, "ContextObserver",
+                               "chrome://remote/content/domains/ContextObserver.jsm");
+
 class ContentProcessDomain extends Domain {
+  destructor() {
+    super.destructor();
+
+    if (this._contextObserver) {
+      this._contextObserver.destructor();
+    }
+  }
+
   // helpers
 
   get content() {
     return this.session.content;
   }
 
   get docShell() {
     return this.session.docShell;
   }
 
   get chromeEventHandler() {
     return this.docShell.chromeEventHandler;
   }
+
+  get contextObserver() {
+    if (!this._contextObserver) {
+      this._contextObserver = new ContextObserver(this.chromeEventHandler);
+    }
+    return this._contextObserver;
+  }
 }
new file mode 100644
--- /dev/null
+++ b/remote/domains/ContextObserver.jsm
@@ -0,0 +1,103 @@
+/* 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";
+
+/**
+ * Helper class to coordinate Runtime and Page events.
+ * Events have to be sent in the following order:
+ *  - Runtime.executionContextDestroyed
+ *  - Page.frameNavigated
+ *  - Runtime.executionContextCreated
+ *
+ * This class also handles the special case of Pages going from/to the BF cache.
+ * When you navigate to a new URL, the previous document may be stored in the BF Cache.
+ * All its asynchronous operations are frozen (XHR, timeouts, ...) and a `pagehide` event
+ * is fired for this document. We then navigate to the new URL.
+ * If the user navigates back to the previous page, the page is resurected from the
+ * cache. A `pageshow` event is fired and its asynchronous operations are resumed.
+ *
+ * When a page is in the BF Cache, we should consider it as frozen and shouldn't try
+ * to execute any javascript. So that the ExecutionContext should be considered as
+ * being destroyed and the document navigated.
+ */
+
+var EXPORTED_SYMBOLS = ["ContextObserver"];
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
+
+class ContextObserver {
+  constructor(chromeEventHandler) {
+    this.chromeEventHandler = chromeEventHandler;
+    EventEmitter.decorate(this);
+
+    this.chromeEventHandler.addEventListener("DOMWindowCreated", this,
+      {mozSystemGroup: true});
+
+    // Listen for pageshow and pagehide to track pages going in/out to/from the BF Cache
+    this.chromeEventHandler.addEventListener("pageshow", this,
+      {mozSystemGroup: true});
+    this.chromeEventHandler.addEventListener("pagehide", this,
+      {mozSystemGroup: true});
+
+    Services.obs.addObserver(this, "inner-window-destroyed");
+  }
+
+  destructor() {
+    this.chromeEventHandler.removeEventListener("DOMWindowCreated", this,
+      {mozSystemGroup: true});
+    this.chromeEventHandler.removeEventListener("pageshow", this,
+      {mozSystemGroup: true});
+    this.chromeEventHandler.removeEventListener("pagehide", this,
+      {mozSystemGroup: true});
+    Services.obs.removeObserver(this, "inner-window-destroyed");
+  }
+
+  handleEvent({type, target, persisted}) {
+    const window = target.defaultView;
+    if (window.top != this.chromeEventHandler.ownerGlobal) {
+      // Ignore iframes for now.
+      return;
+    }
+    const { windowUtils } = window;
+    const frameId = windowUtils.outerWindowID;
+    const id = windowUtils.currentInnerWindowID;
+    switch (type) {
+    case "DOMWindowCreated":
+      // Do not pass `id` here as that's the new document ID instead of the old one
+      // that is destroyed. Instead, pass the frameId and let the listener figure out
+      // what ExecutionContext to destroy.
+      this.emit("context-destroyed", { frameId });
+      this.emit("frame-navigated", { frameId, window });
+      this.emit("context-created", { id, window });
+      break;
+    case "pageshow":
+      // `persisted` is true when this is about a page being resurected from BF Cache
+      if (!persisted) {
+        return;
+      }
+      // XXX(ochameau) we might have to emit FrameNavigate here to properly handle BF Cache
+      // scenario in Page domain events
+      this.emit("context-created", { id, window });
+      break;
+
+    case "pagehide":
+      // `persisted` is true when this is about a page being frozen into BF Cache
+      if (!persisted) {
+        return;
+      }
+      this.emit("context-destroyed", { id });
+      break;
+    }
+  }
+
+  // "inner-window-destroyed" observer service listener
+  observe(subject, topic, data) {
+    const innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+    this.emit("context-destroyed", { id: innerWindowID });
+  }
+}
+
+
--- a/remote/domains/content/Page.jsm
+++ b/remote/domains/content/Page.jsm
@@ -9,49 +9,42 @@ var EXPORTED_SYMBOLS = ["Page"];
 const {ContentProcessDomain} = ChromeUtils.import("chrome://remote/content/domains/ContentProcessDomain.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const {UnsupportedError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
 
 class Page extends ContentProcessDomain {
   constructor(session) {
     super(session);
     this.enabled = false;
+
+    this.onFrameNavigated = this.onFrameNavigated.bind(this);
   }
 
   destructor() {
     this.disable();
   }
 
-  QueryInterface(iid) {
-    if (iid.equals(Ci.nsIWebProgressListener) ||
-      iid.equals(Ci.nsISupportsWeakReference) ||
-      iid.equals(Ci.nsIObserver)) {
-      return this;
-    }
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  }
-
   // commands
 
   async enable() {
     if (!this.enabled) {
       this.enabled = true;
-      this.chromeEventHandler.addEventListener("DOMWindowCreated", this,
-        {mozSystemGroup: true});
+      this.contextObserver.on("frame-navigated", this.onFrameNavigated);
+
       this.chromeEventHandler.addEventListener("DOMContentLoaded", this,
         {mozSystemGroup: true});
       this.chromeEventHandler.addEventListener("pageshow", this,
         {mozSystemGroup: true});
     }
   }
 
   disable() {
     if (this.enabled) {
-      this.chromeEventHandler.removeEventListener("DOMWindowCreated", this,
-        {mozSystemGroup: true});
+      this.contextObserver.off("frame-navigated", this.onFrameNavigated);
+
       this.chromeEventHandler.removeEventListener("DOMContentLoaded", this,
         {mozSystemGroup: true});
       this.chromeEventHandler.removeEventListener("pageshow", this,
         {mozSystemGroup: true});
       this.enabled = false;
     }
   }
 
@@ -95,38 +88,40 @@ class Page extends ContentProcessDomain 
   setLifecycleEventsEnabled() {}
   addScriptToEvaluateOnNewDocument() {}
   createIsolatedWorld() {}
 
   url() {
     return this.content.location.href;
   }
 
+  onFrameNavigated(name, { frameId, window }) {
+    const url = window.location.href;
+    this.emit("Page.frameNavigated", {
+      frame: {
+        id: frameId,
+        // frameNavigated is only emitted for the top level document
+        // so that it never has a parent.
+        parentId: null,
+        url,
+      },
+    });
+  }
+
   handleEvent({type, target}) {
     if (target.defaultView != this.content) {
       // Ignore iframes for now
       return;
     }
 
     const timestamp = Date.now();
     const frameId = target.defaultView.windowUtils.outerWindowID;
     const url = target.location.href;
 
     switch (type) {
-    case "DOMWindowCreated":
-      this.emit("Page.frameNavigated", {
-        frame: {
-          id: frameId,
-          // frameNavigated is only emitted for the top level document
-          // so that it never has a parent.
-          parentId: null,
-          url,
-        },
-      });
-      break;
     case "DOMContentLoaded":
       this.emit("Page.domContentEventFired", {timestamp});
       break;
 
     case "pageshow":
       this.emit("Page.loadEventFired", {timestamp, frameId});
       // XXX this should most likely be sent differently
       this.emit("Page.navigatedWithinDocument", {timestamp, frameId, url});
--- a/remote/domains/content/Runtime.jsm
+++ b/remote/domains/content/Runtime.jsm
@@ -17,56 +17,49 @@ addDebuggerToGlobal(Cu.getGlobalForObjec
 class Runtime extends ContentProcessDomain {
   constructor(session) {
     super(session);
     this.enabled = false;
 
     // Map of all the ExecutionContext instances:
     // [Execution context id (Number) => ExecutionContext instance]
     this.contexts = new Map();
+
+    this.onContextCreated = this.onContextCreated.bind(this);
+    this.onContextDestroyed = this.onContextDestroyed.bind(this);
   }
 
   destructor() {
     this.disable();
   }
 
   // commands
 
   async enable() {
     if (!this.enabled) {
       this.enabled = true;
-      this.chromeEventHandler.addEventListener("DOMWindowCreated", this,
-        {mozSystemGroup: true});
-
-      // Listen for pageshow and pagehide to track pages going in/out to/from the BF Cache
-      this.chromeEventHandler.addEventListener("pageshow", this,
-        {mozSystemGroup: true});
-      this.chromeEventHandler.addEventListener("pagehide", this,
-        {mozSystemGroup: true});
-
-      Services.obs.addObserver(this, "inner-window-destroyed");
+      this.contextObserver.on("context-created", this.onContextCreated);
+      this.contextObserver.on("context-destroyed", this.onContextDestroyed);
 
       // Spin the event loop in order to send the `executionContextCreated` event right
       // after we replied to `enable` request.
       Services.tm.dispatchToMainThread(() => {
-        this._createContext(this.content);
+        this.onContextCreated("context-created", {
+          id: this.content.windowUtils.currentInnerWindowID,
+          window: this.content,
+        });
       });
     }
   }
 
   disable() {
     if (this.enabled) {
       this.enabled = false;
-      this.chromeEventHandler.removeEventListener("DOMWindowCreated", this,
-        {mozSystemGroup: true});
-      this.chromeEventHandler.removeEventListener("pageshow", this,
-        {mozSystemGroup: true});
-      this.chromeEventHandler.removeEventListener("pagehide", this,
-        {mozSystemGroup: true});
-      Services.obs.removeObserver(this, "inner-window-destroyed");
+      this.contextObserver.off("context-created", this.onContextCreated);
+      this.contextObserver.off("context-destroyed", this.onContextDestroyed);
     }
   }
 
   evaluate(request) {
     const context = this.contexts.get(request.contextId);
     if (!context) {
       throw new Error(`Unable to find execution context with id: ${request.contextId}`);
     }
@@ -115,91 +108,77 @@ class Runtime extends ContentProcessDoma
   get _debugger() {
     if (this.__debugger) {
       return this.__debugger;
     }
     this.__debugger = new Debugger();
     return this.__debugger;
   }
 
-  handleEvent({type, target, persisted}) {
-    if (target.defaultView != this.content) {
-      // Ignore iframes for now.
-      return;
-    }
-    switch (type) {
-    case "DOMWindowCreated":
-      this._createContext(target.defaultView);
-      break;
-
-    case "pageshow":
-      // `persisted` is true when this is about a page being resurected from BF Cache
-      if (!persisted) {
-        return;
+  getContextByFrameId(frameId) {
+    for (const ctx of this.contexts.values()) {
+      if (ctx.frameId === frameId) {
+        return ctx;
       }
-      this._createContext(target.defaultView);
-      break;
-
-    case "pagehide":
-      // `persisted` is true when this is about a page being frozen into BF Cache
-      if (!persisted) {
-        return;
-      }
-      const id = target.defaultView.windowUtils.currentInnerWindowID;
-      this._destroyContext(id);
-      break;
     }
-  }
-
-  observe(subject, topic, data) {
-    const innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-    this._destroyContext(innerWindowID);
+    return null;
   }
 
   /**
    * Helper method in order to instantiate the ExecutionContext for a given
    * DOM Window as well as emitting the related `Runtime.executionContextCreated`
    * event.
    *
    * @param {Window} window
    *     The window object of the newly instantiated document.
    */
-  _createContext(window) {
-    const { windowUtils } = window;
-    const id = windowUtils.currentInnerWindowID;
+  onContextCreated(name, { id, window }) {
     if (this.contexts.has(id)) {
       return;
     }
 
     const context = new ExecutionContext(this._debugger, window);
     this.contexts.set(id, context);
 
-    const frameId = windowUtils.outerWindowID;
     this.emit("Runtime.executionContextCreated", {
       context: {
         id,
         auxData: {
           isDefault: window == this.content,
-          frameId,
+          frameId: context.frameId,
         },
       },
     });
   }
 
   /**
    * Helper method to destroy the ExecutionContext of the given id. Also emit
    * the related `Runtime.executionContextDestroyed` event.
+   * ContextObserver will call this method with either `id` or `frameId` argument
+   * being set.
    *
    * @param {Number} id
    *     The execution context id to destroy.
+   * @param {Number} frameId
+   *     The frame id of execution context to destroy.
+   * Eiter `id` or `frameId` is passed.
    */
-  _destroyContext(id) {
-    const context = this.contexts.get(id);
+  onContextDestroyed(name, { id, frameId }) {
+    let context;
+    if (id && frameId) {
+      throw new Error("Expects only id *or* frameId argument to be passed");
+    }
+
+    if (id) {
+      context = this.contexts.get(id);
+    } else {
+      context = this.getContextByFrameId(frameId);
+    }
 
     if (context) {
       context.destructor();
-      this.contexts.delete(id);
+      this.contexts.delete(context.id);
       this.emit("Runtime.executionContextDestroyed", {
-        executionContextId: id,
+        executionContextId: context.id,
       });
     }
   }
 }
--- a/remote/domains/content/runtime/ExecutionContext.jsm
+++ b/remote/domains/content/runtime/ExecutionContext.jsm
@@ -26,16 +26,22 @@ function uuid() {
  *   The debuggable context's global object. This is typically the document window
  *   object. But it can also be any global object, like a worker global scope object.
  */
 class ExecutionContext {
   constructor(dbg, debuggee) {
     this._debugger = dbg;
     this._debuggee = this._debugger.addDebuggee(debuggee);
 
+    // Here, we assume that debuggee is a window object and we will propably have
+    // to adapt that once we cover workers or contexts that aren't a document.
+    const { windowUtils } = debuggee;
+    this.id = windowUtils.currentInnerWindowID;
+    this.frameId = windowUtils.outerWindowID;
+
     this._remoteObjects = new Map();
   }
 
   destructor() {
     this._debugger.removeDebuggee(this._debuggee);
   }
 
   hasRemoteObject(id) {
--- a/remote/jar.mn
+++ b/remote/jar.mn
@@ -26,16 +26,17 @@ remote.jar:
   content/targets/MainProcessTarget.jsm (targets/MainProcessTarget.jsm)
   content/targets/TabTarget.jsm (targets/TabTarget.jsm)
   content/targets/Target.jsm (targets/Target.jsm)
   content/targets/Targets.jsm (targets/Targets.jsm)
 
   # domains
   content/domains/ContentProcessDomain.jsm (domains/ContentProcessDomain.jsm)
   content/domains/ContentProcessDomains.jsm (domains/ContentProcessDomains.jsm)
+  content/domains/ContextObserver.jsm (domains/ContextObserver.jsm)
   content/domains/Domain.jsm (domains/Domain.jsm)
   content/domains/Domains.jsm (domains/Domains.jsm)
   content/domains/ParentProcessDomains.jsm (domains/ParentProcessDomains.jsm)
   content/domains/content/DOM.jsm (domains/content/DOM.jsm)
   content/domains/content/Emulation.jsm (domains/content/Emulation.jsm)
   content/domains/content/Input.jsm (domains/content/Input.jsm)
   content/domains/content/Log.jsm (domains/content/Log.jsm)
   content/domains/content/Network.jsm (domains/content/Network.jsm)
--- a/remote/test/browser/browser.ini
+++ b/remote/test/browser/browser.ini
@@ -4,14 +4,15 @@ prefs = remote.enabled=true
 support-files =
   chrome-remote-interface.js
   head.js
 skip-if = debug || asan # bug 1546945
 
 [browser_cdp.js]
 [browser_main_target.js]
 [browser_page_frameNavigated.js]
+[browser_page_runtime_events.js]
 [browser_runtime_evaluate.js]
 [browser_runtime_callFunctionOn.js]
 [browser_runtime_executionContext.js]
 skip-if = os == "mac" || (verify && os == 'win') # bug 1547961
 [browser_tabs.js]
 [browser_target.js]
new file mode 100644
--- /dev/null
+++ b/remote/test/browser/browser_page_runtime_events.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global getCDP */
+
+// Assert the order of Runtime.executionContextDestroyed, Page.frameNavigated and Runtime.executionContextCreated
+
+const TEST_URI = "data:text/html;charset=utf-8,default-test-page";
+
+add_task(async function testCDP() {
+  // Open a test page, to prevent debugging the random default page
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI);
+
+  // Start the CDP server
+  RemoteAgent.listen(Services.io.newURI("http://localhost:9222"));
+
+  // Retrieve the chrome-remote-interface library object
+  const CDP = await getCDP();
+
+  // Connect to the server
+  const client = await CDP({
+    target(list) {
+      // Ensure debugging the right target, i.e. the one for our test tab.
+      return list.find(target => {
+        return target.url == TEST_URI;
+      });
+    },
+  });
+  ok(true, "CDP client has been instantiated");
+
+  const {Page, Runtime} = client;
+
+  const events = [];
+  function assertReceivedEvents(expected, message) {
+    Assert.deepEqual(events, expected, message);
+    // Empty the list of received events
+    events.splice(0);
+  }
+  Page.frameNavigated(() => {
+    events.push("frameNavigated");
+  });
+  Runtime.executionContextCreated(() => {
+    events.push("executionContextCreated");
+  });
+  Runtime.executionContextDestroyed(() => {
+    events.push("executionContextDestroyed");
+  });
+
+  // turn on navigation related events, such as DOMContentLoaded et al.
+  await Page.enable();
+  ok(true, "Page domain has been enabled");
+
+  const onExecutionContextCreated = Runtime.executionContextCreated();
+  await Runtime.enable();
+  ok(true, "Runtime domain has been enabled");
+
+  // Runtime.enable will dispatch `executionContextCreated` for the existing document
+  let { context } = await onExecutionContextCreated;
+  ok(!!context.id, "The execution context has an id");
+  ok(context.auxData.isDefault, "The execution context is the default one");
+  ok(!!context.auxData.frameId, "The execution context has a frame id set");
+
+  assertReceivedEvents(["executionContextCreated"], "Received only executionContextCreated event after Runtime.enable call");
+
+  const { frameTree } = await Page.getFrameTree();
+  ok(!!frameTree.frame, "getFrameTree exposes one frame");
+  is(frameTree.childFrames.length, 0, "getFrameTree reports no child frame");
+  ok(!!frameTree.frame.id, "getFrameTree's frame has an id");
+  is(frameTree.frame.url, TEST_URI, "getFrameTree's frame has the right url");
+  is(frameTree.frame.id, context.auxData.frameId, "getFrameTree and executionContextCreated refers about the same frame Id");
+
+  const onFrameNavigated = Page.frameNavigated();
+  const onExecutionContextDestroyed = Runtime.executionContextDestroyed();
+  const onExecutionContextCreated2 = Runtime.executionContextCreated();
+  const url = "data:text/html;charset=utf-8,test-page";
+  const { frameId } = await Page.navigate({ url  });
+  ok(true, "A new page has been loaded");
+  ok(frameId, "Page.navigate returned a frameId");
+  is(frameId, frameTree.frame.id, "The Page.navigate's frameId is the same than " +
+    "getFrameTree's one");
+
+  const frameNavigated = await onFrameNavigated;
+  ok(!frameNavigated.frame.parentId, "frameNavigated is for the top level document and" +
+    " has a null parentId");
+  is(frameNavigated.frame.id, frameId, "frameNavigated id is the same than the one " +
+    "returned by Page.navigate");
+  is(frameNavigated.frame.name, undefined, "frameNavigated name isn't implemented yet");
+  is(frameNavigated.frame.url, url, "frameNavigated url is the same being given to " +
+    "Page.navigate");
+
+  const { executionContextId } = await onExecutionContextDestroyed;
+  ok(executionContextId, "The destroyed event reports an id");
+  is(executionContextId, context.id, "The destroyed event is for the first reported execution context");
+
+  ({ context } = await onExecutionContextCreated2);
+  ok(!!context.id, "The execution context has an id");
+  ok(context.auxData.isDefault, "The execution context is the default one");
+  is(context.auxData.frameId, frameId, "The execution context frame id is the same " +
+    "the one returned by Page.navigate");
+
+  isnot(executionContextId, context.id, "The destroyed id is different from the " +
+    "created one");
+
+  assertReceivedEvents(["executionContextDestroyed", "frameNavigated", "executionContextCreated"],
+    "Received frameNavigated between the two execution context events during navigation to another URL");
+
+  await client.close();
+  ok(true, "The client is closed");
+
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  await RemoteAgent.close();
+});
--- a/services/common/tests/unit/moz.build
+++ b/services/common/tests/unit/moz.build
@@ -1,9 +1,5 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
-
-TEST_DIRS += [
-    'test_blocklist_signatures'
-]
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -1,29 +1,26 @@
 [DEFAULT]
 head = head_global.js head_helpers.js head_http.js
 firefox-appdir = browser
 support-files =
   test_storage_adapter/**
-  test_blocklist_signatures/**
 
 # Test load modules first so syntax failures are caught early.
 [test_load_modules.js]
 
 [test_blocklist_onecrl.js]
 # Skip signature tests for Thunderbird (Bug 1341983).
 skip-if = appname == "thunderbird"
 tags = blocklist
 [test_blocklist_pinning.js]
 tags = blocklist
 
 [test_kinto.js]
 tags = blocklist
-[test_blocklist_signatures.js]
-tags = remote-settings blocklist
 [test_storage_adapter.js]
 tags = remote-settingsblocklist
 [test_storage_adapter_shutdown.js]
 tags = remote-settings blocklist
 
 [test_utils_atob.js]
 [test_utils_convert_string.js]
 [test_utils_dateprefs.js]
--- a/services/settings/RemoteSettingsClient.jsm
+++ b/services/settings/RemoteSettingsClient.jsm
@@ -30,18 +30,16 @@ XPCOMUtils.defineLazyGlobalGetters(this,
 
 // IndexedDB name.
 const DB_NAME = "remote-settings";
 
 const TELEMETRY_COMPONENT = "remotesettings";
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "gServerURL",
                                       "services.settings.server");
-XPCOMUtils.defineLazyPreferenceGetter(this, "gChangesPath",
-                                      "services.settings.changes.path");
 
 /**
  * cacheProxy returns an object Proxy that will memoize properties of the target.
  * @param {Object} target the object to wrap.
  * @returns {Proxy}
  */
 function cacheProxy(target) {
   const cache = new Map();
@@ -250,17 +248,17 @@ class RemoteSettingsClient extends Event
   /**
    * Synchronize the local database with the remote server.
    *
    * @param {Object} options See #maybeSync() options.
    */
   async sync(options) {
     // We want to know which timestamp we are expected to obtain in order to leverage
     // cache busting. We don't provide ETag because we don't want a 304.
-    const { changes } = await Utils.fetchLatestChanges(gServerURL + gChangesPath, {
+    const { changes } = await Utils.fetchLatestChanges(gServerURL, {
       filters: {
         collection: this.collectionName,
         bucket: this.bucketName,
       },
     });
     if (changes.length === 0) {
       throw new Error(`Unknown collection "${this.identifier}"`);
     }
--- a/services/settings/Utils.jsm
+++ b/services/settings/Utils.jsm
@@ -7,16 +7,17 @@
 ];
 
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 
 var Utils = {
+  CHANGES_PATH: "/buckets/monitor/collections/changes/records",
 
   /**
    * Check if local data exist for the specified client.
    *
    * @param {RemoteSettingsClient} client
    * @return {bool} Whether it exists or not.
    */
   async hasLocalData(client) {
@@ -38,36 +39,39 @@ var Utils = {
       return true;
     } catch (e) {
       return false;
     }
   },
 
   /**
    * Fetch the list of remote collections and their timestamp.
-   * @param {String} url               The poll URL (eg. `http://${server}{pollingEndpoint}`)
+   * @param {String} serverUrl         The server URL (eg. `https://server.org/v1`)
    * @param {int}    expectedTimestamp The timestamp that the server is supposed to return.
    *                                   We obtained it from the Megaphone notification payload,
    *                                   and we use it only for cache busting (Bug 1497159).
    * @param {String} lastEtag          (optional) The Etag of the latest poll to be matched
    *                                   by the server (eg. `"123456789"`).
    * @param {Object} filters
    */
-  async fetchLatestChanges(url, options = {}) {
+  async fetchLatestChanges(serverUrl, options = {}) {
     const { expectedTimestamp, lastEtag = "", filters = {} } = options;
+
     //
     // Fetch the list of changes objects from the server that looks like:
     // {"data":[
     //   {
     //     "host":"kinto-ota.dev.mozaws.net",
     //     "last_modified":1450717104423,
     //     "bucket":"blocklists",
     //     "collection":"certificates"
     //    }]}
 
+    let url = serverUrl + Utils.CHANGES_PATH;
+
     // Use ETag to obtain a `304 Not modified` when no change occurred,
     // and `?_since` parameter to only keep entries that weren't processed yet.
     const headers = {};
     const params = { ...filters };
     if (lastEtag != "") {
       headers["If-None-Match"] = lastEtag;
       params._since = lastEtag;
     }
--- a/services/settings/moz.build
+++ b/services/settings/moz.build
@@ -1,15 +1,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/.
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Remote Settings Client')
 
+TEST_DIRS += ['test']
+
 DIRS += [
     'dumps',
 ]
 
 EXTRA_COMPONENTS += [
     'servicesSettings.manifest',
 ]
 
--- a/services/settings/remote-settings.js
+++ b/services/settings/remote-settings.js
@@ -28,36 +28,36 @@ ChromeUtils.defineModuleGetter(this, "Fi
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 
 const PREF_SETTINGS_DEFAULT_BUCKET     = "services.settings.default_bucket";
 const PREF_SETTINGS_BRANCH             = "services.settings.";
 const PREF_SETTINGS_SERVER             = "server";
 const PREF_SETTINGS_DEFAULT_SIGNER     = "default_signer";
 const PREF_SETTINGS_SERVER_BACKOFF     = "server.backoff";
-const PREF_SETTINGS_CHANGES_PATH       = "changes.path";
 const PREF_SETTINGS_LAST_UPDATE        = "last_update_seconds";
 const PREF_SETTINGS_LAST_ETAG          = "last_etag";
 const PREF_SETTINGS_CLOCK_SKEW_SECONDS = "clock_skew_seconds";
 const PREF_SETTINGS_LOAD_DUMP          = "load_dump";
 
-
 // Telemetry identifiers.
 const TELEMETRY_COMPONENT = "remotesettings";
 const TELEMETRY_SOURCE_POLL = "settings-changes-monitoring";
 const TELEMETRY_SOURCE_SYNC = "settings-sync";
 
 // Push broadcast id.
 const BROADCAST_ID = "remote-settings/monitor_changes";
 
+// Signer to be used when not specified (see Ci.nsIContentSignatureVerifier).
+const DEFAULT_SIGNER = "remote-settings.content-signature.mozilla.org";
+
 XPCOMUtils.defineLazyGetter(this, "gPrefs", () => {
   return Services.prefs.getBranch(PREF_SETTINGS_BRANCH);
 });
 XPCOMUtils.defineLazyPreferenceGetter(this, "gServerURL", PREF_SETTINGS_BRANCH + PREF_SETTINGS_SERVER);
-XPCOMUtils.defineLazyPreferenceGetter(this, "gChangesPath", PREF_SETTINGS_BRANCH + PREF_SETTINGS_CHANGES_PATH);
 
 /**
  * Default entry filtering function, in charge of excluding remote settings entries
  * where the JEXL expression evaluates into a falsy value.
  * @param {Object}            entry       The Remote Settings entry to be excluded or kept.
  * @param {ClientEnvironment} environment Information about version, language, platform etc.
  * @returns {?Object} the entry or null if excluded.
  */
@@ -79,20 +79,19 @@ async function jexlFilterFunc(entry, env
 }
 
 
 function remoteSettingsFunction() {
   const _clients = new Map();
   let _invalidatePolling = false;
 
   // If not explicitly specified, use the default signer.
-  const defaultSigner = gPrefs.getCharPref(PREF_SETTINGS_DEFAULT_SIGNER);
   const defaultOptions = {
     bucketNamePref: PREF_SETTINGS_DEFAULT_BUCKET,
-    signerName: defaultSigner,
+    signerName: DEFAULT_SIGNER,
     filterFunc: jexlFilterFunc,
   };
 
   /**
    * RemoteSettings constructor.
    *
    * @param {String} collectionName The remote settings identifier
    * @param {Object} options Advanced options
@@ -107,22 +106,16 @@ function remoteSettingsFunction() {
       _clients.set(collectionName, c);
       // Invalidate the polling status, since we want the new collection to
       // be taken into account.
       _invalidatePolling = true;
     }
     return _clients.get(collectionName);
   };
 
-  Object.defineProperty(remoteSettings, "pollingEndpoint", {
-    get() {
-      return gServerURL + gChangesPath;
-    },
-  });
-
   /**
    * Internal helper to retrieve existing instances of clients or new instances
    * with default options if possible, or `null` if bucket/collection are unknown.
    */
   async function _client(bucketName, collectionName) {
     // Check if a client was registered for this bucket/collection. Potentially
     // with some specific options like signer, filter function etc.
     const client = _clients.get(collectionName);
@@ -181,17 +174,17 @@ function remoteSettingsFunction() {
     Services.obs.notifyObservers(null, "remote-settings:changes-poll-start", JSON.stringify({ expectedTimestamp }));
 
     // Do we have the latest version already?
     // Every time we register a new client, we have to fetch the whole list again.
     const lastEtag = _invalidatePolling ? "" : gPrefs.getCharPref(PREF_SETTINGS_LAST_ETAG, "");
 
     let pollResult;
     try {
-      pollResult = await Utils.fetchLatestChanges(remoteSettings.pollingEndpoint, { expectedTimestamp, lastEtag });
+      pollResult = await Utils.fetchLatestChanges(gServerURL, { expectedTimestamp, lastEtag });
     } catch (e) {
       // Report polling error to Uptake Telemetry.
       let reportStatus;
       if (/JSON\.parse/.test(e.message)) {
         reportStatus = UptakeTelemetry.STATUS.PARSE_ERROR;
       } else if (/content-type/.test(e.message)) {
         reportStatus = UptakeTelemetry.STATUS.CONTENT_ERROR;
       } else if (/Server/.test(e.message)) {
@@ -286,17 +279,17 @@ function remoteSettingsFunction() {
     Services.obs.notifyObservers(null, "remote-settings:changes-poll-end");
   };
 
   /**
    * Returns an object with polling status information and the list of
    * known remote settings collections.
    */
   remoteSettings.inspect = async () => {
-    const { changes, currentEtag: serverTimestamp } = await Utils.fetchLatestChanges(remoteSettings.pollingEndpoint);
+    const { changes, currentEtag: serverTimestamp } = await Utils.fetchLatestChanges(gServerURL);
 
     const collections = await Promise.all(changes.map(async (change) => {
       const { bucket, collection, last_modified: serverTimestamp } = change;
       const client = await _client(bucket, collection);
       if (!client) {
         return null;
       }
       const kintoCol = await client.openCollection();
@@ -309,21 +302,22 @@ function remoteSettingsFunction() {
         serverTimestamp,
         lastCheck,
         signerName: client.signerName,
       };
     }));
 
     return {
       serverURL: gServerURL,
+      pollingEndpoint: gServerURL + Utils.CHANGES_PATH,
       serverTimestamp,
       localTimestamp: gPrefs.getCharPref(PREF_SETTINGS_LAST_ETAG, null),
       lastCheck: gPrefs.getIntPref(PREF_SETTINGS_LAST_UPDATE, 0),
       mainBucket: Services.prefs.getCharPref(PREF_SETTINGS_DEFAULT_BUCKET),
-      defaultSigner,
+      defaultSigner: DEFAULT_SIGNER,
       collections: collections.filter(c => !!c),
     };
   };
 
   /**
    * Startup function called from nsBrowserGlue.
    */
   remoteSettings.init = () => {
--- a/services/settings/servicesSettings.manifest
+++ b/services/settings/servicesSettings.manifest
@@ -1,5 +1,7 @@
 # Register resource aliases
 resource services-settings resource://gre/modules/services-settings/
 
 # Schedule polling of remote settings changes
-category update-timer RemoteSettingsComponents @mozilla.org/services/settings;1,getService,services-settings-poll-changes,services.settings.poll_interval,86400
+# (default 24H, max 72H)
+# see syntax https://searchfox.org/mozilla-central/rev/cc280c4be94ff8cf64a27cc9b3d6831ffa49fa45/toolkit/components/timermanager/UpdateTimerManager.jsm#155
+category update-timer RemoteSettingsComponents @mozilla.org/services/settings;1,getService,services-settings-poll-changes,services.settings.poll_interval,86400,259200
new file mode 100644
--- /dev/null
+++ b/services/settings/test/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += [
+    'unit'
+]
copy from services/common/tests/moz.build
copy to services/settings/test/unit/moz.build
--- a/services/common/tests/moz.build
+++ b/services/settings/test/unit/moz.build
@@ -1,11 +1,9 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
-
 TEST_DIRS += [
-    'unit'
+    'test_remote_settings_signatures'
 ]
--- a/services/settings/test/unit/test_remote_settings_jexl_filters.js
+++ b/services/settings/test/unit/test_remote_settings_jexl_filters.js
@@ -99,20 +99,20 @@ add_task(async function test_support_of_
 });
 
 add_task(async function test_support_of_preferences_filters() {
   await createRecords([{
     willMatch: true,
     filter_expression: '"services.settings.last_etag"|preferenceValue == 42',
   }, {
     willMatch: true,
-    filter_expression: '"services.settings.changes.path"|preferenceExists == true',
+    filter_expression: '"services.settings.default_bucket"|preferenceExists == true',
   }, {
     willMatch: true,
-    filter_expression: '"services.settings.changes.path"|preferenceIsUserSet == false',
+    filter_expression: '"services.settings.default_bucket"|preferenceIsUserSet == false',
   }, {
     willMatch: true,
     filter_expression: '"services.settings.last_etag"|preferenceIsUserSet == true',
   }]);
 
   // Set a pref for the user.
   Services.prefs.setIntPref("services.settings.last_etag", 42);
 
--- a/services/settings/test/unit/test_remote_settings_poll.js
+++ b/services/settings/test/unit/test_remote_settings_poll.js
@@ -7,32 +7,33 @@ const { setTimeout } = ChromeUtils.impor
 const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js");
 const { Kinto } = ChromeUtils.import("resource://services-common/kinto-offline-client.js");
 const { pushBroadcastService } = ChromeUtils.import("resource://gre/modules/PushBroadcastService.jsm");
 const {
   RemoteSettings,
   remoteSettingsBroadcastHandler,
   BROADCAST_ID,
 } = ChromeUtils.import("resource://services-settings/remote-settings.js");
+const { Utils } = ChromeUtils.import("resource://services-settings/Utils.jsm");
 const { TelemetryTestUtils } = ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm");
 
 
 const IS_ANDROID = AppConstants.platform == "android";
 
 const PREF_SETTINGS_SERVER = "services.settings.server";
 const PREF_SETTINGS_SERVER_BACKOFF = "services.settings.server.backoff";
 const PREF_LAST_UPDATE = "services.settings.last_update_seconds";
 const PREF_LAST_ETAG = "services.settings.last_etag";
 const PREF_CLOCK_SKEW_SECONDS = "services.settings.clock_skew_seconds";
 
 const DB_NAME = "remote-settings";
 // Telemetry report result.
 const TELEMETRY_HISTOGRAM_POLL_KEY = "settings-changes-monitoring";
 const TELEMETRY_HISTOGRAM_SYNC_KEY = "settings-sync";
-const CHANGES_PATH = "/v1/buckets/monitor/collections/changes/records";
+const CHANGES_PATH = "/v1" + Utils.CHANGES_PATH;
 
 var server;
 
 async function clear_state() {
   // set up prefs so the kinto updater talks to the test server
   Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
     `http://localhost:${server.identity.primaryPort}/v1`);
 
rename from services/common/tests/unit/test_blocklist_signatures.js
rename to services/settings/test/unit/test_remote_settings_signatures.js
--- a/services/common/tests/unit/test_blocklist_signatures.js
+++ b/services/settings/test/unit/test_remote_settings_signatures.js
@@ -1,22 +1,22 @@
+/* import-globals-from ../../../common/tests/unit/head_helpers.js */
 "use strict";
 
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
-const { BlocklistClients } = ChromeUtils.import("resource://services-common/blocklist-clients.js");
+const { RemoteSettings } = ChromeUtils.import("resource://services-settings/remote-settings.js");
 const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js");
 
-let server;
+const PREF_SETTINGS_SERVER = "services.settings.server";
+const PREF_SIGNATURE_ROOT  = "security.content.signature.root_hash";
+const SIGNER_NAME          = "onecrl.content-signature.mozilla.org";
 
-const PREF_SETTINGS_SERVER             = "services.settings.server";
-const PREF_SIGNATURE_ROOT              = "security.content.signature.root_hash";
-
-const CERT_DIR = "test_blocklist_signatures/";
+const CERT_DIR = "test_remote_settings_signatures/";
 const CHAIN_FILES =
     ["collection_signing_ee.pem",
      "collection_signing_int.pem",
      "collection_signing_root.pem"];
 
 function getFileData(file) {
   const stream = Cc["@mozilla.org/network/file-input-stream;1"]
                    .createInstance(Ci.nsIFileInputStream);
@@ -43,42 +43,69 @@ function setRoot() {
 function getCertChain() {
   const chain = [];
   for (let file of CHAIN_FILES) {
     chain.push(getFileData(do_get_file(CERT_DIR + file)));
   }
   return chain.join("\n");
 }
 
-async function checkRecordCount(client, count) {
-  // Check we have the expected number of records
-  const records = await client.get();
-  Assert.equal(count, records.length);
+let server;
+let client;
+
+function run_test() {
+  // Signature verification is enabled by default. We use a custom signer
+  // because these tests were originally written for OneCRL.
+  client = RemoteSettings("signed", { signerName: SIGNER_NAME });
+
+  // set the content signing root to our test root
+  setRoot();
+
+  // Set up an HTTP Server
+  server = new HttpServer();
+  server.start(-1);
+
+  run_next_test();
+
+  registerCleanupFunction(() => server.stop(() => {}));
 }
 
-let OneCRLBlocklistClient;
+add_task(async function test_check_signatures() {
+  // First, perform a signature verification with known data and signature
+  // to ensure things are working correctly
+  let verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
+                   .createInstance(Ci.nsIContentSignatureVerifier);
+
+  const emptyData = "[]";
+  const emptySignature = "p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
 
-// Check to ensure maybeSync is called with correct values when a changes
-// document contains information on when a collection was last modified
-add_task(async function test_check_signatures() {
+  ok(await verifier.asyncVerifyContentSignature(emptyData, emptySignature, getCertChain(), SIGNER_NAME));
+
+  const collectionData = '[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]';
+  const collectionSignature = "p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
+
+  ok(await verifier.asyncVerifyContentSignature(collectionData, collectionSignature, getCertChain(), SIGNER_NAME));
+});
+
+add_task(async function test_check_synchronization_with_signatures() {
   const port = server.identity.primaryPort;
 
   // Telemetry reports.
-  const TELEMETRY_HISTOGRAM_KEY = OneCRLBlocklistClient.identifier;
+  const TELEMETRY_HISTOGRAM_KEY = client.identifier;
 
   // a response to give the client when the cert chain is expected
   function makeMetaResponseBody(lastModified, signature) {
     return {
       data: {
-        id: "certificates",
+        id: "signed",
         last_modified: lastModified,
         signature: {
-          x5u: `http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem`,
+          x5u: `http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem`,
           public_key: "fake",
-          "content-signature": `x5u=http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem;p384ecdsa=${signature}`,
+          "content-signature": `x5u=http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem;p384ecdsa=${signature}`,
           signature_encoding: "rs_base64url",
           signature,
           hash_algorithm: "sha384",
           ref: "1yryrnmzou5rf31ou80znpnq8n",
         },
       },
     };
   }
@@ -122,33 +149,16 @@ add_task(async function test_check_signa
       const keyParts = key.split(":");
       const valueParts = keyParts[1].split("?");
       const path = valueParts[0];
 
       server.registerPathHandler(path, handleResponse.bind(null, 2000));
     }
   }
 
-  // First, perform a signature verification with known data and signature
-  // to ensure things are working correctly
-  let verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
-                   .createInstance(Ci.nsIContentSignatureVerifier);
-
-  const emptyData = "[]";
-  const emptySignature = "p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
-  const name = "onecrl.content-signature.mozilla.org";
-  ok(await verifier.asyncVerifyContentSignature(emptyData, emptySignature,
-                                                getCertChain(), name));
-
-  const collectionData = '[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]';
-  const collectionSignature = "p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
-
-  ok(await verifier.asyncVerifyContentSignature(collectionData, collectionSignature,
-                                                getCertChain(), name));
-
   // set up prefs so the kinto updater talks to the test server
   Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
     `http://localhost:${server.identity.primaryPort}/v1`);
 
   // These are records we'll use in the test collections
   const RECORD1 = {
     details: {
       bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
@@ -270,33 +280,32 @@ add_task(async function test_check_signa
   // The collection metadata containing the signature for the empty
   // collection.
   const RESPONSE_META_EMPTY_SIG =
     makeMetaResponse(1000, RESPONSE_BODY_META_EMPTY_SIG,
                      "RESPONSE_META_EMPTY_SIG");
 
   // Here, we map request method and path to the available responses
   const emptyCollectionResponses = {
-    "GET:/test_blocklist_signatures/test_cert_chain.pem?": [RESPONSE_CERT_CHAIN],
+    "GET:/test_remote_settings_signatures/test_cert_chain.pem?": [RESPONSE_CERT_CHAIN],
     "GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=1000&_sort=-last_modified":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=1000&_sort=-last_modified":
       [RESPONSE_EMPTY_INITIAL],
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=1000":
+    "GET:/v1/buckets/main/collections/signed?_expected=1000":
       [RESPONSE_META_EMPTY_SIG],
   };
 
   // .. and use this map to register handlers for each path
   registerHandlers(emptyCollectionResponses);
 
   let startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
 
   // With all of this set up, we attempt a sync. This will resolve if all is
   // well and throw if something goes wrong.
-  // We don't want to load initial json dumps in this test suite.
-  await OneCRLBlocklistClient.maybeSync(1000, { loadDump: false });
+  await client.maybeSync(1000);
 
   let endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
 
   // ensure that a success histogram is tracked when a succesful sync occurs.
   let expectedIncrements = {[UptakeTelemetry.STATUS.SUCCESS]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 
 
@@ -318,23 +327,23 @@ add_task(async function test_check_signa
     "dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy");
 
   // A signature response for the collection containg RECORD1 and RECORD2
   const RESPONSE_META_TWO_ITEMS_SIG =
     makeMetaResponse(3000, RESPONSE_BODY_META_TWO_ITEMS_SIG,
                      "RESPONSE_META_TWO_ITEMS_SIG");
 
   const twoItemsResponses = {
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=3000&_sort=-last_modified&_since=1000":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=3000&_sort=-last_modified&_since=1000":
       [RESPONSE_TWO_ADDED],
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=3000":
+    "GET:/v1/buckets/main/collections/signed?_expected=3000":
       [RESPONSE_META_TWO_ITEMS_SIG],
   };
   registerHandlers(twoItemsResponses);
-  await OneCRLBlocklistClient.maybeSync(3000);
+  await client.maybeSync(3000);
 
 
   // Check the collection with one addition and one removal has a valid
   // signature
 
   // Remove RECORD1, add RECORD3
   const RESPONSE_ONE_ADDED_ONE_REMOVED = {
     comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
@@ -350,45 +359,45 @@ add_task(async function test_check_signa
     "MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw");
 
   // signature response for the collection containing RECORD2 and RECORD3
   const RESPONSE_META_THREE_ITEMS_SIG =
     makeMetaResponse(4000, RESPONSE_BODY_META_THREE_ITEMS_SIG,
                      "RESPONSE_META_THREE_ITEMS_SIG");
 
   const oneAddedOneRemovedResponses = {
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=4000&_sort=-last_modified&_since=3000":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=4000&_sort=-last_modified&_since=3000":
       [RESPONSE_ONE_ADDED_ONE_REMOVED],
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=4000":
+    "GET:/v1/buckets/main/collections/signed?_expected=4000":
       [RESPONSE_META_THREE_ITEMS_SIG],
   };
   registerHandlers(oneAddedOneRemovedResponses);
-  await OneCRLBlocklistClient.maybeSync(4000);
+  await client.maybeSync(4000);
 
   // Check the signature is still valid with no operation (no changes)
 
   // Leave the collection unchanged
   const RESPONSE_EMPTY_NO_UPDATE = {
     comment: "RESPONSE_EMPTY_NO_UPDATE ",
     sampleHeaders: [
       "Content-Type: application/json; charset=UTF-8",
       "ETag: \"4000\"",
     ],
     status: {status: 200, statusText: "OK"},
     responseBody: JSON.stringify({"data": []}),
   };
 
   const noOpResponses = {
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=4100&_sort=-last_modified&_since=4000":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=4100&_sort=-last_modified&_since=4000":
       [RESPONSE_EMPTY_NO_UPDATE],
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=4100":
+    "GET:/v1/buckets/main/collections/signed?_expected=4100":
       [RESPONSE_META_THREE_ITEMS_SIG],
   };
   registerHandlers(noOpResponses);
-  await OneCRLBlocklistClient.maybeSync(4100);
+  await client.maybeSync(4100);
 
 
   // Check the collection is reset when the signature is invalid
 
   // Prepare a (deliberately) bad signature to check the collection state is
   // reset if something is inconsistent
   const RESPONSE_COMPLETE_INITIAL = {
     comment: "RESPONSE_COMPLETE_INITIAL ",
@@ -415,41 +424,41 @@ add_task(async function test_check_signa
 
   const RESPONSE_META_BAD_SIG =
       makeMetaResponse(4000, RESPONSE_BODY_META_BAD_SIG, "RESPONSE_META_BAD_SIG");
 
   const badSigGoodSigResponses = {
     // In this test, we deliberately serve a bad signature initially. The
     // subsequent signature returned is a valid one for the three item
     // collection.
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=5000":
+    "GET:/v1/buckets/main/collections/signed?_expected=5000":
       [RESPONSE_META_BAD_SIG, RESPONSE_META_THREE_ITEMS_SIG],
     // The first collection state is the three item collection (since
     // there's a sync with no updates) - but, since the signature is wrong,
     // another request will be made...
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=5000&_sort=-last_modified&_since=4000":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified&_since=4000":
       [RESPONSE_EMPTY_NO_UPDATE],
     // The next request is for the full collection. This will be checked
     // against the valid signature - so the sync should succeed.
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_sort=-last_modified":
+    "GET:/v1/buckets/main/collections/signed/records?_sort=-last_modified":
       [RESPONSE_COMPLETE_INITIAL],
     // The next request is for the full collection sorted by id. This will be
     // checked against the valid signature - so the sync should succeed.
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=5000&_sort=id":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=id":
       [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID],
   };
 
   registerHandlers(badSigGoodSigResponses);
 
   startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
 
   let syncEventSent = false;
-  OneCRLBlocklistClient.on("sync", ({ data }) => { syncEventSent = true; });
+  client.on("sync", ({ data }) => { syncEventSent = true; });
 
-  await OneCRLBlocklistClient.maybeSync(5000);
+  await client.maybeSync(5000);
 
   endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
 
   // since we only fixed the signature, and no data was changed, the sync event
   // was not sent.
   equal(syncEventSent, false);
 
   // ensure that the failure count is incremented for a succesful sync with an
@@ -458,155 +467,133 @@ add_task(async function test_check_signa
   expectedIncrements = {[UptakeTelemetry.STATUS.SIGNATURE_ERROR]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 
 
   const badSigGoodOldResponses = {
     // In this test, we deliberately serve a bad signature initially. The
     // subsequent sitnature returned is a valid one for the three item
     // collection.
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=5000":
+    "GET:/v1/buckets/main/collections/signed?_expected=5000":
       [RESPONSE_META_BAD_SIG, RESPONSE_META_EMPTY_SIG],
     // The first collection state is the current state (since there's no update
     // - but, since the signature is wrong, another request will be made)
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=5000&_sort=-last_modified&_since=4000":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified&_since=4000":
       [RESPONSE_EMPTY_NO_UPDATE],
     // The next request is for the full collection sorted by id. This will be
     // checked against the valid signature and last_modified times will be
     // compared. Sync should fail, even though the signature is good,
     // because the local collection is newer.
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=5000&_sort=id":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=id":
       [RESPONSE_EMPTY_INITIAL],
   };
 
   // ensure our collection hasn't been replaced with an older, empty one
-  await checkRecordCount(OneCRLBlocklistClient, 2);
+  equal((await client.get()).length, 2);
 
   registerHandlers(badSigGoodOldResponses);
 
   syncEventSent = false;
-  OneCRLBlocklistClient.on("sync", ({ data }) => { syncEventSent = true; });
+  client.on("sync", ({ data }) => { syncEventSent = true; });
 
-  await OneCRLBlocklistClient.maybeSync(5000);
+  await client.maybeSync(5000);
 
   // Local data was unchanged, since it was never than the one returned by the server,
   // thus the sync event is not sent.
   equal(syncEventSent, false);
 
   const badLocalContentGoodSigResponses = {
     // In this test, we deliberately serve a bad signature initially. The
     // subsequent signature returned is a valid one for the three item
     // collection.
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=5000":
+    "GET:/v1/buckets/main/collections/signed?_expected=5000":
       [RESPONSE_META_BAD_SIG, RESPONSE_META_THREE_ITEMS_SIG],
     // The next request is for the full collection. This will be checked
     // against the valid signature - so the sync should succeed.
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=5000&_sort=-last_modified":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified":
       [RESPONSE_COMPLETE_INITIAL],
     // The next request is for the full collection sorted by id. This will be
     // checked against the valid signature - so the sync should succeed.
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=5000&_sort=id":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=id":
       [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID],
   };
 
   registerHandlers(badLocalContentGoodSigResponses);
 
   // we create a local state manually here, in order to test that the sync event data
   // properly contains created, updated, and deleted records.
   // the final server collection contains RECORD2 and RECORD3
-  const kintoCol = await OneCRLBlocklistClient.openCollection();
+  const kintoCol = await client.openCollection();
   await kintoCol.clear();
   await kintoCol.create({ ...RECORD2, last_modified: 1234567890, serialNumber: "abc" }, { synced: true, useRecordId: true });
   const localId = "0602b1b2-12ab-4d3a-b6fb-593244e7b035";
   await kintoCol.create({ id: localId }, { synced: true, useRecordId: true });
 
   let syncData;
-  OneCRLBlocklistClient.on("sync", ({ data }) => { syncData = data; });
+  client.on("sync", ({ data }) => { syncData = data; });
 
-  await OneCRLBlocklistClient.maybeSync(5000, { loadDump: false });
+  await client.maybeSync(5000);
 
   // Local data was unchanged, since it was never than the one returned by the server.
   equal(syncData.current.length, 2);
   equal(syncData.created.length, 1);
   equal(syncData.created[0].id, RECORD3.id);
   equal(syncData.updated.length, 1);
   equal(syncData.updated[0].old.serialNumber, "abc");
   equal(syncData.updated[0].new.serialNumber, RECORD2.serialNumber);
   equal(syncData.deleted.length, 1);
   equal(syncData.deleted[0].id, localId);
 
 
   const allBadSigResponses = {
     // In this test, we deliberately serve only a bad signature.
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=6000":
+    "GET:/v1/buckets/main/collections/signed?_expected=6000":
       [RESPONSE_META_BAD_SIG],
     // The first collection state is the three item collection (since
     // there's a sync with no updates) - but, since the signature is wrong,
     // another request will be made...
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=6000&_sort=-last_modified&_since=4000":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=6000&_sort=-last_modified&_since=4000":
       [RESPONSE_EMPTY_NO_UPDATE],
     // The next request is for the full collection sorted by id. This will be
     // checked against the valid signature - so the sync should succeed.
-    "GET:/v1/buckets/security-state/collections/onecrl/records?_expected=6000&_sort=id":
+    "GET:/v1/buckets/main/collections/signed/records?_expected=6000&_sort=id":
       [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID],
   };
 
   startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
   registerHandlers(allBadSigResponses);
   try {
-    await OneCRLBlocklistClient.maybeSync(6000);
+    await client.maybeSync(6000);
     do_throw("Sync should fail (the signature is intentionally bad)");
   } catch (e) {
-    await checkRecordCount(OneCRLBlocklistClient, 2);
+    equal((await client.get()).length, 2);
   }
 
   // Ensure that the failure is reflected in the accumulated telemetry:
   endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
   expectedIncrements = {[UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 
 
   const missingSigResponses = {
     // In this test, we deliberately serve metadata without the signature attribute.
     // As if the collection was not signed.
-    "GET:/v1/buckets/security-state/collections/onecrl?_expected=6000":
+    "GET:/v1/buckets/main/collections/signed?_expected=6000":
       [RESPONSE_META_NO_SIG],
   };
 
   startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
   registerHandlers(missingSigResponses);
   try {
-    await OneCRLBlocklistClient.maybeSync(6000);
+    await client.maybeSync(6000);
     do_throw("Sync should fail (the signature is missing)");
   } catch (e) {
-    await checkRecordCount(OneCRLBlocklistClient, 2);
+    equal((await client.get()).length, 2);
   }
 
   // Ensure that the failure is reflected in the accumulated telemetry:
   endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
   expectedIncrements = {
     [UptakeTelemetry.STATUS.SIGNATURE_ERROR]: 1,
     [UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR]: 0,  // Not retried since missing.
   };
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
-
-function run_test() {
-  // Signature verification is evabled by default.
-  ({OneCRLBlocklistClient} = BlocklistClients.initialize());
-
-  // get a signature verifier to ensure nsNSSComponent is initialized
-  Cc["@mozilla.org/security/contentsignatureverifier;1"]
-    .createInstance(Ci.nsIContentSignatureVerifier);
-
-  // set the content signing root to our test root
-  setRoot();
-
-  // Set up an HTTP Server
-  server = new HttpServer();
-  server.start(-1);
-
-  run_next_test();
-
-  registerCleanupFunction(function() {
-    server.stop(function() { });
-  });
-}
rename from services/common/tests/unit/test_blocklist_signatures/collection_signing_ee.pem.certspec
rename to services/settings/test/unit/test_remote_settings_signatures/collection_signing_ee.pem.certspec
rename from services/common/tests/unit/test_blocklist_signatures/collection_signing_int.pem.certspec
rename to services/settings/test/unit/test_remote_settings_signatures/collection_signing_int.pem.certspec
rename from services/common/tests/unit/test_blocklist_signatures/collection_signing_root.pem.certspec
rename to services/settings/test/unit/test_remote_settings_signatures/collection_signing_root.pem.certspec
rename from services/common/tests/unit/test_blocklist_signatures/moz.build
rename to services/settings/test/unit/test_remote_settings_signatures/moz.build
--- a/services/common/tests/unit/test_blocklist_signatures/moz.build
+++ b/services/settings/test/unit/test_remote_settings_signatures/moz.build
@@ -7,12 +7,8 @@
 test_certificates = (
     'collection_signing_root.pem',
     'collection_signing_int.pem',
     'collection_signing_ee.pem',
 )
 
 for test_certificate in test_certificates:
     GeneratedTestCertificate(test_certificate)
-
-with Files('**'):
-    BUG_COMPONENT = ('Toolkit', 'Blocklist Implementation')
-
--- a/services/settings/test/unit/xpcshell.ini
+++ b/services/settings/test/unit/xpcshell.ini
@@ -1,11 +1,14 @@
 [DEFAULT]
 head = ../../../common/tests/unit/head_global.js ../../../common/tests/unit/head_helpers.js
 firefox-appdir = browser
 tags = remote-settings
+support-files =
+  test_remote_settings_signatures/**
 
 [test_attachments_downloader.js]
 support-files = test_attachments_downloader/**
 [test_remote_settings.js]
 [test_remote_settings_poll.js]
 [test_remote_settings_worker.js]
 [test_remote_settings_jexl_filters.js]
+[test_remote_settings_signatures.js]
--- a/taskcluster/ci/config.yml
+++ b/taskcluster/ci/config.yml
@@ -323,32 +323,45 @@ workers:
             implementation: generic-worker
             os: linux
             worker-type: 'gecko-{alias}'
         t-osx-1010:
             provisioner: releng-hardware
             implementation: generic-worker
             os: macosx
             worker-type: 'gecko-{alias}'
+        t-osx-1014:
+            provisioner: releng-hardware
+            implementation: generic-worker
+            os: macosx
+            worker-type: 'gecko-{alias}'
         t-linux-xlarge-pgo:
             provisioner: aws-provisioner-v1
             implementation: docker-worker
             os: linux
             worker-type:
                 by-level:
                     '3': 'gecko-{level}-t-linux-xlarge'
                     default: 'gecko-t-linux-xlarge'
         t-osx-1010-pgo:
             provisioner: releng-hardware
             implementation: generic-worker
             os: macosx
             worker-type:
                 by-level:
                     '3': 'gecko-{level}-t-osx-1010'
                     default: 'gecko-t-osx-1010'
+        t-osx-1014-pgo:
+            provisioner: releng-hardware
+            implementation: generic-worker
+            os: macosx
+            worker-type:
+                by-level:
+                    '3': 'gecko-{level}-t-osx-1014'
+                    default: 'gecko-t-osx-1014'
         t-win10-64(|-gpu):
             provisioner: aws-provisioner-v1
             implementation: generic-worker
             os: windows
             worker-type: 'gecko-{alias}'
         t-win10-64(-hw|-ux):
             provisioner: releng-hardware
             implementation: generic-worker
--- a/taskcluster/ci/source-test/python.yml
+++ b/taskcluster/ci/source-test/python.yml
@@ -3,17 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 ---
 job-defaults:
     platform: linux64/opt
     always-target: true
     worker-type:
         by-platform:
             linux64.*: t-linux-xlarge
-            macosx64.*: t-osx-1010
+            macosx1010-64.*: t-osx-1010
             windows10-64.*: t-win10-64
     worker:
         by-platform:
             linux64.*:
                 docker-image: {in-tree: "lint"}
                 max-run-time: 3600
             default:
                 max-run-time: 3600
@@ -97,17 +97,17 @@ mochitest-harness:
             - 'testing/mozharness/mozharness/mozilla/structuredlog.py'
             - 'testing/mozharness/mozharness/mozilla/testing/errors.py'
             - 'testing/profiles/**'
 
 mozbase:
     description: testing/mozbase unit tests
     platform:
         - linux64/opt
-        - macosx64/opt
+        - macosx1010-64/opt
         - windows10-64/opt
     python-version: [2, 3]
     treeherder:
         symbol: mb
     run:
         using: python-test
         subsuite: mozbase
     when:
@@ -127,17 +127,17 @@ mozharness:
     when:
         files-changed:
             - 'testing/mozharness/**'
 
 mozlint:
     description: python/mozlint unit tests
     platform:
         - linux64/opt
-        - macosx64/opt
+        - macosx1010-64/opt
         - windows10-64/opt
     python-version: [2]
     treeherder:
         symbol: ml
     run:
         using: python-test
         subsuite: mozlint
     when:
--- a/taskcluster/ci/test/compiled.yml
+++ b/taskcluster/ci/test/compiled.yml
@@ -99,27 +99,25 @@ jittest:
             (?=windows).*(?!-ccov)...../.*: []  # redundant with SM(p)
             windows10-aarch64/opt: ['try', 'mozilla-central']
             android-hw-.*-api-16/opt: ['try']
             android-hw-.*-aarch64/opt: ['try']
             default: built-projects
     chunks:
         by-test-platform:
             windows.*: 1
-            windows10-64-ccov/debug: 6
-            macosx.*: 1
-            macosx64/debug: 3
-            macosx64-ccov/debug: 4
+            macosx.*/opt: 1
+            macosx.*/debug: 3
             android-em-4.3-arm7-api-15/debug: 20
             android.*: 10
             default: 6
     max-run-time:
         by-test-platform:
             windows10-64-ccov/debug: 7200
-            macosx64-ccov/debug: 7200
+            macosx.*-ccov/debug: 7200
             default: 3600
     mozharness:
         chunked:
             by-test-platform:
                 windows.*: false
                 macosx.*: false
                 default: true
     tier:
--- a/taskcluster/ci/test/misc.yml
+++ b/taskcluster/ci/test/misc.yml
@@ -92,17 +92,17 @@ test-verify:
     allow-software-gl-layers: false
     run-on-projects:
         by-test-platform:
             android-em-4.3-arm7-api-16/opt: ['try']
             # do not run on ccov
             .*-ccov/.*: []
             .*-asan/.*: []
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
-            macosx64(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-qr)?/opt: ['mozilla-central', 'try']
             # do not run on beta or release: usually just confirms earlier results
             default: ['trunk', 'try']
     tier: 2
     mozharness:
         script:
             by-test-platform:
                 android-em.*: android_emulator_unittest.py
                 default: desktop_unittest.py
@@ -140,17 +140,17 @@ test-verify-gpu:
     max-run-time: 10800
     allow-software-gl-layers: false
     run-on-projects:
         by-test-platform:
             # do not run on ccov
             .*-ccov/.*: []
             .*-asan/.*: []
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
-            macosx64(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-qr)?/opt: ['mozilla-central', 'try']
             # do not run on beta or release: usually just confirms earlier results
             default: ['trunk', 'try']
     tier: 2
     mozharness:
         script:
             by-test-platform:
                 android-em.*: android_emulator_unittest.py
                 default: desktop_unittest.py
--- a/taskcluster/ci/test/mochitest.yml
+++ b/taskcluster/ci/test/mochitest.yml
@@ -66,17 +66,17 @@ mochitest:
         by-test-platform:
             android-em-4.3-arm7-api-16/debug: 60
             android-em-4.*: 24
             android-em-7.*: 4
             linux.*/debug: 16
             linux64-asan/opt: 10
             linux64-.*cov/opt: 10
             windows10-64-ccov/debug: 10
-            macosx64-ccov/debug: 10
+            macosx.*64-ccov/debug: 10
             default: 5
     e10s:
         by-test-platform:
             linux32/debug: both
             default: true
     max-run-time:
         by-test-platform:
             android-em.*: 7200
@@ -116,24 +116,24 @@ mochitest-browser-chrome:
     tier:
         by-test-platform:
             windows10-aarch64.*: 2
             default: default
     chunks:
         by-test-platform:
             linux.*/debug: 16
             linux64-asan/opt: 16
-            macosx64/debug: 12
+            macosx.*64/debug: 12
             default: 7
     max-run-time:
         by-test-platform:
             linux64-.*cov/opt: 7200
             windows10-64-ccov/debug: 7200
             windows10-aarch64/*: 7200
-            macosx64-ccov/debug: 10800
+            macosx.*64-ccov/debug: 10800
             linux.*/debug: 5400
             default: 3600
     mozharness:
         mochitest-flavor: browser
         chunked: true
     # Bug 1281241: migrating to m3.large instances
     instance-size: default
     allow-software-gl-layers: false
@@ -161,17 +161,17 @@ browser-screenshots:
     treeherder-symbol: M(ss)
     loopback-video: true
     run-on-projects:
         by-test-platform:
             windows7-32(?:-pgo|-shippable)(?:-qr)?/opt: ['mozilla-central', 'integration']
             windows10-64(?:-pgo|-shippable)(?:-qr)?/opt: ['mozilla-central', 'integration']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central']
             linux64-(?:pgo|shippable)(?:-qr)?/opt: ['mozilla-central', 'integration']
-            macosx64-shippable/opt: ['mozilla-central', 'integration']
+            macosx.*64-shippable/opt: ['mozilla-central', 'integration']
             default: []
     max-run-time: 3600
     mozharness:
         mochitest-flavor: browser
     allow-software-gl-layers: false
 
 mochitest-chrome:
     description: "Mochitest chrome run"
@@ -216,24 +216,24 @@ mochitest-devtools-chrome:
     loopback-video: true
     tier:
         by-test-platform:
             windows10-aarch64.*: 2
             default: default
     max-run-time:
         by-test-platform:
             windows10-64-ccov/debug: 7200
-            macosx64-ccov/debug: 9000
+            macosx.*64-ccov/debug: 9000
             linux64-ccov/debug: 7200
             default: 5400
     chunks:
         by-test-platform:
             .*-ccov/debug: 16
             linux64/debug: 12
-            macosx64/debug: 8
+            macosx.*64/debug: 8
             .*-asan/opt: 8
             default: 5
     mozharness:
         mochitest-flavor: chrome
         chunked: true
     instance-size:
         by-test-platform:
             linux64-asan/opt: xlarge  # runs out of memory on default/m3.large
@@ -247,18 +247,18 @@ mochitest-devtools-webreplay:
     loopback-video: true
     tier: 2
     max-run-time: 900
     mozharness:
         mochitest-flavor: chrome
     allow-software-gl-layers: false
     run-on-projects:
         by-test-platform:
-            macosx64/opt: ['mozilla-central', 'try']
-            macosx64.*/opt: ['trunk', 'try']
+            macosx.*64/opt: ['mozilla-central', 'try']
+            macosx.*64.*/opt: ['trunk', 'try']
             default: []
 
 mochitest-gpu:
     description: "Mochitest GPU run"
     suite:
         name: mochitest-plain-gpu
     treeherder-symbol: M(gpu)
     loopback-video: true
@@ -286,17 +286,17 @@ mochitest-gpu:
                     - --mochitest-suite=mochitest-plain-gpu,mochitest-chrome-gpu,mochitest-browser-chrome-gpu
 
 mochitest-media:
     description: "Mochitest media run"
     treeherder-symbol: M(mda)
     max-run-time:
         by-test-platform:
             windows10-64-ccov/debug: 7200
-            macosx64-ccov/debug: 7200
+            macosx.*64-ccov/debug: 7200
             default: 5400
     run-on-projects:
         by-test-platform:
             android-hw-.*-api-16/opt: ['try']
             android-em-4.3-arm7-api-16/opt: ['try']
             windows10-aarch64/opt: ['try', 'mozilla-central']
             default: built-projects
     variants:
@@ -312,28 +312,28 @@ mochitest-media:
     instance-size:
         by-test-platform:
             android-em.*: xlarge
             default: large
     chunks:
         by-test-platform:
             android-em-7.*: 1
             windows10-64.*: 1
-            macosx64.*/opt: 2
-            macosx64.*/debug: 4
+            macosx.*64.*/opt: 2
+            macosx.*64.*/debug: 4
             windows10-aarch64/.*: 2
             windows7-32-shippable/.*: 2
             linux64(-shippable|-devedition|-.*qr)/opt: 2
             default: 3
     mozharness:
         mochitest-flavor: plain
         chunked:
             by-test-platform:
                 android.*: false
-                macosx64.*: false
+                macosx.*64.*: false
                 windows10-64.*: false
                 default: true
     tier:
         by-test-platform:
             android-hw.*: 2
             windows10-aarch64.*: 2
             default: default
 
@@ -392,17 +392,17 @@ mochitest-webgl1-core:
     loopback-video: true
     tier:
         by-test-platform:
             windows10-aarch64.*: 2
             default: default
     max-run-time:
         by-test-platform:
             windows.*: 5400
-            macosx64-ccov/debug: 7200
+            macosx.*64-ccov/debug: 7200
             default: 3600
     # Bug 1296733: llvmpipe with mesa 9.2.1 lacks thread safety
     allow-software-gl-layers: false
     mozharness:
         mochitest-flavor: plain
 
 mochitest-webgl1-ext:
     description: "Mochitest webgl1-ext run"
--- a/taskcluster/ci/test/raptor.yml
+++ b/taskcluster/ci/test/raptor.yml
@@ -15,17 +15,17 @@ job-defaults:
             default: /home/cltbld
     run-on-projects:
         by-test-platform:
             windows10-64-ux/opt: ['try', 'mozilla-central']
             windows10-aarch64/opt: ['try', 'mozilla-central']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             android-hw-.*-api-16/opt: ['try']
             android-hw-.*-aarch64/opt: ['try']
-            macosx64(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['try', 'trunk', 'mozilla-beta']
     tier:
         by-test-platform:
             windows10-aarch64/.*: 2
             windows10-64-ccov/.*: 3
             linux64-ccov/.*: 3
             android-hw-g5.*: 2
             default: 1
--- a/taskcluster/ci/test/reftest.yml
+++ b/taskcluster/ci/test/reftest.yml
@@ -86,23 +86,23 @@ jsreftest:
             android-em.*: 40
             windows.*: 2
             windows10-64-ccov/debug: 5
             linux64-ccov/.*: 5
             linux64-qr/opt: 4
             linux64-qr/debug: 5
             linux32/debug: 5
             linux64/debug: 5
-            macosx64-ccov/debug: 5
+            macosx.*64-ccov/debug: 5
             default: 3
     max-run-time:
         by-test-platform:
             android-em.*: 7200
             windows10-64-ccov/debug: 7200
-            macosx64-ccov/debug: 7200
+            macosx.*64-ccov/debug: 7200
             default: 3600
     tier:
         by-test-platform:
             windows10-aarch64.*: 2
             default: default
 
 reftest:
     description: "Reftest run"
@@ -113,39 +113,39 @@ reftest:
             default: default
     virtualization: virtual-with-gpu
     chunks:
         by-test-platform:
             android-em-4.3-arm7-api-16/debug: 56
             android-em-4.*: 28
             android-em-7.*: 5
             linux64(-shippable|-devedition|-qr)?/opt: 5
-            macosx64.*/opt: 2
-            macosx64.*/debug: 3
-            macosx64-ccov/debug: 6
+            macosx101.*-64/opt: 2
+            macosx101.*-64/debug: 3
+            macosx.*64-ccov/debug: 6
             windows.*/opt: 2
             windows.*/debug: 4
             windows10-64-ccov/debug: 6
             default: 8
     e10s:
         by-test-platform:
             linux32/debug: both
             default: true
     max-run-time:
         by-test-platform:
             android-em.*: 7200
             windows10-64-ccov/debug: 5400
             windows10-64-asan/opt: 5400
-            macosx64-ccov/debug: 5400
+            macosx.*64-ccov/debug: 5400
             default: 3600
     mozharness:
         chunked:
             by-test-platform:
                 android-em.*: false
-                macosx64/opt: false
+                macosx.*64/opt: false
                 windows10-64.*/opt: false
                 default: true
     tier:
         by-test-platform:
             windows10-aarch64.*: 2
             default: default
 
 reftest-gpu:
--- a/taskcluster/ci/test/talos.yml
+++ b/taskcluster/ci/test/talos.yml
@@ -36,17 +36,17 @@ talos-bcv:
     treeherder-symbol: T(bcv)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             .*-qr/.*: []  # this test is not useful with webrender
             (?:windows10-64|windows7-32|linux64)/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     tier:
         by-test-platform:
             windows10-aarch64/.*: 2
             .*-qr/.*: 3  # this should be disabled but might run via try syntax anyway, so explicitly downgrade to tier-3
             default: default
     max-run-time: 1800
     mozharness:
@@ -58,17 +58,17 @@ talos-chrome:
     try-name: chromez
     treeherder-symbol: T(c)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time: 1200
     mozharness:
         extra-options:
             - --suite=chromez
 
 talos-damp:
     description: "Talos devtools (damp)"
@@ -76,66 +76,66 @@ talos-damp:
     treeherder-symbol: T(damp)
     max-run-time: 5400
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']  # Bug 1407593
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['try']  # Bug 1544360
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     mozharness:
         extra-options:
             - --suite=damp
 
 talos-dromaeojs:
     description: "Talos dromaeojs"
     try-name: dromaeojs
     treeherder-symbol: T(d)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time: 2100
     mozharness:
         extra-options:
             - --suite=dromaeojs
 
 talos-flex:
     description: "Talos XUL flexbox emulation enabled"
     try-name: flex
     treeherder-symbol: T(f)
     tier: 3
     run-on-projects:
         by-test-platform:
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-central', 'try']
     max-run-time: 1800
     mozharness:
         extra-options:
             - --suite=flex
 
 talos-g1:
     description: "Talos g1"
     try-name: g1
     treeherder-symbol: T(g1)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time:
         by-test-platform:
             linux64.*: 3600
             default: 7200
     mozharness:
         extra-options:
             - --suite=g1
@@ -145,17 +145,17 @@ talos-g3:
     try-name: g3
     treeherder-symbol: T(g3)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time: 900
     mozharness:
         extra-options:
             - --suite=g3
 
 talos-g4:
     description: "Talos g4"
@@ -163,17 +163,17 @@ talos-g4:
     treeherder-symbol: T(g4)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             windows10-64-ux/opt: ['try', 'mozilla-central']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time:
         by-test-platform:
             linux64.*: 1500
             default: 1800
     mozharness:
         extra-options:
             - --suite=g4
@@ -183,17 +183,17 @@ talos-g5:
     try-name: g5
     treeherder-symbol: T(g5)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     mozharness:
         extra-options:
             - --suite=g5
     max-run-time:
         by-test-platform:
             linux64.*: 1200
             default: 1800
@@ -201,17 +201,17 @@ talos-g5:
 talos-h1:
     description: "Talos h1"
     try-name: h1
     treeherder-symbol: T(h1)
     run-on-projects:
         by-test-platform:
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     mozharness:
         extra-options:
             - --suite=h1
     max-run-time:
         by-test-platform:
             linux64.*: 900
             default: 1800
@@ -219,17 +219,17 @@ talos-h1:
 talos-h2:
     description: "Talos h2"
     try-name: h2
     treeherder-symbol: T(h2)
     run-on-projects:
         by-test-platform:
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time:
         by-test-platform:
             linux64.*: 900
             default: 7200
     mozharness:
         extra-options:
             - --suite=h2
@@ -237,17 +237,17 @@ talos-h2:
 talos-motionmark:
     description: "Talos motionmark"
     try-name: motionmark
     treeherder-symbol: T(mm)
     run-on-projects:
         by-test-platform:
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-central', 'try']
     max-run-time: 3600
     tier:
         by-test-platform:
             windows10-64-ccov/.*: 3
             windows10-aarch64/.*: 2
             linux64-ccov/.*: 3
             default: 2
@@ -260,134 +260,134 @@ talos-other:
     try-name: other
     treeherder-symbol: T(o)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time: 1500
     mozharness:
         extra-options:
             - --suite=other
 
 talos-sessionrestore-many-windows:
     description: "Talos sessionrestore-many-windows"
     try-name: sessionrestore-many-windows
     treeherder-symbol: T(smw)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-central', 'try']
     max-run-time: 1500
     mozharness:
         extra-options:
             - --suite=sessionrestore-many-windows
 
 talos-perf-reftest:
     description: "Talos perf-reftest"
     try-name: perf-reftest
     treeherder-symbol: T(p)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['trunk', 'try']
     max-run-time: 1200
     mozharness:
         extra-options:
             - --suite=perf-reftest
 
 talos-perf-reftest-singletons:
     description: "Talos perf-reftest singletons"
     try-name: perf-reftest-singletons
     treeherder-symbol: T(ps)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['trunk', 'try']
     max-run-time: 1200
     mozharness:
         extra-options:
             - --suite=perf-reftest-singletons
 
 talos-speedometer:
     description: "Talos speedometer"
     try-name: speedometer
     treeherder-symbol: T(sp)
     run-on-projects:
         by-test-platform:
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time: 1500
     mozharness:
         extra-options:
             - --suite=speedometer
 
 talos-svgr:
     description: "Talos svgr"
     try-name: svgr
     treeherder-symbol: T(s)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time: 1800
     mozharness:
         extra-options:
             - --suite=svgr
 
 talos-tp5o:
     description: "Talos tp5o"
     try-name: tp5o
     treeherder-symbol: T(tp)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     max-run-time: 1800
     mozharness:
         extra-options:
             - --suite=tp5o
 
 talos-tp6:
     description: "Talos tp6"
     try-name: tp6
     treeherder-symbol: T(tp6)
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['trunk', 'try']
     max-run-time: 1200
     mozharness:
         extra-options:
             - --suite=tp6
 
 talos-tp6-stylo-threads:
     description: "Talos Stylo sequential tp6"
@@ -395,34 +395,34 @@ talos-tp6-stylo-threads:
     treeherder-symbol: Tss(tp6)
     max-run-time: 1200
     run-on-projects:
         by-test-platform:
             linux64-ccov/.*: ['try']
             windows10-64-ccov/.*: ['try']
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     mozharness:
         extra-options:
             - --suite=tp6-stylo-threads
 
 talos-tabswitch:
     description: "Talos page scroll (tabswitch)"
     try-name: tabswitch
     treeherder-symbol: T(tabswitch)
     max-run-time: 900
     run-on-projects:
         by-test-platform:
             windows10-64-ccov/.*: ['try']
             linux64-ccov/.*: ['try']  # Bug 1407593
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
             windows10-aarch64/opt: ['mozilla-central', 'try']
-            macosx64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-shippable)?(?:-qr)?/opt: ['mozilla-central', 'try']
             default: ['mozilla-beta', 'trunk', 'try']
     mozharness:
         extra-options:
             - --suite=tabswitch
 
 talos-xperf:
     description: "Talos xperf"
     try-name: xperf
--- a/taskcluster/ci/test/test-platforms.yml
+++ b/taskcluster/ci/test/test-platforms.yml
@@ -271,49 +271,88 @@ windows10-64-shippable-qr/opt:
         - windows-qr-tests
         - windows-talos
         - raptor-firefox
         - web-platform-tests
 
 ##
 # MacOS X platforms (matching /macosx.*/)
 
-macosx64/debug:
+# macosx1014-64-shippable/opt:
+#    build-platform: macosx64-shippable/opt
+#    test-sets:
+#        - macosx1014-64-tests
+#        - macosx64-talos
+#        - desktop-screenshot-capture
+#        - awsy
+#        - raptor-chromium
+#        - raptor-firefox
+#        - raptor-profiling
+#        - marionette-media-tests
+
+# macosx1014-64/debug:
+#    build-platform: macosx64-shippable/opt
+#    test-sets:
+#        - macosx1014-64-tests
+#        - marionette-media-tests
+
+# macosx1014-64-devedition/opt:
+#    build-platform: macosx64-devedition-nightly/opt
+#    test-sets:
+#        - macosx1014-64-tests
+
+# macosx1014-64-shippable-qr/opt:
+#    build-platform: macosx64-shippable/opt
+#    test-sets:
+#        - macosx1014-64-qr-tests
+
+# macosx1014-64-qr/debug:
+#    build-platform: macosx64/debug
+#    test-sets:
+#        - macosx1014-64-qr-tests
+
+# macosx1014-64-ccov/debug:
+#    build-platform: macosx64-ccov/debug
+#    test-sets:
+#        - macosx1014-64-tests
+
+macosx1010-64/debug:
     build-platform: macosx64/debug
     test-sets:
         - macosx64-tests
         - marionette-media-tests
 
-macosx64-shippable/opt:
+macosx1010-64-shippable/opt:
     build-platform: macosx64-shippable/opt
     test-sets:
         - macosx64-talos
         - macosx64-tests
         - desktop-screenshot-capture
         - awsy
         - raptor-chromium
         - raptor-firefox
         - raptor-profiling
         - marionette-media-tests
-macosx64-devedition/opt:
+
+macosx1010-64-devedition/opt:
     build-platform: macosx64-devedition-nightly/opt
     test-sets:
         - macosx64-tests
 
-macosx64-shippable-qr/opt:
+macosx1010-64-shippable-qr/opt:
     build-platform: macosx64-shippable/opt
     test-sets:
         - macosx64-qr-tests
 
-macosx64-qr/debug:
+macosx1010-64-qr/debug:
     build-platform: macosx64/debug
     test-sets:
         - macosx64-qr-tests
 
-macosx64-ccov/debug:
+macosx1010-64-ccov/debug:
     build-platform: macosx64-ccov/debug
     test-sets:
         - macosx64-tests
 
 ##
 # Android platforms (matching /android-em.*/)
 #
 # android-em test platforms execute on android emulators.
--- a/taskcluster/ci/test/test-sets.yml
+++ b/taskcluster/ci/test/test-sets.yml
@@ -303,16 +303,51 @@ windows-talos:
     # - talos-h1 Bug 1487031 - Disabled for not finding actionable regressions
 
 marionette-gpu-tests:
     - marionette-gpu
 
 marionette-media-tests:
     - marionette-media
 
+# macosx1014-64-tests:
+#    - cppunit
+#    - crashtest
+#    - firefox-ui-functional-local
+#    - firefox-ui-functional-remote
+#    - gtest
+#    - jittest
+#    - jsreftest
+#    - marionette
+#    - mochitest
+#    - mochitest-a11y
+#    - mochitest-browser-chrome
+#    - mochitest-chrome
+#    - mochitest-devtools-chrome
+#    - mochitest-devtools-webreplay
+#    - mochitest-gpu
+#    - mochitest-media
+#    - mochitest-webgl1-core
+#    - mochitest-webgl1-ext
+#    - mochitest-webgl2-core
+#     - mochitest-webgl2-ext test  # timeouts
+#    - reftest
+#    - telemetry-tests-client
+#    - test-verify
+#    - test-verify-gpu
+#    - test-verify-wpt
+#    - web-platform-tests
+#    - web-platform-tests-reftests
+#    - web-platform-tests-wdspec
+#    - xpcshell
+
+# macosx1014-64-qr-tests:
+#    - crashtest
+#    - reftest
+
 macosx64-tests:
     - cppunit
     - crashtest
     - firefox-ui-functional-local
     - firefox-ui-functional-remote
     - gtest
     - jittest
     - jsreftest
--- a/taskcluster/ci/test/web-platform.yml
+++ b/taskcluster/ci/test/web-platform.yml
@@ -37,26 +37,24 @@ job-defaults:
 web-platform-tests:
     description: "Web platform test run"
     suite: web-platform-tests
     treeherder-symbol: W(wpt)
     chunks:
         by-test-platform:
             android.*: 18
             linux.*/debug: 18
-            macosx64.*/opt: 8
-            macosx64/debug: 16
+            macosx*/opt: 8
+            macosx.*/debug: 16
             windows10.*/debug: 18
             windows10-aarch64/opt: 12
-            macosx64-ccov/debug: 24
             default: 12
     max-run-time:
         by-test-platform:
-            linux64-ccov/debug: 10800
-            windows10-64-ccov/debug: 10800
+            .*-ccov/debug: 10800
             default: 7200
     e10s:
         by-test-platform:
             linux32/debug: both
             default: true
     run-on-projects:
         by-test-platform:
             android.*: ['mozilla-central', 'try']
@@ -77,18 +75,18 @@ web-platform-tests:
             - --test-type=testharness
 
 web-platform-tests-headless:
     description: "Web platform test headless run"
     suite: web-platform-tests
     treeherder-symbol: W(wptH)
     chunks:
         by-test-platform:
-            macosx64.*/opt: 8
-            macosx64/debug: 16
+            macosx.*/opt: 8
+            macosx.*/debug: 16
     e10s:
         by-test-platform:
             macosx.*: true
             default: true
     max-run-time: 7200
     run-on-projects: []  # disabled pending releng approval
     mozharness:
         chunked: true
@@ -109,17 +107,17 @@ web-platform-tests-reftests:
         name: web-platform-tests-reftests
     schedules-component: web-platform-tests-reftests
     treeherder-symbol: W(Wr)
     chunks:
         by-test-platform:
             .*-ccov/debug: 8
             linux64(-qr|-asan)/.*: 6
             linux64(-shippable|-devedition)?/opt: 3
-            macosx64/debug: 6
+            macosx10(10|14)-64/debug: 6
             windows.*-(32|64)(-qr)?/debug: 5
             android.*: 6
             default: 4
     e10s:
         by-test-platform:
             linux32/debug: both
             default: true
     run-on-projects:
@@ -164,18 +162,17 @@ web-platform-tests-reftests-headless:
 web-platform-tests-wdspec:
     description: "Web platform webdriver-spec run"
     suite:
         name: web-platform-tests-wdspec
     schedules-component: web-platform-tests-wdspec
     treeherder-symbol: W(Wd)
     chunks:
         by-test-platform:
-            linux64-ccov/debug: 4
-            windows10-64-ccov/debug: 4
+            .*-ccov/debug: 4
             default: 2
     mozharness:
         extra-options:
             - --test-type=wdspec
     run-on-projects:
         by-test-platform:
             windows10-aarch64/opt: ['try', 'mozilla-central']
             android.*: ['mozilla-central', 'try']
@@ -216,17 +213,17 @@ test-verify-wpt:
     treeherder-symbol: TVw
     max-run-time: 10800
     run-on-projects:
         by-test-platform:
             # do not run on ccov
             .*-ccov/.*: []
             .*-asan/.*: []
             (?:windows10-64|windows7-32|linux64)(?:-qr)?/opt: ['mozilla-central', 'try']
-            macosx64(?:-qr)?/opt: ['mozilla-central', 'try']
+            macosx.*64(?:-qr)?/opt: ['mozilla-central', 'try']
             # do not run on beta or release: usually just confirms earlier results
             default: ['trunk', 'try']
     tier: 2
     mozharness:
         extra-options:
             - --verify
 
 test-coverage-wpt:
--- a/taskcluster/ci/test/xpcshell.yml
+++ b/taskcluster/ci/test/xpcshell.yml
@@ -42,20 +42,19 @@ xpcshell:
             windows10-64-asan/opt: []  # No XPCShell on ASAN yet
             windows10-aarch64/opt: ['try', 'mozilla-central']
             android-em-4.3-arm7-api-16/opt: ['try']
             default: built-projects
     chunks:
         by-test-platform:
             android-em-4.3-arm7-api-16/.*: 8
             android-em-7.*: 3
-            macosx64-ccov/debug: 8
-            macosx.*/.*: 2
+            macosx10(10|14)-64/.*: 2
             linux64(-qr)?/debug: 6
-            (linux.*|windows.*)-ccov/debug: 6
+            .*-ccov/debug: 6
             windows(7-32|10-64)(-shippable|-devedition|-asan|.*-qr)?/.*: 2
             windows10-aarch64/opt: 3
             default: 5
     instance-size:
         by-test-platform:
             android-em.*: xlarge
             default: default
     max-run-time:
--- a/taskcluster/taskgraph/transforms/job/mozharness_test.py
+++ b/taskcluster/taskgraph/transforms/job/mozharness_test.py
@@ -307,16 +307,22 @@ def mozharness_test_on_generic_worker(co
             'mozharness\\scripts\\' + normpath(mozharness['script'])
         ]
     elif is_bitbar:
         mh_command = [
             bitbar_wrapper,
             'bash',
             "./{}".format(bitbar_script)
         ]
+    elif is_macosx and 'macosx1014-64' in test['test-platform']:
+        mh_command = [
+            '/usr/local/bin/python2',
+            '-u',
+            'mozharness/scripts/' + mozharness['script']
+        ]
     else:
         # is_linux or is_macosx
         mh_command = [
             'python2.7',
             '-u',
             'mozharness/scripts/' + mozharness['script']
         ]
 
--- a/taskcluster/taskgraph/transforms/tests.py
+++ b/taskcluster/taskgraph/transforms/tests.py
@@ -140,17 +140,18 @@ WINDOWS_WORKER_TYPES = {
       'virtual': 't-win10-64',
       'virtual-with-gpu': 't-win10-64-gpu',
       'hardware': 't-win10-64-ux',
     },
 }
 
 # os x worker types keyed by test-platform
 MACOSX_WORKER_TYPES = {
-    'macosx64': 't-osx-1010',
+    'macosx1010-64': 't-osx-1010',
+    'macosx1014-64': 'releng-hardware/gecko-t-osx-1014',
 }
 
 
 def runs_on_central(test):
     return match_run_on_projects('mozilla-central', test['run-on-projects'])
 
 
 TEST_VARIANTS = {
@@ -686,19 +687,22 @@ def set_target(config, tests):
 @transforms.add
 def set_treeherder_machine_platform(config, tests):
     """Set the appropriate task.extra.treeherder.machine.platform"""
     translation = {
         # Linux64 build platforms for asan and pgo are specified differently to
         # treeherder.
         'linux64-asan/opt': 'linux64/asan',
         'linux64-pgo/opt': 'linux64/pgo',
-        'macosx64/debug': 'osx-10-10/debug',
-        'macosx64/opt': 'osx-10-10/opt',
-        'macosx64-shippable/opt': 'osx-10-10-shippable/opt',
+        'macosx1010-64/debug': 'osx-10-10/debug',
+        'macosx1010-64/opt': 'osx-10-10/opt',
+        'macosx1010-64-shippable/opt': 'osx-10-10-shippable/opt',
+        'macosx1014-64/debug': 'osx-10-14/debug',
+        'macosx1014-64/opt': 'osx-10-14/opt',
+        'macosx1014-64-shippable/opt': 'osx-10-14-shippable/opt',
         'win64-asan/opt': 'windows10-64/asan',
         'win64-aarch64/opt': 'windows10-aarch64/opt',
         'win32-pgo/opt': 'windows7-32/pgo',
         'win64-pgo/opt': 'windows10-64/pgo',
         # The build names for Android platforms have partially evolved over the
         # years and need to be translated.
         'android-api-16/debug': 'android-em-4-3-armv7-api16/debug',
         'android-api-16-ccov/debug': 'android-em-4-3-armv7-api16-ccov/debug',
@@ -773,24 +777,32 @@ def set_tier(config, tests):
                                          'windows10-64-shippable/opt',
                                          'windows10-64-devedition/opt',
                                          'windows10-64-nightly/opt',
                                          'windows10-64-asan/opt',
                                          'windows10-64-qr/opt',
                                          'windows10-64-qr/debug',
                                          'windows10-64-pgo-qr/opt',
                                          'windows10-64-shippable-qr/opt',
-                                         'macosx64/opt',
-                                         'macosx64/debug',
-                                         'macosx64-nightly/opt',
-                                         'macosx64-shippable/opt',
-                                         'macosx64-devedition/opt',
-                                         'macosx64-qr/opt',
-                                         'macosx64-shippable-qr/opt',
-                                         'macosx64-qr/debug',
+                                         'macosx1010-64/opt',
+                                         'macosx1010-64/debug',
+                                         'macosx1010-64-nightly/opt',
+                                         'macosx1010-64-shippable/opt',
+                                         'macosx1010-64-devedition/opt',
+                                         'macosx1010-64-qr/opt',
+                                         'macosx1010-64-shippable-qr/opt',
+                                         'macosx1010-64-qr/debug',
+                                         'macosx1014-64/opt',
+                                         'macosx1014-64/debug',
+                                         'macosx1014-64-nightly/opt',
+                                         'macosx1014-64-shippable/opt',
+                                         'macosx1014-64-devedition/opt',
+                                         'macosx1014-64-qr/opt',
+                                         'macosx1014-64-shippable-qr/opt',
+                                         'macosx1014-64-qr/debug',
                                          'android-em-4.3-arm7-api-16/opt',
                                          'android-em-4.3-arm7-api-16/debug',
                                          'android-em-4.3-arm7-api-16/pgo',
                                          'android-em-4.2-x86/opt',
                                          'android-em-7.0-x86_64/opt',
                                          'android-em-7.0-x86_64/debug',
                                          'android-em-7.0-x86/opt']:
                 test['tier'] = 1
@@ -1163,18 +1175,20 @@ def set_worker_type(config, tests):
     """Set the worker type based on the test platform."""
     for test in tests:
         # during the taskcluster migration, this is a bit tortured, but it
         # will get simpler eventually!
         test_platform = test['test-platform']
         if test.get('worker-type'):
             # This test already has its worker type defined, so just use that (yields below)
             pass
-        elif test_platform.startswith('macosx'):
-            test['worker-type'] = MACOSX_WORKER_TYPES['macosx64']
+        elif test_platform.startswith('macosx1010-64'):
+            test['worker-type'] = MACOSX_WORKER_TYPES['macosx1010-64']
+        elif test_platform.startswith('macosx1014-64'):
+            test['worker-type'] = MACOSX_WORKER_TYPES['macosx1014-64']
         elif test_platform.startswith('win'):
             # figure out what platform the job needs to run on
             if test['virtualization'] == 'hardware':
                 # some jobs like talos and reftest run on real h/w - those are all win10
                 if test_platform.startswith('windows10-64-ux'):
                     win_worker_type_platform = WINDOWS_WORKER_TYPES['windows10-64-ux']
                 elif test_platform.startswith('windows10-aarch64'):
                     win_worker_type_platform = WINDOWS_WORKER_TYPES['windows10-aarch64']
--- a/taskcluster/taskgraph/try_option_syntax.py
+++ b/taskcluster/taskgraph/try_option_syntax.py
@@ -128,17 +128,18 @@ UNITTEST_PLATFORM_PRETTY_NAMES = {
         'linux64-asan',
         'linux64-stylo-sequential'
     ],
     'Android 4.3 Emulator': ['android-em-4.3-arm7-api-16'],
     'Android 4.3 Emulator PGO': ['android-em-4-3-armv7-api16-pgo'],
     'Android 7.0 Moto G5 32bit': ['android-hw-g5-7.0-arm7-api-16'],
     'Android 8.0 Google Pixel 2 32bit': ['android-hw-p2-8.0-arm7-api-16'],
     'Android 8.0 Google Pixel 2 64bit': ['android-hw-p2-8.0-android-aarch64'],
-    '10.10': ['macosx64'],
+    '10.10': ['macosx1010-64'],
+    '10.14': ['macosx1014-64'],
     # other commonly-used substrings for platforms not yet supported with
     # in-tree taskgraphs:
     # '10.10.5': [..TODO..],
     # '10.6': [..TODO..],
     # '10.8': [..TODO..],
     # 'Android 2.3 API9': [..TODO..],
     'Windows 7':  ['windows7-32'],
     'Windows 7 VM':  ['windows7-32-vm'],
@@ -597,16 +598,18 @@ class TryOptionSyntax(object):
                 if attr(attr_name) == test['test']:
                     break
             else:
                 return False
             if 'only_chunks' in test and attr('test_chunk') not in test['only_chunks']:
                 return False
             tier = task.task['extra']['treeherder']['tier']
             if 'platforms' in test:
+                if 'all' in test['platforms']:
+                    return True
                 platform = attr('test_platform', '').split('/')[0]
                 # Platforms can be forced by syntax like "-u xpcshell[Windows 8]"
                 return platform in test['platforms']
             elif tier != 1:
                 # Require tier 2/3 tests to be specifically enabled if there
                 # are other platforms that run this test suite as tier 1
                 name = attr('unittest_try_name')
                 test_tiers = self.test_tiers.get(name)
--- a/taskcluster/taskgraph/util/seta.py
+++ b/taskcluster/taskgraph/util/seta.py
@@ -96,18 +96,18 @@ class SETA(object):
 
             seta_conversions = {
                 # old: new
                 'test-linux32/opt': 'test-linux32-shippable/opt',
                 'test-linux64/opt': 'test-linux64-shippable/opt',
                 'test-linux64-pgo/opt': 'test-linux64-shippable/opt',
                 'test-linux64-pgo-qr/opt': 'test-linux64-shippable-qr/opt',
                 'test-linux64-qr/opt': 'test-linux64-shippable-qr/opt',
-                'test-macosx64/opt': 'test-macosx64-shippable/opt',
-                'test-macosx64-qr/opt': 'test-macosx64-shippable-qr',
+                'test-macosx64/opt': 'test-macosx1010-64-shippable/opt',
+                'test-macosx64-qr/opt': 'test-macosx1010-64-shippable-qr',
                 'test-windows7-32/opt': 'test-windows7-32-shippable/opt',
                 'test-windows7-32-pgo/opt': 'test-windows7-32-shippable/opt',
                 'test-windows10-64/opt': 'test-windows10-64-shippable/opt',
                 'test-windows10-64-pgo/opt': 'test-windows10-64-shippable/opt',
                 'test-windows10-64-pgo-qr/opt': 'test-windows10-64-shippable-qr/opt',
                 'test-windows10-64-qr/opt': 'test-windows10-64-shippable-qr/opt',
                 }
             # Now add new variants to the low-value set
--- a/taskcluster/taskgraph/util/workertypes.py
+++ b/taskcluster/taskgraph/util/workertypes.py
@@ -21,16 +21,17 @@ WORKER_TYPES = {
     'scriptworker-prov-v1/balrogworker-v1': ('balrog', None),
     'scriptworker-prov-v1/beetmoverworker-v1': ('beetmover', None),
     'scriptworker-prov-v1/pushapk-v1': ('push-apk', None),
     "scriptworker-prov-v1/signing-linux-v1": ('scriptworker-signing', None),
     "scriptworker-prov-v1/shipit": ('shipit', None),
     "scriptworker-prov-v1/shipit-dev": ('shipit', None),
     "scriptworker-prov-v1/treescript-v1": ('treescript', None),
     'terraform-packet/gecko-t-linux': ('docker-worker', 'linux'),
+    'releng-hardware/gecko-t-osx-1014': ('generic-worker', 'macosx'),
 }
 
 
 @memoize
 def worker_type_implementation(graph_config, worker_type):
     """Get the worker implementation and OS for the given workerType, where the
     OS represents the host system, not the target OS, in the case of
     cross-compiles."""
--- a/testing/raptor/raptor/raptor.py
+++ b/testing/raptor/raptor/raptor.py
@@ -78,17 +78,17 @@ class SignalHandler:
 
 class SignalHandlerException(Exception):
     pass
 
 
 class Raptor(object):
     """Container class for Raptor"""
 
-    def __init__(self, app, binary, run_local=False, obj_path=None,
+    def __init__(self, app, binary, run_local=False, obj_path=None, profile_class=None,
                  gecko_profile=False, gecko_profile_interval=None, gecko_profile_entries=None,
                  symbols_path=None, host=None, power_test=False, memory_test=False,
                  is_release_build=False, debug_mode=False, post_startup_delay=None,
                  interrupt_handler=None, e10s=True, **kwargs):
 
         # Override the magic --host HOST_IP with the value of the environment variable.
         if host == 'HOST_IP':
             host = os.environ['HOST_IP']
@@ -116,17 +116,17 @@ class Raptor(object):
         self.log = get_default_logger(component='raptor-main')
         self.control_server = None
         self.playback = None
         self.benchmark = None
         self.benchmark_port = 0
         self.gecko_profiler = None
         self.post_startup_delay = post_startup_delay
         self.device = None
-        self.profile_class = app
+        self.profile_class = profile_class or app
         self.firefox_android_apps = FIREFOX_ANDROID_APPS
         self.interrupt_handler = interrupt_handler
 
         # debug mode is currently only supported when running locally
         self.debug_mode = debug_mode if self.config['run_local'] else False
 
         # if running debug-mode reduce the pause after browser startup
         if self.debug_mode:
@@ -134,16 +134,19 @@ class Raptor(object):
             self.log.info("debug-mode enabled, reducing post-browser startup pause to %d ms"
                           % self.post_startup_delay)
 
         self.log.info("main raptor init, config is: %s" % str(self.config))
 
         # create results holder
         self.results_handler = RaptorResultsHandler()
 
+        self.build_browser_profile()
+        self.start_control_server()
+
     @property
     def profile_data_dir(self):
         if 'MOZ_DEVELOPER_REPO_DIR' in os.environ:
             return os.path.join(os.environ['MOZ_DEVELOPER_REPO_DIR'], 'testing', 'profiles')
         if build:
             return os.path.join(build.topsrcdir, 'testing', 'profiles')
         return os.path.join(here, 'profile_data')
 
@@ -172,16 +175,58 @@ class Raptor(object):
         self.install_raptor_webext()
 
         if test.get("preferences") is not None:
             self.set_browser_test_prefs(test['preferences'])
 
         # if 'alert_on' was provided in the test INI, add to our config for results/output
         self.config['subtest_alert_on'] = test.get('alert_on')
 
+    def run_tests(self, tests, test_names):
+        try:
+            for test in tests:
+                self.run_test(test, timeout=int(test['page_timeout']))
+
+            return self.process_results(test_names)
+
+        finally:
+            self.clean_up()
+
+    def run_test(self, test, timeout=None):
+        raise NotImplementedError()
+
+    def wait_for_test_finish(self, test, timeout):
+        # convert timeout to seconds and account for page cycles
+        timeout = int(timeout / 1000) * int(test.get('page_cycles', 1))
+        # account for the pause the raptor webext runner takes after browser startup
+        # and the time an exception is propagated through the framework
+        timeout += (int(self.post_startup_delay / 1000) + 10)
+
+        # if geckoProfile enabled, give browser more time for profiling
+        if self.config['gecko_profile'] is True:
+            timeout += 5 * 60
+
+        elapsed_time = 0
+        while not self.control_server._finished:
+            if self.config['enable_control_server_wait']:
+                response = self.control_server_wait_get()
+                if response == 'webext_status/__raptor_shutdownBrowser':
+                    if self.config['memory_test']:
+                        generate_android_memory_profile(self, test['name'])
+                    self.control_server_wait_continue()
+            time.sleep(1)
+            # we only want to force browser-shutdown on timeout if not in debug mode;
+            # in debug-mode we leave the browser running (require manual shutdown)
+            if not self.debug_mode:
+                elapsed_time += 1
+                if elapsed_time > (timeout) - 5:  # stop 5 seconds early
+                    self.log.info("application timed out after {} seconds".format(timeout))
+                    self.control_server.wait_for_quit()
+                    break
+
     def run_test_teardown(self):
         self.check_for_crashes()
 
         if self.playback is not None:
             self.playback.stop()
 
         self.remove_raptor_webext()
 
@@ -189,28 +234,28 @@ class Raptor(object):
         if self.config['gecko_profile'] is True:
             self.gecko_profiler.symbolicate()
             # clean up the temp gecko profiling folders
             self.log.info("cleaning up after gecko profiling")
             self.gecko_profiler.clean()
 
     def set_browser_test_prefs(self, raw_prefs):
         # add test specific preferences
-        self.log.info("preferences were configured for the test, however \
-                        we currently do not install them on non Firefox browsers.")
+        self.log.info("setting test-specific Firefox preferences")
+        self.profile.set_preferences(json.loads(raw_prefs))
 
-    def create_browser_profile(self):
+    def build_browser_profile(self):
         self.profile = create_profile(self.profile_class)
 
-        # Merge in base profiles
+        # Merge extra profile data from testing/profiles
         with open(os.path.join(self.profile_data_dir, 'profiles.json'), 'r') as fh:
             base_profiles = json.load(fh)['raptor']
 
-        for name in base_profiles:
-            path = os.path.join(self.profile_data_dir, name)
+        for profile in base_profiles:
+            path = os.path.join(self.profile_data_dir, profile)
             self.log.info("Merging profile: {}".format(path))
             self.profile.merge(path)
 
         # add profile dir to our config
         self.config['local_profile_dir'] = self.profile.profile
 
     def start_control_server(self):
         self.control_server = RaptorControlServer(self.results_handler, self.debug_mode)
@@ -326,45 +371,16 @@ class Raptor(object):
         upload_dir = os.getenv('MOZ_UPLOAD_DIR')
         if not upload_dir:
             self.log.critical("Profiling ignored because MOZ_UPLOAD_DIR was not set")
         else:
             self.gecko_profiler = GeckoProfile(upload_dir,
                                                self.config,
                                                test)
 
-    def wait_for_test_finish(self, test, timeout):
-        # convert timeout to seconds and account for page cycles
-        timeout = int(timeout / 1000) * int(test.get('page_cycles', 1))
-        # account for the pause the raptor webext runner takes after browser startup
-        # and the time an exception is propagated through the framework
-        timeout += (int(self.post_startup_delay / 1000) + 10)
-
-        # if geckoProfile enabled, give browser more time for profiling
-        if self.config['gecko_profile'] is True:
-            timeout += 5 * 60
-
-        elapsed_time = 0
-        while not self.control_server._finished:
-            if self.config['enable_control_server_wait']:
-                response = self.control_server_wait_get()
-                if response == 'webext_status/__raptor_shutdownBrowser':
-                    if self.config['memory_test']:
-                        generate_android_memory_profile(self, test['name'])
-                    self.control_server_wait_continue()
-            time.sleep(1)
-            # we only want to force browser-shutdown on timeout if not in debug mode;
-            # in debug-mode we leave the browser running (require manual shutdown)
-            if not self.debug_mode:
-                elapsed_time += 1
-                if elapsed_time > (timeout) - 5:  # stop 5 seconds early
-                    self.log.info("application timed out after {} seconds".format(timeout))
-                    self.control_server.wait_for_quit()
-                    break
-
     def process_results(self, test_names):
         # when running locally output results in build/raptor.json; when running
         # in production output to a local.json to be turned into tc job artifact
         if self.config.get('run_local', False):
             if 'MOZ_DEVELOPER_REPO_DIR' in os.environ:
                 raptor_json_path = os.path.join(os.environ['MOZ_DEVELOPER_REPO_DIR'],
                                                 'testing', 'mozharness', 'build', 'raptor.json')
             else:
@@ -408,17 +424,19 @@ class Raptor(object):
     def control_server_wait_clear(self, state):
         response = requests.post("http://127.0.0.1:%s/" % self.control_server.port,
                                  json={"type": "wait-clear", "data": state})
         return response.content
 
 
 class RaptorDesktop(Raptor):
 
-    def create_browser_handler(self):
+    def __init__(self, *args, **kwargs):
+        super(RaptorDesktop, self).__init__(*args, **kwargs)
+
         # create the desktop browser runner
         self.log.info("creating browser runner using mozrunner")
         self.output_handler = OutputHandler()
         process_args = {
             'processOutputLine': [self.output_handler],
         }
         runner_cls = runners[self.config['app']]
         self.runner = runner_cls(
@@ -481,17 +499,17 @@ class RaptorDesktop(Raptor):
                     self.start_playback(test)
 
                 if self.config['host'] not in ('localhost', '127.0.0.1'):
                     self.delete_proxy_settings_from_profile()
 
             else:
                 # initial browser profile was already created before run_test was called;
                 # now additional browser cycles we want to create a new one each time
-                self.create_browser_profile()
+                self.build_browser_profile()
 
                 self.run_test_setup(test)
 
             # now start the browser/app under test
             self.launch_desktop_browser(test)
 
             # set our control server flag to indicate we are running the browser/app
             self.control_server._finished = False
@@ -577,21 +595,16 @@ class RaptorDesktopFirefox(RaptorDesktop
         # if geckoProfile is enabled, initialize it
         if self.config['gecko_profile'] is True:
             self._init_gecko_profiling(test)
             # tell the control server the gecko_profile dir; the control server will
             # receive the actual gecko profiles from the web ext and will write them
             # to disk; then profiles are picked up by gecko_profile.symbolicate
             self.control_server.gecko_profile_dir = self.gecko_profiler.gecko_profile_dir
 
-    def set_browser_test_prefs(self, raw_prefs):
-        # add test specific preferences
-        self.log.info("setting test-specific Firefox preferences")
-        self.profile.set_preferences(json.loads(raw_prefs))
-
 
 class RaptorDesktopChrome(RaptorDesktop):
 
     def setup_chrome_desktop_for_playback(self):
         # if running a pageload test on google chrome, add the cmd line options
         # to turn on the proxy and ignore security certificate errors
         # if using host localhost, 127.0.0.1.
         chrome_args = [
@@ -613,84 +626,86 @@ class RaptorDesktopChrome(RaptorDesktop)
         if self.debug_mode:
             self.runner.cmdargs.extend(['--auto-open-devtools-for-tabs'])
 
         if test.get('playback') is not None:
             self.setup_chrome_desktop_for_playback()
 
         self.start_runner_proc()
 
+    def set_browser_test_prefs(self, raw_prefs):
+        # add test-specific preferences
+        self.log.info("preferences were configured for the test, however \
+                        we currently do not install them on non-Firefox browsers.")
+
 
 class RaptorAndroid(Raptor):
-    def __init__(self, app, binary, activity=None, intent=None, **kwargs):
-        super(RaptorAndroid, self).__init__(app, binary, **kwargs)
 
-        # on android, when creating the browser profile, we want to use a 'firefox' type profile
-        self.profile_class = "firefox"
+    def __init__(self, app, binary, activity=None, intent=None, **kwargs):
+        super(RaptorAndroid, self).__init__(app, binary, profile_class="firefox", **kwargs)
+
         self.config.update({
             'activity': activity,
             'intent': intent,
         })
 
+        self.remote_test_root = os.path.abspath(os.path.join(os.sep, 'sdcard', 'raptor'))
+        self.remote_profile = os.path.join(self.remote_test_root, "profile")
+
     def set_reverse_port(self, port):
         tcp_port = "tcp:{}".format(port)
         self.device.create_socket_connection('reverse', tcp_port, tcp_port)
 
-    def serve_benchmark_source(self, *args, **kwargs):
-        super(RaptorAndroid, self).serve_benchmark_source(*args, **kwargs)
-
-        # for Android we must make the benchmarks server available to the device
-        if self.config['host'] in ('localhost', '127.0.0.1'):
-            self.log.info("making the raptor benchmarks server port available to device")
-            self.set_reverse_port(self.benchmark_port)
-
-    def start_control_server(self):
-        super(RaptorAndroid, self).start_control_server()
-
-        # for Android we must make the control server available to the device
+    def set_reverse_ports(self, is_benchmark=False):
+        # Make services running on the host available to the device
         if self.config['host'] in ('localhost', '127.0.0.1'):
             self.log.info("making the raptor control server port available to device")
             self.set_reverse_port(self.control_server.port)
 
-    def start_playback(self, test):
-        super(RaptorAndroid, self).start_playback(test)
-
-        # for Android we must make the playback server available to the device
         if self.config['host'] in ('localhost', '127.0.0.1'):
             self.log.info("making the raptor playback server port available to device")
             self.set_reverse_port(8080)
 
-    def create_browser_handler(self):
-        # create the android device handler; it gets initiated and sets up adb etc
-        self.log.info("creating android device handler using mozdevice")
-        self.device = ADBDevice(verbose=True)
-        self.device.clear_logcat()
+        if is_benchmark and self.config['host'] in ('localhost', '127.0.0.1'):
+            self.log.info("making the raptor benchmarks server port available to device")
+            self.set_reverse_port(self.benchmark_port)
+
+    def setup_adb_device(self):
+        if self.device is None:
+            self.device = ADBDevice(verbose=True)
+            self.tune_performance()
+
+        self.log.info("creating remote root folder for raptor: %s" % self.remote_test_root)
+        self.device.rm(self.remote_test_root, force=True, recursive=True)
+        self.device.mkdir(self.remote_test_root)
+        self.device.chmod(self.remote_test_root, recursive=True, root=True)
+
         self.clear_app_data()
 
     def tune_performance(self):
-        """Sets various performance-oriented parameters, to reduce jitter.
+        """Set various performance-oriented parameters, to reduce jitter.
 
         For more information, see https://bugzilla.mozilla.org/show_bug.cgi?id=1547135.
         """
         self.log.info("tuning android device performance")
         self.set_scheduler()
         self.set_svc_power_stayon()
         device_name = self.device.shell_output('getprop ro.product.model')
         if (self.device._have_su or self.device._have_android_su):
             # all commands require root shell from here on
             self.set_virtual_memory_parameters()
             self.turn_off_services()
             self.set_cpu_performance_parameters(device_name)
             self.set_gpu_performance_parameters(device_name)
             self.set_kernel_performance_parameters()
         self.device.clear_logcat()
 
-    def _set_value_and_check_exitcode(self, file_name, value):
+    def _set_value_and_check_exitcode(self, file_name, value, root=False):
         self.log.info('setting {} to {}'.format(file_name, value))
-        process = self.device.shell(' '.join(['echo', str(value), '>', str(file_name)]), root=True)
+        process = self.device.shell(' '.join(['echo', str(value), '>', str(file_name)]), root=root)
         if process.exitcode == 0:
             self.log.info('successfully set {} to {}'.format(file_name, value))
         else:
             self.log.warning('command failed with exitcode {}'.format(str(process.exitcode)))
 
     def set_svc_power_stayon(self):
         self.log.info('set device to stay awake on usb')
         self.device.shell('svc power stayon usb')
@@ -748,17 +763,17 @@ class RaptorAndroid(Raptor):
         self.log.info('setting virtual memory parameters')
         commands = {
             '/proc/sys/vm/swappiness': 0,
             '/proc/sys/vm/dirty_ratio': 85,
             '/proc/sys/vm/dirty_background_ratio': 70
         }
 
         for key, value in commands.items():
-            self._set_value_and_check_exitcode(key, value)
+            self._set_value_and_check_exitcode(key, value, root=True)
 
     def set_cpu_performance_parameters(self, device_name):
         self.log.info('setting cpu performance parameters')
         commands = {}
 
         if device_name == 'Pixel 2':
             # MSM8998 (4x 2.35GHz, 4x 1.9GHz)
             # values obtained from:
@@ -780,17 +795,17 @@ class RaptorAndroid(Raptor):
                     'cpufreq/scaling_governor'.format(x): 'performance',
                     '/sys/devices/system/cpu/cpu{}/'
                     'cpufreq/scaling_min_freq'.format(x): '1401000'
                 })
         else:
             pass
 
         for key, value in commands.items():
-            self._set_value_and_check_exitcode(key, value)
+            self._set_value_and_check_exitcode(key, value, root=True)
 
     def set_gpu_performance_parameters(self, device_name):
         self.log.info('setting gpu performance parameters')
         commands = {
             '/sys/class/kgsl/kgsl-3d0/bus_split': '0',
             '/sys/class/kgsl/kgsl-3d0/force_bus_on': '1',
             '/sys/class/kgsl/kgsl-3d0/force_rail_on': '1',
             '/sys/class/kgsl/kgsl-3d0/force_clk_on': '1',
@@ -816,62 +831,48 @@ class RaptorAndroid(Raptor):
             commands.update({
                 '/sys/devices/soc/1c00000.qcom,kgsl-3d0/devfreq/'
                 '1c00000.qcom,kgsl-3d0/governor': 'performance',
                 '/sys/devices/soc/1c00000.qcom,kgsl-3d0/kgsl/kgsl-3d0/min_clock_mhz': '450',
             })
         else:
             pass
         for key, value in commands.items():
-            self._set_value_and_check_exitcode(key, value)
+            self._set_value_and_check_exitcode(key, value, root=True)
 
     def set_kernel_performance_parameters(self):
         self.log.info('setting kernel performance parameters')
         commands = {
             '/sys/kernel/debug/msm-bus-dbg/shell-client/update_request': '1',
             '/sys/kernel/debug/msm-bus-dbg/shell-client/mas': '1',
             '/sys/kernel/debug/msm-bus-dbg/shell-client/ab': '0',
             '/sys/kernel/debug/msm-bus-dbg/shell-client/slv': '512',
         }
         for key, value in commands.items():
-            self._set_value_and_check_exitcode(key, value)
+            self._set_value_and_check_exitcode(key, value, root=True)
 
     def clear_app_data(self):
         self.log.info("clearing %s app data" % self.config['binary'])
         self.device.shell("pm clear %s" % self.config['binary'])
 
-    def create_raptor_sdcard_folder(self):
-        # for android/geckoview, create a top-level raptor folder on the device
-        # sdcard; if it already exists remove it so we start fresh each time
-        self.device_raptor_dir = "/sdcard/raptor"
-        self.config['device_raptor_dir'] = self.device_raptor_dir
-        if self.device.is_dir(self.device_raptor_dir):
-            self.log.info("deleting existing device raptor dir: %s" % self.device_raptor_dir)
-            self.device.rm(self.device_raptor_dir, recursive=True)
-        self.log.info("creating raptor folder on sdcard: %s" % self.device_raptor_dir)
-        self.device.mkdir(self.device_raptor_dir)
-        self.device.chmod(self.device_raptor_dir, recursive=True)
-
-    def copy_profile_onto_device(self):
-        # for geckoview/fennec we must copy the profile onto the device and set perms
+    def copy_profile_to_device(self):
+        """Copy the profile to the device, and update permissions of all files."""
         if not self.device.is_app_installed(self.config['binary']):
             raise Exception('%s is not installed' % self.config['binary'])
-        self.device_profile = os.path.join(self.device_raptor_dir, "profile")
 
-        if self.device.is_dir(self.device_profile):
-            self.log.info("deleting existing device profile folder: %s" % self.device_profile)
-            self.device.rm(self.device_profile, recursive=True)
-        self.log.info("creating profile folder on device: %s" % self.device_profile)
-        self.device.mkdir(self.device_profile)
+        try:
+            self.log.info("copying profile to device: %s" % self.remote_profile)
+            self.device.rm(self.remote_profile, force=True, recursive=True)
+            # self.device.mkdir(self.remote_profile)
+            self.device.push(self.profile.profile, self.remote_profile)
+            self.device.chmod(self.remote_profile, recursive=True, root=True)
 
-        self.log.info("copying firefox profile onto the device")
-        self.log.info("note: the profile folder being copied is: %s" % self.profile.profile)
-        self.log.info('the adb push cmd copies that profile dir to a new temp dir before copy')
-        self.device.push(self.profile.profile, self.device_profile)
-        self.device.chmod(self.device_profile, recursive=True)
+        except Exception:
+            self.log.error("Unable to copy profile to device.")
+            raise
 
     def turn_on_android_app_proxy(self):
         # for geckoview/android pageload playback we can't use a policy to turn on the
         # proxy; we need to set prefs instead; note that the 'host' may be different
         # than '127.0.0.1' so we must set the prefs accordingly
         self.log.info("setting profile prefs to turn on the android app proxy")
         proxy_prefs = {}
         proxy_prefs["network.proxy.type"] = 1
@@ -880,17 +881,17 @@ class RaptorAndroid(Raptor):
         proxy_prefs["network.proxy.ssl"] = self.config['host']
         proxy_prefs["network.proxy.ssl_port"] = 8080
         proxy_prefs["network.proxy.no_proxies_on"] = self.config['host']
         self.profile.set_preferences(proxy_prefs)
 
     def launch_firefox_android_app(self, test_name):
         self.log.info("starting %s" % self.config['app'])
 
-        extra_args = ["-profile", self.device_profile,
+        extra_args = ["-profile", self.remote_profile,
                       "--es", "env0", "LOG_VERBOSE=1",
                       "--es", "env1", "R_LOG_LEVEL=6"]
 
         try:
             # make sure the android app is not already running
             self.device.stop_application(self.config['binary'])
 
             if self.config['app'] == "fennec":
@@ -938,16 +939,33 @@ class RaptorAndroid(Raptor):
             _source = os.path.join(source_dir, next_file)
             _dest = os.path.join(target_dir, next_file)
             if os.path.exists(_source):
                 self.log.info("copying %s to %s" % (_source, _dest))
                 shutil.copyfile(_source, _dest)
             else:
                 self.log.critical("unable to find ssl cert db file: %s" % _source)
 
+    def run_tests(self, tests, test_names):
+        self.setup_adb_device()
+
+        return super(RaptorAndroid, self).run_tests(tests, test_names)
+
+    def run_test_setup(self, test):
+        super(RaptorAndroid, self).run_test_setup(test)
+
+        is_benchmark = test.get('type') == "benchmark"
+        self.set_reverse_ports(is_benchmark=is_benchmark)
+
+    def run_test_teardown(self):
+        self.log.info('removing reverse socket connections')
+        self.device.remove_socket_connections('reverse')
+
+        super(RaptorAndroid, self).run_test_teardown()
+
     def run_test(self, test, timeout=None):
         # tests will be run warm (i.e. NO browser restart between page-cycles)
         # unless otheriwse specified in the test INI by using 'cold = true'
         try:
             if test.get('cold', False) is True:
                 self.run_test_cold(test, timeout)
             else:
                 self.run_test_warm(test, timeout)
@@ -994,18 +1012,16 @@ class RaptorAndroid(Raptor):
         for test['browser_cycle'] in range(1, test['expected_browser_cycles'] + 1):
 
             self.log.info("begin browser cycle %d of %d for test %s"
                           % (test['browser_cycle'], test['expected_browser_cycles'], test['name']))
 
             self.run_test_setup(test)
 
             if test['browser_cycle'] == 1:
-                self.create_raptor_sdcard_folder()
-
                 if test.get('playback') is not None:
                     self.start_playback(test)
 
                     # an ssl cert db has now been created in the profile; copy it out so we
                     # can use the same cert db in future test cycles / browser restarts
                     local_cert_db_dir = tempfile.mkdtemp()
                     self.log.info("backing up browser ssl cert db that was created via certutil")
                     self.copy_cert_db(self.config['local_profile_dir'], local_cert_db_dir)
@@ -1017,30 +1033,30 @@ class RaptorAndroid(Raptor):
                 # double-check to ensure app has been shutdown
                 self.device.stop_application(self.config['binary'])
 
                 # clear the android app data before the next app startup
                 self.clear_app_data()
 
                 # initial browser profile was already created before run_test was called;
                 # now additional browser cycles we want to create a new one each time
-                self.create_browser_profile()
+                self.build_browser_profile()
 
                 if test.get('playback') is not None:
                     # get cert db from previous cycle profile and copy into new clean profile
                     # this saves us from having to start playback again / recreate cert db etc.
                     self.log.info("copying existing ssl cert db into new browser profile")
                     self.copy_cert_db(local_cert_db_dir, self.config['local_profile_dir'])
 
                 self.run_test_setup(test)
 
             if test.get('playback') is not None:
                 self.turn_on_android_app_proxy()
 
-            self.copy_profile_onto_device()
+            self.copy_profile_to_device()
 
             # now start the browser/app under test
             self.launch_firefox_android_app(test['name'])
 
             # set our control server flag to indicate we are running the browser/app
             self.control_server._finished = False
 
             self.wait_for_test_finish(test, timeout)
@@ -1056,29 +1072,28 @@ class RaptorAndroid(Raptor):
 
     def run_test_warm(self, test, timeout=None):
         self.log.info("test %s is running in warm mode; browser will NOT be restarted between "
                       "page cycles" % test['name'])
         if self.config['power_test']:
             init_android_power_test(self)
 
         self.run_test_setup(test)
-        self.create_raptor_sdcard_folder()
 
         if test.get('playback') is not None:
             self.start_playback(test)
 
         if self.config['host'] not in ('localhost', '127.0.0.1'):
             self.delete_proxy_settings_from_profile()
 
         if test.get('playback') is not None:
             self.turn_on_android_app_proxy()
 
         self.clear_app_data()
-        self.copy_profile_onto_device()
+        self.copy_profile_to_device()
 
         # now start the browser/app under test
         self.launch_firefox_android_app(test['name'])
 
         # set our control server flag to indicate we are running the browser/app
         self.control_server._finished = False
 
         self.wait_for_test_finish(test, timeout)
@@ -1094,31 +1109,31 @@ class RaptorAndroid(Raptor):
         self.device._verbose = False
         logcat = self.device.get_logcat()
         self.device._verbose = verbose
         if logcat:
             if mozcrash.check_for_java_exception(logcat, "raptor"):
                 return
         try:
             dump_dir = tempfile.mkdtemp()
-            remote_dir = posixpath.join(self.device_profile, 'minidumps')
+            remote_dir = posixpath.join(self.remote_profile, 'minidumps')
             if not self.device.is_dir(remote_dir):
                 self.log.error("No crash directory (%s) found on remote device" % remote_dir)
                 return
             self.device.pull(remote_dir, dump_dir)
             mozcrash.log_crashes(self.log, dump_dir, self.config['symbols_path'])
         finally:
             try:
                 shutil.rmtree(dump_dir)
             except Exception:
                 self.log.warning("unable to remove directory: %s" % dump_dir)
 
     def clean_up(self):
-        self.log.info('removing reverse socket connections')
-        self.device.remove_socket_connections('reverse')
+        self.log.info("removing test folder for raptor: %s" % self.remote_test_root)
+        self.device.rm(self.remote_test_root, force=True, recursive=True)
 
         super(RaptorAndroid, self).clean_up()
 
 
 def main(args=sys.argv[1:]):
     args = parse_args()
     commandline.setup_logging('raptor', args, {'tbpl': sys.stdout})
     LOG = get_default_logger(component='raptor-main')
@@ -1165,31 +1180,17 @@ def main(args=sys.argv[1:]):
                           is_release_build=args.is_release_build,
                           debug_mode=args.debug_mode,
                           post_startup_delay=args.post_startup_delay,
                           activity=args.activity,
                           intent=args.intent,
                           interrupt_handler=SignalHandler(),
                           )
 
-    raptor.create_browser_profile()
-    raptor.create_browser_handler()
-    if type(raptor) == RaptorAndroid:
-        # only Raptor Android supports device performance tuning
-        raptor.tune_performance()
-    raptor.start_control_server()
-
-    try:
-        for next_test in raptor_test_list:
-            raptor.run_test(next_test, timeout=int(next_test['page_timeout']))
-
-        success = raptor.process_results(raptor_test_names)
-
-    finally:
-        raptor.clean_up()
+    success = raptor.run_tests(raptor_test_list, raptor_test_names)
 
     if not success:
         # didn't get test results; test timed out or crashed, etc. we want job to fail
         LOG.critical("TEST-UNEXPECTED-FAIL: no raptor test results were found for %s" %
                      ', '.join(raptor_test_names))
         os.sys.exit(1)
 
     # if we have results but one test page timed out (i.e. one tp6 test page didn't load
--- a/testing/raptor/test/conftest.py
+++ b/testing/raptor/test/conftest.py
@@ -5,17 +5,17 @@ import os
 import sys
 
 import pytest
 
 from argparse import Namespace
 
 # need this so raptor imports work both from /raptor and via mach
 here = os.path.abspath(os.path.dirname(__file__))
-if os.environ.get('SCRIPTSPATH', None) is not None:
+if os.environ.get('SCRIPTSPATH') is not None:
     # in production it is env SCRIPTS_PATH
     mozharness_dir = os.environ['SCRIPTSPATH']
 else:
     # locally it's in source tree
     mozharness_dir = os.path.join(here, '../../mozharness')
 sys.path.insert(0, mozharness_dir)
 
 from raptor.raptor import RaptorDesktopFirefox
--- a/testing/raptor/test/test_raptor.py
+++ b/testing/raptor/test/test_raptor.py
@@ -20,45 +20,41 @@ else:
     # locally it's in source tree
     mozharness_dir = os.path.join(here, '../../mozharness')
 sys.path.insert(0, mozharness_dir)
 
 from raptor.raptor import RaptorDesktopFirefox, RaptorDesktopChrome, RaptorAndroid
 
 
 class TestBrowserThread(threading.Thread):
-        def __init__(self, raptor_instance, test, timeout=None):
+        def __init__(self, raptor_instance, tests, names):
             super(TestBrowserThread, self).__init__()
             self.raptor_instance = raptor_instance
-            self.test = test
-            self.timeout = timeout
+            self.tests = tests
+            self.names = names
             self.exc = None
 
         def run(self):
             try:
-                self.raptor_instance.run_test(self.test, self.timeout)
+                self.raptor_instance.run_tests(self.tests, self.names)
             except BaseException:
                 self.exc = sys.exc_info()
 
 
 @pytest.mark.parametrize("raptor_class, app_name", [
                          [RaptorDesktopFirefox, "firefox"],
                          [RaptorDesktopChrome, "chrome"],
                          [RaptorDesktopChrome, "chromium"],
                          [RaptorAndroid, "fennec"],
                          [RaptorAndroid, "geckoview"],
                          ])
-def test_create_profile(options, raptor_class, app_name, get_prefs):
+def test_build_profile(options, raptor_class, app_name, get_prefs):
     options['app'] = app_name
     raptor = raptor_class(**options)
 
-    if app_name in ["fennec", "geckoview"]:
-        raptor.profile_class = "firefox"
-    raptor.create_browser_profile()
-
     assert isinstance(raptor.profile, BaseProfile)
     if app_name != 'firefox':
         return
 
     # These prefs are set in mozprofile
     firefox_prefs = [
         'user_pref("app.update.checkInstallTime", false);',
         'user_pref("app.update.disabledForTesting", true);',
@@ -72,22 +68,16 @@ def test_create_profile(options, raptor_
     with open(prefs_file, 'r') as fh:
         prefs = fh.read()
         for firefox_pref in firefox_prefs:
             assert firefox_pref in prefs
         assert raptor_pref in prefs
 
 
 def test_start_and_stop_server(raptor):
-    assert raptor.control_server is None
-
-    raptor.create_browser_profile()
-    raptor.create_browser_handler()
-    raptor.start_control_server()