Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
authorBogdan Tara <btara@mozilla.com>
Fri, 16 Nov 2018 11:51:51 +0200
changeset 503192 9ea67e7f7a0f4c37a58202346634b771ed360986
parent 503191 26048fa38e564f18c3e1a87b83c36a8d328a6e26 (current diff)
parent 503159 4d6d3403eb6b015ebd2e6949d57dd518d07d024f (diff)
child 503193 298abb6be2dce482ecb8af9ec6ef78abf2c26e1d
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
browser/components/payments/content/paymentDialogWrapper.css
browser/components/payments/content/paymentDialogWrapper.xul
browser/components/search/test/.eslintrc.js
browser/components/search/test/426329.xml
browser/components/search/test/483086-1.xml
browser/components/search/test/483086-2.xml
browser/components/search/test/SearchTestUtils.jsm
browser/components/search/test/browser.ini
browser/components/search/test/browser_426329.js
browser/components/search/test/browser_483086.js
browser/components/search/test/browser_aboutSearchReset.js
browser/components/search/test/browser_addEngine.js
browser/components/search/test/browser_amazon.js
browser/components/search/test/browser_bing.js
browser/components/search/test/browser_contextSearchTabPosition.js
browser/components/search/test/browser_contextmenu.js
browser/components/search/test/browser_ddg.js
browser/components/search/test/browser_eBay.js
browser/components/search/test/browser_google.js
browser/components/search/test/browser_google_behavior.js
browser/components/search/test/browser_healthreport.js
browser/components/search/test/browser_hiddenOneOffs_cleanup.js
browser/components/search/test/browser_hiddenOneOffs_diacritics.js
browser/components/search/test/browser_oneOffContextMenu.js
browser/components/search/test/browser_oneOffContextMenu_setDefault.js
browser/components/search/test/browser_oneOffHeader.js
browser/components/search/test/browser_private_search_perwindowpb.js
browser/components/search/test/browser_searchEngine_behaviors.js
browser/components/search/test/browser_searchbar_keyboard_navigation.js
browser/components/search/test/browser_searchbar_openpopup.js
browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
browser/components/search/test/browser_tooManyEnginesOffered.js
browser/components/search/test/browser_webapi.js
browser/components/search/test/google_codes/browser.ini
browser/components/search/test/head.js
browser/components/search/test/opensearch.html
browser/components/search/test/test.html
browser/components/search/test/testEngine.xml
browser/components/search/test/testEngine_diacritics.xml
browser/components/search/test/testEngine_dupe.xml
browser/components/search/test/testEngine_missing_namespace.xml
browser/components/search/test/testEngine_mozsearch.xml
browser/components/search/test/tooManyEnginesOffered.html
browser/components/search/test/webapi.html
dom/canvas/WebGLContextExtensions.cpp
dom/canvas/test/webgl-mochitest/ensure-exts/test_EXT_texture_compression_bptc.html
dom/canvas/test/webgl-mochitest/ensure-exts/test_EXT_texture_compression_rgtc.html
toolkit/components/search/tests/xpcshell/test_urltelemetry.js
--- a/browser/base/content/test/favicons/browser.ini
+++ b/browser/base/content/test/favicons/browser.ini
@@ -57,8 +57,12 @@ support-files =
 support-files =
   file_insecure_favicon.html
   file_favicon.png
 [browser_title_flicker.js]
 support-files =
   file_with_slow_favicon.html
   blank.html
   file_favicon.png
+[browser_favicon_cache.js]
+support-files =
+  cookie_favicon.sjs
+  cookie_favicon.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/favicons/browser_favicon_cache.js
@@ -0,0 +1,40 @@
+add_task(async () => {
+  const testPath = "http://example.com/browser/browser/base/content/test/favicons/cookie_favicon.html";
+  const resetPath = "http://example.com/browser/browser/base/content/test/favicons/cookie_favicon.sjs?reset";
+
+  let tab = BrowserTestUtils.addTab(gBrowser, testPath);
+  gBrowser.selectedTab = tab;
+  let browser = tab.linkedBrowser;
+
+  let faviconPromise = waitForLinkAvailable(browser);
+  await BrowserTestUtils.browserLoaded(browser);
+  await faviconPromise;
+  let cookies = Services.cookies.getCookiesFromHost("example.com", browser.contentPrincipal.originAttributes);
+  let seenCookie = false;
+  for (let cookie of cookies) {
+    if (cookie.name == "faviconCookie") {
+      seenCookie = true;
+      is(cookie.value, 1, "Should have seen the right initial cookie.");
+    }
+  }
+  ok(seenCookie, "Should have seen the cookie.");
+
+  faviconPromise = waitForLinkAvailable(browser);
+  BrowserTestUtils.loadURI(browser, testPath);
+  await BrowserTestUtils.browserLoaded(browser);
+  await faviconPromise;
+  cookies = Services.cookies.getCookiesFromHost("example.com", browser.contentPrincipal.originAttributes);
+  seenCookie = false;
+  for (let cookie of cookies) {
+    if (cookie.name == "faviconCookie") {
+      seenCookie = true;
+      is(cookie.value, 1, "Should have seen the cached cookie.");
+    }
+  }
+  ok(seenCookie, "Should have seen the cookie.");
+
+  // Reset the cookie so if this test is run again it will still pass.
+  await fetch(resetPath);
+
+  BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/favicons/cookie_favicon.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset='utf-8'>
+    <title>Favicon Test for caching</title>
+    <link rel="icon" type="image/png" href="cookie_favicon.sjs" />
+  </head>
+  <body>
+    Favicon!!
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/favicons/cookie_favicon.sjs
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+  if (request.queryString == "reset") {
+    setState("cache_cookie", "0");
+    response.setStatusLine(request.httpVersion, 200, "Ok");
+    response.write("Reset");
+    return;
+  }
+
+  let state = getState("cache_cookie");
+  if (!state) {
+    state = 0;
+  }
+
+  response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+  response.setHeader("Set-Cookie", `faviconCookie=${++state}`);
+  response.setHeader("Location", "http://example.com/browser/browser/base/content/test/favicons/moz.png");
+  setState("cache_cookie", `${state}`);
+}
--- a/browser/components/enterprisepolicies/EnterprisePolicies.js
+++ b/browser/components/enterprisepolicies/EnterprisePolicies.js
@@ -402,17 +402,19 @@ class JSONPoliciesProvider {
 class WindowsGPOPoliciesProvider {
   constructor() {
     this._policies = null;
 
     let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(Ci.nsIWindowsRegKey);
 
     // Machine policies override user policies, so we read
     // user policies first and then replace them if necessary.
+    log.debug("root = HKEY_CURRENT_USER");
     this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER);
+    log.debug("root = HKEY_LOCAL_MACHINE");
     this._readData(wrk, wrk.ROOT_KEY_LOCAL_MACHINE);
   }
 
   get hasPolicies() {
     return this._policies !== null;
   }
 
   get policies() {
@@ -421,18 +423,17 @@ class WindowsGPOPoliciesProvider {
 
   get failed() {
     return this._failed;
   }
 
   _readData(wrk, root) {
     wrk.open(root, "SOFTWARE\\Policies", wrk.ACCESS_READ);
     if (wrk.hasChild("Mozilla\\Firefox")) {
-      let isMachineRoot = (root == wrk.ROOT_KEY_LOCAL_MACHINE);
-      this._policies = WindowsGPOParser.readPolicies(wrk, this._policies, isMachineRoot);
+      this._policies = WindowsGPOParser.readPolicies(wrk, this._policies);
     }
     wrk.close();
   }
 }
 
 class macOSPoliciesProvider {
   constructor() {
     this._policies = null;
--- a/browser/components/enterprisepolicies/WindowsGPOParser.jsm
+++ b/browser/components/enterprisepolicies/WindowsGPOParser.jsm
@@ -14,89 +14,75 @@ XPCOMUtils.defineLazyGetter(this, "log",
     prefix: "GPOParser.jsm",
     // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
     // messages during development. See LOG_LEVELS in Console.jsm for details.
     maxLogLevel: "error",
     maxLogLevelPref: PREF_LOGLEVEL,
   });
 });
 
-XPCOMUtils.defineLazyModuleGetters(this, {
-  schema: "resource:///modules/policies/schema.jsm",
-});
-
 var EXPORTED_SYMBOLS = ["WindowsGPOParser"];
 
 var WindowsGPOParser = {
-  readPolicies(wrk, policies, isMachineRoot) {
+  readPolicies(wrk, policies) {
     let childWrk = wrk.openChild("Mozilla\\Firefox", wrk.ACCESS_READ);
     if (!policies) {
       policies = {};
     }
     try {
-      policies = registryToObject(childWrk, policies, isMachineRoot);
+      policies = registryToObject(childWrk, policies);
     } catch (e) {
       log.error(e);
     } finally {
       childWrk.close();
     }
     // Need an extra check here so we don't
     // JSON.stringify if we aren't in debug mode
     if (log._maxLogLevel == "debug") {
-      log.debug("root = " + isMachineRoot ? "HKEY_LOCAL_MACHINE" : "HKEY_CURRENT_USER");
       log.debug(JSON.stringify(policies, null, 2));
     }
     return policies;
   },
 };
 
-function registryToObject(wrk, policies, isMachineRoot) {
+function registryToObject(wrk, policies) {
   if (!policies) {
     policies = {};
   }
   if (wrk.valueCount > 0) {
     if (wrk.getValueName(0) == "1") {
       // If the first item is 1, just assume it is an array
       let array = [];
       for (let i = 0; i < wrk.valueCount; i++) {
         array.push(readRegistryValue(wrk, wrk.getValueName(i)));
       }
       // If it's an array, it shouldn't have any children
       return array;
     }
     for (let i = 0; i < wrk.valueCount; i++) {
       let name = wrk.getValueName(i);
-      if (!isMachineRoot && isMachineOnlyPolicy(name)) {
-        continue;
-      }
       let value = readRegistryValue(wrk, name);
       policies[name] = value;
     }
   }
   if (wrk.childCount > 0) {
     if (wrk.getChildName(0) == "1") {
       // If the first item is 1, it's an array of objects
       let array = [];
       for (let i = 0; i < wrk.childCount; i++) {
         let name = wrk.getChildName(i);
-        if (!isMachineRoot && isMachineOnlyPolicy(name)) {
-          continue;
-        }
         let childWrk = wrk.openChild(name, wrk.ACCESS_READ);
         array.push(registryToObject(childWrk));
         childWrk.close();
       }
       // If it's an array, it shouldn't have any children
       return array;
     }
     for (let i = 0; i < wrk.childCount; i++) {
       let name = wrk.getChildName(i);
-        if (!isMachineRoot && isMachineOnlyPolicy(name)) {
-        continue;
-      }
       let childWrk = wrk.openChild(name, wrk.ACCESS_READ);
       policies[name] = registryToObject(childWrk);
       childWrk.close();
     }
   }
   return policies;
 }
 
@@ -109,17 +95,8 @@ function readRegistryValue(wrk, value) {
     case wrk.TYPE_INT:
       return wrk.readIntValue(value);
     case wrk.TYPE_INT64:
       return wrk.readInt64Value(value);
   }
   // unknown type
   return null;
 }
-
-function isMachineOnlyPolicy(name) {
-  if (schema.properties[name] &&
-      schema.properties[name].machine_only) {
-    log.error(`Policy ${name} is only allowed under the HKEY_LOCAL_MACHINE root`);
-    return true;
-  }
-  return false;
-}
--- a/browser/components/enterprisepolicies/content/aboutPolicies.css
+++ b/browser/components/enterprisepolicies/content/aboutPolicies.css
@@ -109,20 +109,16 @@ tbody:nth-child(4n + 1) {
   fill: var(--newtab-icon-primary-color);
   height: 14px;
   vertical-align: middle;
   width: 14px;
   margin-top: -.125rem;
   margin-left: .5rem;
 }
 
-.icon.machine-only {
-  background-image: url("chrome://browser/skin/developer.svg");
-}
-
 .collapsible {
   cursor: pointer;
   border: none;
   outline: none;
 }
 
 .content {
   display: none;
--- a/browser/components/enterprisepolicies/content/aboutPolicies.js
+++ b/browser/components/enterprisepolicies/content/aboutPolicies.js
@@ -17,28 +17,16 @@ function col(text, className) {
   if (className) {
     column.classList.add(className);
   }
   let content = document.createTextNode(text);
   column.appendChild(content);
   return column;
 }
 
-function machine_only_col(text) {
-  let icon = document.createElement("span");
-  icon.classList.add("icon");
-  icon.classList.add("machine-only");
-  icon.setAttribute("data-l10n-id", "gpo-machine-only");
-  let column = document.createElement("td");
-  let content = document.createTextNode(text);
-  column.appendChild(content);
-  column.appendChild(icon);
-  return column;
-}
-
 function addMissingColumns() {
   const table = document.getElementById("activeContent");
   let maxColumns = 0;
 
   // count the number of columns per row and set the max number of columns
   for (let i = 0, length = table.rows.length; i < length; i++) {
     if (maxColumns < table.rows[i].cells.length) {
       maxColumns = table.rows[i].cells.length;
@@ -241,22 +229,17 @@ function generateDocumentation() {
   for (let policyName in schema.properties) {
     let main_tbody = document.createElement("tbody");
     main_tbody.classList.add("collapsible");
     main_tbody.addEventListener("click", function() {
       let content = this.nextElementSibling;
       content.classList.toggle("content");
     });
     let row = document.createElement("tr");
-    if (AppConstants.platform == "win" &&
-        schema.properties[policyName].machine_only) {
-      row.appendChild(machine_only_col(policyName));
-    } else {
-      row.appendChild(col(policyName));
-    }
+    row.appendChild(col(policyName));
     let descriptionColumn = col("");
     let stringID = string_mapping[policyName] || policyName;
     descriptionColumn.setAttribute("data-l10n-id", `policy-${stringID}`);
     row.appendChild(descriptionColumn);
     main_tbody.appendChild(row);
     let sec_tbody = document.createElement("tbody");
     sec_tbody.classList.add("content");
     sec_tbody.classList.add("content-style");
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -1,15 +1,13 @@
 {
   "$schema": "http://json-schema.org/draft-04/schema#",
   "type": "object",
   "properties": {
     "AppUpdateURL": {
-      "machine_only": true,
-
       "type": "URL"
     },
 
     "Authentication": {
       "type": "object",
       "properties": {
         "SPNEGO" : {
           "type": "array",
@@ -158,18 +156,16 @@
         },
         "Locked": {
           "type": "boolean"
         }
       }
     },
 
     "DisableAppUpdate": {
-      "machine_only": true,
-
       "type": "boolean"
     },
 
     "DisableBuiltinPDFViewer": {
       "type": "boolean"
     },
 
     "DisableDeveloperTools": {
@@ -237,24 +233,20 @@
       }
     },
 
     "DisableSetDesktopBackground": {
       "type": "boolean"
     },
 
     "DisableSystemAddonUpdate": {
-      "machine_only": true,
-
       "type": "boolean"
     },
 
     "DisableTelemetry": {
-      "machine_only": true,
-
       "type": "boolean"
     },
 
     "DisplayBookmarksToolbar": {
       "type": "boolean"
     },
 
     "DisplayMenuBar": {
@@ -274,18 +266,16 @@
         "Locked": {
           "type": "boolean"
         }
       },
       "required": ["Value"]
     },
 
     "Extensions": {
-      "machine_only": true,
-
       "type": "object",
       "properties": {
         "Install" : {
           "type": "array",
           "items": {
             "type": "string"
           }
         },
@@ -333,18 +323,16 @@
       }
     },
 
     "HardwareAcceleration": {
       "type": "boolean"
     },
 
     "Homepage": {
-      "machine_only": true,
-
       "type": "object",
       "properties": {
         "URL": {
           "type": "URL"
         },
         "Locked": {
           "type": "boolean"
         },
@@ -382,24 +370,20 @@
       "type": "boolean"
     },
 
     "OfferToSaveLogins": {
       "type": "boolean"
     },
 
     "OverrideFirstRunPage": {
-      "machine_only": true,
-
       "type": "URLorEmpty"
     },
 
     "OverridePostUpdatePage": {
-      "machine_only": true,
-
       "type": "URLorEmpty"
     },
 
     "Permissions": {
       "type": "object",
       "properties": {
         "Camera": {
           "type": "object",
@@ -666,18 +650,16 @@
     "SecurityDevices": {
       "type": "object",
       "patternProperties": {
         "^.*$": { "type": "string" }
       }
     },
 
     "WebsiteFilter": {
-      "machine_only": "true",
-
       "type": "object",
       "properties": {
         "Block": {
           "type": "array",
           "items": {
             "type": "string"
           }
         },
deleted file mode 100644
--- a/browser/components/payments/content/paymentDialogWrapper.css
+++ /dev/null
@@ -1,11 +0,0 @@
-/* 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 {
-  margin: 0;
-}
-
-#paymentRequestFrame {
-  border: none;
-}
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -2,25 +2,26 @@
  * 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/. */
 
 /**
  * Runs in the privileged outer dialog. Each dialog loads this script in its
  * own scope.
  */
 
+/* exported paymentDialogWrapper */
+
 "use strict";
 
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 
 const paymentUISrv = Cc["@mozilla.org/dom/payments/payment-ui-service;1"]
                      .getService(Ci.nsIPaymentUIService);
 
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
                                "resource:///modules/BrowserWindowTracker.jsm");
 ChromeUtils.defineModuleGetter(this, "OSKeyStore",
                                "resource://formautofill/OSKeyStore.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
@@ -85,17 +86,17 @@ class TempCollection {
 
   getAll() {
     return this._data;
   }
 }
 
 var paymentDialogWrapper = {
   componentsLoaded: new Map(),
-  frame: null,
+  frameWeakRef: null,
   mm: null,
   request: null,
   temporaryStore: null,
 
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference,
   ]),
@@ -133,25 +134,34 @@ var paymentDialogWrapper = {
   async _convertProfileAddressToPaymentAddress(guid) {
     let addressData = this.temporaryStore.addresses.get(guid) ||
                       await formAutofillStorage.addresses.get(guid);
     if (!addressData) {
       throw new Error(`Address not found: ${guid}`);
     }
 
     let address = this.createPaymentAddress({
+      addressLines: addressData["street-address"].split("\n"),
+      city: addressData["address-level2"],
       country: addressData.country,
-      addressLines: addressData["street-address"].split("\n"),
-      region: addressData["address-level1"],
-      city: addressData["address-level2"],
       dependentLocality: addressData["address-level3"],
+      organization: addressData.organization,
+      phone: addressData.tel,
       postalCode: addressData["postal-code"],
-      organization: addressData.organization,
       recipient: addressData.name,
-      phone: addressData.tel,
+      region: addressData["address-level1"],
+      // TODO (bug 1474905), The regionCode will be available when bug 1474905 is fixed
+      // and the region text box is changed to a dropdown with the regionCode being the
+      // value of the option and the region being the label for the option.
+      // A regionCode should be either the empty string or one to three code points
+      // that represent a region as the code element of an [ISO3166-2] country subdivision
+      // name (i.e., the characters after the hyphen in an ISO3166-2 country subdivision
+      // code element, such as "CA" for the state of California in the USA, or "11" for
+      // the Lisbon district of Portugal).
+      regionCode: "",
     });
 
     return address;
   },
 
   /**
    * @param {string} guid The GUID of the basic card record from storage.
    * @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
@@ -197,44 +207,74 @@ var paymentDialogWrapper = {
     return methodData;
   },
 
   init(requestId, frame) {
     if (!requestId || typeof(requestId) != "string") {
       throw new Error("Invalid PaymentRequest ID");
     }
 
-    window.addEventListener("unload", this);
-
     // The Request object returned by the Payment Service is live and
     // will automatically get updated if event.updateWith is used.
     this.request = paymentSrv.getPaymentRequestById(requestId);
 
     if (!this.request) {
       throw new Error(`PaymentRequest not found: ${requestId}`);
     }
 
-    this.frame = frame;
-    this.mm = frame.frameLoader.messageManager;
-    this.mm.addMessageListener("paymentContentToChrome", this);
+    this._attachToFrame(frame);
     this.mm.loadFrameScript("chrome://payments/content/paymentDialogFrameScript.js", true);
     // Until we have bug 1446164 and bug 1407418 we use form autofill's temporary
     // shim for data-localization* attributes.
     this.mm.loadFrameScript("chrome://formautofill/content/l10n.js", true);
-    if (AppConstants.platform == "win") {
-      this.frame.setAttribute("selectmenulist", "ContentSelectDropdown-windows");
-    }
-    this.frame.setAttribute("src", "resource://payments/paymentRequest.xhtml");
+    frame.setAttribute("src", "resource://payments/paymentRequest.xhtml");
 
     this.temporaryStore = {
       addresses: new TempCollection("addresses"),
       creditCards: new TempCollection("creditCards"),
     };
   },
 
+  uninit() {
+    try {
+      Services.obs.removeObserver(this, "message-manager-close");
+      Services.obs.removeObserver(this, "formautofill-storage-changed");
+    } catch (ex) {
+      // Observers may not have been added yet
+    }
+  },
+
+  /**
+   * Code here will be re-run at various times, e.g. initial show and
+   * when a tab is detached to a different window.
+   *
+   * Code that should only run once belongs in `init`.
+   * Code to only run upon detaching should be in `changeAttachedFrame`.
+   *
+   * @param {Element} frame
+   */
+  _attachToFrame(frame) {
+    this.frameWeakRef = Cu.getWeakReference(frame);
+    this.mm = frame.frameLoader.messageManager;
+    this.mm.addMessageListener("paymentContentToChrome", this);
+    Services.obs.addObserver(this, "message-manager-close", true);
+  },
+
+  /**
+   * Called only when a frame is changed from one to another.
+   *
+   * @param {Element} frame
+   */
+  changeAttachedFrame(frame) {
+    this.mm.removeMessageListener("paymentContentToChrome", this);
+    this._attachToFrame(frame);
+    // This isn't in `attachToFrame` because we only want to do it once we've sent records.
+    Services.obs.addObserver(this, "formautofill-storage-changed", true);
+  },
+
   createShowResponse({
     acceptStatus,
     methodName = "",
     methodData = null,
     payerName = "",
     payerEmail = "",
     payerPhone = "",
   }) {
@@ -265,27 +305,27 @@ var paymentDialogWrapper = {
                                    expiryMonth,
                                    expiryYear,
                                    cardSecurityCode,
                                    billingAddress);
     return basicCardResponseData;
   },
 
   createPaymentAddress({
+    addressLines = [],
+    city = "",
     country = "",
-    addressLines = [],
+    dependentLocality = "",
+    organization = "",
+    postalCode = "",
+    phone = "",
+    recipient = "",
     region = "",
     regionCode = "",
-    city = "",
-    dependentLocality = "",
-    postalCode = "",
     sortingCode = "",
-    organization = "",
-    recipient = "",
-    phone = "",
   }) {
     const paymentAddress = Cc["@mozilla.org/dom/payments/payment-address;1"]
                            .createInstance(Ci.nsIPaymentAddress);
     const addressLine = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     for (let line of addressLines) {
       const address = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
       address.data = line;
       addressLine.appendElement(address);
@@ -450,20 +490,22 @@ var paymentDialogWrapper = {
       if (result !== undefined) {
         obj[key] = result;
       }
     }
     return obj;
   },
 
   async initializeFrame() {
+    // We don't do this earlier as it's only necessary once this function sends
+    // the initial saved records.
     Services.obs.addObserver(this, "formautofill-storage-changed", true);
 
     let requestSerialized = this._serializeRequest(this.request);
-    let chromeWindow = window.frameElement.ownerGlobal;
+    let chromeWindow = this.frameWeakRef.get().ownerGlobal;
     let isPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWindow);
 
     let [savedAddresses, savedBasicCards] =
       await Promise.all([this.fetchSavedAddresses(), this.fetchSavedPaymentCards()]);
 
     this.sendMessageToContent("showPaymentRequest", {
       request: requestSerialized,
       savedAddresses,
@@ -479,17 +521,17 @@ var paymentDialogWrapper = {
     if (!Services.prefs.getBoolPref("devtools.chrome.enabled", false)) {
       Cu.reportError("devtools.chrome.enabled must be enabled to debug the frame");
       return;
     }
     let {
       gDevToolsBrowser,
     } = ChromeUtils.import("resource://devtools/client/framework/gDevTools.jsm", {});
     gDevToolsBrowser.openContentProcessToolbox({
-      selectedBrowser: document.getElementById("paymentRequestFrame").frameLoader,
+      selectedBrowser: this.frameWeakRef.get(),
     });
   },
 
   onOpenPreferences() {
     BrowserWindowTracker.getTopWindow().openPreferences("privacy-form-autofill");
   },
 
   onPaymentCancel() {
@@ -630,54 +672,45 @@ var paymentDialogWrapper = {
         // there will be no formautofill-storage-changed event to update state
         // so add updated collection here
         Object.assign(responseMessage.stateChange, {
           tempBasicCards: this.temporaryStore.creditCards.getAll(),
         });
       }
     } catch (ex) {
       responseMessage.error = true;
+      Cu.reportError(ex);
     } finally {
       this.sendMessageToContent("updateAutofillRecord:Response", responseMessage);
     }
   },
 
   /**
-   * @implement {nsIDOMEventListener}
-   * @param {Event} event
-   */
-  handleEvent(event) {
-    switch (event.type) {
-      case "unload": {
-        // Remove the observer to avoid message manager errors while the dialog
-        // is closing and tests are cleaning up autofill storage.
-        Services.obs.removeObserver(this, "formautofill-storage-changed");
-        break;
-      }
-      default: {
-        throw new Error("Unexpected event handled");
-      }
-    }
-  },
-
-  /**
    * @implements {nsIObserver}
    * @param {nsISupports} subject
    * @param {string} topic
    * @param {string} data
    */
   observe(subject, topic, data) {
     switch (topic) {
       case "formautofill-storage-changed": {
         if (data == "notifyUsed") {
           break;
         }
         this.onAutofillStorageChange();
         break;
       }
+      case "message-manager-close": {
+        if (this.mm && subject == this.mm) {
+          // Remove the observer to avoid message manager errors while the dialog
+          // is closing and tests are cleaning up autofill storage.
+          Services.obs.removeObserver(this, "formautofill-storage-changed");
+        }
+        break;
+      }
     }
   },
 
   receiveMessage({data}) {
     let {messageType} = data;
 
     switch (messageType) {
       case "debugFrame": {
@@ -704,17 +737,17 @@ var paymentDialogWrapper = {
         this.onOpenPreferences();
         break;
       }
       case "paymentCancel": {
         this.onPaymentCancel();
         break;
       }
       case "paymentDialogReady": {
-        window.dispatchEvent(new Event("tabmodaldialogready", {
+        this.frameWeakRef.get().dispatchEvent(new Event("tabmodaldialogready", {
           bubbles: true,
         }));
         break;
       }
       case "pay": {
         this.onPay(data);
         break;
       }
@@ -723,15 +756,8 @@ var paymentDialogWrapper = {
         break;
       }
       default: {
         throw new Error(`paymentDialogWrapper: Unexpected messageType: ${messageType}`);
       }
     }
   },
 };
-
-if ("document" in this) {
-  // Running in a browser, not a unit test
-  let frame = document.getElementById("paymentRequestFrame");
-  let requestId = (new URLSearchParams(window.location.search)).get("requestId");
-  paymentDialogWrapper.init(requestId, frame);
-}
deleted file mode 100644
--- a/browser/components/payments/content/paymentDialogWrapper.xul
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 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/. -->
-<?xml-stylesheet href="chrome://global/skin/global.css"?>
-<?xml-stylesheet href="chrome://payments/content/paymentDialogWrapper.css"?>
-
-<!DOCTYPE window>
-<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml"
-        xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
-  <popupset>
-    <!-- for select dropdowns. The menupopup is what shows the list of options,
-         and the popuponly menulist makes things like the menuactive attributes
-         work correctly on the menupopup. ContentSelectDropdown expects the
-         popuponly menulist to be its immediate parent. -->
-    <menulist popuponly="true" id="ContentSelectDropdown" hidden="true">
-      <menupopup rolluponmousewheel="true"
-                 activateontab="true" position="after_start"
-                 level="parent"
-                 />
-    </menulist>
-
-    <!-- same as above but with additional attributes for Windows -->
-    <menulist popuponly="true" id="ContentSelectDropdown-windows" hidden="true">
-      <menupopup rolluponmousewheel="true"
-                 activateontab="true" position="after_start"
-                 level="parent"
-                 consumeoutsideclicks="false" ignorekeys="shortcuts"
-                 />
-    </menulist>
-  </popupset>
-
-  <browser type="content"
-           id="paymentRequestFrame"
-           disablehistory="true"
-           nodefaultsrc="true"
-           remote="true"
-           selectmenulist="ContentSelectDropdown"
-           style="height:100vh;width:100vw"
-           transparent="true"></browser>
-  <script type="application/javascript" src="chrome://payments/content/paymentDialogWrapper.js"></script>
-</window>
--- a/browser/components/payments/jar.mn
+++ b/browser/components/payments/jar.mn
@@ -1,18 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 browser.jar:
 %   content payments %content/payments/
     content/payments/paymentDialogFrameScript.js      (content/paymentDialogFrameScript.js)
-    content/payments/paymentDialogWrapper.css         (content/paymentDialogWrapper.css)
     content/payments/paymentDialogWrapper.js          (content/paymentDialogWrapper.js)
-    content/payments/paymentDialogWrapper.xul         (content/paymentDialogWrapper.xul)
 
 %   resource payments %res/payments/
     res/payments                                      (res/paymentRequest.*)
     res/payments/components/                          (res/components/*.css)
     res/payments/components/                          (res/components/*.js)
     res/payments/components/                          (res/components/*.svg)
     res/payments/containers/                          (res/containers/*.js)
     res/payments/containers/                          (res/containers/*.css)
--- a/browser/components/payments/paymentUIService.js
+++ b/browser/components/payments/paymentUIService.js
@@ -12,16 +12,17 @@
  * For now the UI is shown in a native dialog but that is likely to change.
  * Tests should try to avoid relying on that implementation detail.
  */
 
 "use strict";
 
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
+ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
                                "resource:///modules/BrowserWindowTracker.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "paymentSrv",
                                    "@mozilla.org/dom/payments/payment-request-service;1",
@@ -37,59 +38,61 @@ function PaymentUIService() {
     });
   });
   this.log.debug("constructor");
 }
 
 PaymentUIService.prototype = {
   classID: Components.ID("{01f8bd55-9017-438b-85ec-7c15d2b35cdc}"),
   QueryInterface: ChromeUtils.generateQI([Ci.nsIPaymentUIService]),
-  DIALOG_URL: "chrome://payments/content/paymentDialogWrapper.xul",
-  REQUEST_ID_PREFIX: "paymentRequest-",
 
   // nsIPaymentUIService implementation:
 
   showPayment(requestId) {
     this.log.debug("showPayment:", requestId);
     let request = paymentSrv.getPaymentRequestById(requestId);
     let merchantBrowser = this.findBrowserByOuterWindowId(request.topOuterWindowId);
     let chromeWindow = merchantBrowser.ownerGlobal;
     let {gBrowser} = chromeWindow;
     let browserContainer = gBrowser.getBrowserContainer(merchantBrowser);
     let container = chromeWindow.document.createElementNS(XHTML_NS, "div");
     container.dataset.requestId = requestId;
     container.classList.add("paymentDialogContainer");
     container.hidden = true;
-    let paymentsBrowser = chromeWindow.document.createElementNS(XHTML_NS, "iframe");
-    paymentsBrowser.classList.add("paymentDialogContainerFrame");
-    paymentsBrowser.setAttribute("type", "content");
-    paymentsBrowser.setAttribute("remote", "true");
-    paymentsBrowser.setAttribute("src", `${this.DIALOG_URL}?requestId=${requestId}`);
+    let paymentsBrowser = this._createPaymentFrame(chromeWindow.document, requestId);
+
+    let pdwGlobal = {};
+    Services.scriptloader.loadSubScript("chrome://payments/content/paymentDialogWrapper.js",
+                                        pdwGlobal);
+
+    paymentsBrowser.paymentDialogWrapper = pdwGlobal.paymentDialogWrapper;
+
+    // Create an <html:div> wrapper to absolutely position the <xul:browser>
+    // because XUL elements don't support position:absolute.
+    let absDiv = chromeWindow.document.createElementNS(XHTML_NS, "div");
+    container.appendChild(absDiv);
+
     // append the frame to start the loading
-    container.appendChild(paymentsBrowser);
+    absDiv.appendChild(paymentsBrowser);
     browserContainer.prepend(container);
 
+    // Initialize the wrapper once the <browser> is connected.
+    paymentsBrowser.paymentDialogWrapper.init(requestId, paymentsBrowser);
+
+    this._attachBrowserEventListeners(merchantBrowser);
+
     // Only show the frame and change the UI when the dialog is ready to show.
     paymentsBrowser.addEventListener("tabmodaldialogready", function readyToShow() {
       if (!container) {
         // The dialog was closed by the DOM code before it was ready to be shown.
         return;
       }
       container.hidden = false;
-
-      // Prevent focusing or interacting with the <browser>.
-      merchantBrowser.setAttribute("tabmodalPromptShowing", "true");
-
-      // Darken the merchant content area.
-      let tabModalBackground = chromeWindow.document.createXULElement("box");
-      tabModalBackground.classList.add("tabModalBackground", "paymentDialogBackground");
-      // Insert the same way as <tabmodalprompt>.
-      merchantBrowser.parentNode.insertBefore(tabModalBackground,
-                                              merchantBrowser.nextElementSibling);
-    }, {
+      this._showDialog(merchantBrowser);
+    }.bind(this), {
       once: true,
     });
   },
 
   abortPayment(requestId) {
     this.log.debug("abortPayment:", requestId);
     let abortResponse = Cc["@mozilla.org/dom/payments/payment-abort-action-response;1"]
                           .createInstance(Ci.nsIPaymentAbortActionResponse);
@@ -115,89 +118,129 @@ PaymentUIService.prototype = {
       case "fail":
       case "timeout":
         break;
       default:
         closed = this.closeDialog(requestId);
         break;
     }
 
-    let dialogContainer;
+    let paymentFrame;
     if (!closed) {
       // We need to call findDialog before we respond below as getPaymentRequestById
       // may fail due to the request being removed upon completion.
-      dialogContainer = this.findDialog(requestId).dialogContainer;
-      if (!dialogContainer) {
+      paymentFrame = this.findDialog(requestId).paymentFrame;
+      if (!paymentFrame) {
         this.log.error("completePayment: no dialog found");
         return;
       }
     }
 
     let responseCode = closed ?
         Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED :
         Ci.nsIPaymentActionResponse.COMPLETE_FAILED;
     let completeResponse = Cc["@mozilla.org/dom/payments/payment-complete-action-response;1"]
                              .createInstance(Ci.nsIPaymentCompleteActionResponse);
     completeResponse.init(requestId, responseCode);
     paymentSrv.respondPayment(completeResponse.QueryInterface(Ci.nsIPaymentActionResponse));
 
     if (!closed) {
-      dialogContainer.querySelector("iframe").contentWindow.paymentDialogWrapper.updateRequest();
+      paymentFrame.paymentDialogWrapper.updateRequest();
     }
   },
 
   updatePayment(requestId) {
-    let {dialogContainer} = this.findDialog(requestId);
+    let {paymentFrame} = this.findDialog(requestId);
     this.log.debug("updatePayment:", requestId);
-    if (!dialogContainer) {
+    if (!paymentFrame) {
       this.log.error("updatePayment: no dialog found");
       return;
     }
-    dialogContainer.querySelector("iframe").contentWindow.paymentDialogWrapper.updateRequest();
+    paymentFrame.paymentDialogWrapper.updateRequest();
   },
 
   closePayment(requestId) {
     this.closeDialog(requestId);
   },
 
   // other helper methods
 
+  _createPaymentFrame(doc, requestId) {
+    let frame = doc.createXULElement("browser");
+    frame.classList.add("paymentDialogContainerFrame");
+    frame.setAttribute("type", "content");
+    frame.setAttribute("remote", "true");
+    frame.setAttribute("disablehistory", "true");
+    frame.setAttribute("nodefaultsrc", "true");
+    frame.setAttribute("transparent", "true");
+    frame.setAttribute("selectmenulist", "ContentSelectDropdown");
+    frame.setAttribute("autocompletepopup", "PopupAutoComplete");
+    return frame;
+  },
+
+  _attachBrowserEventListeners(merchantBrowser) {
+    merchantBrowser.addEventListener("SwapDocShells", this);
+  },
+
+  _showDialog(merchantBrowser) {
+    let chromeWindow = merchantBrowser.ownerGlobal;
+    // Prevent focusing or interacting with the <browser>.
+    merchantBrowser.setAttribute("tabmodalPromptShowing", "true");
+
+    // Darken the merchant content area.
+    let tabModalBackground = chromeWindow.document.createXULElement("box");
+    tabModalBackground.classList.add("tabModalBackground", "paymentDialogBackground");
+    // Insert the same way as <tabmodalprompt>.
+    merchantBrowser.parentNode.insertBefore(tabModalBackground,
+                                            merchantBrowser.nextElementSibling);
+  },
+
   /**
    * @param {string} requestId - Payment Request ID of the dialog to close.
    * @returns {boolean} whether the specified dialog was closed.
    */
   closeDialog(requestId) {
     let {
       browser,
       dialogContainer,
+      paymentFrame,
     } = this.findDialog(requestId);
     if (!dialogContainer) {
       return false;
     }
     this.log.debug(`closing: ${requestId}`);
+    paymentFrame.paymentDialogWrapper.uninit();
     dialogContainer.remove();
+    browser.removeEventListener("SwapDocShells", this);
+
     if (!dialogContainer.hidden) {
       // If the container is no longer hidden then the background was added after
       // `tabmodaldialogready` so remove it.
       browser.parentElement.querySelector(".paymentDialogBackground").remove();
 
       if (!browser.tabModalPromptBox || browser.tabModalPromptBox.listPrompts().length == 0) {
         browser.removeAttribute("tabmodalPromptShowing");
       }
     }
     return true;
   },
 
+  getDialogContainerForMerchantBrowser(merchantBrowser) {
+    return merchantBrowser.ownerGlobal.gBrowser.getBrowserContainer(merchantBrowser)
+                          .querySelector(".paymentDialogContainer");
+  },
+
   findDialog(requestId) {
     for (let win of BrowserWindowTracker.orderedWindows) {
       for (let dialogContainer of win.document.querySelectorAll(".paymentDialogContainer")) {
         if (dialogContainer.dataset.requestId == requestId) {
           return {
             dialogContainer,
-            browser: dialogContainer.parentElement.querySelector("browser"),
+            paymentFrame: dialogContainer.querySelector(".paymentDialogContainerFrame"),
+            browser: dialogContainer.parentElement.querySelector(".browserStack > browser"),
           };
         }
       }
     }
     return {};
   },
 
   findBrowserByOuterWindowId(outerWindowId) {
@@ -208,11 +251,54 @@ PaymentUIService.prototype = {
       }
       return browser;
     }
 
     this.log.error("findBrowserByOuterWindowId: No browser found for outerWindowId:",
                    outerWindowId);
     return null;
   },
+
+  _moveDialogToNewBrowser(oldBrowser, newBrowser) {
+    // Re-attach event listeners to the new browser.
+    newBrowser.addEventListener("SwapDocShells", this);
+
+    let dialogContainer = this.getDialogContainerForMerchantBrowser(oldBrowser);
+    let newBrowserContainer = newBrowser.ownerGlobal.gBrowser.getBrowserContainer(newBrowser);
+
+    // Clone the container tree
+    let newDialogContainer = newBrowserContainer.ownerDocument.importNode(dialogContainer, true);
+
+    let oldFrame = dialogContainer.querySelector(".paymentDialogContainerFrame");
+    let newFrame = newDialogContainer.querySelector(".paymentDialogContainerFrame");
+
+    // We need a document to be synchronously loaded in order to do the swap and
+    // there's no point in wasting resources loading a dialog we're going to replace.
+    newFrame.setAttribute("src", "about:blank");
+    newFrame.setAttribute("nodefaultsrc", "true");
+
+    newBrowserContainer.prepend(newDialogContainer);
+
+    // Force the <browser> to be created so that it'll have a document loaded and frame created.
+    // See `ourChildDocument` and `ourFrame` checks in nsFrameLoader::SwapWithOtherLoader.
+    /* eslint-disable-next-line no-unused-expressions */
+    newFrame.clientTop;
+
+    // Swap the frameLoaders to preserve the frame state
+    newFrame.swapFrameLoaders(oldFrame);
+    newFrame.paymentDialogWrapper = oldFrame.paymentDialogWrapper;
+    newFrame.paymentDialogWrapper.changeAttachedFrame(newFrame);
+    dialogContainer.remove();
+
+    this._showDialog(newBrowser);
+  },
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "SwapDocShells": {
+        this._moveDialogToNewBrowser(event.target, event.detail);
+        break;
+      }
+    }
+  },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PaymentUIService]);
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -43,29 +43,33 @@ export default class AddressForm extends
     this.persistCheckbox = new LabelledCheckbox();
     this.persistCheckbox.className = "persist-checkbox";
 
     // Combination of AddressErrors and PayerErrors as keys
     this._errorFieldMap = {
       addressLine: "#street-address",
       city: "#address-level2",
       country: "#country",
+      dependentLocality: "#address-level3",
       email: "#email",
       // Bug 1472283 is on file to support
       // additional-name and family-name.
       // XXX: For now payer name errors go on the family-name and address-errors
       //      go on the given-name so they don't overwrite each other.
       name: "#family-name",
       organization: "#organization",
       phone: "#tel",
       postalCode: "#postal-code",
       // Bug 1472283 is on file to support
       // additional-name and family-name.
       recipient: "#given-name",
       region: "#address-level1",
+      // Bug 1474905 is on file to properly support regionCode. See
+      // full note in paymentDialogWrapper.js
+      regionCode: "#address-level1",
     };
 
     // The markup is shared with form autofill preferences.
     let url = "formautofill/editAddress.xhtml";
     this.promiseReady = this._fetchMarkup(url).then(doc => {
       this.form = doc.getElementById("form");
       return this.form;
     });
@@ -188,20 +192,27 @@ export default class AddressForm extends
 
     // Add validation to some address fields
     this.updateRequiredState();
 
     // Show merchant errors for the appropriate address form.
     let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state,
                                                                      addressPage.selectedStateKey);
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
+      let errorText = "";
+      // Never show errors on an 'add' screen as they would be for a different address.
+      if (editing && merchantFieldErrors) {
+        if (errorName == "region" || errorName == "regionCode") {
+          errorText = merchantFieldErrors.regionCode || merchantFieldErrors.region || "";
+        } else {
+          errorText = merchantFieldErrors[errorName] || "";
+        }
+      }
       let container = this.form.querySelector(errorSelector + "-container");
       let field = this.form.querySelector(errorSelector);
-      // Never show errors on an 'add' screen as they would be for a different address.
-      let errorText = (editing && merchantFieldErrors && merchantFieldErrors[errorName]) || "";
       field.setCustomValidity(errorText);
       let span = paymentRequest.maybeCreateFieldErrorElement(container);
       span.textContent = errorText;
     }
 
     this.updateSaveButtonState();
   }
 
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -254,16 +254,31 @@ let ADDRESSES_1 = {
     "given-name": "Kristin",
     "guid": "missing-country",
     "name": "Kristin Bogard",
     "postal-code": "H0H 0H0",
     "street-address": "123 Yonge Street\nSuite 2300",
     "tel": "+1 416 555-5555",
     timeLastUsed: 90000,
   },
+  TimBR: {
+    "given-name": "Timothy",
+    "additional-name": "João",
+    "family-name": "Berners-Lee",
+    organization: "World Wide Web Consortium",
+    "street-address": "Rua Adalberto Pajuaba, 404",
+    "address-level3": "Campos Elísios",
+    "address-level2": "Ribeirão Preto",
+    "address-level1": "SP",
+    "postal-code": "14055-220",
+    country: "BR",
+    tel: "+0318522222222",
+    email: "timbr@example.org",
+    timeLastUsed: 110000,
+  },
 };
 
 let DUPED_ADDRESSES = {
   "a9e830667189": {
     "street-address": "Unit 1\n1505 Northeast Kentucky Industrial Parkway \n",
     "address-level2": "Greenup",
     "address-level1": "KY",
     "postal-code": "41144",
@@ -477,21 +492,23 @@ let buttonActions = {
       cardholderName: "",
       cardSecurityCode: "",
       expiryMonth: "",
       expiryYear: "",
       billingAddress: {
         addressLine: "Can only buy from ROADS, not DRIVES, BOULEVARDS, or STREETS",
         city: "Can only buy from CITIES, not TOWNSHIPS or VILLAGES",
         country: "Can only buy from US, not CA",
+        dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
         organization: "Can only buy from CORPORATIONS, not CONSORTIUMS",
         phone: "Only allowed to buy from area codes that start with 9",
         postalCode: "Only allowed to buy from postalCodes that start with 0",
         recipient: "Can only buy from names that start with J",
         region: "Can only buy from regions that start with M",
+        regionCode: "Regions must be 1 to 3 characters in length",
       },
     };
     requestStore.setState({
       request,
     });
   },
 
 
@@ -568,21 +585,23 @@ let buttonActions = {
 
   setShippingAddressErrors() {
     let request = Object.assign({}, requestStore.getState().request);
     request.paymentDetails = Object.assign({}, requestStore.getState().request.paymentDetails);
     request.paymentDetails.shippingAddressErrors = {
       addressLine: "Can only ship to ROADS, not DRIVES, BOULEVARDS, or STREETS",
       city: "Can only ship to CITIES, not TOWNSHIPS or VILLAGES",
       country: "Can only ship to USA, not CA",
+      dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
       organization: "Can only ship to CORPORATIONS, not CONSORTIUMS",
       phone: "Only allowed to ship to area codes that start with 9",
       postalCode: "Only allowed to ship to postalCodes that start with 0",
       recipient: "Can only ship to names that start with J",
       region: "Can only ship to regions that start with M",
+      regionCode: "Regions must be 1 to 3 characters in length",
     };
     requestStore.setState({
       request,
     });
   },
 
   toggleDirectionality() {
     let body = paymentDialog.ownerDocument.body;
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -120,29 +120,33 @@ var paymentRequest = {
     // Handle getting called before the DOM is ready.
     log.debug("onShowPaymentRequest:", detail);
     await this.domReadyPromise;
 
     log.debug("onShowPaymentRequest: domReadyPromise resolved");
     log.debug("onShowPaymentRequest, isPrivate?", detail.isPrivate);
 
     let paymentDialog = document.querySelector("payment-dialog");
-    let hasSavedAddresses = Object.keys(detail.savedAddresses).length != 0;
-    let hasSavedCards = Object.keys(detail.savedBasicCards).length != 0;
-    let shippingRequested = detail.request.paymentOptions.requestShipping;
     let state = {
       request: detail.request,
       savedAddresses: detail.savedAddresses,
       savedBasicCards: detail.savedBasicCards,
+      // Temp records can exist upon a reload during development.
+      tempAddresses: detail.tempAddresses,
+      tempBasicCards: detail.tempBasicCards,
       isPrivate: detail.isPrivate,
       page: {
         id: "payment-summary",
       },
     };
 
+    let hasSavedAddresses = Object.keys(this.getAddresses(state)).length != 0;
+    let hasSavedCards = Object.keys(this.getBasicCards(state)).length != 0;
+    let shippingRequested = state.request.paymentOptions.requestShipping;
+
     // Onboarding wizard flow.
     if (!hasSavedAddresses && (shippingRequested || !hasSavedCards)) {
       state.page = {
         id: "address-page",
         onboardingWizard: true,
       };
 
       state["address-page"] = {
--- a/browser/components/payments/res/unprivileged-fallbacks.js
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -74,29 +74,34 @@ var PaymentDialogUtils = {
           {fieldId: "postal-code"},
           {fieldId: "address-level2"},
         ],
         postalCodePattern: "\\d{5}",
         countryRequiredFields: ["street-address", "address-level2", "postal-code"],
       };
     }
 
+    let fieldsOrder = [
+      {fieldId: "name", newLine: true},
+      {fieldId: "street-address", newLine: true},
+      {fieldId: "address-level2"},
+      {fieldId: "address-level1"},
+      {fieldId: "postal-code"},
+      {fieldId: "organization"},
+    ];
+    if (country == "BR") {
+      fieldsOrder.splice(2, 0, {fieldId: "address-level3"});
+    }
+
     return {
       addressLevel3Label: "suburb",
       addressLevel2Label: "city",
       addressLevel1Label: country == "US" ? "state" : "province",
       postalCodeLabel: country == "US" ? "zip" : "postalCode",
-      fieldsOrder: [
-        {fieldId: "name", newLine: true},
-        {fieldId: "street-address", newLine: true},
-        {fieldId: "address-level2"},
-        {fieldId: "address-level1"},
-        {fieldId: "postal-code"},
-        {fieldId: "organization"},
-      ],
+      fieldsOrder,
       // The following values come from addressReferences.js and should not be changed.
       /* eslint-disable-next-line max-len */
       postalCodePattern: country == "US" ? "(\\d{5})(?:[ \\-](\\d{4}))?" : "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d",
       countryRequiredFields: country == "US" || country == "CA" ?
         ["street-address", "address-level2", "address-level1", "postal-code"] :
         ["street-address", "address-level2", "postal-code"],
     };
   },
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -462,21 +462,23 @@ var PaymentTestUtils = {
       error: "Cannot ship with option 1 on days that end with Y",
     },
     fieldSpecificErrors: {
       error: "There are errors related to specific parts of the address",
       shippingAddressErrors: {
         addressLine: "Can only ship to ROADS, not DRIVES, BOULEVARDS, or STREETS",
         city: "Can only ship to CITIES, not TOWNSHIPS or VILLAGES",
         country: "Can only ship to USA, not CA",
+        dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
         organization: "Can only ship to CORPORATIONS, not CONSORTIUMS",
         phone: "Only allowed to ship to area codes that start with 9",
         postalCode: "Only allowed to ship to postalCodes that start with 0",
         recipient: "Can only ship to names that start with J",
         region: "Can only ship to regions that start with M",
+        regionCode: "Regions must be 1 to 3 characters in length (sometimes ;) )",
       },
     },
   },
 
   Options: {
     requestShippingOption: {
       requestShipping: true,
     },
@@ -487,16 +489,30 @@ var PaymentTestUtils = {
     requestPayerNameEmailAndPhone: {
       requestPayerName: true,
       requestPayerEmail: true,
       requestPayerPhone: true,
     },
   },
 
   Addresses: {
+    TimBR: {
+      "given-name": "Timothy",
+      "additional-name": "João",
+      "family-name": "Berners-Lee",
+      organization: "World Wide Web Consortium",
+      "street-address": "Rua Adalberto Pajuaba, 404",
+      "address-level3": "Campos Elísios",
+      "address-level2": "Ribeirão Preto",
+      "address-level1": "SP",
+      "postal-code": "14055-220",
+      country: "BR",
+      tel: "+0318522222222",
+      email: "timbr@example.org",
+    },
     TimBL: {
       "given-name": "Timothy",
       "additional-name": "John",
       "family-name": "Berners-Lee",
       organization: "World Wide Web Consortium",
       "street-address": "32 Vassar Street\nMIT Room 32-G524",
       "address-level2": "Cambridge",
       "address-level1": "MA",
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -21,9 +21,10 @@ skip-if = debug && (os == 'mac' || os ==
 [browser_payment_completion.js]
 [browser_profile_storage.js]
 [browser_request_serialization.js]
 [browser_request_shipping.js]
 [browser_retry.js]
 [browser_shippingaddresschange_error.js]
 [browser_show_dialog.js]
 skip-if = os == 'win' && debug # bug 1418385
+[browser_tab_modal.js]
 [browser_total.js]
--- a/browser/components/payments/test/browser/browser_dropdowns.js
+++ b/browser/components/payments/test/browser/browser_dropdowns.js
@@ -35,19 +35,16 @@ add_task(async function test_dropdown() 
       content.document.querySelector("#country").scrollIntoView();
     });
 
     info("going to open the country <select>");
     await BrowserTestUtils.synthesizeMouseAtCenter("#country", {}, frame);
 
     let event = await popupshownPromise;
     let expectedPopupID = "ContentSelectDropdown";
-    if (AppConstants.platform == "win") {
-      expectedPopupID = "ContentSelectDropdown-windows";
-    }
     is(event.target.parentElement.id, expectedPopupID, "Checked menulist of opened popup");
 
     event.target.hidePopup(true);
 
     info("clicking cancel");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
--- a/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
+++ b/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
@@ -121,21 +121,24 @@ add_task(async function test_show_field_
 
       await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" && state["address-page"].guid;
       }, "Check edit page state");
 
       // check errors and make corrections
       let {shippingAddressErrors} = PTU.Details.fieldSpecificErrors;
       is(content.document.querySelectorAll("address-form .error-text:not(:empty)").length,
-         Object.keys(shippingAddressErrors).length,
-         "Each error should be presented");
+         Object.keys(shippingAddressErrors).length - 1,
+         "Each error should be presented, but only one of region and regionCode are displayed");
       let errorFieldMap =
         Cu.waiveXrays(content.document.querySelector("address-form"))._errorFieldMap;
       for (let [errorName, errorValue] of Object.entries(shippingAddressErrors)) {
+        if (errorName == "region" || errorName == "regionCode") {
+          errorValue = shippingAddressErrors.regionCode;
+        }
         let fieldSelector = errorFieldMap[errorName];
         let containerSelector = fieldSelector + "-container";
         let container = content.document.querySelector(containerSelector);
         try {
           is(container.querySelector(".error-text").textContent, errorValue,
              "Field specific error should be associated with " + errorName);
         } catch (ex) {
           ok(false, `no container for ${errorName}. selector= ${containerSelector}`);
--- a/browser/components/payments/test/browser/browser_show_dialog.js
+++ b/browser/components/payments/test/browser/browser_show_dialog.js
@@ -255,80 +255,8 @@ add_task(async function test_supportedNe
     await spawnPaymentDialogTask(frame, async () => {
       ok(!content.document.getElementById("pay").disabled, "pay button should not be disabled");
     });
 
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
-
-add_task(async function test_tab_modal() {
-  await BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: BLANK_PAGE_URL,
-  }, async browser => {
-    let {win, frame} = await setupPaymentDialog(browser, {
-      methodData,
-      details,
-      merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
-    });
-
-    await TestUtils.waitForCondition(() => {
-      return !document.querySelector(".paymentDialogContainer").hidden;
-    }, "Waiting for container to be visible after the dialog's ready");
-
-    ok(!EventUtils.isHidden(win.frameElement), "Frame should be visible");
-
-    let {
-      bottom: toolboxBottom,
-    } = document.getElementById("navigator-toolbox").getBoundingClientRect();
-
-    let {x, y} = win.frameElement.getBoundingClientRect();
-    ok(y > 0, "Frame should have y > 0");
-    // Inset by 10px since the corner point doesn't return the frame due to the
-    // border-radius.
-    is(document.elementFromPoint(x + 10, y + 10), win.frameElement,
-       "Check .paymentDialogContainerFrame is visible");
-
-    info("Click to the left of the dialog over the content area");
-    isnot(document.elementFromPoint(x - 10, y + 50), browser,
-          "Check clicks on the merchant content area don't go to the browser");
-    is(document.elementFromPoint(x - 10, y + 50),
-       document.querySelector(".paymentDialogBackground"),
-       "Check clicks on the merchant content area go to the payment dialog background");
-
-    ok(y < toolboxBottom - 2, "Dialog should overlap the toolbox by at least 2px");
-
-    ok(browser.hasAttribute("tabmodalPromptShowing"), "Check browser has @tabmodalPromptShowing");
-
-    await BrowserTestUtils.withNewTab({
-      gBrowser,
-      url: BLANK_PAGE_URL,
-    }, async newBrowser => {
-      let {
-        x: x2,
-        y: y2,
-      } = win.frameElement.getBoundingClientRect();
-      is(x2, x, "Check x-coordinate is the same");
-      is(y2, y, "Check y-coordinate is the same");
-      isnot(document.elementFromPoint(x + 10, y + 10), win.frameElement,
-            "Check .paymentDialogContainerFrame is hidden");
-      ok(!newBrowser.hasAttribute("tabmodalPromptShowing"),
-         "Check second browser doesn't have @tabmodalPromptShowing");
-    });
-
-    let {
-      x: x3,
-      y: y3,
-    } = win.frameElement.getBoundingClientRect();
-    is(x3, x, "Check x-coordinate is the same again");
-    is(y3, y, "Check y-coordinate is the same again");
-    is(document.elementFromPoint(x + 10, y + 10), win.frameElement,
-       "Check .paymentDialogContainerFrame is visible again");
-
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
-    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
-
-    await BrowserTestUtils.waitForCondition(() => !browser.hasAttribute("tabmodalPromptShowing"),
-                                            "Check @tabmodalPromptShowing was removed");
-  });
-});
copy from browser/components/payments/test/browser/browser_show_dialog.js
copy to browser/components/payments/test/browser/browser_tab_modal.js
--- a/browser/components/payments/test/browser/browser_show_dialog.js
+++ b/browser/components/payments/test/browser/browser_tab_modal.js
@@ -1,309 +1,63 @@
 "use strict";
 
 const methodData = [PTU.MethodData.basicCard];
 const details = Object.assign({}, PTU.Details.twoShippingOptions, PTU.Details.total2USD);
 
-add_task(async function test_show_abort_dialog() {
-  await BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: BLANK_PAGE_URL,
-  }, async browser => {
-    let {win} =
-      await setupPaymentDialog(browser, {
-        methodData,
-        details,
-        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
-      }
-    );
-
-    // abort the payment request
-    ContentTask.spawn(browser, null, async () => content.rq.abort());
-    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
-  });
-});
-
-add_task(async function test_show_manualAbort_dialog() {
-  await BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: BLANK_PAGE_URL,
-  }, async browser => {
-    let {win, frame} =
-      await setupPaymentDialog(browser, {
-        methodData,
-        details,
-        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
-      }
-    );
-
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
-    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
-  });
-});
-
-add_task(async function test_show_completePayment() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(false, "Cannot test OS key store login on official builds.");
-    return;
-  }
-  let {address1GUID, card1GUID} = await addSampleAddressesAndBasicCard();
-
-  let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
-                                          (subject, data) => data == "update");
-  info("associating the card with the billing address");
-  await formAutofillStorage.creditCards.update(card1GUID, {
-    billingAddressGUID: address1GUID,
-  }, true);
-  await onChanged;
+async function checkTabModal(browser, win, msg) {
+  info(`checkTabModal: ${msg}`);
+  let doc = browser.ownerDocument;
+  await TestUtils.waitForCondition(() => {
+    return !doc.querySelector(".paymentDialogContainer").hidden;
+  }, "Waiting for container to be visible after the dialog's ready");
+  is(doc.querySelectorAll(".paymentDialogContainer").length, 1,
+     "Only 1 paymentDialogContainer");
+  ok(!EventUtils.isHidden(win.frameElement), "Frame should be visible");
 
-  await BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: BLANK_PAGE_URL,
-  }, async browser => {
-    let {win, frame} =
-      await setupPaymentDialog(browser, {
-        methodData,
-        details,
-        options: PTU.Options.requestShippingOption,
-        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
-      }
-    );
-
-    info("select the shipping address");
-    await selectPaymentDialogShippingAddressByCountry(frame, "US");
-
-    info("entering CSC");
-    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
-      securityCode: "999",
-    });
-    info("clicking pay");
-    await loginAndCompletePayment(frame);
-
-    // Add a handler to complete the payment above.
-    info("acknowledging the completion from the merchant page");
-    let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
-
-    let {shippingAddress} = result.response;
-    checkPaymentAddressMatchesStorageAddress(shippingAddress, PTU.Addresses.TimBL, "Shipping");
-
-    is(result.response.methodName, "basic-card", "Check methodName");
-    let {methodDetails} = result;
-    checkPaymentMethodDetailsMatchesCard(methodDetails, PTU.BasicCards.JohnDoe, "Payment method");
-    is(methodDetails.cardSecurityCode, "999", "Check cardSecurityCode");
-    is(typeof methodDetails.methodName, "undefined", "Check methodName wasn't included");
-
-    checkPaymentAddressMatchesStorageAddress(methodDetails.billingAddress, PTU.Addresses.TimBL,
-                                             "Billing address");
-
-    is(result.response.shippingOption, "2", "Check shipping option");
-
-    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
-  });
-});
-
-add_task(async function test_show_completePayment2() {
-  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
-    todo(false, "Cannot test OS key store login on official builds.");
-    return;
-  }
-
-  await BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: BLANK_PAGE_URL,
-  }, async browser => {
-    let {win, frame} =
-      await setupPaymentDialog(browser, {
-        methodData,
-        details,
-        options: PTU.Options.requestShippingOption,
-        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
-      }
-    );
-
-    await ContentTask.spawn(browser, {
-      eventName: "shippingoptionchange",
-    }, PTU.ContentTasks.promisePaymentRequestEvent);
-
-    info("changing shipping option to '1' from default selected option of '2'");
-    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.selectShippingOptionById, "1");
-
-    await ContentTask.spawn(browser, {
-      eventName: "shippingoptionchange",
-    }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
-    info("got shippingoptionchange event");
+  let {
+    bottom: toolboxBottom,
+  } = doc.getElementById("navigator-toolbox").getBoundingClientRect();
 
-    info("select the shipping address");
-    await selectPaymentDialogShippingAddressByCountry(frame, "US");
-
-    info("entering CSC");
-    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
-      securityCode: "123",
-    });
-
-    info("clicking pay");
-    await loginAndCompletePayment(frame);
-
-    // Add a handler to complete the payment above.
-    info("acknowledging the completion from the merchant page");
-    let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
-
-    is(result.response.shippingOption, "1", "Check shipping option");
-
-    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
-  });
-});
-
-add_task(async function test_localized() {
-  await BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: BLANK_PAGE_URL,
-  }, async browser => {
-    let {win, frame} =
-      await setupPaymentDialog(browser, {
-        methodData,
-        details,
-        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
-      }
-    );
-
-    await spawnPaymentDialogTask(frame, async function check_l10n() {
-      await ContentTaskUtils.waitForCondition(() => {
-        let telephoneLabel = content.document.querySelector("#tel-container > .label-text");
-        return telephoneLabel && telephoneLabel.textContent.includes("Phone");
-      }, "Check that the telephone number label is localized");
-
-      await ContentTaskUtils.waitForCondition(() => {
-        let ccNumberField = content.document.querySelector("#cc-number");
-        if (!ccNumberField) {
-          return false;
-        }
-        let ccNumberLabel = ccNumberField.parentElement.querySelector(".label-text");
-        return ccNumberLabel.textContent.includes("Number");
-      }, "Check that the cc-number label is localized");
-
-      const L10N_ATTRIBUTE_SELECTOR = "[data-localization], [data-localization-region]";
-      await ContentTaskUtils.waitForCondition(() => {
-        return content.document.querySelectorAll(L10N_ATTRIBUTE_SELECTOR).length === 0;
-      }, "Check that there are no unlocalized strings");
-    });
-
-    // abort the payment request
-    ContentTask.spawn(browser, null, async () => content.rq.abort());
-    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
-  });
-});
-
-add_task(async function test_supportedNetworks() {
-  await setupFormAutofillStorage();
-  await cleanupFormAutofillStorage();
+  let {x, y} = win.frameElement.getBoundingClientRect();
+  ok(y > 0, "Frame should have y > 0");
+  // Inset by 10px since the corner point doesn't return the frame due to the
+  // border-radius.
+  is(doc.elementFromPoint(x + 10, y + 10), win.frameElement,
+     "Check .paymentDialogContainerFrame is visible");
 
-  let address1GUID = await addAddressRecord(PTU.Addresses.TimBL);
-  let visaCardGUID = await addCardRecord(Object.assign({}, PTU.BasicCards.JohnDoe, {
-    billingAddressGUID: address1GUID,
-  }));
-  let masterCardGUID = await addCardRecord(Object.assign({}, PTU.BasicCards.JaneMasterCard, {
-    billingAddressGUID: address1GUID,
-  }));
-
-  let cardMethod = {
-    supportedMethods: "basic-card",
-    data: {
-      supportedNetworks: ["visa"],
-    },
-  };
-
-  await BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: BLANK_PAGE_URL,
-  }, async browser => {
-    let {win, frame} =
-      await setupPaymentDialog(browser, {
-        methodData: [cardMethod],
-        details,
-        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
-      }
-    );
-
-    info("entering CSC");
-    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
-      securityCode: "789",
-    });
+  info("Click to the left of the dialog over the content area");
+  isnot(doc.elementFromPoint(x - 10, y + 50), browser,
+        "Check clicks on the merchant content area don't go to the browser");
+  is(doc.elementFromPoint(x - 10, y + 50),
+     doc.querySelector(".paymentDialogBackground"),
+     "Check clicks on the merchant content area go to the payment dialog background");
 
-    await spawnPaymentDialogTask(frame, () => {
-      let acceptedCards = content.document.querySelector("accepted-cards");
-      ok(acceptedCards && !content.isHidden(acceptedCards),
-         "accepted-cards element is present and visible");
-      is(Cu.waiveXrays(acceptedCards).acceptedItems.length, 1,
-         "accepted-cards element has 1 item");
-    });
+  ok(y < toolboxBottom - 2, "Dialog should overlap the toolbox by at least 2px");
 
-    info("select the mastercard using guid: " + masterCardGUID);
-    await spawnPaymentDialogTask(frame,
-                                 PTU.DialogContentTasks.selectPaymentOptionByGuid,
-                                 masterCardGUID);
+  ok(browser.hasAttribute("tabmodalPromptShowing"), "Check browser has @tabmodalPromptShowing");
 
-    info("spawn task to check pay button with mastercard selected");
-    await spawnPaymentDialogTask(frame, async () => {
-      ok(content.document.getElementById("pay").disabled, "pay button should be disabled");
-    });
-
-    info("select the visa using guid: " + visaCardGUID);
-    await spawnPaymentDialogTask(frame,
-                                 PTU.DialogContentTasks.selectPaymentOptionByGuid,
-                                 visaCardGUID);
-
-    info("spawn task to check pay button");
-    await spawnPaymentDialogTask(frame, async () => {
-      ok(!content.document.getElementById("pay").disabled, "pay button should not be disabled");
-    });
-
-    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
-    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
-  });
-});
+  return {
+    x,
+    y,
+  };
+}
 
 add_task(async function test_tab_modal() {
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} = await setupPaymentDialog(browser, {
       methodData,
       details,
       merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
     });
 
-    await TestUtils.waitForCondition(() => {
-      return !document.querySelector(".paymentDialogContainer").hidden;
-    }, "Waiting for container to be visible after the dialog's ready");
-
-    ok(!EventUtils.isHidden(win.frameElement), "Frame should be visible");
-
-    let {
-      bottom: toolboxBottom,
-    } = document.getElementById("navigator-toolbox").getBoundingClientRect();
-
-    let {x, y} = win.frameElement.getBoundingClientRect();
-    ok(y > 0, "Frame should have y > 0");
-    // Inset by 10px since the corner point doesn't return the frame due to the
-    // border-radius.
-    is(document.elementFromPoint(x + 10, y + 10), win.frameElement,
-       "Check .paymentDialogContainerFrame is visible");
-
-    info("Click to the left of the dialog over the content area");
-    isnot(document.elementFromPoint(x - 10, y + 50), browser,
-          "Check clicks on the merchant content area don't go to the browser");
-    is(document.elementFromPoint(x - 10, y + 50),
-       document.querySelector(".paymentDialogBackground"),
-       "Check clicks on the merchant content area go to the payment dialog background");
-
-    ok(y < toolboxBottom - 2, "Dialog should overlap the toolbox by at least 2px");
-
-    ok(browser.hasAttribute("tabmodalPromptShowing"), "Check browser has @tabmodalPromptShowing");
+    let {x, y} = await checkTabModal(browser, win, "initial dialog");
 
     await BrowserTestUtils.withNewTab({
       gBrowser,
       url: BLANK_PAGE_URL,
     }, async newBrowser => {
       let {
         x: x2,
         y: y2,
@@ -314,21 +68,130 @@ add_task(async function test_tab_modal()
             "Check .paymentDialogContainerFrame is hidden");
       ok(!newBrowser.hasAttribute("tabmodalPromptShowing"),
          "Check second browser doesn't have @tabmodalPromptShowing");
     });
 
     let {
       x: x3,
       y: y3,
-    } = win.frameElement.getBoundingClientRect();
+    } = await checkTabModal(browser, win, "after tab switch back");
     is(x3, x, "Check x-coordinate is the same again");
     is(y3, y, "Check y-coordinate is the same again");
-    is(document.elementFromPoint(x + 10, y + 10), win.frameElement,
-       "Check .paymentDialogContainerFrame is visible again");
 
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
 
     await BrowserTestUtils.waitForCondition(() => !browser.hasAttribute("tabmodalPromptShowing"),
                                             "Check @tabmodalPromptShowing was removed");
   });
 });
+
+add_task(async function test_detachToNewWindow() {
+  let tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  });
+  let browser = tab.linkedBrowser;
+
+  let {
+    frame,
+    requestId,
+  } = await setupPaymentDialog(browser, {
+    methodData,
+    details,
+    merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+  });
+
+  is(Object.values(frame.paymentDialogWrapper.temporaryStore.addresses.getAll()).length, 0,
+     "Check initial temp. address store");
+  is(Object.values(frame.paymentDialogWrapper.temporaryStore.creditCards.getAll()).length, 0,
+     "Check initial temp. card store");
+
+  info("Create some temp. records so we can later check if they are preserved");
+  let address1 = {...PTU.Addresses.Temp};
+  let card1 = {...PTU.BasicCards.JaneMasterCard, ...{"cc-csc": "123"}};
+
+  await fillInBillingAddressForm(frame, address1, {
+    setPersistCheckedValue: false,
+  });
+
+  await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+  await spawnPaymentDialogTask(frame, async function waitForPageChange() {
+    let {
+      PaymentTestUtils: PTU,
+    } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+    await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "basic-card-page";
+    }, "Wait for basic-card-page");
+  });
+
+  await fillInCardForm(frame, card1, {
+    checkboxSelector: "basic-card-form .persist-checkbox",
+    setPersistCheckedValue: false,
+  });
+
+  await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.clickPrimaryButton);
+
+  let {temporaryStore} = frame.paymentDialogWrapper;
+  TestUtils.waitForCondition(() => {
+    return Object.values(temporaryStore.addresses.getAll()).length == 1;
+  }, "Check address store");
+  TestUtils.waitForCondition(() => {
+    return Object.values(temporaryStore.creditCards.getAll()).length == 1;
+  }, "Check card store");
+
+  let windowLoadedPromise = BrowserTestUtils.waitForNewWindow();
+  let newWin = gBrowser.replaceTabWithWindow(tab);
+  await windowLoadedPromise;
+
+  info("tab was detached");
+  let newBrowser = newWin.gBrowser.selectedBrowser;
+  ok(newBrowser, "Found new <browser>");
+
+  let widget = await TestUtils.waitForCondition(async () => getPaymentWidget(requestId));
+  await checkTabModal(newBrowser, widget, "after detach");
+
+  let state = await spawnPaymentDialogTask(widget.frameElement, async function checkAfterDetach() {
+    let {
+      PaymentTestUtils: PTU,
+    } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+    return PTU.DialogContentUtils.getCurrentState(content);
+  });
+
+  is(Object.values(state.tempAddresses).length, 1, "Check still 1 temp. address in state");
+  is(Object.values(state.tempBasicCards).length, 1, "Check still 1 temp. basic card in state");
+
+  temporaryStore = widget.frameElement.paymentDialogWrapper.temporaryStore;
+  is(Object.values(temporaryStore.addresses.getAll()).length, 1, "Check address store in wrapper");
+  is(Object.values(temporaryStore.creditCards.getAll()).length, 1, "Check card store in wrapper");
+
+  info("Check that the message manager and formautofill-storage-changed observer are connected");
+  is(Object.values(state.savedAddresses).length, 0, "Check 0 saved addresses");
+  await addAddressRecord(PTU.Addresses.TimBL2);
+  await spawnPaymentDialogTask(widget.frameElement, async function waitForSavedAddress() {
+    let {
+      PaymentTestUtils: PTU,
+    } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+    await PTU.DialogContentUtils.waitForState(content, function checkSavedAddresses(s) {
+      return Object.values(s.savedAddresses).length == 1;
+    }, "Check 1 saved address in state");
+  });
+
+  info("re-attach the tab back in the original window to test the event listeners were added");
+
+  let tab3 = gBrowser.adoptTab(newWin.gBrowser.selectedTab, 1, true);
+  widget = await TestUtils.waitForCondition(async () => getPaymentWidget(requestId));
+  is(widget.frameElement.ownerGlobal, window, "Check widget is back in first window");
+  await checkTabModal(tab3.linkedBrowser, widget, "after re-attaching");
+
+  temporaryStore = widget.frameElement.paymentDialogWrapper.temporaryStore;
+  is(Object.values(temporaryStore.addresses.getAll()).length, 1, "Check temp addresses in wrapper");
+  is(Object.values(temporaryStore.creditCards.getAll()).length, 1, "Check temp cards in wrapper");
+
+  spawnPaymentDialogTask(widget.frameElement, PTU.DialogContentTasks.manuallyClickCancel);
+  await BrowserTestUtils.waitForCondition(() => widget.closed, "dialog should be closed");
+  await BrowserTestUtils.removeTab(tab3);
+});
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -40,26 +40,31 @@ function getPaymentRequests() {
  * @returns {Promise}
  */
 async function getPaymentWidget(requestId) {
   return BrowserTestUtils.waitForCondition(() => {
     let {dialogContainer} = paymentUISrv.findDialog(requestId);
     if (!dialogContainer) {
       return false;
     }
-    let browserIFrame = dialogContainer.querySelector("iframe");
-    if (!browserIFrame) {
+    let paymentFrame = dialogContainer.querySelector(".paymentDialogContainerFrame");
+    if (!paymentFrame) {
       return false;
     }
-    return browserIFrame.contentWindow;
+    return {
+      get closed() {
+        return !paymentFrame.isConnected;
+      },
+      frameElement: paymentFrame,
+    };
   }, "payment dialog should be opened");
 }
 
 async function getPaymentFrame(widget) {
-  return widget.document.getElementById("paymentRequestFrame");
+  return widget.frameElement;
 }
 
 function waitForMessageFromWidget(messageType, widget = null) {
   info("waitForMessageFromWidget: " + messageType);
   return new Promise(resolve => {
     Services.mm.addMessageListener("paymentContentToChrome", function onMessage({data, target}) {
       if (data.messageType != messageType) {
         return;
@@ -418,21 +423,22 @@ async function navigateToAddAddressPage(
 
     info("navigateToAddAddressPage: wait for address page");
     await PaymentTestUtils.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "address-page" && !state.page.guid;
     }, "Check add page state");
   }, aOptions);
 }
 
-async function fillInBillingAddressForm(frame, aAddress) {
+async function fillInBillingAddressForm(frame, aAddress, aOptions) {
   // For now billing and shipping address forms have the same fields but that may
   // change so use separarate helpers.
   return fillInShippingAddressForm(frame, aAddress, {
     expectedSelectedStateKey: ["basic-card-page", "billingAddressGUID"],
+    ...aOptions,
   });
 }
 
 async function fillInShippingAddressForm(frame, aAddress, aOptions) {
   let address = Object.assign({}, aAddress);
   // Email isn't used on address forms, only payer/contact ones.
   delete address.email;
   return fillInAddressForm(frame, address, {
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -405,17 +405,17 @@ add_task(async function test_field_valid
   form.remove();
 });
 
 add_task(async function test_merchantShippingAddressErrors() {
   let form = new AddressForm();
   await form.promiseReady;
 
   // Merchant errors only make sense when editing a record so add one.
-  let address1 = deepClone(PTU.Addresses.TimBL);
+  let address1 = deepClone(PTU.Addresses.TimBR);
   address1.guid = "9864798564";
 
   const state = {
     page: {
       id: "address-page",
     },
     "address-page": {
       guid: address1.guid,
@@ -423,48 +423,57 @@ add_task(async function test_merchantShi
       title: "Sample page title",
     },
     request: {
       paymentDetails: {
         shippingAddressErrors: {
           addressLine: "Street address needs to start with a D",
           city: "City needs to start with a B",
           country: "Country needs to start with a C",
+          dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
           organization: "organization needs to start with an A",
           phone: "Telephone needs to start with a 9",
           postalCode: "Postal code needs to start with a 0",
           recipient: "Name needs to start with a Z",
           region: "Region needs to start with a Y",
+          regionCode: "Regions must be 1 to 3 characters in length (sometimes ;) )",
         },
       },
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
     },
   };
   await form.requestStore.setState(state);
   display.appendChild(form);
   await asyncElementRendered();
 
   function checkValidationMessage(selector, property) {
+    let expected = state.request.paymentDetails.shippingAddressErrors[property];
+    let container = form.form.querySelector(selector + "-container");
+    ok(!isHidden(container), selector + "-container should be visible");
     is(form.form.querySelector(selector).validationMessage,
-       state.request.paymentDetails.shippingAddressErrors[property],
+       expected,
        "Validation message should match for " + selector);
   }
 
   ok(form.saveButton.disabled, "Save button should be disabled due to validation errors");
 
   checkValidationMessage("#street-address", "addressLine");
   checkValidationMessage("#address-level2", "city");
+  checkValidationMessage("#address-level3", "dependentLocality");
   checkValidationMessage("#country", "country");
   checkValidationMessage("#organization", "organization");
   checkValidationMessage("#tel", "phone");
   checkValidationMessage("#postal-code", "postalCode");
   checkValidationMessage("#given-name", "recipient");
-  checkValidationMessage("#address-level1", "region");
+  checkValidationMessage("#address-level1", "regionCode");
+  isnot(form.form.querySelector("#address-level1"),
+        state.request.paymentDetails.shippingAddressErrors.region,
+        "When both region and regionCode are supplied we only show the 'regionCode' error");
 
   // TODO: bug 1482808 - the save button should be enabled after editing the fields
 
   form.remove();
 });
 
 add_task(async function test_customMerchantValidity_reset() {
   let form = new AddressForm();
new file mode 100644
--- /dev/null
+++ b/browser/components/search/SearchTelemetry.jsm
@@ -0,0 +1,142 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["SearchTelemetry"];
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", null);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+const SEARCH_COUNTS_HISTOGRAM_KEY = "SEARCH_COUNTS";
+
+// Used to identify various parameters (query, code, etc.) in search URLS.
+const SEARCH_PROVIDER_INFO = {
+  "google": {
+    "regexp": /^https:\/\/www\.(google)\.(?:.+)\/search/,
+    "queryParam": "q",
+    "codeParam": "client",
+    "codePrefixes": ["firefox"],
+    "followonParams": ["oq", "ved", "ei"],
+  },
+  "duckduckgo": {
+    "regexp": /^https:\/\/(duckduckgo)\.com\//,
+    "queryParam": "q",
+    "codeParam": "t",
+    "codePrefixes": ["ff"],
+  },
+  "yahoo": {
+    "regexp": /^https:\/\/(?:.*)search\.(yahoo)\.com\/search/,
+    "queryParam": "p",
+  },
+  "baidu": {
+    "regexp": /^https:\/\/www\.(baidu)\.com\/(?:s|baidu)/,
+    "queryParam": "wd",
+    "codeParam": "tn",
+    "codePrefixes": ["monline_dg"],
+    "followonParams": ["oq"],
+  },
+  "bing": {
+    "regexp": /^https:\/\/www\.(bing)\.com\/search/,
+    "queryParam": "q",
+    "codeParam": "pc",
+    "codePrefixes": ["MOZ", "MZ"],
+  },
+};
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "loggingEnabled", BROWSER_SEARCH_PREF + "log", false);
+
+class TelemetryHandler {
+  constructor() {
+    this.__searchProviderInfo = null;
+  }
+
+  overrideSearchTelemetryForTests(infoByProvider) {
+    if (infoByProvider) {
+      for (let info of Object.values(infoByProvider)) {
+        info.regexp = new RegExp(info.regexp);
+      }
+      this.__searchProviderInfo = infoByProvider;
+    } else {
+      this.__searchProviderInfo = SEARCH_PROVIDER_INFO;
+    }
+  }
+
+  recordSearchURLTelemetry(url) {
+    let entry = Object.entries(this._searchProviderInfo).find(
+      ([_, info]) => info.regexp.test(url)
+    );
+    if (!entry) {
+      return;
+    }
+    let [provider, searchProviderInfo] = entry;
+    let queries = new URLSearchParams(url.split("#")[0].split("?")[1]);
+    if (!queries.get(searchProviderInfo.queryParam)) {
+      return;
+    }
+    // Default to organic to simplify things.
+    // We override type in the sap cases.
+    let type = "organic";
+    let code;
+    if (searchProviderInfo.codeParam) {
+      code = queries.get(searchProviderInfo.codeParam);
+      if (code &&
+          searchProviderInfo.codePrefixes.some(p => code.startsWith(p))) {
+        if (searchProviderInfo.followonParams &&
+           searchProviderInfo.followonParams.some(p => queries.has(p))) {
+          type = "sap-follow-on";
+        } else {
+          type = "sap";
+        }
+      } else if (provider == "bing") {
+        // Bing requires lots of extra work related to cookies.
+        let secondaryCode = queries.get("form");
+        // This code is used for all Bing follow-on searches.
+        if (secondaryCode == "QBRE") {
+          for (let cookie of Services.cookies.getCookiesFromHost("www.bing.com", {})) {
+            if (cookie.name == "SRCHS") {
+              // If this cookie is present, it's probably an SAP follow-on.
+              // This might be an organic follow-on in the same session,
+              // but there is no way to tell the difference.
+              if (searchProviderInfo.codePrefixes.some(p => cookie.value.startsWith("PC=" + p))) {
+                type = "sap-follow-on";
+                code = cookie.value.split("=")[1];
+                break;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    let payload = `${provider}.in-content:${type}:${code || "none"}`;
+    let histogram = Services.telemetry.getKeyedHistogramById(SEARCH_COUNTS_HISTOGRAM_KEY);
+    histogram.add(payload);
+    LOG("recordSearchURLTelemetry: " + payload);
+  }
+
+  get _searchProviderInfo() {
+    if (!this.__searchProviderInfo) {
+      this.__searchProviderInfo = SEARCH_PROVIDER_INFO;
+    }
+    return this.__searchProviderInfo;
+  }
+}
+
+/**
+ * Outputs aText to the JavaScript console as well as to stdout.
+ */
+function LOG(aText) {
+  if (loggingEnabled) {
+    dump(`*** SearchTelemetry: ${aText}\n"`);
+    Services.console.logStringMessage(aText);
+  }
+}
+
+var SearchTelemetry = new TelemetryHandler();
--- a/browser/components/search/moz.build
+++ b/browser/components/search/moz.build
@@ -1,19 +1,25 @@
 # -*- 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/.
 
-BROWSER_CHROME_MANIFESTS += [
-    'test/browser.ini',
-    'test/google_codes/browser.ini',
+EXTRA_JS_MODULES += [
+    'SearchTelemetry.jsm',
 ]
 
+BROWSER_CHROME_MANIFESTS += [
+    'test/browser/browser.ini',
+    'test/browser/google_codes/browser.ini',
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
 TESTING_JS_MODULES += [
-    'test/SearchTestUtils.jsm',
+    'test/browser/SearchTestUtils.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Search')
rename from browser/components/search/test/.eslintrc.js
rename to browser/components/search/test/browser/.eslintrc.js
rename from browser/components/search/test/426329.xml
rename to browser/components/search/test/browser/426329.xml
--- a/browser/components/search/test/426329.xml
+++ b/browser/components/search/test/browser/426329.xml
@@ -1,11 +1,11 @@
 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                        xmlns:moz="http://www.mozilla.org/2006/browser/search/">
   <ShortName>Bug 426329</ShortName>
   <Description>426329 Search</Description>
   <InputEncoding>utf-8</InputEncoding>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/test.html">
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/test.html">
     <Param name="test" value="{searchTerms}"/>
   </Url>
-  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/test.html</moz:SearchForm>
+  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/test.html</moz:SearchForm>
 </OpenSearchDescription>
rename from browser/components/search/test/483086-1.xml
rename to browser/components/search/test/browser/483086-1.xml
--- a/browser/components/search/test/483086-1.xml
+++ b/browser/components/search/test/browser/483086-1.xml
@@ -1,10 +1,10 @@
 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                        xmlns:moz="http://www.mozilla.org/2006/browser/search/">
   <ShortName>483086a</ShortName>
   <Description>Bug 483086 Test 1</Description>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
     <Param name="test" value="{searchTerms}"/>
   </Url>
   <moz:SearchForm>foo://example.com</moz:SearchForm>
 </OpenSearchDescription>
rename from browser/components/search/test/483086-2.xml
rename to browser/components/search/test/browser/483086-2.xml
--- a/browser/components/search/test/483086-2.xml
+++ b/browser/components/search/test/browser/483086-2.xml
@@ -1,10 +1,10 @@
 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                        xmlns:moz="http://www.mozilla.org/2006/browser/search/">
   <ShortName>483086b</ShortName>
   <Description>Bug 483086 Test 2</Description>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
     <Param name="test" value="{searchTerms}"/>
   </Url>
   <moz:SearchForm>http://example.com</moz:SearchForm>
 </OpenSearchDescription>
rename from browser/components/search/test/SearchTestUtils.jsm
rename to browser/components/search/test/browser/SearchTestUtils.jsm
rename from browser/components/search/test/browser.ini
rename to browser/components/search/test/browser/browser.ini
rename from browser/components/search/test/browser_426329.js
rename to browser/components/search/test/browser/browser_426329.js
--- a/browser/components/search/test/browser_426329.js
+++ b/browser/components/search/test/browser/browser_426329.js
@@ -1,14 +1,14 @@
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 ChromeUtils.defineModuleGetter(this, "FormHistory",
   "resource://gre/modules/FormHistory.jsm");
 
 function expectedURL(aSearchTerms) {
-  const ENGINE_HTML_BASE = "http://mochi.test:8888/browser/browser/components/search/test/test.html";
+  const ENGINE_HTML_BASE = "http://mochi.test:8888/browser/browser/components/search/test/browser/test.html";
   var searchArg = Services.textToSubURI.ConvertAndEscape("utf-8", aSearchTerms);
   return ENGINE_HTML_BASE + "?test=" + searchArg;
 }
 
 function simulateClick(aEvent, aTarget) {
   var event = document.createEvent("MouseEvent");
   var ctrlKeyArg  = aEvent.ctrlKey || false;
   var altKeyArg   = aEvent.altKey || false;
@@ -76,17 +76,17 @@ function promiseSetEngine() {
 
           Services.obs.removeObserver(observer, "browser-search-engine-modified");
           resolve();
           break;
       }
     }
 
     Services.obs.addObserver(observer, "browser-search-engine-modified");
-    ss.addEngine("http://mochi.test:8888/browser/browser/components/search/test/426329.xml",
+    ss.addEngine("http://mochi.test:8888/browser/browser/components/search/test/browser/426329.xml",
                  "data:image/x-icon,%00", false);
   });
 }
 
 function promiseRemoveEngine() {
   return new Promise(resolve => {
     var ss = Services.search;
 
rename from browser/components/search/test/browser_483086.js
rename to browser/components/search/test/browser/browser_483086.js
--- a/browser/components/search/test/browser_483086.js
+++ b/browser/components/search/test/browser/browser_483086.js
@@ -18,17 +18,17 @@ function test() {
       case "engine-removed":
         Services.obs.removeObserver(observer, "browser-search-engine-modified");
         test2();
         break;
     }
   }
 
   Services.obs.addObserver(observer, "browser-search-engine-modified");
-  gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/483086-1.xml",
+  gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/browser/483086-1.xml",
                 "data:image/x-icon;%00", false);
 }
 
 function test2() {
   function observer(aSubject, aTopic, aData) {
     switch (aData) {
       case "engine-added":
         let engine = gSS.getEngineByName("483086b");
@@ -39,11 +39,11 @@ function test2() {
       case "engine-removed":
         Services.obs.removeObserver(observer, "browser-search-engine-modified");
         finish();
         break;
     }
   }
 
   Services.obs.addObserver(observer, "browser-search-engine-modified");
-  gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/483086-2.xml",
+  gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/browser/483086-2.xml",
                 "data:image/x-icon;%00", false);
 }
rename from browser/components/search/test/browser_aboutSearchReset.js
rename to browser/components/search/test/browser/browser_aboutSearchReset.js
rename from browser/components/search/test/browser_addEngine.js
rename to browser/components/search/test/browser/browser_addEngine.js
--- a/browser/components/search/test/browser_addEngine.js
+++ b/browser/components/search/test/browser/browser_addEngine.js
@@ -39,22 +39,22 @@ function checkEngine(checkObj, engineObj
 
 var gTests = [
   {
     name: "opensearch install",
     engine: {
       name: "Foo",
       alias: null,
       description: "Foo Search",
-      searchForm: "http://mochi.test:8888/browser/browser/components/search/test/",
+      searchForm: "http://mochi.test:8888/browser/browser/components/search/test/browser/",
     },
     run() {
       Services.obs.addObserver(observer, "browser-search-engine-modified");
 
-      gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml",
+      gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine.xml",
                     "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC",
                     false);
     },
     added(engine) {
       ok(engine, "engine was added.");
 
       checkEngine(this.engine, engine);
 
rename from browser/components/search/test/browser_amazon.js
rename to browser/components/search/test/browser/browser_amazon.js
rename from browser/components/search/test/browser_bing.js
rename to browser/components/search/test/browser/browser_bing.js
rename from browser/components/search/test/browser_contextSearchTabPosition.js
rename to browser/components/search/test/browser/browser_contextSearchTabPosition.js
rename from browser/components/search/test/browser_contextmenu.js
rename to browser/components/search/test/browser/browser_contextmenu.js
--- a/browser/components/search/test/browser_contextmenu.js
+++ b/browser/components/search/test/browser/browser_contextmenu.js
@@ -11,17 +11,17 @@ add_task(async function() {
 
   // We want select events to be fired.
   await SpecialPowers.pushPrefEnv({set: [["dom.select_events.enabled", true]]});
 
   let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
   let originalValue = envService.get("XPCSHELL_TEST_PROFILE_DIR");
   envService.set("XPCSHELL_TEST_PROFILE_DIR", "1");
 
-  let url = "chrome://mochitests/content/browser/browser/components/search/test/";
+  let url = "chrome://mochitests/content/browser/browser/components/search/test/browser/";
   let resProt = Services.io.getProtocolHandler("resource")
                         .QueryInterface(Ci.nsIResProtocolHandler);
   let originalSubstitution = resProt.getSubstitution("search-plugins");
   resProt.setSubstitution("search-plugins",
                           Services.io.newURI(url));
 
   let searchDonePromise;
   await new Promise(resolve => {
@@ -78,17 +78,17 @@ add_task(async function() {
   is(searchItem.label, "Search " + ENGINE_NAME + " for \u201ctest search\u201d", "Check context menu label");
   is(searchItem.disabled, false, "Check that search context menu item is enabled");
 
   await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
     searchItem.click();
   });
 
   is(gBrowser.currentURI.spec,
-     "http://mochi.test:8888/browser/browser/components/search/test/?test=test+search&ie=utf-8&channel=contextsearch",
+     "http://mochi.test:8888/browser/browser/components/search/test/browser/?test=test+search&ie=utf-8&channel=contextsearch",
      "Checking context menu search URL");
 
   contextMenu.hidePopup();
 
   // Remove the tab opened by the search
   gBrowser.removeCurrentTab();
 
   await new Promise(resolve => {
rename from browser/components/search/test/browser_ddg.js
rename to browser/components/search/test/browser/browser_ddg.js
rename from browser/components/search/test/browser_eBay.js
rename to browser/components/search/test/browser/browser_eBay.js
rename from browser/components/search/test/browser_google.js
rename to browser/components/search/test/browser/browser_google.js
rename from browser/components/search/test/browser_google_behavior.js
rename to browser/components/search/test/browser/browser_google_behavior.js
rename from browser/components/search/test/browser_healthreport.js
rename to browser/components/search/test/browser/browser_healthreport.js
--- a/browser/components/search/test/browser_healthreport.js
+++ b/browser/components/search/test/browser/browser_healthreport.js
@@ -72,17 +72,17 @@ function test() {
         gCUITestUtils.removeSearchBar();
         finish();
         break;
     }
   }
 
   Services.obs.addObserver(observer, "browser-search-engine-modified");
   gCUITestUtils.addSearchBar().then(function() {
-    Services.search.addEngine("http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml",
+    Services.search.addEngine("http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine.xml",
                               "data:image/x-icon,%00", false);
   });
 }
 
 function resetPreferences() {
   Preferences.resetBranch("datareporting.policy.");
   Preferences.set("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
 }
rename from browser/components/search/test/browser_hiddenOneOffs_cleanup.js
rename to browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js
rename from browser/components/search/test/browser_hiddenOneOffs_diacritics.js
rename to browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js
rename from browser/components/search/test/browser_oneOffContextMenu.js
rename to browser/components/search/test/browser/browser_oneOffContextMenu.js
--- a/browser/components/search/test/browser_oneOffContextMenu.js
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu.js
@@ -60,16 +60,16 @@ add_task(async function telemetry() {
   // By default the search will open in the background and the popup will stay open:
   promise = promiseEvent(searchPopup, "popuphidden");
   info("Closing search panel");
   EventUtils.synthesizeKey("KEY_Escape");
   await promise;
 
   // Check the loaded tab.
   Assert.equal(tab.linkedBrowser.currentURI.spec,
-               "http://mochi.test:8888/browser/browser/components/search/test/",
+               "http://mochi.test:8888/browser/browser/components/search/test/browser/",
                "Expected search tab should have loaded");
 
   BrowserTestUtils.removeTab(tab);
 
   // Move the cursor out of the panel area to avoid messing with other tests.
   await EventUtils.synthesizeNativeMouseMove(searchbar);
 });
rename from browser/components/search/test/browser_oneOffContextMenu_setDefault.js
rename to browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
rename from browser/components/search/test/browser_oneOffHeader.js
rename to browser/components/search/test/browser/browser_oneOffHeader.js
rename from browser/components/search/test/browser_private_search_perwindowpb.js
rename to browser/components/search/test/browser/browser_private_search_perwindowpb.js
rename from browser/components/search/test/browser_searchEngine_behaviors.js
rename to browser/components/search/test/browser/browser_searchEngine_behaviors.js
rename from browser/components/search/test/browser_searchbar_keyboard_navigation.js
rename to browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
rename from browser/components/search/test/browser_searchbar_openpopup.js
rename to browser/components/search/test/browser/browser_searchbar_openpopup.js
rename from browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
rename to browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
rename from browser/components/search/test/browser_tooManyEnginesOffered.js
rename to browser/components/search/test/browser/browser_tooManyEnginesOffered.js
rename from browser/components/search/test/browser_webapi.js
rename to browser/components/search/test/browser/browser_webapi.js
rename from browser/components/search/test/google_codes/browser.ini
rename to browser/components/search/test/browser/google_codes/browser.ini
rename from browser/components/search/test/head.js
rename to browser/components/search/test/browser/head.js
rename from browser/components/search/test/opensearch.html
rename to browser/components/search/test/browser/opensearch.html
--- a/browser/components/search/test/opensearch.html
+++ b/browser/components/search/test/browser/opensearch.html
@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html>
 <head>
 <meta charset="UTF-8">
-<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml">
-<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/testEngine_mozsearch.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine_mozsearch.xml">
 </head>
 <body></body>
 </html>
rename from browser/components/search/test/test.html
rename to browser/components/search/test/browser/test.html
rename from browser/components/search/test/testEngine.xml
rename to browser/components/search/test/browser/testEngine.xml
--- a/browser/components/search/test/testEngine.xml
+++ b/browser/components/search/test/browser/testEngine.xml
@@ -1,12 +1,12 @@
 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                        xmlns:moz="http://www.mozilla.org/2006/browser/search/">
   <ShortName>Foo</ShortName>
   <Description>Foo Search</Description>
   <InputEncoding>utf-8</InputEncoding>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
     <Param name="test" value="{searchTerms}"/>
   </Url>
-  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</moz:SearchForm>
+  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</moz:SearchForm>
   <moz:Alias>fooalias</moz:Alias>
 </OpenSearchDescription>
rename from browser/components/search/test/testEngine_diacritics.xml
rename to browser/components/search/test/browser/testEngine_diacritics.xml
--- a/browser/components/search/test/testEngine_diacritics.xml
+++ b/browser/components/search/test/browser/testEngine_diacritics.xml
@@ -1,12 +1,12 @@
 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                        xmlns:moz="http://www.mozilla.org/2006/browser/search/">
   <ShortName>Foo &#9825;</ShortName>
   <Description>Engine whose ShortName contains non-BMP Unicode characters</Description>
   <InputEncoding>utf-8</InputEncoding>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
     <Param name="test" value="{searchTerms}"/>
   </Url>
-  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</moz:SearchForm>
+  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</moz:SearchForm>
   <moz:Alias>diacriticalias</moz:Alias>
 </OpenSearchDescription>
rename from browser/components/search/test/testEngine_dupe.xml
rename to browser/components/search/test/browser/testEngine_dupe.xml
--- a/browser/components/search/test/testEngine_dupe.xml
+++ b/browser/components/search/test/browser/testEngine_dupe.xml
@@ -1,12 +1,12 @@
 <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                        xmlns:moz="http://www.mozilla.org/2006/browser/search/">
   <ShortName>FooDupe</ShortName>
   <Description>Second Engine Search</Description>
   <InputEncoding>utf-8</InputEncoding>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
     <Param name="test" value="{searchTerms}"/>
   </Url>
-  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</moz:SearchForm>
+  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</moz:SearchForm>
   <moz:Alias>secondalias</moz:Alias>
 </OpenSearchDescription>
rename from browser/components/search/test/testEngine_missing_namespace.xml
rename to browser/components/search/test/browser/testEngine_missing_namespace.xml
--- a/browser/components/search/test/testEngine_missing_namespace.xml
+++ b/browser/components/search/test/browser/testEngine_missing_namespace.xml
@@ -1,11 +1,11 @@
 <OpenSearchDescription>
   <ShortName>Foo</ShortName>
   <Description>Foo Search</Description>
   <InputEncoding>utf-8</InputEncoding>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
     <Param name="test" value="{searchTerms}"/>
   </Url>
-  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</moz:SearchForm>
+  <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</moz:SearchForm>
   <moz:Alias>fooalias</moz:Alias>
 </OpenSearchDescription>
rename from browser/components/search/test/testEngine_mozsearch.xml
rename to browser/components/search/test/browser/testEngine_mozsearch.xml
--- a/browser/components/search/test/testEngine_mozsearch.xml
+++ b/browser/components/search/test/browser/testEngine_mozsearch.xml
@@ -1,14 +1,14 @@
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
   <ShortName>Foo</ShortName>
   <Description>Foo Search</Description>
   <InputEncoding>utf-8</InputEncoding>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
-  <Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?suggestions&amp;locale={moz:locale}&amp;test={searchTerms}"/>
-  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/">
+  <Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?suggestions&amp;locale={moz:locale}&amp;test={searchTerms}"/>
+  <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/">
     <Param name="test" value="{searchTerms}"/>
     <Param name="ie" value="utf-8"/>
     <MozParam name="channel" condition="purpose" purpose="keyword" value="keywordsearch"/>
     <MozParam name="channel" condition="purpose" purpose="contextmenu" value="contextsearch"/>
   </Url>
-  <SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</SearchForm>
+  <SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</SearchForm>
 </SearchPlugin>
rename from browser/components/search/test/tooManyEnginesOffered.html
rename to browser/components/search/test/browser/tooManyEnginesOffered.html
--- a/browser/components/search/test/tooManyEnginesOffered.html
+++ b/browser/components/search/test/browser/tooManyEnginesOffered.html
@@ -1,13 +1,13 @@
 <!DOCTYPE html>
 <html>
 <head>
 <meta charset="UTF-8">
-<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/engine1.xml">
-<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/engine2.xml">
-<link rel="search" type="application/opensearchdescription+xml" title="engine3" href="http://mochi.test:8888/browser/browser/components/search/test/engine3.xml">
-<link rel="search" type="application/opensearchdescription+xml" title="engine4" href="http://mochi.test:8888/browser/browser/components/search/test/engine4.xml">
-<link rel="search" type="application/opensearchdescription+xml" title="engine5" href="http://mochi.test:8888/browser/browser/components/search/test/engine5.xml">
-<link rel="search" type="application/opensearchdescription+xml" title="engine6" href="http://mochi.test:8888/browser/browser/components/search/test/engine6.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine1.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine2.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine3" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine3.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine4" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine4.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine5" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine5.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine6" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine6.xml">
 </head>
 <body></body>
 </html>
rename from browser/components/search/test/webapi.html
rename to browser/components/search/test/browser/webapi.html
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/xpcshell-test"
+  ]
+};
rename from toolkit/components/search/tests/xpcshell/test_urltelemetry.js
rename to browser/components/search/test/unit/test_urlTelemetry.js
--- a/toolkit/components/search/tests/xpcshell/test_urltelemetry.js
+++ b/browser/components/search/test/unit/test_urlTelemetry.js
@@ -1,83 +1,86 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource:///modules/SearchTelemetry.jsm");
+
 add_task(async function test_parsing_search_urls() {
   let hs;
   // Google search access point.
-  Services.search.recordSearchURLTelemetry("https://www.google.com/search?q=test&ie=utf-8&oe=utf-8&client=firefox-b-1-ab");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.google.com/search?q=test&ie=utf-8&oe=utf-8&client=firefox-b-1-ab");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:sap:firefox-b-1-ab" in hs, "The histogram must contain the correct key");
 
   // Google search access point follow-on.
-  Services.search.recordSearchURLTelemetry("https://www.google.com/search?client=firefox-b-1-ab&ei=EI_VALUE&q=test2&oq=test2&gs_l=GS_L_VALUE");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.google.com/search?client=firefox-b-1-ab&ei=EI_VALUE&q=test2&oq=test2&gs_l=GS_L_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:sap-follow-on:firefox-b-1-ab" in hs, "The histogram must contain the correct key");
 
   // Google organic.
-  Services.search.recordSearchURLTelemetry("https://www.google.com/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.google.com/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Google organic UK.
-  Services.search.recordSearchURLTelemetry("https://www.google.co.uk/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.google.co.uk/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("google.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Yahoo organic.
-  Services.search.recordSearchURLTelemetry("https://search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
+  SearchTelemetry.recordSearchURLTelemetry("https://search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("yahoo.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Yahoo organic UK.
-  Services.search.recordSearchURLTelemetry("https://uk.search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
+  SearchTelemetry.recordSearchURLTelemetry("https://uk.search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("yahoo.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // Bing search access point.
-  Services.search.recordSearchURLTelemetry("https://www.bing.com/search?q=test&pc=MOZI&form=MOZLBR");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.bing.com/search?q=test&pc=MOZI&form=MOZLBR");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("bing.in-content:sap:MOZI" in hs, "The histogram must contain the correct key");
 
   // Bing organic.
-  Services.search.recordSearchURLTelemetry("https://www.bing.com/search?q=test&qs=n&form=QBLH&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.bing.com/search?q=test&qs=n&form=QBLH&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("bing.in-content:organic:none" in hs, "The histogram must contain the correct key");
 
   // DuckDuckGo search access point.
-  Services.search.recordSearchURLTelemetry("https://duckduckgo.com/?q=test&t=ffab");
+  SearchTelemetry.recordSearchURLTelemetry("https://duckduckgo.com/?q=test&t=ffab");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("duckduckgo.in-content:sap:ffab" in hs, "The histogram must contain the correct key");
 
   // DuckDuckGo organic.
-  Services.search.recordSearchURLTelemetry("https://duckduckgo.com/?q=test&t=hi&ia=news");
+  SearchTelemetry.recordSearchURLTelemetry("https://duckduckgo.com/?q=test&t=hi&ia=news");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("duckduckgo.in-content:organic:hi" in hs, "The histogram must contain the correct key");
 
   // Baidu search access point.
-  Services.search.recordSearchURLTelemetry("https://www.baidu.com/baidu?wd=test&tn=monline_dg&ie=utf-8");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.baidu.com/baidu?wd=test&tn=monline_dg&ie=utf-8");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("baidu.in-content:sap:monline_dg" in hs, "The histogram must contain the correct key");
 
   // Baidu search access point follow-on.
-  Services.search.recordSearchURLTelemetry("https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=monline_dg&wd=test2&oq=test&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn&rsv_enter=1&rsv_sug3=2&rsv_sug2=0&inputT=227&rsv_sug4=397");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=monline_dg&wd=test2&oq=test&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn&rsv_enter=1&rsv_sug3=2&rsv_sug2=0&inputT=227&rsv_sug4=397");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("baidu.in-content:sap-follow-on:monline_dg" in hs, "The histogram must contain the correct key");
 
   // Baidu organic.
-  Services.search.recordSearchURLTelemetry("https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=test&rn=&oq=&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn");
+  SearchTelemetry.recordSearchURLTelemetry("https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=test&rn=&oq=&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn");
   hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
   Assert.ok(hs);
   Assert.ok("baidu.in-content:organic:baidu" in hs, "The histogram must contain the correct key");
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/unit/xpcshell.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+firefox-appdir = browser
+
+[test_urlTelemetry.js]
--- a/browser/components/shell/HeadlessShell.jsm
+++ b/browser/components/shell/HeadlessShell.jsm
@@ -25,16 +25,20 @@ function loadContentWindow(webNavigation
         // Ignore inner-frame events
         if (progress != webProgress) {
           return;
         }
         // Ignore events that don't change the document
         if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
           return;
         }
+        // Ignore the initial about:blank
+        if (uri != location.spec) {
+          return;
+        }
         let contentWindow = docShell.domWindow;
         progressListeners.delete(progressListener);
         webProgress.removeProgressListener(progressListener);
         contentWindow.addEventListener("load", (event) => {
           resolve(contentWindow);
         }, { once: true });
       },
       QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
--- a/browser/components/shell/test/test_headless_screenshot.html
+++ b/browser/components/shell/test/test_headless_screenshot.html
@@ -71,17 +71,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   }
 
   async function testWindowSizePositive(width, height) {
     let size = width + "";
     if (height) {
       size += "," + height;
     }
 
-    await runFirefox(["-url", "http://mochi.test:8888/headless.html", "-screenshot", screenshotPath, "-window-size", size]);
+    await runFirefox(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", screenshotPath, "-window-size", size]);
 
     let saved = await OS.File.exists(screenshotPath);
     ok(saved, "A screenshot should be saved in the tmp directory");
     if (!saved) {
       return;
     }
 
     let data = await OS.File.read(screenshotPath);
@@ -104,43 +104,43 @@ https://bugzilla.mozilla.org/show_bug.cg
     await OS.File.remove(screenshotPath);
   }
 
   (async function() {
     SimpleTest.waitForExplicitFinish();
 
     // Test all four basic variations of the "screenshot" argument
     // when a file path is specified.
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", "-screenshot", screenshotPath], screenshotPath);
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", `-screenshot=${screenshotPath}`], screenshotPath);
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", "--screenshot", screenshotPath], screenshotPath);
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", `--screenshot=${screenshotPath}`], screenshotPath);
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", screenshotPath], screenshotPath);
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", `-screenshot=${screenshotPath}`], screenshotPath);
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "--screenshot", screenshotPath], screenshotPath);
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", `--screenshot=${screenshotPath}`], screenshotPath);
 
     // Test variations of the "screenshot" argument when a file path
     // isn't specified.
-    await testFileCreationPositive(["-screenshot", "http://mochi.test:8888/headless.html"], "screenshot.png");
-    await testFileCreationPositive(["http://mochi.test:8888/headless.html", "-screenshot"], "screenshot.png");
-    await testFileCreationPositive(["--screenshot", "http://mochi.test:8888/headless.html"], "screenshot.png");
-    await testFileCreationPositive(["http://mochi.test:8888/headless.html", "--screenshot"], "screenshot.png");
+    await testFileCreationPositive(["-screenshot", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html"], "screenshot.png");
+    await testFileCreationPositive(["http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot"], "screenshot.png");
+    await testFileCreationPositive(["--screenshot", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html"], "screenshot.png");
+    await testFileCreationPositive(["http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "--screenshot"], "screenshot.png");
 
     // Test invalid URL arguments (either no argument or too many arguments).
     await testFileCreationNegative(["-screenshot"], "screenshot.png");
-    await testFileCreationNegative(["http://mochi.test:8888/headless.html", "http://mochi.test:8888/headless.html", "-screenshot"], "screenshot.png");
+    await testFileCreationNegative(["http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "http://mochi.test:8888/headless.html", "-screenshot"], "screenshot.png");
 
     // Test all four basic variations of the "window-size" argument.
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", "-screenshot", "-window-size", "800"], "screenshot.png");
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", "-screenshot", "-window-size=800"], "screenshot.png");
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", "-screenshot", "--window-size", "800"], "screenshot.png");
-    await testFileCreationPositive(["-url", "http://mochi.test:8888/headless.html", "-screenshot", "--window-size=800"], "screenshot.png");
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size", "800"], "screenshot.png");
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size=800"], "screenshot.png");
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "--window-size", "800"], "screenshot.png");
+    await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "--window-size=800"], "screenshot.png");
 
     // Test other variations of the "window-size" argument.
     await testWindowSizePositive(800, 600);
     await testWindowSizePositive(1234);
-    await testFileCreationNegative(["-url", "http://mochi.test:8888/headless.html", "-screenshot", "-window-size", "hello"], "screenshot.png");
-    await testFileCreationNegative(["-url", "http://mochi.test:8888/headless.html", "-screenshot", "-window-size", "800,"], "screenshot.png");
+    await testFileCreationNegative(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size", "hello"], "screenshot.png");
+    await testFileCreationNegative(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size", "800,"], "screenshot.png");
 
     SimpleTest.finish();
   })();
   </script>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1378010">Mozilla Bug 1378010</a>
 <p id="display"></p>
--- a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
@@ -45,17 +45,17 @@ add_task(async function() {
   // is complete before starting, otherwise onLocationChange for this tab could
   // override the value we set with an empty value.
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
   registerCleanupFunction(function() {
     URLBarSetURI();
     BrowserTestUtils.removeTab(tab);
   });
 
-  let lotsOfSpaces = new Array(200).fill("%20").join("");
+  let lotsOfSpaces = "%20".repeat(200);
 
   // اسماء.شبكة
   let rtlDomain = "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629";
 
   // Mix the direction of the tests to cover more cases, and to ensure the
   // textoverflow attribute changes every time, because tewtVal waits for that.
   await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "end");
   await testVal(`https://mozilla.org/`);
--- a/browser/confvars.sh
+++ b/browser/confvars.sh
@@ -26,19 +26,16 @@ if test "$OS_ARCH" = "WINNT"; then
       fi
     fi
   fi
 fi
 
 # Enable building ./signmar and running libmar signature tests
 MOZ_ENABLE_SIGNMAR=1
 
-MOZ_APP_VERSION=$FIREFOX_VERSION
-MOZ_APP_VERSION_DISPLAY=$FIREFOX_VERSION_DISPLAY
-
 if [ "${MOZ_BROWSER_XHTML}" = "1" ]; then
   BROWSER_CHROME_URL=chrome://browser/content/browser.xhtml
 else
   BROWSER_CHROME_URL=chrome://browser/content/browser.xul
 fi
 
 # MOZ_APP_DISPLAYNAME will be set by branding/configure.sh
 # MOZ_BRANDING_DIRECTORY is the default branding directory used when none is
--- a/browser/installer/windows/moz.build
+++ b/browser/installer/windows/moz.build
@@ -1,13 +1,13 @@
 # -*- 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/.
 
-DEFINES['APP_VERSION'] = CONFIG['FIREFOX_VERSION']
+DEFINES['APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 
 DEFINES['MOZ_APP_NAME'] = CONFIG['MOZ_APP_NAME']
 DEFINES['MOZ_APP_DISPLAYNAME'] = CONFIG['MOZ_APP_DISPLAYNAME']
 DEFINES['MOZILLA_VERSION'] = CONFIG['MOZILLA_VERSION']
 
 SPHINX_TREES['installer'] = 'docs'
--- a/browser/locales/en-US/browser/aboutPolicies.ftl
+++ b/browser/locales/en-US/browser/aboutPolicies.ftl
@@ -7,16 +7,8 @@ about-policies-title = Enterprise Polici
 # 'Active' is used to describe the policies that are currently active
 active-policies-tab = Active
 errors-tab = Errors
 documentation-tab = Documentation
 
 policy-name = Policy Name
 policy-value = Policy Value
 policy-errors = Policy Errors
-
-# 'gpo-machine-only' policies are related to the Group Policy features
-# on Windows. Please use the same terminology that is used on Windows
-# to describe Group Policy.
-# These policies can only be set at the computer-level settings, while
-# the other policies can also be set at the user-level.
-gpo-machine-only =
-  .title = When using Group Policy, this policy can only be set at the computer level.
--- a/browser/locales/en-US/browser/policies/policies-descriptions.ftl
+++ b/browser/locales/en-US/browser/policies/policies-descriptions.ftl
@@ -74,18 +74,17 @@ policy-DNSOverHTTPS = Configure DNS over
 
 policy-DontCheckDefaultBrowser = Disable check for default browser on startup.
 
 # “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. See also:
-# https://github.com/mozilla/policy-templates/blob/master/README.md#extensions-machine-only
+# 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-FlashPlugin = Allow or deny usage of the Flash plugin.
 
 policy-HardwareAcceleration = If false, turn off hardware acceleration.
 
 # “lock” means that the user won’t be able to change this setting
 policy-Homepage = Set and optionally lock the homepage.
@@ -112,11 +111,10 @@ policy-SanitizeOnShutdown = Clear all na
 
 policy-SearchBar = Set the default location of the search bar. The user is still allowed to customize it.
 
 policy-SearchEngines = Configure search engine settings. This policy is only available on the Extended Support Release (ESR) version.
 
 # For more information, see https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/PKCS11/Module_Installation
 policy-SecurityDevices = Install PKCS #11 modules.
 
-# “format” refers to the format used for the value of this policy. See also:
-# https://github.com/mozilla/policy-templates/blob/master/README.md#websitefilter-machine-only
+# “format” refers to the format used for the value of this policy.
 policy-WebsiteFilter = Block websites from being visited. See documentation for more details on the format.
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -7,20 +7,23 @@
 
 var EXPORTED_SYMBOLS = [
   "BrowserUsageTelemetry",
   "URLBAR_SELECTED_RESULT_TYPES",
   "URLBAR_SELECTED_RESULT_METHODS",
   "MINIMUM_TAB_COUNT_INTERVAL_MS",
  ];
 
-ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", null);
 
-ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
-                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+  SearchTelemetry: "resource:///modules/SearchTelemetry.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
 
 // The upper bound for the count of the visited unique domain names.
 const MAX_UNIQUE_VISITED_DOMAINS = 100;
 
 // Observed topic names.
 const TAB_RESTORING_TOPIC = "SSTabRestoring";
 const TELEMETRY_SUBSESSIONSPLIT_TOPIC = "internal-telemetry-after-subsession-split";
 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
@@ -199,19 +202,20 @@ let URICountListener = {
     // probe.
     if (shouldCountURI) {
       Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
     }
 
     if (!this.isHttpURI(uri)) {
       return;
     }
+
     if (shouldRecordSearchCount(browser.getTabBrowser()) &&
         !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
-      Services.search.recordSearchURLTelemetry(uriSpec);
+      SearchTelemetry.recordSearchURLTelemetry(uriSpec);
     }
 
     if (!shouldCountURI) {
       return;
     }
 
     // Update the URI counts.
     Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
--- a/browser/modules/FaviconLoader.jsm
+++ b/browser/modules/FaviconLoader.jsm
@@ -73,17 +73,19 @@ class FaviconLoad {
       iconInfo.node,
       iconInfo.node.nodePrincipal,
       iconInfo.node.nodePrincipal,
       (Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
        Ci.nsILoadInfo.SEC_ALLOW_CHROME |
        Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT),
       Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON);
 
-    this.channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND;
+    this.channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND |
+                              Ci.nsIRequest.VALIDATE_NEVER |
+                              Ci.nsIRequest.LOAD_FROM_CACHE;
     // Sometimes node is a document and sometimes it is an element. This is
     // the easiest single way to get to the load group in both those cases.
     this.channel.loadGroup = iconInfo.node.ownerGlobal.document.documentLoadGroup;
     this.channel.notificationCallbacks = this;
 
     if (Services.prefs.getBoolPref("network.http.tailing.enabled", true) &&
         this.channel instanceof Ci.nsIClassOfService) {
       this.channel.addClassFlags(Ci.nsIClassOfService.Tail | Ci.nsIClassOfService.Throttleable);
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -13,18 +13,18 @@ support-files =
   browser_BrowserErrorReporter.html
 [browser_BrowserWindowTracker.js]
 [browser_ContentSearch.js]
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
-  !/browser/components/search/test/head.js
-  !/browser/components/search/test/testEngine.xml
+  !/browser/components/search/test/browser/head.js
+  !/browser/components/search/test/browser/testEngine.xml
 [browser_LiveBookmarkMigrator.js]
 [browser_PageActions.js]
 [browser_PermissionUI.js]
 [browser_PermissionUI_prompts.js]
 [browser_ProcessHangNotifications.js]
 skip-if = !e10s
 [browser_SitePermissions.js]
 [browser_SitePermissions_combinations.js]
--- a/browser/modules/test/browser/browser_ContentSearch.js
+++ b/browser/modules/test/browser/browser_ContentSearch.js
@@ -1,31 +1,31 @@
 /* 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/. */
 
 const TEST_MSG = "ContentSearchTest";
 const CONTENT_SEARCH_MSG = "ContentSearch";
 const TEST_CONTENT_SCRIPT_BASENAME = "contentSearch.js";
 
-/* import-globals-from ../../../components/search/test/head.js */
+/* import-globals-from ../../../components/search/test/browser/head.js */
 Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/browser/components/search/test/head.js",
+  "chrome://mochitests/content/browser/browser/components/search/test/browser/head.js",
   this);
 
 let originalEngine = Services.search.defaultEngine;
 
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
     set: [["browser.newtab.preload", false]],
   });
 
   await promiseNewEngine("testEngine.xml", {
     setAsCurrent: true,
-    testPath: "chrome://mochitests/content/browser/browser/components/search/test/",
+    testPath: "chrome://mochitests/content/browser/browser/components/search/test/browser/",
   });
 
   registerCleanupFunction(() => {
     Services.search.defaultEngine = originalEngine;
   });
 });
 
 add_task(async function GetState() {
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
@@ -9,16 +9,19 @@ const SUGGESTION_ENGINE_NAME = "browser_
 const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
 
 ChromeUtils.defineModuleGetter(this, "URLBAR_SELECTED_RESULT_TYPES",
                                "resource:///modules/BrowserUsageTelemetry.jsm");
 
 ChromeUtils.defineModuleGetter(this, "URLBAR_SELECTED_RESULT_METHODS",
                                "resource:///modules/BrowserUsageTelemetry.jsm");
 
+ChromeUtils.defineModuleGetter(this, "SearchTelemetry",
+                              "resource:///modules/SearchTelemetry.jsm");
+
 function checkHistogramResults(resultIndexes, expected, histogram) {
   for (let [i, val] of Object.entries(resultIndexes.values)) {
     if (i == expected) {
       Assert.equal(val, 1,
         `expected counts should match for ${histogram} index ${i}`);
     } else {
       Assert.equal(!!val, false,
         `unexpected counts should be zero for ${histogram} index ${i}`);
@@ -571,29 +574,25 @@ add_task(async function test_suggestion_
       URLBAR_SELECTED_RESULT_METHODS.rightClickEnter,
       "FX_URLBAR_SELECTED_RESULT_METHOD");
 
     BrowserTestUtils.removeTab(tab);
   });
 });
 
 add_task(async function test_privateWindow() {
-  // Mock the search service's search provider info so that its
+  // Mock the search telemetry search provider info so that its
   // recordSearchURLTelemetry() function adds the in-content SEARCH_COUNTS
   // telemetry for our test engine.
-  Services.search.QueryInterface(Ci.nsIObserver).observe(
-    null,
-    "test:setSearchProviderInfo",
-    JSON.stringify({
-      "example": {
-        "regexp": "^http://example\\.com/",
-        "queryParam": "q",
-      },
-    })
-  );
+  SearchTelemetry.overrideSearchTelemetryForTests({
+    "example": {
+      "regexp": "^http://example\\.com/",
+      "queryParam": "q",
+    },
+  });
 
   let search_hist = getAndClearKeyedHistogram("SEARCH_COUNTS");
 
   // First, do a bunch of searches in a private window.
   let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
 
   info("Search in a private window and the pref does not exist");
   let p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
@@ -685,11 +684,10 @@ add_task(async function test_privateWind
 
   // SEARCH_COUNTS should be incremented.
   checkKeyedHistogram(search_hist, "other-MozSearch.urlbar", 7);
   checkKeyedHistogram(search_hist, "example.in-content:organic:none", 7);
 
   await BrowserTestUtils.closeWindow(win);
 
   // Reset the search provider info.
-  Services.search.QueryInterface(Ci.nsIObserver)
-    .observe(null, "test:setSearchProviderInfo", "");
+  SearchTelemetry.overrideSearchTelemetryForTests();
 });
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -651,27 +651,21 @@ notification[value="translation"] menuli
   :root[tabsintitlebar] #toolbar-menubar[autohide="true"] {
     height: var(--tab-min-height);
   }
   :root[tabsintitlebar][sizemode="normal"] #toolbar-menubar[autohide="true"] {
     height: calc(var(--tab-min-height) + var(--space-above-tabbar));
   }
 
   /* Add extra space to titlebar for dragging */
-  :root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar,
-  :root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar {
+  :root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar > .toolbar-items,
+  :root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar > .toolbar-items {
     padding-top: var(--space-above-tabbar);
   }
 
-  /* Center items (window caption buttons, private browsing indicator,
-   * accessibility indicator, etc) vertically. */
-  :root[sizemode="normal"] #TabsToolbar > .titlebar-item {
-    margin-top: calc(-1 * var(--space-above-tabbar));
-  }
-
   /* Make #TabsToolbar transparent as we style underlying #titlebar with
    * -moz-window-titlebar (Gtk+ theme). */
   :root[tabsintitlebar][sizemode="normal"]:not([inFullscreen]) #TabsToolbar,
   :root[tabsintitlebar][sizemode="maximized"] #TabsToolbar,
   :root[tabsintitlebar] #toolbar-menubar {
     -moz-appearance: none;
   }
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -101,17 +101,16 @@
 .titlebar-buttonbox-container {
   -moz-box-align: center;
 }
 
 /* These would be margin-inline-start/end if it wasn't for the fact that OS X
  * doesn't reverse the order of the items in the titlebar in RTL mode. */
 .titlebar-buttonbox {
   margin-left: 12px;
-  margin-top: calc(-1 * var(--space-above-tabbar));
 }
 
 /* The fullscreen button doesnt show on Yosemite(10.10) or above so dont give it a
    border there */
 @media (-moz-mac-yosemite-theme: 0) {
   .titlebar-spacer[type="fullscreen-button"] {
     margin-right: 4px;
   }
@@ -620,17 +619,17 @@ html|input.urlbar-input {
 }
 
 :-moz-any(.keyboard-focused-tab, .tabbrowser-tab:focus:not([aria-activedescendant])) > .tab-stack > .tab-content > .tab-label-container:not([pinned]),
 :-moz-any(.keyboard-focused-tab, .tabbrowser-tab:focus:not([aria-activedescendant])) > .tab-stack > .tab-content > .tab-icon-image[pinned],
 :-moz-any(.keyboard-focused-tab, .tabbrowser-tab:focus:not([aria-activedescendant])) > .tab-stack > .tab-content > .tab-throbber[pinned] {
   box-shadow: var(--focus-ring-box-shadow);
 }
 
-#TabsToolbar {
+#TabsToolbar > .toolbar-items {
   padding-top: var(--space-above-tabbar);
 }
 
 #TabsToolbar:not(:-moz-lwtheme) {
   color: #333;
   text-shadow: @loweredShadow@;
 }
 
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -1,29 +1,29 @@
 /* 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/. */
 
 %filter substitution
 %define glassActiveBorderColor rgb(37, 44, 51)
 %define glassInactiveBorderColor rgb(102, 102, 102)
 
-@media (-moz-os-version: windows-win7) {
+@media (-moz-os-version: windows-win7),
+       (-moz-os-version: windows-win8) {
   @media (-moz-windows-classic: 0) {
     #main-window[sizemode="normal"] > #navigator-toolbox > #titlebar > #toolbar-menubar:not([autohide="true"]) > #menubar-items,
     #main-window[sizemode="normal"] > #navigator-toolbox > #titlebar > #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar > .toolbar-items {
       margin-top: 1px;
     }
     /**
-     * For all Windows configurations except for Windows Aero and
-     * Windows Aero Basic, the -moz-window-button-box appearance on
-     * the .titlebar-buttonbox adds an unwanted margin at the top of
-     * the button box.
+     * Except for Windows 8, Windows 7 Aero and Windows 7 Aero Basic, the
+     * -moz-window-button-box appearance on the .titlebar-buttonbox adds an
+     * unwanted margin at the top of the button box.
      *
-     * For Windows Aero:
+     * For Windows 8 and Windows Aero (which both use the compositor):
      *   We want the -moz-window-button-box applied in the restored case,
      *   and -moz-window-button-box-maximized in the maximized case.
      *
      * For Windows Aero Basic:
      *   The margin is also unwanted in the maximized case, but we want
      *   it in the restored window case.
      */
     #main-window[sizemode="normal"] .titlebar-buttonbox {
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -96,25 +96,25 @@
 @media (-moz-windows-default-theme) {
   @media not all and (-moz-os-version: windows-win7) {
     #toolbar-menubar:not(:-moz-lwtheme):-moz-window-inactive {
       color: ThreeDShadow;
     }
   }
 }
 
-:root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar,
-:root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar {
+:root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar > .toolbar-items,
+:root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar > .toolbar-items {
   padding-top: var(--space-above-tabbar);
 }
 
 /* Add 4px extra margin on top of the tabs toolbar on Windows 7. */
 @media (-moz-os-version: windows-win7) {
-  :root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar,
-  :root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar {
+  :root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar > .toolbar-items,
+  :root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar > .toolbar-items {
     padding-top: calc(var(--space-above-tabbar) + 4px);
   }
 }
 
 #navigator-toolbox,
 .browser-toolbar {
   -moz-appearance: none;
 }
@@ -324,17 +324,18 @@
    */
   z-index: 1;
 }
 
 .titlebar-buttonbox-container {
   -moz-box-align: stretch;
 }
 
-@media (-moz-os-version: windows-win7) {
+@media (-moz-os-version: windows-win7),
+       (-moz-os-version: windows-win8) {
   /* Preserve window control buttons position at the top of the button box. */
   .titlebar-buttonbox-container {
     -moz-box-align: start;
   }
 }
 
 /* titlebar command buttons */
 
@@ -956,28 +957,16 @@ notification[value="translation"] {
 
 /* Prevent titlebar items (window caption buttons, private browsing indicator,
  * accessibility indicator, etc) from overlapping the nav bar's shadow on the
  * tab bar. */
 #TabsToolbar > .titlebar-item {
   margin-bottom: @navbarTabsShadowSize@;
 }
 
-/* Center titlebar items vertically. */
-:root[sizemode="normal"] #TabsToolbar > .titlebar-item {
-  margin-top: calc(-1 * var(--space-above-tabbar));
-}
-
-/* Compensate for 4px extra margin on top of the tabs toolbar on Windows 7. */
-@media (-moz-os-version: windows-win7) {
-  :root[sizemode="normal"] #TabsToolbar > .titlebar-item {
-    margin-top: calc(-1 * (var(--space-above-tabbar) + 4px));
-  }
-}
-
 :root:not([privatebrowsingmode=temporary]) .accessibility-indicator,
 .private-browsing-indicator {
   margin-inline-end: 12px;
 }
 
 :root:not([accessibilitymode]) .private-browsing-indicator,
 .accessibility-indicator {
   margin-inline-start: 12px;
--- a/browser/themes/windows/share.svg
+++ b/browser/themes/windows/share.svg
@@ -1,7 +1,7 @@
 <!-- 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 16 16" width="16" height="16">
-  <path fill="context-fill" d="M 15.707 4.293 l -4 -4 a 1 1 0 0 0 -1.414 1.414 L 12.585 4 H 11 a 7.008 7.008 0 0 0 -7 7 a 1 1 0 0 0 2 0 a 5.006 5.006 0 0 1 5 -5 h 1.585 l -2.293 2.293 a 1 1 0 1 0 1.414 1.414 l 4 -4 a 1 1 0 0 0 0.001 -1.414 Z" />
-  <path fill="context-fill" d="M 13 11 a 1 1 0 0 0 -1 1 v 1 a 1 1 0 0 1 -1 1 H 3 a 1 1 0 0 1 -1 -1 V 6 a 1 1 0 0 1 1 -1 h 1 a 1 1 0 0 0 0 -2 H 3 a 3 3 0 0 0 -3 3 v 7 a 3 3 0 0 0 3 3 h 8 a 3 3 0 0 0 3 -3 v -1 a 1 1 0 0 0 -1 -1 Z" />
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M 15.707 4.293 l -4 -4 a 1 1 0 0 0 -1.414 1.414 L 12.585 4 H 11 a 7.008 7.008 0 0 0 -7 7 a 1 1 0 0 0 2 0 a 5.006 5.006 0 0 1 5 -5 h 1.585 l -2.293 2.293 a 1 1 0 1 0 1.414 1.414 l 4 -4 a 1 1 0 0 0 0.001 -1.414 Z" />
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M 13 11 a 1 1 0 0 0 -1 1 v 1 a 1 1 0 0 1 -1 1 H 3 a 1 1 0 0 1 -1 -1 V 6 a 1 1 0 0 1 1 -1 h 1 a 1 1 0 0 0 0 -2 H 3 a 3 3 0 0 0 -3 3 v 7 a 3 3 0 0 0 3 3 h 8 a 3 3 0 0 0 3 -3 v -1 a 1 1 0 0 0 -1 -1 Z" />
 </svg>
--- a/build/moz.configure/init.configure
+++ b/build/moz.configure/init.configure
@@ -1031,30 +1031,87 @@ def build_project(include_project_config
     return ret
 
 
 set_config('MOZ_BUILD_APP', build_project)
 set_define('MOZ_BUILD_APP', build_project)
 add_old_configure_assignment('MOZ_BUILD_APP', build_project)
 
 
+# This is temporary until js/src/configure and configure are merged.
+# Use instead of option() in js/moz.configure and more generally, for
+# options that are shared between configure and js/src/configure.
+@template
+def js_option(*args, **kwargs):
+    opt = option(*args, **kwargs)
+
+    @depends(opt.option, build_project, when=kwargs.get('when'))
+    def js_option(value, build_project):
+        if build_project != 'js':
+            return value.format(opt.option)
+
+    add_old_configure_arg(js_option)
+
+
+js_option(env='MOZILLA_OFFICIAL',
+          help='Build an official release')
+
+
+@depends('MOZILLA_OFFICIAL')
+def mozilla_official(official):
+    if official:
+        return True
+
+
+set_config('MOZILLA_OFFICIAL', mozilla_official)
+set_define('MOZILLA_OFFICIAL', mozilla_official)
+add_old_configure_assignment('MOZILLA_OFFICIAL', mozilla_official)
+
+
 # set RELEASE_OR_BETA and NIGHTLY_BUILD variables depending on the cycle we're in
 # The logic works like this:
 # - if we have "a1" in GRE_MILESTONE, we're building Nightly (define NIGHTLY_BUILD)
 # - otherwise, if we have "a" in GRE_MILESTONE, we're building Nightly or Aurora
 # - otherwise, we're building Release/Beta (define RELEASE_OR_BETA)
-@depends(check_build_environment, '--help')
+@depends(check_build_environment, build_project, '--help')
 @imports(_from='__builtin__', _import='open')
+@imports('os')
 @imports('re')
-def milestone(build_env, _):
-    milestone_path = os.path.join(build_env.topsrcdir,
-                                  'config',
-                                  'milestone.txt')
-    with open(milestone_path, 'r') as fh:
-        milestone = fh.read().splitlines()[-1]
+def milestone(build_env, build_project, _):
+    versions = []
+    paths = ['config/milestone.txt']
+    if build_project == 'js':
+        paths = paths * 3
+    else:
+        paths += [
+            'browser/config/version.txt',
+            'browser/config/version_display.txt',
+        ]
+    for f in ('version.txt', 'version_display.txt'):
+        f = os.path.join(build_project, f)
+        if not os.path.exists(os.path.join(build_env.topsrcdir, f)):
+            break
+        paths.append(f)
+
+    for p in paths:
+        with open(os.path.join(build_env.topsrcdir, p), 'r') as fh:
+            content = fh.read().splitlines()
+            if not content:
+                die('Could not find a version number in {}'.format(p))
+            versions.append(content[-1])
+
+    milestone, firefox_version, firefox_version_display = versions[:3]
+
+    # version.txt content from the project directory if there is one, otherwise
+    # the firefox version.
+    app_version = versions[3] if len(versions) > 3 else firefox_version
+    # version_display.txt content from the project directory if there is one,
+    # otherwise version.txt content from the project directory, otherwise the
+    # firefox version for display.
+    app_version_display = versions[-1] if len(versions) > 3 else firefox_version_display
 
     is_nightly = is_release_or_beta = None
 
     if 'a1' in milestone:
         is_nightly = True
     elif 'a' not in milestone:
         is_release_or_beta = True
 
@@ -1066,17 +1123,19 @@ def milestone(build_env, _):
     # patch leve (bugs 572659 and 870868).
     #
     # Only expose major milestone and alpha version in the symbolversion
     # string; as the name suggests, we use it for symbol versioning on Linux.
     return namespace(version=milestone,
                      uaversion='%s.0' % major_version,
                      symbolversion='%s%s' % (major_version, ab_patch),
                      is_nightly=is_nightly,
-                     is_release_or_beta=is_release_or_beta)
+                     is_release_or_beta=is_release_or_beta,
+                     app_version=app_version,
+                     app_version_display=app_version_display)
 
 
 set_config('GRE_MILESTONE', milestone.version)
 set_config('NIGHTLY_BUILD', milestone.is_nightly)
 set_define('NIGHTLY_BUILD', milestone.is_nightly)
 add_old_configure_assignment('NIGHTLY_BUILD', milestone.is_nightly)
 set_config('RELEASE_OR_BETA', milestone.is_release_or_beta)
 set_define('RELEASE_OR_BETA', milestone.is_release_or_beta)
@@ -1086,23 +1145,43 @@ set_define('MOZILLA_VERSION', depends(mi
 set_config('MOZILLA_VERSION', milestone.version)
 set_define('MOZILLA_VERSION_U', milestone.version)
 set_define('MOZILLA_UAVERSION', depends(milestone)(lambda m: '"%s"' % m.uaversion))
 set_config('MOZILLA_SYMBOLVERSION', milestone.symbolversion)
 # JS configure still wants to look at these.
 add_old_configure_assignment('MOZILLA_VERSION', milestone.version)
 add_old_configure_assignment('MOZILLA_SYMBOLVERSION', milestone.symbolversion)
 
-# The app update channel is 'default' when not supplied. The value is used in
-# the application's confvars.sh (and is made available to a project specific
-# moz.configure).
+set_config('MOZ_APP_VERSION', milestone.app_version)
+set_config('MOZ_APP_VERSION_DISPLAY', milestone.app_version_display)
+add_old_configure_assignment('MOZ_APP_VERSION', milestone.app_version)
+
+
+# The app update channel is 'default' when not supplied, and MOZILLA_OFFICIAL
+# is not set. When MOZILLA_OFFICIAL is set, the default is derived from
+# the application display version.
+@depends(milestone, mozilla_official)
+def default_update_channel(milestone, mozilla_official):
+    if not mozilla_official:
+        return 'default'
+    if milestone.is_release_or_beta:
+        if 'esr' in milestone.app_version_display:
+            return 'esr'
+        if 'b' in milestone.app_version_display:
+            return 'beta'
+        return 'release'
+    if milestone.is_nightly:
+        return 'nightly'
+    return 'default'
+
+
 option('--enable-update-channel',
        nargs=1,
        help='Select application update channel',
-       default='default')
+       default=default_update_channel)
 
 
 @depends('--enable-update-channel')
 def update_channel(channel):
     if channel[0] == '':
         return 'default'
     return channel[0].lower()
 
@@ -1182,23 +1261,8 @@ def all_configure_options():
         elif (option.help == 'Help missing for old configure options' and
                 option in __sandbox__._raw_options):
             result.append(__sandbox__._raw_options[option])
 
     return quote(*result)
 
 
 set_config('MOZ_CONFIGURE_OPTIONS', all_configure_options)
-
-
-# This is temporary until js/src/configure and configure are merged.
-# Use instead of option() in js/moz.configure and more generally, for
-# options that are shared between configure and js/src/configure.
-@template
-def js_option(*args, **kwargs):
-    opt = option(*args, **kwargs)
-
-    @depends(opt.option, build_project, when=kwargs.get('when'))
-    def js_option(value, build_project):
-        if build_project != 'js':
-            return value.format(opt.option)
-
-    add_old_configure_arg(js_option)
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -113,26 +113,16 @@ def yasm_asflags(yasm, target):
         if asflags:
             asflags += ['-rnasm', '-pnasm']
         return asflags
 
 
 set_config('YASM_ASFLAGS', yasm_asflags)
 
 
-@depends(yasm_asflags)
-def have_yasm(value):
-    if value:
-        return True
-
-
-set_config('HAVE_YASM', have_yasm)
-# Until the YASM variable is not necessary in old-configure.
-add_old_configure_assignment('YASM', have_yasm)
-
 # nasm detection
 # ==============================================================
 nasm = check_prog('NASM', ['nasm'], allow_missing=True)
 
 
 @depends_if(nasm)
 @checking('nasm version')
 def nasm_version(nasm):
@@ -173,25 +163,33 @@ def nasm_asflags(nasm, target):
                 asflags = ['-f', 'elf32']
             elif target.cpu == 'x86_64':
                 asflags = ['-f', 'elf64']
         return asflags
 
 
 set_config('NASM_ASFLAGS', nasm_asflags)
 
-
 @depends(nasm_asflags)
 def have_nasm(value):
     if value:
         return True
 
 
+@depends(nasm_asflags, yasm_asflags)
+def have_yasm(nasm_asflags, yasm_asflags):
+    if nasm_asflags or yasm_asflags:
+        return True
+
 set_config('HAVE_NASM', have_nasm)
 
+set_config('HAVE_YASM', have_yasm)
+# Until the YASM variable is not necessary in old-configure.
+add_old_configure_assignment('YASM', have_yasm)
+
 # Android NDK
 # ==============================================================
 
 
 @depends('--disable-compile-environment', build_project, '--help')
 def compiling_android(compile_env, build_project, _):
     return compile_env and build_project in ('mobile/android', 'js')
 
--- a/config/external/icu/data/moz.build
+++ b/config/external/icu/data/moz.build
@@ -11,16 +11,17 @@ Library('icudata')
 if CONFIG['OS_ARCH'] == 'WINNT':
     if CONFIG['CPU_ARCH'] == 'x86':
         ASFLAGS += ['-DPREFIX']
 elif CONFIG['OS_ARCH'] == 'Darwin':
     ASFLAGS += ['-DPREFIX']
 
 data_symbol = 'icudt%s_dat' % CONFIG['MOZ_ICU_VERSION']
 asflags = [
+    '-I%s/config/external/icu/data/' % TOPSRCDIR,
     '-DICU_DATA_FILE="%s"' % CONFIG['ICU_DATA_FILE'],
     '-DICU_DATA_SYMBOL=%s' % data_symbol,
 ]
 LOCAL_INCLUDES += ['.']
 
 if CONFIG['OS_TARGET'] == 'WINNT' and CONFIG['CPU_ARCH'] == 'aarch64':
     icudata = 'icudata.asm'
     GENERATED_FILES += [icudata]
--- a/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
+++ b/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
@@ -30,17 +30,17 @@ const {
   REQUEST_TABS_START,
   REQUEST_TABS_SUCCESS,
   REQUEST_WORKERS_FAILURE,
   REQUEST_WORKERS_START,
   REQUEST_WORKERS_SUCCESS,
   RUNTIMES,
 } = require("../constants");
 
-function inspectDebugTarget(type, id) {
+function inspectDebugTarget({ type, id, front }) {
   return async (_, getState) => {
     const runtime = getCurrentRuntime(getState().runtimes);
     const { runtimeDetails, type: runtimeType } = runtime;
 
     switch (type) {
       case DEBUG_TARGETS.TAB: {
         // Open tab debugger in new window.
         if (runtimeType === RUNTIMES.NETWORK || runtimeType === RUNTIMES.USB) {
@@ -60,17 +60,17 @@ function inspectDebugTarget(type, id) {
           await debugRemoteAddon(id, devtoolsClient);
         } else if (runtimeType === RUNTIMES.THIS_FIREFOX) {
           debugLocalAddon(id);
         }
         break;
       }
       case DEBUG_TARGETS.WORKER: {
         // Open worker toolbox in new window.
-        gDevToolsBrowser.openWorkerToolbox(runtimeDetails.client, id);
+        gDevToolsBrowser.openWorkerToolbox(front);
         break;
       }
 
       default: {
         console.error("Failed to inspect the debug target of " +
                       `type: ${ type } id: ${ id }`);
       }
     }
--- a/devtools/client/aboutdebugging-new/src/components/debugtarget/InspectAction.js
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/InspectAction.js
@@ -21,17 +21,17 @@ class InspectAction extends PureComponen
     return {
       dispatch: PropTypes.func.isRequired,
       target: PropTypes.object.isRequired,
     };
   }
 
   inspect() {
     const { dispatch, target } = this.props;
-    dispatch(Actions.inspectDebugTarget(target.type, target.id));
+    dispatch(Actions.inspectDebugTarget(target));
   }
 
   render() {
     return Localized(
       {
         id: "about-debugging-debug-target-inspect-button",
       },
       dom.button(
--- a/devtools/client/aboutdebugging-new/src/middleware/worker-component-data.js
+++ b/devtools/client/aboutdebugging-new/src/middleware/worker-component-data.js
@@ -37,36 +37,37 @@ function getServiceWorkerStatus(isActive
   // We cannot get service worker registrations unless the registration is in
   // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
   // display a custom state "registering" for now. See Bug 1153292.
   return SERVICE_WORKER_STATUSES.REGISTERING;
 }
 
 function toComponentData(workers, isServiceWorker) {
   return workers.map(worker => {
+    // Here `worker` is the worker object created by RootFront.listAllWorkers
     const type = DEBUG_TARGETS.WORKER;
-    const id = worker.workerTargetActor;
+    const front = worker.workerTargetFront;
     const icon = "chrome://devtools/skin/images/debugging-workers.svg";
     let { fetch, name, registrationActor, scope } = worker;
     let isActive = false;
     let isRunning = false;
     let status = null;
 
     if (isServiceWorker) {
       fetch = fetch ? SERVICE_WORKER_FETCH_STATES.LISTENING
                     : SERVICE_WORKER_FETCH_STATES.NOT_LISTENING;
       isActive = worker.active;
-      isRunning = !!worker.workerTargetActor;
+      isRunning = !!worker.workerTargetFront;
       status = getServiceWorkerStatus(isActive, isRunning);
     }
 
     return {
       name,
       icon,
-      id,
+      front,
       type,
       details: {
         fetch,
         isActive,
         isRunning,
         registrationActor,
         scope,
         status,
--- a/devtools/client/aboutdebugging/components/workers/Panel.js
+++ b/devtools/client/aboutdebugging/components/workers/Panel.js
@@ -51,20 +51,20 @@ class WorkersPanel extends Component {
 
     this.state = this.initialState;
   }
 
   componentDidMount() {
     const client = this.props.client;
     // When calling RootFront.listAllWorkers, ContentProcessTargetActor are created
     // for each content process, which sends `workerListChanged` events.
-    // Until we create a Front for ContentProcessTargetActor, we should listen for these
-    // event on DebuggerClient. After that, we have to listen on the related fronts
-    // directly.
-    client.addListener("workerListChanged", this.updateWorkers);
+    client.mainRoot.onFront("contentProcessTarget", front => {
+      front.on("workerListChanged", this.updateWorkers);
+      this.state.contentProcessFronts.push(front);
+    });
     client.mainRoot.on("workerListChanged", this.updateWorkers);
 
     client.mainRoot.on("serviceWorkerRegistrationListChanged", this.updateWorkers);
     client.mainRoot.on("processListChanged", this.updateWorkers);
     client.addListener("registration-changed", this.updateWorkers);
 
     // Some notes about these observers:
     // - nsIPrefBranch.addObserver observes prefixes. In reality, watching
@@ -85,31 +85,37 @@ class WorkersPanel extends Component {
     this.updateWorkers();
   }
 
   componentWillUnmount() {
     const client = this.props.client;
     client.mainRoot.off("processListChanged", this.updateWorkers);
     client.mainRoot.off("serviceWorkerRegistrationListChanged", this.updateWorkers);
     client.mainRoot.off("workerListChanged", this.updateWorkers);
-    client.removeListener("workerListChanged", this.updateWorkers);
+    for (const front of this.state.contentProcessFronts) {
+      front.off("workerListChanged", this.updateWorkers);
+    }
     client.removeListener("registration-changed", this.updateWorkers);
 
     Services.prefs.removeObserver(PROCESS_COUNT_PREF, this.updateMultiE10S);
     Services.prefs.removeObserver(MULTI_OPTOUT_PREF, this.updateMultiE10S);
   }
 
   get initialState() {
     return {
       workers: {
         service: [],
         shared: [],
         other: [],
       },
       processCount: 1,
+
+      // List of ContentProcessTargetFront registered from componentWillMount
+      // from which we listen for worker list changes
+      contentProcessFronts: [],
     };
   }
 
   updateMultiE10S() {
     // We watch the pref but set the state based on
     // nsIXULRuntime.maxWebProcessCount.
     const processCount = Services.appinfo.maxWebProcessCount;
     this.setState({ processCount });
--- a/devtools/client/aboutdebugging/components/workers/ServiceWorkerTarget.js
+++ b/devtools/client/aboutdebugging/components/workers/ServiceWorkerTarget.js
@@ -28,17 +28,17 @@ class ServiceWorkerTarget extends Compon
         active: PropTypes.bool,
         fetch: PropTypes.bool.isRequired,
         icon: PropTypes.string,
         name: PropTypes.string.isRequired,
         url: PropTypes.string,
         scope: PropTypes.string.isRequired,
         // registrationActor can be missing in e10s.
         registrationActor: PropTypes.string,
-        workerTargetActor: PropTypes.string,
+        workerTargetFront: PropTypes.object,
       }).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
@@ -80,33 +80,30 @@ class ServiceWorkerTarget extends Compon
   }
 
   debug() {
     if (!this.isRunning()) {
       // If the worker is not running, we can't debug it.
       return;
     }
 
-    const { client, target } = this.props;
-    gDevToolsBrowser.openWorkerToolbox(client, target.workerTargetActor);
+    const { workerTargetFront } = this.props.target;
+    gDevToolsBrowser.openWorkerToolbox(workerTargetFront);
   }
 
   push() {
     if (!this.isActive() || !this.isRunning()) {
       // If the worker is not running, we can't push to it.
       // If the worker is not active, the registration might be unavailable and the
       // push will not succeed.
       return;
     }
 
-    const { client, target } = this.props;
-    client.request({
-      to: target.workerTargetActor,
-      type: "push",
-    });
+    const { workerTargetFront } = this.props.target;
+    workerTargetFront.push();
   }
 
   start() {
     if (!this.isActive() || this.isRunning()) {
       // If the worker is not active or if it is already running, we can't start it.
       return;
     }
 
@@ -144,17 +141,17 @@ class ServiceWorkerTarget extends Compon
       type: "getPushSubscription",
     }, ({ subscription }) => {
       this.setState({ pushSubscription: subscription });
     });
   }
 
   isRunning() {
     // We know the target is running if it has a worker actor.
-    return !!this.props.target.workerTargetActor;
+    return !!this.props.target.workerTargetFront;
   }
 
   isActive() {
     return this.props.target.active;
   }
 
   getServiceWorkerStatus() {
     if (this.isActive() && this.isRunning()) {
--- a/devtools/client/aboutdebugging/components/workers/Target.js
+++ b/devtools/client/aboutdebugging/components/workers/Target.js
@@ -22,29 +22,29 @@ const Strings = Services.strings.createB
 class WorkerTarget extends Component {
   static get propTypes() {
     return {
       client: PropTypes.instanceOf(DebuggerClient).isRequired,
       debugDisabled: PropTypes.bool,
       target: PropTypes.shape({
         icon: PropTypes.string,
         name: PropTypes.string.isRequired,
-        workerTargetActor: PropTypes.string,
+        workerTargetFront: PropTypes.object,
       }).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.debug = this.debug.bind(this);
   }
 
   debug() {
-    const { client, target } = this.props;
-    gDevToolsBrowser.openWorkerToolbox(client, target.workerTargetActor);
+    const { workerTargetFront } = this.props.target;
+    gDevToolsBrowser.openWorkerToolbox(workerTargetFront);
   }
 
   render() {
     const { target, debugDisabled } = this.props;
 
     return dom.li({ className: "target-container" },
       dom.img({
         className: "target-icon",
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
@@ -40,30 +40,38 @@ add_task(async function testWebExtension
   } = await setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, addonFile);
 
   // Be careful, this JS function is going to be executed in the addon toolbox,
   // which lives in another process. So do not try to use any scope variable!
   const env = Cc["@mozilla.org/process/environment;1"]
         .getService(Ci.nsIEnvironment);
   const testScript = function() {
     /* eslint-disable no-undef */
-    toolbox.selectTool("inspector").then(async inspector => {
-      const nodeActor = await inspector.walker.querySelector(
-        inspector.walker.rootNode, "body");
+    // This is webextension toolbox process. So we can't access mochitest framework.
+    const waitUntil = async function(predicate, interval = 10) {
+      if (await predicate()) {
+        return true;
+      }
+      return new Promise(resolve => {
+        toolbox.win.setTimeout(function() {
+          waitUntil(predicate, interval).then(() => resolve(true));
+        }, interval);
+      });
+    };
 
-      if (!nodeActor) {
-        throw new Error("nodeActor not found");
-      }
+    toolbox.selectTool("inspector").then(async inspector => {
+      let nodeActor;
 
-      if (!(nodeActor.inlineTextChild)) {
-        throw new Error("inlineTextChild not found");
-      }
+      dump(`Wait the fallback window to be fully loaded\n`);
+      await waitUntil(async () => {
+        nodeActor = await inspector.walker.querySelector(inspector.walker.rootNode, "h1");
+        return nodeActor && nodeActor.inlineTextChild;
+      });
 
       dump("Got a nodeActor with an inline text child\n");
-
       const expectedValue = "Your addon does not have any document opened yet.";
       const actualValue = nodeActor.inlineTextChild._form.nodeValue;
 
       if (actualValue !== expectedValue) {
         throw new Error(
           `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
         );
       }
--- a/devtools/client/application/src/components/Worker.js
+++ b/devtools/client/application/src/components/Worker.js
@@ -30,17 +30,17 @@ class Worker extends Component {
       client: PropTypes.instanceOf(DebuggerClient).isRequired,
       debugDisabled: PropTypes.bool,
       worker: PropTypes.shape({
         active: PropTypes.bool,
         name: PropTypes.string.isRequired,
         scope: PropTypes.string.isRequired,
         // registrationActor can be missing in e10s.
         registrationActor: PropTypes.string,
-        workerTargetActor: PropTypes.string,
+        workerTargetFront: PropTypes.object,
       }).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.debug = this.debug.bind(this);
@@ -49,18 +49,18 @@ class Worker extends Component {
   }
 
   debug() {
     if (!this.isRunning()) {
       console.log("Service workers cannot be debugged if they are not running");
       return;
     }
 
-    const { client, worker } = this.props;
-    gDevToolsBrowser.openWorkerToolbox(client, worker.workerTargetActor);
+    const { workerTargetFront } = this.props.worker;
+    gDevToolsBrowser.openWorkerToolbox(workerTargetFront);
   }
 
   start() {
     if (!this.isActive() || this.isRunning()) {
       console.log("Running or inactive service workers cannot be started");
       return;
     }
 
@@ -76,17 +76,17 @@ class Worker extends Component {
     client.request({
       to: worker.registrationActor,
       type: "unregister",
     });
   }
 
   isRunning() {
     // We know the worker is running if it has a worker actor.
-    return !!this.props.worker.workerTargetActor;
+    return !!this.props.worker.workerTargetFront;
   }
 
   isActive() {
     return this.props.worker.active;
   }
 
   getServiceWorkerStatus() {
     if (this.isActive() && this.isRunning()) {
--- a/devtools/client/debugger/new/panel.js
+++ b/devtools/client/debugger/new/panel.js
@@ -1,16 +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/. */
 "use strict";
 
 const { Task } = require("devtools/shared/task");
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const { gDevTools } = require("devtools/client/framework/devtools");
+const { gDevToolsBrowser } = require("devtools/client/framework/devtools-browser");
 const { TargetFactory } = require("devtools/client/framework/target");
 const { Toolbox } = require("devtools/client/framework/toolbox");
 loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
 const L10N = new LocalizationHelper(DBG_STRINGS_URI);
 
 function DebuggerPanel(iframeWindow, toolbox) {
@@ -62,22 +63,18 @@ DebuggerPanel.prototype = {
   _getState: function() {
     return this._store.getState();
   },
 
   openLink: function(url) {
     openContentLink(url);
   },
 
-  openWorkerToolbox: async function(worker) {
-    const [response, workerTargetFront] =
-      await this.toolbox.target.client.attachWorker(worker.actor);
-    const workerTarget = TargetFactory.forWorker(workerTargetFront);
-    const toolbox = await gDevTools.showToolbox(workerTarget, "jsdebugger", Toolbox.HostType.WINDOW);
-    toolbox.once("destroy", () => workerTargetFront.detach());
+  openWorkerToolbox: function(workerTargetFront) {
+    return gDevToolsBrowser.openWorkerToolbox(workerTargetFront, "jsdebugger");
   },
 
   getFrames: function() {
     let frames = this._selectors.getFrames(this._getState());
 
     // Frames is null when the debugger is not paused.
     if (!frames) {
       return {
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Workers.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Workers.js
@@ -11,17 +11,17 @@ import "./Workers.css";
 import actions from "../../actions";
 import { getWorkers } from "../../selectors";
 import { basename } from "../../utils/path";
 import type { Worker } from "../../types";
 
 export class Workers extends PureComponent {
   props: {
     workers: List<Worker>,
-    openWorkerToolbox: string => void
+    openWorkerToolbox: object => void
   };
 
   renderWorkers(workers) {
     const { openWorkerToolbox } = this.props;
     return workers.map(worker => (
       <div
         className="worker"
         key={worker.actor}
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-chrome-debugging.js
@@ -23,22 +23,16 @@ function initDebuggerClient() {
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
   DebuggerServer.allowChromeProcess = true;
 
   let transport = DebuggerServer.connectPipe();
   return new DebuggerClient(transport);
 }
 
-async function attachThread(client, actor) {
-  let [response, targetFront] = await client.attachTarget(actor);
-  let [response2, threadClient] = await targetFront.attachThread(null);
-  return threadClient;
-}
-
 function onNewSource(event, packet) {
   if (packet.source.url.startsWith("chrome:")) {
     ok(true, "Received a new chrome source: " + packet.source.url);
     gThreadClient.removeListener("newSource", onNewSource);
     gNewChromeSource.resolve();
   }
 }
 
@@ -58,19 +52,20 @@ registerCleanupFunction(function() {
 });
 
 add_task(async function() {
   gClient = initDebuggerClient();
 
   const [type] = await gClient.connect();
   is(type, "browser", "Root actor should identify itself as a browser.");
 
-  const response = await gClient.mainRoot.getMainProcess();
-  let actor = response.form.actor;
-  gThreadClient = await attachThread(gClient, actor);
+  const front = await gClient.mainRoot.getMainProcess();
+  await front.attach();
+  const [, threadClient] = await front.attachThread();
+  gThreadClient = threadClient;
   gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
 
   // listen for a new source and global
   gThreadClient.addListener("newSource", onNewSource);
   await gNewChromeSource.promise;
 
   await resumeAndCloseConnection();
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_promises-chrome-allocation-stack.js
@@ -24,18 +24,18 @@ function test() {
     requestLongerTimeout(10);
 
     DebuggerServer.init();
     DebuggerServer.registerAllActors();
     DebuggerServer.allowChromeProcess = true;
 
     let client = new DebuggerClient(DebuggerServer.connectPipe());
     yield connect(client);
-    let chrome = yield client.mainRoot.getMainProcess();
-    let [, targetFront] = yield attachTarget(client, chrome.form);
+    let targetFront = yield client.mainRoot.getMainProcess();
+    yield targetFront.attach();
     yield targetFront.attachThread();
 
     yield testGetAllocationStack(client, chrome.form, () => {
       let p = new Promise(() => {});
       p.name = "p";
       let q = p.then();
       q.name = "q";
       let r = p.catch(() => {});
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -946,21 +946,16 @@ function findWorker(workers, url) {
   for (let worker of workers) {
     if (worker.url === url) {
       return worker;
     }
   }
   return null;
 }
 
-function attachWorker(targetFront, worker) {
-  info("Attaching to worker with url '" + worker.url + "'.");
-  return targetFront.attachWorker(worker.actor);
-}
-
 function waitForWorkerListChanged(targetFront) {
   info("Waiting for worker list to change.");
   return targetFront.once("workerListChanged");
 }
 
 function attachThread(workerTargetFront, options) {
   info("Attaching to thread.");
   return workerTargetFront.attachThread(options);
@@ -1130,18 +1125,17 @@ async function initWorkerDebugger(TAB_UR
 
   let tab = await addTab(TAB_URL);
   let { tabs } = await listTabs(client);
   let [, targetFront] = await attachTarget(client, findTab(tabs, TAB_URL));
 
   await createWorkerInTab(tab, WORKER_URL);
 
   let { workers } = await listWorkers(targetFront);
-  let [, workerTargetFront] = await attachWorker(targetFront,
-                                             findWorker(workers, WORKER_URL));
+  let workerTargetFront = findWorker(workers, WORKER_URL);
 
   let toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerTargetFront),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   let debuggerPanel = toolbox.getCurrentPanel();
   let gDebugger = debuggerPanel.panelWin;
 
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -274,17 +274,18 @@ Tools.memory = {
   icon: "chrome://devtools/skin/images/tool-memory.svg",
   url: "chrome://devtools/content/memory/index.xhtml",
   visibilityswitch: "devtools.memory.enabled",
   label: l10n("memory.label"),
   panelLabel: l10n("memory.panelLabel"),
   tooltip: l10n("memory.tooltip"),
 
   isTargetSupported: function(target) {
-    return target.getTrait("heapSnapshots") && !target.isAddon;
+    return target.getTrait("heapSnapshots") && !target.isAddon
+      && !target.isWorkerTarget;
   },
 
   build: function(frame, target) {
     return new MemoryPanel(frame, target);
   },
 };
 
 Tools.netMonitor = {
@@ -299,17 +300,17 @@ Tools.netMonitor = {
   get tooltip() {
     return l10n("netmonitor.tooltip2",
     (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") +
     l10n("netmonitor.commandkey"));
   },
   inMenu: true,
 
   isTargetSupported: function(target) {
-    return target.getTrait("networkMonitor");
+    return target.getTrait("networkMonitor") && !target.isWorkerTarget;
   },
 
   build: function(iframeWindow, toolbox) {
     return new NetMonitorPanel(iframeWindow, toolbox);
   },
 };
 
 Tools.storage = {
--- a/devtools/client/framework/connect/connect.js
+++ b/devtools/client/framework/connect/connect.js
@@ -124,18 +124,18 @@ var onConnectionReady = async function([
   // Build the Remote Process button
   // If Fx<39, chrome target actors were used to be exposed on RootActor
   // but in Fx>=39, chrome is debuggable via getProcess() and ParentProcessTargetActor
   if (globals.consoleActor || gClient.mainRoot.traits.allowChromeProcess) {
     const a = document.createElement("a");
     a.onclick = function() {
       if (gClient.mainRoot.traits.allowChromeProcess) {
         gClient.mainRoot.getMainProcess()
-               .then(aResponse => {
-                 openToolbox(aResponse.form, true);
+               .then(front => {
+                 openToolbox(null, true, null, front);
                });
       } else if (globals.consoleActor) {
         openToolbox(globals, true, "webconsole", false);
       }
     };
     a.title = a.textContent = L10N.getStr("mainProcess");
     a.className = "remote-process";
     a.href = "#";
@@ -217,21 +217,22 @@ function showError(type) {
 function handleConnectionTimeout() {
   showError("timeout");
 }
 
 /**
  * The user clicked on one of the buttons.
  * Opens the toolbox.
  */
-function openToolbox(form, chrome = false, tool = "webconsole") {
+function openToolbox(form, chrome = false, tool = "webconsole", activeTab = null) {
   const options = {
-    form: form,
+    form,
+    activeTab,
     client: gClient,
-    chrome: chrome,
+    chrome,
   };
   TargetFactory.forRemoteTab(options).then((target) => {
     const hostType = Toolbox.HostType.WINDOW;
     gDevTools.showToolbox(target, tool, hostType).then((toolbox) => {
       toolbox.once("destroyed", function() {
         gClient.close();
       });
     }, console.error);
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -313,20 +313,18 @@ var gDevToolsBrowser = exports.gDevTools
     DebuggerServer.init();
     DebuggerServer.registerAllActors();
     DebuggerServer.allowChromeProcess = true;
 
     const transport = DebuggerServer.connectPipe();
     const client = new DebuggerClient(transport);
 
     await client.connect();
-    const { form } = await client.mainRoot.getProcess(processId);
-    const front = await client.mainRoot.attachContentProcessTarget(form);
+    const front = await client.mainRoot.getProcess(processId);
     const options = {
-      form,
       activeTab: front,
       client,
       chrome: true,
     };
     const target = await TargetFactory.forRemoteTab(options);
     // Ensure closing the connection in order to cleanup
     // the debugger client and also the server created in the
     // content process
@@ -371,24 +369,24 @@ var gDevToolsBrowser = exports.gDevTools
     Services.prompt.alert(null, "", msg);
     return Promise.reject(msg);
   },
 
   /**
    * Open a window-hosted toolbox to debug the worker associated to the provided
    * worker actor.
    *
-   * @param  {DebuggerClient} client
-   * @param  {Object} workerTargetActor
-   *         worker actor form to debug
+   * @param  {WorkerTargetFront} workerTargetFront
+   *         worker actor front to debug
+   * @param  {String} toolId (optional)
+   *        The id of the default tool to show
    */
-  async openWorkerToolbox(client, workerTargetActor) {
-    const [, workerTargetFront] = await client.attachWorker(workerTargetActor);
+  async openWorkerToolbox(workerTargetFront, toolId) {
     const workerTarget = TargetFactory.forWorker(workerTargetFront);
-    const toolbox = await gDevTools.showToolbox(workerTarget, null, Toolbox.HostType.WINDOW);
+    const toolbox = await gDevTools.showToolbox(workerTarget, toolId, Toolbox.HostType.WINDOW);
     toolbox.once("destroy", () => workerTargetFront.detach());
   },
 
   /**
    * Install WebIDE widget
    */
   // Used by itself
   installWebIDEWidget() {
--- a/devtools/client/framework/target-from-url.js
+++ b/devtools/client/framework/target-from-url.js
@@ -46,17 +46,17 @@ exports.targetFromURL = async function t
   if (!type) {
     throw new Error("targetFromURL, missing type parameter");
   }
   let id = params.get("id");
   // Allows to spawn a chrome enabled target for any context
   // (handy to debug chrome stuff in a content process)
   let chrome = params.has("chrome");
 
-  let form;
+  let form, front;
   if (type === "tab") {
     // Fetch target for a remote tab
     id = parseInt(id, 10);
     if (isNaN(id)) {
       throw new Error(`targetFromURL, wrong tab id '${id}', should be a number`);
     }
     try {
       const response = await client.getTab({ outerWindowID: id });
@@ -70,18 +70,17 @@ exports.targetFromURL = async function t
   } else if (type == "process") {
     // Fetch target for a remote chrome actor
     DebuggerServer.allowChromeProcess = true;
     try {
       id = parseInt(id, 10);
       if (isNaN(id)) {
         id = 0;
       }
-      const response = await client.mainRoot.getProcess(id);
-      form = response.form;
+      front = await client.mainRoot.getProcess(id);
       chrome = true;
     } catch (ex) {
       if (ex.error == "noProcess") {
         throw new Error(`targetFromURL, process with id '${id}' doesn't exist`);
       }
       throw ex;
     }
   } else if (type == "window") {
@@ -102,17 +101,17 @@ exports.targetFromURL = async function t
         throw new Error(`targetFromURL, window with id '${id}' doesn't exist`);
       }
       throw ex;
     }
   } else {
     throw new Error(`targetFromURL, unsupported type '${type}' parameter`);
   }
 
-  return TargetFactory.forRemoteTab({ client, form, chrome });
+  return TargetFactory.forRemoteTab({ client, form, activeTab: front, chrome });
 };
 
 /**
  * Create a DebuggerClient for a given URL object having various query parameters:
  *
  * host:
  *    {String} The hostname or IP address to connect to.
  * port:
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -123,28 +123,16 @@ const TargetFactory = exports.TargetFact
     return targetPromise;
   },
 
   forWorker: function(workerTargetFront) {
     let target = targets.get(workerTargetFront);
     if (target == null) {
       target = new Target({
         client: workerTargetFront.client,
-        // Fake a form attribute until all Target is merged with the Front itself
-        // and will receive form attribute natively.
-        get form() {
-          return {
-            actor: workerTargetFront.actorID,
-            traits: {},
-            // /!\ This depends on WorkerTargetFront.attach being called before this.
-            // It happens that WorkerTargetFront is instantiated from attachWorker,
-            // which instiate this class *and* calls `attach`.
-            consoleActor: workerTargetFront.consoleActor,
-          };
-        },
         activeTab: workerTargetFront,
         chrome: false,
       });
       targets.set(workerTargetFront, target);
     }
     return target;
   },
 
@@ -195,38 +183,39 @@ const TargetFactory = exports.TargetFact
  * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
  * as well as firefox parent process, or just one of its content process.
  * A Target is related to a given TargetActor, for which we pass the form as
  * argument.
  *
  * For now, only workers are having a distinct Target class called WorkerTarget.
  *
  * @param {Object} form
- *                 The TargetActor's form to be connected to.
+ *                  The TargetActor's form to be connected to. Null if front is passed.
+ * @param {Front} activeTab
+ *                  If we already have a front for this target, pass it here. Null if
+ *                  form is passed.
  * @param {DebuggerClient} client
- *                 The DebuggerClient instance to be used to debug this target.
+ *                  The DebuggerClient instance to be used to debug this target.
  * @param {Boolean} chrome
  *                  True, if we allow to see privileged resources like JSM, xpcom,
  *                  frame scripts...
- * @param {Front}   activeTab (optional)
- *                  If we already have a front for this target, pass it here.
  * @param {xul:tab} tab (optional)
  *                  If the target is a local Firefox tab, a reference to the firefox
  *                  frontend tab object.
  */
 function Target({ form, client, chrome, activeTab = null, tab = null }) {
   EventEmitter.decorate(this);
   this.destroy = this.destroy.bind(this);
   this._onTabNavigated = this._onTabNavigated.bind(this);
   this.activeConsole = null;
   this.activeTab = activeTab;
 
   this._form = form;
-  this._url = form.url;
-  this._title = form.title;
+  this._url = this.form.url;
+  this._title = this.form.title;
 
   this._client = client;
   this._chrome = chrome;
 
   // When debugging local tabs, we also have a reference to the Firefox tab
   // This is used to:
   // * distinguish local tabs from remote (see target.isLocalTab)
   // * being able to hookup into Firefox UI (see Hosts)
@@ -242,18 +231,18 @@ function Target({ form, client, chrome, 
   // * xpcshell debugging (it uses ParentProcessTargetActor, which inherits from
   //                       BrowsingContextActor, but doesn't have any valid browsing
   //                       context to attach to.)
   // Starting with FF64, BrowsingContextTargetActor exposes a traits to help identify
   // the target actors inheriting from it. It also help identify the xpcshell debugging
   // target actor that doesn't have any valid browsing context.
   // (Once FF63 is no longer supported, we can remove the `else` branch and only look
   // for the traits)
-  if (this._form.traits && ("isBrowsingContext" in this._form.traits)) {
-    this._isBrowsingContext = this._form.traits.isBrowsingContext;
+  if (this.form.traits && ("isBrowsingContext" in this.form.traits)) {
+    this._isBrowsingContext = this.form.traits.isBrowsingContext;
   } else {
     this._isBrowsingContext = !this.isLegacyAddon && !this.isContentProcess && !this.isWorkerTarget;
   }
 
   // Cache of already created targed-scoped fronts
   // [typeName:string => Front instance]
   this.fronts = new Map();
   // Temporary fix for bug #1493131 - inspector has a different life cycle
@@ -270,18 +259,16 @@ Target.prototype = {
    * internally with `target.actorHasMethod`. Takes advantage of caching if
    * definition was fetched previously with the corresponding actor information.
    * Actors are lazily loaded, so not only must the tool using a specific actor
    * be in use, the actors are only registered after invoking a method (for
    * performance reasons, added in bug 988237), so to use these actor detection
    * methods, one must already be communicating with a specific actor of that
    * type.
    *
-   * Must be a remote target.
-   *
    * @return {Promise}
    * {
    *   "category": "actor",
    *   "typeName": "longstractor",
    *   "methods": [{
    *     "name": "substring",
    *     "request": {
    *       "type": "substring",
@@ -310,34 +297,34 @@ Target.prototype = {
     }
     const description = await this.client.mainRoot.protocolDescription();
     this._protocolDescription = description;
     return description.types[actorName];
   },
 
   /**
    * Returns a boolean indicating whether or not the specific actor
-   * type exists. Must be a remote target.
+   * type exists.
    *
    * @param {String} actorName
    * @return {Boolean}
    */
   hasActor: function(actorName) {
     if (this.form) {
       return !!this.form[actorName + "Actor"];
     }
     return false;
   },
 
   /**
    * Queries the protocol description to see if an actor has
    * an available method. The actor must already be lazily-loaded (read
    * the restrictions in the `getActorDescription` comments),
    * so this is for use inside of tool. Returns a promise that
-   * resolves to a boolean. Must be a remote target.
+   * resolves to a boolean.
    *
    * @param {String} actorName
    * @param {String} methodName
    * @return {Promise}
    */
   actorHasMethod: function(actorName, methodName) {
     return this.getActorDescription(actorName).then(desc => {
       if (desc && desc.methods) {
@@ -363,17 +350,19 @@ Target.prototype = {
     return this.client.traits[traitName];
   },
 
   get tab() {
     return this._tab;
   },
 
   get form() {
-    return this._form;
+    // Target constructor either receive a form or a Front.
+    // If a front is passed, fetch the form from it.
+    return this._form || this.activeTab.targetForm;
   },
 
   // Get a promise of the RootActor's form
   get root() {
     return this.client.mainRoot.rootForm;
   },
 
   // Temporary fix for bug #1493131 - inspector has a different life cycle
@@ -428,17 +417,17 @@ Target.prototype = {
   // interface and requires to call `attach` request before being used and
   // `detach` during cleanup.
   get isBrowsingContext() {
     return this._isBrowsingContext;
   },
 
   get name() {
     if (this.isAddon) {
-      return this._form.name;
+      return this.form.name;
     }
     return this._title;
   },
 
   get url() {
     return this._url;
   },
 
@@ -446,34 +435,34 @@ Target.prototype = {
     return this.isLegacyAddon || this.isWebExtension;
   },
 
   get isWorkerTarget() {
     return this.activeTab && this.activeTab.typeName === "workerTarget";
   },
 
   get isLegacyAddon() {
-    return !!(this._form && this._form.actor &&
-      this._form.actor.match(/conn\d+\.addon(Target)?\d+/));
+    return !!(this.form && this.form.actor &&
+      this.form.actor.match(/conn\d+\.addon(Target)?\d+/));
   },
 
   get isWebExtension() {
-    return !!(this._form && this._form.actor && (
-      this._form.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
-      this._form.actor.match(/child\d+\/webExtension(Target)?\d+/)
+    return !!(this.form && this.form.actor && (
+      this.form.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
+      this.form.actor.match(/child\d+\/webExtension(Target)?\d+/)
     ));
   },
 
   get isContentProcess() {
     // browser content toolbox's form will be of the form:
     //   server0.conn0.content-process0/contentProcessTarget7
     // while xpcshell debugging will be:
     //   server1.conn0.contentProcessTarget7
-    return !!(this._form && this._form.actor &&
-      this._form.actor.match(/conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/));
+    return !!(this.form && this.form.actor &&
+      this.form.actor.match(/conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/));
   },
 
   get isLocalTab() {
     return !!this._tab;
   },
 
   get isMultiProcess() {
     return !this.window;
@@ -529,67 +518,82 @@ Target.prototype = {
    */
   attach() {
     if (this._attach) {
       return this._attach;
     }
 
     // Attach the target actor
     const attachBrowsingContextTarget = async () => {
-      const [, targetFront] = await this._client.attachTarget(this._form.actor);
-      this.activeTab = targetFront;
+      // Some BrowsingContextTargetFront are already instantiated and passed as
+      // contructor's argument, like for ParentProcessTargetActor.
+      // For them, we only need to attach them.
+      // The call to attachTarget is to be removed once all Target are having a front
+      // passed as contructor's argument.
+      if (!this.activeTab) {
+        const [, targetFront] = await this._client.attachTarget(this.form.actor);
+        this.activeTab = targetFront;
+      } else {
+        await this.activeTab.attach();
+      }
 
       this.activeTab.on("tabNavigated", this._onTabNavigated);
       this._onFrameUpdate = packet => {
         this.emit("frame-update", packet);
       };
       this.activeTab.on("frameUpdate", this._onFrameUpdate);
     };
 
     // Attach the console actor
     const attachConsole = async () => {
       const [, consoleClient] = await this._client.attachConsole(
-        this._form.consoleActor, []);
+        this.form.consoleActor, []);
       this.activeConsole = consoleClient;
 
       this._onInspectObject = packet => this.emit("inspect-object", packet);
       this.activeConsole.on("inspectObject", this._onInspectObject);
     };
 
     this._attach = (async () => {
-      if (this._form.isWebExtension &&
+      if (this.form.isWebExtension &&
           this.client.mainRoot.traits.webExtensionAddonConnect) {
         // The addonTargetActor form is related to a WebExtensionActor instance,
         // which isn't a target actor on its own, it is an actor living in the parent
         // process with access to the addon metadata, it can control the addon (e.g.
         // reloading it) and listen to the AddonManager events related to the lifecycle of
         // the addon (e.g. when the addon is disabled or uninstalled).
         // To retrieve the target actor instance, we call its "connect" method, (which
         // fetches the target actor form from a WebExtensionTargetActor instance).
         const {form} = await this._client.request({
-          to: this._form.actor, type: "connect",
+          to: this.form.actor, type: "connect",
         });
 
         this._form = form;
-        this._url = form.url;
-        this._title = form.title;
+        this._url = this.form.url;
+        this._title = this.form.title;
       }
 
       // AddonTargetActor and ContentProcessTargetActor don't inherit from
       // BrowsingContextTargetActor (i.e. this.isBrowsingContext=false) and don't need
       // to be attached via DebuggerClient.attachTarget.
       if (this.isBrowsingContext) {
         await attachBrowsingContextTarget();
       } else if (this.isLegacyAddon) {
-        const [, addonTargetFront] = await this._client.attachAddon(this._form);
+        const [, addonTargetFront] = await this._client.attachAddon(this.form);
         this.activeTab = addonTargetFront;
-      } else if (this.isWorkerTarget || this.isContentProcess) {
-        // Worker and Content process targets are the first target to have their front already
-        // instantiated. The plan is to have all targets to have their front passed as
-        // constructor argument.
+
+      // Worker and Content process targets are the first target to have their front already
+      // instantiated. The plan is to have all targets to have their front passed as
+      // constructor argument.
+      } else if (this.isWorkerTarget) {
+        // Worker is the first front to be completely migrated to have only its attach
+        // method being called from Target.attach. Other fronts should be refactored.
+        await this.activeTab.attach();
+      } else if (this.isContentProcess) {
+        // ContentProcessTarget is the only one target without any attach request.
       } else {
         throw new Error(`Unsupported type of target. Expected target of one of the` +
           ` following types: BrowsingContext, ContentProcess, Worker or ` +
           `Addon (legacy).`);
       }
 
       // _setupRemoteListeners has to be called after the potential call to `attachTarget`
       // as it depends on `activeTab` which is set by this method.
@@ -671,17 +675,17 @@ Target.prototype = {
       // These events should be ultimately listened from the thread client as
       // they are coming from it and no longer go through the Target Actor/Front.
       this._onSourceUpdated = packet => this.emit("source-updated", packet);
       this.activeTab.on("newSource", this._onSourceUpdated);
       this.activeTab.on("updatedSource", this._onSourceUpdated);
     } else {
       this._onTabDetached = (type, packet) => {
         // We have to filter message to ensure that this detach is for this tab
-        if (packet.from == this._form.actor) {
+        if (packet.from == this.form.actor) {
           this.destroy();
         }
       };
       this.client.addListener("tabDetached", this._onTabDetached);
 
       this._onSourceUpdated = (type, packet) => this.emit("source-updated", packet);
       this.client.addListener("newSource", this._onSourceUpdated);
       this.client.addListener("updatedSource", this._onSourceUpdated);
@@ -807,31 +811,31 @@ Target.prototype = {
 
   /**
    * Clean up references to what this target points to.
    */
   _cleanup: function() {
     if (this._tab) {
       targets.delete(this._tab);
     } else {
-      promiseTargets.delete(this._form);
+      promiseTargets.delete(this.form);
     }
 
     this.activeTab = null;
     this.activeConsole = null;
     this._client = null;
     this._tab = null;
     this._form = null;
     this._attach = null;
     this._title = null;
     this._url = null;
   },
 
   toString: function() {
-    const id = this._tab ? this._tab : (this._form && this._form.actor);
+    const id = this._tab ? this._tab : (this.form && this.form.actor);
     return `Target:${id}`;
   },
 
   /**
    * Log an error of some kind to the tab's console.
    *
    * @param {String} text
    *                 The text to log.
--- a/devtools/client/framework/test/browser_target_remote.js
+++ b/devtools/client/framework/test/browser_target_remote.js
@@ -2,20 +2,20 @@
 /* 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/ */
 
 // Ensure target is closed if client is closed directly
 function test() {
   waitForExplicitFinish();
 
-  getParentProcessActors((client, response) => {
+  getParentProcessActors((client, front) => {
     const options = {
-      form: response,
-      client: client,
+      activeTab: front,
+      client,
       chrome: true,
     };
 
     TargetFactory.forRemoteTab(options).then(target => {
       target.on("close", () => {
         ok(true, "Target was closed");
         finish();
       });
--- a/devtools/client/framework/test/browser_target_support.js
+++ b/devtools/client/framework/test/browser_target_support.js
@@ -47,20 +47,20 @@ async function testTarget(client, target
 
   close(target, client);
 }
 
 // Ensure target is closed if client is closed directly
 function test() {
   waitForExplicitFinish();
 
-  getParentProcessActors((client, response) => {
+  getParentProcessActors((client, front) => {
     const options = {
-      form: response,
-      client: client,
+      activeTab: front,
+      client,
       chrome: true,
     };
 
     TargetFactory.forRemoteTab(options).then(testTarget.bind(null, client));
   });
 }
 
 function close(target, client) {
--- a/devtools/client/framework/test/head.js
+++ b/devtools/client/framework/test/head.js
@@ -30,18 +30,18 @@ function getParentProcessActors(callback
 
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
   DebuggerServer.allowChromeProcess = true;
 
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   client.connect()
     .then(() => client.mainRoot.getMainProcess())
-    .then(response => {
-      callback(client, response.form);
+    .then(front => {
+      callback(client, front);
     });
 
   SimpleTest.registerCleanupFunction(() => {
     DebuggerServer.destroy();
   });
 }
 
 function getSourceActor(aSources, aURL) {
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -88,18 +88,18 @@ var connect = async function() {
   await gClient.connect();
 
   appendStatusMessage("Get root form for toolbox");
   if (addonID) {
     const { addons } = await gClient.listAddons();
     const addonTargetActor = addons.filter(addon => addon.id === addonID).pop();
     await openToolbox({form: addonTargetActor, chrome: true});
   } else {
-    const response = await gClient.mainRoot.getMainProcess();
-    await openToolbox({form: response.form, chrome: true});
+    const front = await gClient.mainRoot.getMainProcess();
+    await openToolbox({activeTab: front, chrome: true});
   }
 };
 
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
@@ -136,23 +136,24 @@ window.addEventListener("load", async fu
     console.error(e);
   }
 }, { once: true });
 
 function onCloseCommand(event) {
   window.close();
 }
 
-async function openToolbox({ form, chrome }) {
+async function openToolbox({ form, activeTab, chrome }) {
   let options = {
-    form: form,
+    form,
+    activeTab,
     client: gClient,
-    chrome: chrome,
+    chrome,
   };
-  appendStatusMessage(`Create toolbox target: ${JSON.stringify(arguments, null, 2)}`);
+  appendStatusMessage(`Create toolbox target: ${JSON.stringify({form, chrome}, null, 2)}`);
   const target = await TargetFactory.forRemoteTab(options);
   const frame = document.getElementById("toolbox-iframe");
 
   // Remember the last panel that was used inside of this profile.
   // But if we are testing, then it should always open the debugger panel.
   const selectedTool =
     Services.prefs.getCharPref("devtools.browsertoolbox.panel",
       Services.prefs.getCharPref("devtools.toolbox.selectedTool",
--- a/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js
+++ b/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js
@@ -216,25 +216,27 @@ class FlexItemSizingProperties extends P
         this.renderReasons(reasons)
       )
     );
   }
 
   renderMaximumSizeSection(flexItemSizing, properties, dimension) {
     const { clampState, mainMaxSize, mainDeltaSize } = flexItemSizing;
     const grew = mainDeltaSize > 0;
+    const shrank = mainDeltaSize < 0;
     const maxDimensionValue = properties[`max-${dimension}`];
 
     if (clampState !== "clamped_to_max") {
       return null;
     }
 
     const reasons = [];
-    if (grew) {
+    if (grew || shrank) {
       // The item may have wanted to grow more than it did, because it was max-clamped.
+      // Or the item may have wanted shrink more, but it was clamped to its max size.
       reasons.push(getStr("flexbox.itemSizing.clampedToMax"));
     }
 
     return (
       dom.li({ className: "section max" },
         dom.span({ className: "name" },
           getStr("flexbox.itemSizing.maxSizeSectionHeader"),
           maxDimensionValue.length ?
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -2066,18 +2066,22 @@ ScratchpadWindow.prototype = extend(Scra
    */
   async _attach() {
     DebuggerServer.init();
     DebuggerServer.registerAllActors();
     DebuggerServer.allowChromeProcess = true;
 
     const client = new DebuggerClient(DebuggerServer.connectPipe());
     await client.connect();
-    const response = await client.mainRoot.getMainProcess();
-    return { form: response.form, client };
+    const front = await client.mainRoot.getMainProcess();
+    const target = await TargetFactory.forRemoteTab({
+      activeTab: front,
+      client,
+    });
+    return target;
   },
 });
 
 function ScratchpadTarget(aTarget) {
   this._target = aTarget;
 }
 
 ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor;
--- a/devtools/client/shared/test/browser_dbg_WorkerTargetActor.attach.js
+++ b/devtools/client/shared/test/browser_dbg_WorkerTargetActor.attach.js
@@ -39,40 +39,40 @@ function test() {
 
     // If a page still has pending network requests, it will not be moved into
     // the bfcache. Consequently, we cannot use waitForWorkerListChanged here,
     // because the worker is not guaranteed to have finished loading when it is
     // registered. Instead, we have to wait for the promise returned by
     // createWorker in the tab to be resolved.
     yield createWorkerInTab(tab, WORKER1_URL);
     let { workers } = yield listWorkers(targetFront);
-    let [, workerTargetFront1] = yield attachWorker(targetFront,
-                                               findWorker(workers, WORKER1_URL));
+    let workerTargetFront1 = findWorker(workers, WORKER1_URL);
+    yield workerTargetFront1.attach();
     is(workerTargetFront1.isClosed, false, "worker in tab 1 should not be closed");
 
     executeSoon(() => {
       BrowserTestUtils.loadURI(tab.linkedBrowser, TAB2_URL);
     });
     yield waitForWorkerClose(workerTargetFront1);
     is(workerTargetFront1.isClosed, true, "worker in tab 1 should be closed");
 
     yield createWorkerInTab(tab, WORKER2_URL);
     ({ workers } = yield listWorkers(targetFront));
-    const [, workerTargetFront2] = yield attachWorker(targetFront,
-                                               findWorker(workers, WORKER2_URL));
+    const workerTargetFront2 = findWorker(workers, WORKER2_URL);
+    yield workerTargetFront2.attach();
     is(workerTargetFront2.isClosed, false, "worker in tab 2 should not be closed");
 
     executeSoon(() => {
       tab.linkedBrowser.goBack();
     });
     yield waitForWorkerClose(workerTargetFront2);
     is(workerTargetFront2.isClosed, true, "worker in tab 2 should be closed");
 
     ({ workers } = yield listWorkers(targetFront));
-    [, workerTargetFront1] = yield attachWorker(targetFront,
-                                           findWorker(workers, WORKER1_URL));
+    workerTargetFront1 = findWorker(workers, WORKER1_URL);
+    yield workerTargetFront1.attach();
     is(workerTargetFront1.isClosed, false, "worker in tab 1 should not be closed");
 
     yield close(client);
     SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, oldMaxTotalViewers);
     finish();
   });
 }
--- a/devtools/client/shared/test/browser_dbg_worker-window.js
+++ b/devtools/client/shared/test/browser_dbg_worker-window.js
@@ -4,21 +4,16 @@
 "use strict";
 
 // Import helpers for the workers
 /* import-globals-from helper_workers.js */
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/helper_workers.js",
   this);
 
-// The following "connectionClosed" rejection should not be left uncaught. This
-// test has been whitelisted until the issue is fixed.
-ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", this);
-PromiseTestUtils.expectUncaughtRejection(/[object Object]/);
-
 const TAB_URL = EXAMPLE_URL + "doc_WorkerTargetActor.attachThread-tab.html";
 const WORKER_URL = "code_WorkerTargetActor.attachThread-worker.js";
 
 add_task(async function() {
   await pushPrefs(["devtools.scratchpad.enabled", true]);
 
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
@@ -29,18 +24,17 @@ add_task(async function() {
   const tab = await addTab(TAB_URL);
   const { tabs } = await listTabs(client);
   const [, targetFront] = await attachTarget(client, findTab(tabs, TAB_URL));
 
   await listWorkers(targetFront);
   await createWorkerInTab(tab, WORKER_URL);
 
   const { workers } = await listWorkers(targetFront);
-  const [, workerTargetFront] = await attachWorker(targetFront,
-                                             findWorker(workers, WORKER_URL));
+  const workerTargetFront = findWorker(workers, WORKER_URL);
 
   const toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerTargetFront),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   is(toolbox.hostType, "window", "correct host");
 
   await new Promise(done => {
--- a/devtools/client/shared/test/helper_workers.js
+++ b/devtools/client/shared/test/helper_workers.js
@@ -129,21 +129,16 @@ function findWorker(workers, url) {
   for (const worker of workers) {
     if (worker.url === url) {
       return worker;
     }
   }
   return null;
 }
 
-function attachWorker(targetFront, worker) {
-  info("Attaching to worker with url '" + worker.url + "'.");
-  return targetFront.attachWorker(worker.actor);
-}
-
 function waitForWorkerListChanged(targetFront) {
   info("Waiting for worker list to change.");
   return targetFront.once("workerListChanged");
 }
 
 function attachThread(workerTargetFront, options) {
   info("Attaching to thread.");
   return workerTargetFront.attachThread(options);
@@ -185,18 +180,17 @@ async function initWorkerDebugger(TAB_UR
 
   const tab = await addTab(TAB_URL);
   const { tabs } = await listTabs(client);
   const [, targetFront] = await attachTarget(client, findTab(tabs, TAB_URL));
 
   await createWorkerInTab(tab, WORKER_URL);
 
   const { workers } = await listWorkers(targetFront);
-  const [, workerTargetFront] = await attachWorker(targetFront,
-                                             findWorker(workers, WORKER_URL));
+  const workerTargetFront = findWorker(workers, WORKER_URL);
 
   const toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerTargetFront),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   const debuggerPanel = toolbox.getCurrentPanel();
 
   const gDebugger = debuggerPanel.panelWin;
--- a/devtools/client/webconsole/hudservice.js
+++ b/devtools/client/webconsole/hudservice.js
@@ -131,18 +131,18 @@ HUDService.prototype = {
       // (See Bug 1416105 for rationale).
       DebuggerServer.init();
       DebuggerServer.registerActors({ root: true, target: true });
 
       DebuggerServer.allowChromeProcess = true;
 
       const client = new DebuggerClient(DebuggerServer.connectPipe());
       await client.connect();
-      const response = await client.mainRoot.getMainProcess();
-      return { form: response.form, client, chrome: true };
+      const front = await client.mainRoot.getMainProcess();
+      return { activeTab: front, client, chrome: true };
     }
 
     async function openWindow(t) {
       const win = Services.ww.openWindow(null, Tools.webConsole.url,
                                        "_blank", BC_WINDOW_FEATURES, null);
 
       await new Promise(resolve => {
         win.addEventListener("DOMContentLoaded", resolve, {once: true});
--- a/devtools/client/webide/modules/app-manager.js
+++ b/devtools/client/webide/modules/app-manager.js
@@ -250,19 +250,19 @@ var AppManager = exports.AppManager = {
     }, console.error);
   },
 
   getTarget: function() {
     if (this.selectedProject.type == "mainProcess") {
       // Fx >=39 exposes a ParentProcessTargetActor to debug the main process
       if (this.connection.client.mainRoot.traits.allowChromeProcess) {
         return this.connection.client.mainRoot.getMainProcess()
-                   .then(aResponse => {
+                   .then(front => {
                      return TargetFactory.forRemoteTab({
-                       form: aResponse.form,
+                       activeTab: front,
                        client: this.connection.client,
                        chrome: true,
                      });
                    });
       }
       // Fx <39 exposes chrome target actors on the root actor
       return TargetFactory.forRemoteTab({
           form: this._listTabsResponse,
--- a/devtools/docs/backend/protocol.js.md
+++ b/devtools/docs/backend/protocol.js.md
@@ -612,17 +612,18 @@ For more complex situations, you can def
       }
     }
 
     // implementation:
     getTemporaryChild: function (id) {
       if (!this._temporaryParent) {
         // Create an actor to serve as the parent for all temporary children and explicitly
         // add it as a child of this actor.
-        this._temporaryParent = this.manage(new Actor(this.conn));
+        this._temporaryParent = new Actor(this.conn));
+        this.manage(this._temporaryParent);
       }
       return new ChildActor(this.conn, id);
     }
 
     clearTemporaryChildren: function () {
       if (this._temporaryParent) {
         this._temporaryParent.destroy();
         delete this._temporaryParent;
--- a/devtools/server/actors/accessibility/accessible.js
+++ b/devtools/server/actors/accessibility/accessible.js
@@ -236,17 +236,17 @@ const AccessibleActor = ActorClassWithSp
       for (const target of targets) {
         // Target of the relation is not part of the current root document.
         if (target.rootDocument !== doc.rawAccessible) {
           continue;
         }
 
         let targetAcc;
         try {
-          targetAcc = this.walker.attachAccessible(target, doc);
+          targetAcc = this.walker.attachAccessible(target, doc.rawAccessible);
         } catch (e) {
           // Target is not available.
         }
 
         if (targetAcc) {
           if (!relationObject) {
             relationObject = { type, targets: [] };
           }
@@ -289,18 +289,25 @@ const AccessibleActor = ActorClassWithSp
   get _nonEmptyTextLeafs() {
     return this.children().filter(child => this._isValidTextLeaf(child.rawAccessible));
   },
 
   /**
    * Calculate the contrast ratio of the given accessible.
    */
   _getContrastRatio() {
-    return getContrastRatioFor(this._isValidTextLeaf(this.rawAccessible) ?
-      this.rawAccessible.DOMNode.parentNode : this.rawAccessible.DOMNode);
+    if (!this._isValidTextLeaf(this.rawAccessible)) {
+      return null;
+    }
+
+    return getContrastRatioFor(this.rawAccessible.DOMNode.parentNode, {
+      bounds: this.bounds,
+      contexts: this.walker.contexts,
+      win: this.walker.rootWin,
+    });
   },
 
   /**
    * Audit the state of the accessible object.
    *
    * @return {Object|null}
    *         Audit results for the accessible object.
   */
--- a/devtools/server/actors/accessibility/walker.js
+++ b/devtools/server/actors/accessibility/walker.js
@@ -8,21 +8,37 @@ const { Cc, Ci } = require("chrome");
 const Services = require("Services");
 const { Actor, ActorClassWithSpec } = require("devtools/shared/protocol");
 const { accessibleWalkerSpec } = require("devtools/shared/specs/accessibility");
 
 loader.lazyRequireGetter(this, "AccessibleActor", "devtools/server/actors/accessibility/accessible", true);
 loader.lazyRequireGetter(this, "CustomHighlighterActor", "devtools/server/actors/highlighters", true);
 loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
 loader.lazyRequireGetter(this, "events", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "getCurrentZoom", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "InspectorUtils", "InspectorUtils");
 loader.lazyRequireGetter(this, "isDefunct", "devtools/server/actors/utils/accessibility", true);
 loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true);
 loader.lazyRequireGetter(this, "isWindowIncluded", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isXUL", "devtools/server/actors/highlighters/utils/markup", true);
+loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "register", "devtools/server/actors/highlighters", true);
+loader.lazyRequireGetter(this, "removeSheet", "devtools/shared/layout/utils", true);
+
+const kStateHover = 0x00000004; // NS_EVENT_STATE_HOVER
+
+const HIGHLIGHTER_STYLES_SHEET = `data:text/css;charset=utf-8,
+* {
+  transition: none !important;
+}
+
+:-moz-devtools-highlighted {
+  color: transparent !important;
+  text-shadow: none !important;
+}`;
 
 const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
 const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent;
 const nsIAccessibleRole = Ci.nsIAccessibleRole;
 
 const {
   EVENT_TEXT_CHANGED,
   EVENT_TEXT_INSERTED,
@@ -121,16 +137,17 @@ function isStale(accessible) {
 const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
   initialize(conn, targetActor) {
     Actor.prototype.initialize.call(this, conn);
     this.targetActor = targetActor;
     this.refMap = new Map();
     this.setA11yServiceGetter();
     this.onPick = this.onPick.bind(this);
     this.onHovered = this.onHovered.bind(this);
+    this._preventContentEvent = this._preventContentEvent.bind(this);
     this.onKey = this.onKey.bind(this);
     this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
   },
 
   get highlighter() {
     if (!this._highlighter) {
       if (isXUL(this.rootWin)) {
         if (!isTypeRegistered("XULWindowAccessibleHighlighter")) {
@@ -326,17 +343,17 @@ const AccessibleWalkerActor = ActorClass
     if (!Services.appinfo.accessibilityEnabled) {
       return null;
     }
 
     return this.a11yService.getAccessibleFor(rawNode);
   },
 
   async getAncestry(accessible) {
-    if (accessible.indexInParent === -1) {
+    if (!accessible || accessible.indexInParent === -1) {
       return [];
     }
     const doc = await this.getDocument();
     const ancestry = [];
     if (accessible === doc) {
       return ancestry;
     }
 
@@ -468,24 +485,33 @@ const AccessibleWalkerActor = ActorClass
    * @param  {Object} options
    *         Object used for passing options. Available options:
    *         - duration {Number}
    *                    Duration of time that the highlighter should be shown.
    * @return {Boolean}
    *         True if highlighter shows the accessible object.
    */
   highlightAccessible(accessible, options = {}) {
+    this.unhighlight();
     const { bounds } = accessible;
     if (!bounds) {
       return false;
     }
 
+    // Disable potential mouse driven transitions (This is important because accessibility
+    // highlighter temporarily modifies text color related CSS properties. In case where
+    // there are transitions that affect them, there might be unexpected side effects when
+    // taking a snapshot for contrast measurement)
+    loadSheet(this.rootWin, HIGHLIGHTER_STYLES_SHEET);
     const { audit, name, role } = accessible;
-    return this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
-                                 { ...options, ...bounds, name, role, audit });
+    const shown = this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
+                                      { ...options, ...bounds, name, role, audit });
+    // Re-enable transitions.
+    removeSheet(this.rootWin, HIGHLIGHTER_STYLES_SHEET);
+    return shown;
   },
 
   /**
    * Public method used to hide an accessible object highlighter on the client
    * side.
    */
   unhighlight() {
     if (!this._highlighter) {
@@ -508,69 +534,87 @@ const AccessibleWalkerActor = ActorClass
   _isEventAllowed: function({ view }) {
     return this.rootWin instanceof Ci.nsIDOMChromeWindow ||
            isWindowIncluded(this.rootWin, view);
   },
 
   _preventContentEvent(event) {
     event.stopPropagation();
     event.preventDefault();
+
+    const target = event.originalTarget || event.target;
+    if (target !== this._currentTarget) {
+      this._resetStateAndReleaseTarget();
+      this._currentTarget = target;
+      // We use InspectorUtils to save the original hover content state of the target
+      // element (that includes its hover state). In order to not trigger any visual
+      // changes to the element that depend on its hover state we remove the state while
+      // the element is the most current target of the highlighter.
+      //
+      // TODO: This logic can be removed if/when we can use elementsAtPoint API for
+      // determining topmost DOMNode that corresponds to specific coordinates. We would
+      // then be able to use a highlighter overlay that would prevent all pointer events
+      // to content but still render highlighter for the node/element correctly.
+      this._currentTargetHoverState =
+        InspectorUtils.getContentState(target) & kStateHover;
+      InspectorUtils.removeContentState(target, kStateHover);
+    }
   },
 
   /**
    * Click event handler for when picking is enabled.
    *
    * @param  {Object} event
    *         Current click event.
    */
-  async onPick(event) {
+  onPick(event) {
     if (!this._isPicking) {
       return;
     }
 
     this._preventContentEvent(event);
     if (!this._isEventAllowed(event)) {
       return;
     }
 
     // If shift is pressed, this is only a preview click, send the event to
     // the client, but don't stop picking.
     if (event.shiftKey) {
       if (!this._currentAccessible) {
-        this._currentAccessible = await this._findAndAttachAccessible(event);
+        this._currentAccessible = this._findAndAttachAccessible(event);
       }
       events.emit(this, "picker-accessible-previewed", this._currentAccessible);
       return;
     }
 
-    this._stopPickerListeners();
+    this._unsetPickerEnvironment();
     this._isPicking = false;
     if (!this._currentAccessible) {
-      this._currentAccessible = await this._findAndAttachAccessible(event);
+      this._currentAccessible = this._findAndAttachAccessible(event);
     }
     events.emit(this, "picker-accessible-picked", this._currentAccessible);
   },
 
   /**
    * Hover event handler for when picking is enabled.
    *
    * @param  {Object} event
    *         Current hover event.
    */
-  async onHovered(event) {
+  onHovered(event) {
     if (!this._isPicking) {
       return;
     }
 
     this._preventContentEvent(event);
     if (!this._isEventAllowed(event)) {
       return;
     }
 
-    const accessible = await this._findAndAttachAccessible(event);
+    const accessible = this._findAndAttachAccessible(event);
     if (!accessible) {
       return;
     }
 
     if (this._currentAccessible !== accessible) {
       this.highlightAccessible(accessible);
       events.emit(this, "picker-accessible-hovered", accessible);
       this._currentAccessible = accessible;
@@ -621,17 +665,17 @@ const AccessibleWalkerActor = ActorClass
   },
 
   /**
    * Picker method that starts picker content listeners.
    */
   pick: function() {
     if (!this._isPicking) {
       this._isPicking = true;
-      this._startPickerListeners();
+      this._setPickerEnvironment();
     }
   },
 
   /**
    * This pick method also focuses the highlighter's target window.
    */
   pickAndFocus: function() {
     this.pick();
@@ -646,93 +690,145 @@ const AccessibleWalkerActor = ActorClass
     }
 
     const accessible = this.addRef(rawAccessible);
     // There is a chance that ancestry lookup can fail if the accessible is in
     // the detached subtree. At that point the root accessible object would be
     // defunct and accessing it via parent property will throw.
     try {
       let parent = accessible;
-      while (parent && parent != accessibleDocument) {
+      while (parent && parent.rawAccessible != accessibleDocument) {
         parent = parent.parentAcc;
       }
     } catch (error) {
       throw new Error(`Failed to get ancestor for ${accessible}: ${error}`);
     }
 
     return accessible;
   },
 
   /**
-   * Find accessible object that corresponds to a DOMNode and attach (lookup its
-   * ancestry to the root doc) to the AccessibilityWalker tree.
+   * When RDM is used, users can set custom DPR values that are different from the device
+   * they are using. Store true screenPixelsPerCSSPixel value to be able to use accessible
+   * highlighter features correctly.
+   */
+  get pixelRatio() {
+    const { contentViewer } = this.targetActor.docShell;
+    const { windowUtils } = this.rootWin;
+    const overrideDPPX = contentViewer.overrideDPPX;
+    let ratio;
+    if (overrideDPPX) {
+      contentViewer.overrideDPPX = 0;
+      ratio = windowUtils.screenPixelsPerCSSPixel;
+      contentViewer.overrideDPPX = overrideDPPX;
+    } else {
+      ratio = windowUtils.screenPixelsPerCSSPixel;
+    }
+
+    return ratio;
+  },
+
+  /**
+   * Find deepest accessible object that corresponds to the screen coordinates of the
+   * mouse pointer and attach it to the AccessibilityWalker tree.
    *
    * @param  {Object} event
    *         Correspoinding content event.
    * @return {null|Object}
    *         Accessible object, if available, that corresponds to a DOM node.
    */
-  async _findAndAttachAccessible(event) {
-    let target = event.originalTarget || event.target;
-    let rawAccessible;
-    // Find a first accessible object in the target's ancestry, including
-    // target. Note: not all DOM nodes have corresponding accessible objects
-    // (for example, a <DIV> element that is used as a container for other
-    // things) thus we need to find one that does.
-    while (!rawAccessible && target) {
-      rawAccessible = this.getRawAccessibleFor(target);
-      target = target.parentNode;
-    }
-
-    const doc = await this.getDocument();
-    return this.attachAccessible(rawAccessible, doc);
+  _findAndAttachAccessible(event) {
+    const target = event.originalTarget || event.target;
+    const docAcc = this.getRawAccessibleFor(this.rootDoc);
+    const win = target.ownerGlobal;
+    const scale = this.pixelRatio / getCurrentZoom(win);
+    const rawAccessible = docAcc.getDeepestChildAtPoint(
+      event.screenX * scale,
+      event.screenY * scale);
+    return this.attachAccessible(rawAccessible, docAcc);
   },
 
   /**
    * Start picker content listeners.
    */
-  _startPickerListeners: function() {
+  _setPickerEnvironment: function() {
     const target = this.targetActor.chromeEventHandler;
     target.addEventListener("mousemove", this.onHovered, true);
     target.addEventListener("click", this.onPick, true);
     target.addEventListener("mousedown", this._preventContentEvent, true);
     target.addEventListener("mouseup", this._preventContentEvent, true);
+    target.addEventListener("mouseover", this._preventContentEvent, true);
+    target.addEventListener("mouseout", this._preventContentEvent, true);
+    target.addEventListener("mouseleave", this._preventContentEvent, true);
+    target.addEventListener("mouseenter", this._preventContentEvent, true);
     target.addEventListener("dblclick", this._preventContentEvent, true);
     target.addEventListener("keydown", this.onKey, true);
     target.addEventListener("keyup", this._preventContentEvent, true);
   },
 
   /**
-   * If content is still alive, stop picker content listeners.
+   * If content is still alive, stop picker content listeners, reset the hover state for
+   * last target element.
    */
-  _stopPickerListeners: function() {
+  _unsetPickerEnvironment: function() {
     const target = this.targetActor.chromeEventHandler;
 
     if (!target) {
       return;
     }
 
     target.removeEventListener("mousemove", this.onHovered, true);
     target.removeEventListener("click", this.onPick, true);
     target.removeEventListener("mousedown", this._preventContentEvent, true);
     target.removeEventListener("mouseup", this._preventContentEvent, true);
+    target.removeEventListener("mouseover", this._preventContentEvent, true);
+    target.removeEventListener("mouseout", this._preventContentEvent, true);
+    target.removeEventListener("mouseleave", this._preventContentEvent, true);
+    target.removeEventListener("mouseenter", this._preventContentEvent, true);
     target.removeEventListener("dblclick", this._preventContentEvent, true);
     target.removeEventListener("keydown", this.onKey, true);
     target.removeEventListener("keyup", this._preventContentEvent, true);
+
+    this._resetStateAndReleaseTarget();
+  },
+
+  /**
+   * When using accessibility highlighter, we keep track of the most current event pointer
+   * event target. In order to update or release the target, we need to make sure we set
+   * the content state (using InspectorUtils) to its original value.
+   *
+   * TODO: This logic can be removed if/when we can use elementsAtPoint API for
+   * determining topmost DOMNode that corresponds to specific coordinates. We would then
+   * be able to use a highlighter overlay that would prevent all pointer events to content
+   * but still render highlighter for the node/element correctly.
+   */
+  _resetStateAndReleaseTarget() {
+    if (!this._currentTarget) {
+      return;
+    }
+
+    try {
+      if (this._currentTargetHoverState) {
+        InspectorUtils.setContentState(this._currentTarget, kStateHover);
+      }
+    } catch (e) {
+      // DOMNode is already dead.
+    }
+
+    this._currentTarget = null;
+    this._currentTargetState = null;
   },
 
   /**
    * Cacncel picker pick. Remvoe all content listeners and hide the highlighter.
    */
   cancelPick: function() {
-    if (this._highlighter) {
-      this.highlighter.hide();
-    }
+    this.unhighlight();
 
     if (this._isPicking) {
-      this._stopPickerListeners();
+      this._unsetPickerEnvironment();
       this._isPicking = false;
       this._currentAccessible = null;
     }
   },
 });
 
 exports.AccessibleWalkerActor = AccessibleWalkerActor;
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -648,32 +648,49 @@
 }
 
 :-moz-native-anonymous .accessible-infobar-name,
 :-moz-native-anonymous .accessible-infobar-audit {
   color: var(--highlighter-infobar-color);
   max-width: 90%;
 }
 
+:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty):after {
+  margin-inline-start: 2px;
+}
+
 :-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after,
 :-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
   color: #90E274;
 }
 
 :-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
   color: #E57180;
-  content: " ⚠️";
+  content: "⚠️";
 }
 
 :-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
-  content: " AA\2713";
+  content: "AA\2713";
 }
 
 :-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
-  content: " AAA\2713";
+  content: "AAA\2713";
+}
+
+:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio-label,
+:-moz-native-anonymous .accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
+  margin-inline-end: 3px;
+}
+
+:-moz-native-anonymous .accessible-infobar-audit #accessible-contrast-ratio-max {
+  margin-inline-start: 3px;
+}
+
+:-moz-native-anonymous .accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
+  content: "-";
 }
 
 :-moz-native-anonymous .accessible-infobar-name:not(:empty),
 :-moz-native-anonymous .accessible-infobar-audit:not(:empty) {
   border-inline-start: 1px solid #5a6169;
   margin-inline-start: 6px;
   padding-inline-start: 6px;
 }
--- a/devtools/server/actors/highlighters/utils/accessibility.js
+++ b/devtools/server/actors/highlighters/utils/accessibility.js
@@ -470,42 +470,102 @@ class AuditReport {
  * inforbar,
  */
 class ContrastRatio extends AuditReport {
   buildMarkup(root) {
     createNode(this.win, {
       nodeType: "span",
       parent: root,
       attributes: {
+        "class": "contrast-ratio-label",
+        "id": "contrast-ratio-label",
+      },
+      prefix: this.prefix,
+      text: L10N.getStr("accessibility.contrast.ratio.label"),
+    });
+
+    createNode(this.win, {
+      nodeType: "span",
+      parent: root,
+      attributes: {
         "class": "contrast-ratio",
-        "id": "contrast-ratio",
+        "id": "contrast-ratio-error",
+      },
+      prefix: this.prefix,
+      text: L10N.getStr("accessibility.contrast.ratio.error"),
+    });
+
+    createNode(this.win, {
+      nodeType: "span",
+      parent: root,
+      attributes: {
+        "class": "contrast-ratio",
+        "id": "contrast-ratio-min",
       },
       prefix: this.prefix,
     });
+
+    createNode(this.win, {
+      nodeType: "span",
+      parent: root,
+      attributes: {
+        "class": "contrast-ratio",
+        "id": "contrast-ratio-max",
+      },
+      prefix: this.prefix,
+    });
+  }
+
+  _fillAndStyleContrastValue(el, value, isLargeText, stringName) {
+    value = value.toFixed(2);
+    const style = getContrastRatioScoreStyle(value, isLargeText);
+    this.setTextContent(el, stringName ? L10N.getFormatStr(stringName, value) : value);
+    el.classList.add(style);
+    el.removeAttribute("hidden");
   }
 
   /**
    * Update contrast ratio score infobar markup.
    * @param  {Number}
    *         Contrast ratio for an accessible object being highlighted.
    * @return {Boolean}
    *         True if the contrast ratio markup was updated correctly and infobar audit
    *         block should be visible.
    */
   update({ contrastRatio }) {
-    const el = this.getElement("contrast-ratio");
-    ["fail", "AA", "AAA"].forEach(style => el.classList.remove(style));
+    const els = {};
+    for (const key of ["label", "min", "max", "error"]) {
+      const el = els[key] = this.getElement(`contrast-ratio-${key}`);
+      if (["min", "max"].includes(key)) {
+        ["fail", "AA", "AAA"].forEach(className => el.classList.remove(className));
+        this.setTextContent(el, "");
+      }
+
+      el.setAttribute("hidden", true);
+    }
 
     if (!contrastRatio) {
       return false;
     }
 
-    el.classList.add(getContrastRatioScoreStyle(contrastRatio));
-    this.setTextContent(el,
-      L10N.getFormatStr("accessibility.contrast.ratio", contrastRatio.ratio.toFixed(2)));
+    const { isLargeText, error } = contrastRatio;
+    els.label.removeAttribute("hidden");
+    if (error) {
+      els.error.removeAttribute("hidden");
+      return true;
+    }
+
+    if (contrastRatio.value) {
+      this._fillAndStyleContrastValue(els.min, contrastRatio.value, isLargeText);
+      return true;
+    }
+
+    this._fillAndStyleContrastValue(els.min, contrastRatio.min, isLargeText);
+    this._fillAndStyleContrastValue(els.max, contrastRatio.max, isLargeText);
+
     return true;
   }
 }
 
 /**
  * A helper function that calculate accessible object bounds and positioning to
  * be used for highlighting.
  *
@@ -558,25 +618,25 @@ function getBounds(win, { x, y, w, h, zo
   const height = bottom - top;
 
   return { left, right, top, bottom, width, height };
 }
 
 /**
  * Get contrast ratio score styling to be applied on the element that renders the contrast
  * ratio.
- * @param  {Number} options.ratio
+ * @param  {Number} ratio
  *         Value of the contrast ratio for a given accessible object.
- * @param  {Boolean} options.largeText
+ * @param  {Boolean} isLargeText
  *         True if the accessible object contains large text.
  * @return {String}
  *         CSS class that represents the appropriate contrast ratio score styling.
  */
-function getContrastRatioScoreStyle({ ratio, largeText }) {
-  const levels = largeText ? { AA: 3, AAA: 4.5 } : { AA: 4.5, AAA: 7 };
+function getContrastRatioScoreStyle(ratio, isLargeText) {
+  const levels = isLargeText ? { AA: 3, AAA: 4.5 } : { AA: 4.5, AAA: 7 };
 
   let style = "fail";
   if (ratio >= levels.AAA) {
     style = "AAA";
   } else if (ratio >= levels.AA) {
     style = "AA";
   }
 
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -158,35 +158,41 @@ exports.createSVGNode = createSVGNode;
  * @param {Object} Options for the node include:
  * - nodeType: the type of node, defaults to "div".
  * - namespace: the namespace to use to create the node, defaults to XHTML namespace.
  * - attributes: a {name:value} object to be used as attributes for the node.
  * - prefix: a string that will be used to prefix the values of the id and class
  *   attributes.
  * - parent: if provided, the newly created element will be appended to this
  *   node.
+ * - text: if provided, set the text content of the element.
  */
 function createNode(win, options) {
   const type = options.nodeType || "div";
   const namespace = options.namespace || XHTML_NS;
+  const doc = win.document;
 
-  const node = win.document.createElementNS(namespace, type);
+  const node = doc.createElementNS(namespace, type);
 
   for (const name in options.attributes || {}) {
     let value = options.attributes[name];
     if (options.prefix && (name === "class" || name === "id")) {
       value = options.prefix + value;
     }
     node.setAttribute(name, value);
   }
 
   if (options.parent) {
     options.parent.appendChild(node);
   }
 
+  if (options.text) {
+    node.appendChild(doc.createTextNode(options.text));
+  }
+
   return node;
 }
 exports.createNode = createNode;
 
 /**
  * Every highlighters should insert their markup content into the document's
  * canvasFrame anonymous content container (see dom/webidl/Document.webidl).
  *
--- a/devtools/server/actors/highlighters/xul-accessible.js
+++ b/devtools/server/actors/highlighters/xul-accessible.js
@@ -74,32 +74,49 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:te
   }
 
   .accessible-infobar-name,
   .accessible-infobar-audit {
     color: hsl(210, 30%, 85%);
     max-width: 90%;
   }
 
+  .accessible-infobar-audit .accessible-contrast-ratio:not(:empty):after {
+    margin-inline-start: 2px;
+  }
+
   .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after,
   .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
     color: #90E274;
   }
 
   .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
     color: #E57180;
-    content: " ⚠️";
+    content: "⚠️";
   }
 
   .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
-    content: " AA\u2713";
+    content: "AA\u2713";
   }
 
   .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
-    content: " AAA\u2713";
+    content: "AAA\u2713";
+  }
+
+  .accessible-infobar-audit .accessible-contrast-ratio-label,
+  .accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
+    margin-inline-end: 3px;
+  }
+
+  .accessible-infobar-audit #accessible-contrast-ratio-max {
+    margin-inline-start: 3px;
+  }
+
+  .accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
+    content: "-";
   }
 
   .accessible-infobar-name:not(:empty),
   .accessible-infobar-audit:not(:empty) {
     border-inline-start: 1px solid #5a6169;
     margin-inline-start: 6px;
     padding-inline-start: 6px;
   }
--- a/devtools/server/actors/targets/browsing-context.js
+++ b/devtools/server/actors/targets/browsing-context.js
@@ -650,18 +650,17 @@ const browsingContextTargetPrototype = {
       if (this._workerTargetActorPool) {
         this._workerTargetActorPool.destroy();
       }
 
       this._workerTargetActorPool = pool;
       this._workerTargetActorList.onListChanged = this._onWorkerTargetActorListChanged;
 
       return {
-        "from": this.actorID,
-        "workers": actors.map((actor) => actor.form()),
+        workers: actors,
       };
     });
   },
 
   logInPage(request) {
     const {text, category, flags} = request;
     const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
     const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
--- a/devtools/server/actors/targets/webextension.js
+++ b/devtools/server/actors/targets/webextension.js
@@ -71,24 +71,21 @@ const webExtensionTargetPrototype = exte
  *        DebuggerServer.connectToFrame method.
  * @param {string} prefix
  *        the custom RDP prefix to use.
  * @param {string} addonId
  *        the addonId of the target WebExtension.
  */
 webExtensionTargetPrototype.initialize = function(conn, chromeGlobal, prefix, addonId) {
   this.addonId = addonId;
+  this.chromeGlobal = chromeGlobal;
 
   // Try to discovery an existent extension page to attach (which will provide the initial
   // URL shown in the window tittle when the addon debugger is opened).
-  let extensionWindow = this._searchForExtensionWindow();
-  if (!extensionWindow) {
-    this._createFallbackWindow();
-    extensionWindow = this.fallbackWindow;
-  }
+  const extensionWindow = this._searchForExtensionWindow();
 
   parentProcessTargetPrototype.initialize.call(this, conn, extensionWindow);
   this._chromeGlobal = chromeGlobal;
   this._prefix = prefix;
 
   // Redefine the messageManager getter to return the chromeGlobal
   // as the messageManager for this actor (which is the browser XUL
   // element used by the parent actor running in the main process to
@@ -149,59 +146,50 @@ webExtensionTargetPrototype.exit = funct
   this.addon = null;
   this.addonId = null;
 
   return ParentProcessTargetActor.prototype.exit.apply(this);
 };
 
 // Private helpers.
 
-webExtensionTargetPrototype._createFallbackWindow = function() {
+webExtensionTargetPrototype._searchFallbackWindow = function() {
   if (this.fallbackWindow) {
     // Skip if there is already an existent fallback window.
-    return;
+    return this.fallbackWindow;
   }
 
-  // Create an empty hidden window as a fallback (e.g. the background page could be
-  // not defined for the target add-on or not yet when the actor instance has been
-  // created).
-  this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true);
+  // Set and initialized the fallbackWindow (which initially is a empty
+  // about:blank browser), this window is related to a XUL browser element
+  // specifically created for the devtools server and it is never used
+  // or navigated anywhere else.
+  this.fallbackWindow = this.chromeGlobal.content;
+  this.fallbackWindow.location = "data:text/html,<h1>" + FALLBACK_DOC_MESSAGE;
 
-  // Save the reference to the fallback DOMWindow.
-  this.fallbackWindow = this.fallbackWebNav.document.defaultView;
-
-  // Insert the fallback doc message.
-  this.fallbackWindow.document.body.innerText = FALLBACK_DOC_MESSAGE;
+  return this.fallbackWindow;
 };
 
 webExtensionTargetPrototype._destroyFallbackWindow = function() {
-  if (this.fallbackWebNav) {
-    const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
-    // Explicitly close the fallback windowless browser to prevent it to leak
-    // (and to prevent it to freeze devtools xpcshell tests).
-    this.fallbackWebNav.loadURI("about:blank", 0, null, null, null, systemPrincipal);
-    this.fallbackWebNav.close();
-
-    this.fallbackWebNav = null;
+  if (this.fallbackWindow) {
     this.fallbackWindow = null;
   }
 };
 
 // Discovery an extension page to use as a default target window.
 // NOTE: This currently fail to discovery an extension page running in a
 // windowless browser when running in non-oop mode, and the background page
 // is set later using _onNewExtensionWindow.
 webExtensionTargetPrototype._searchForExtensionWindow = function() {
   for (const window of Services.ww.getWindowEnumerator(null)) {
     if (window.document.nodePrincipal.addonId == this.addonId) {
       return window;
     }
   }
 
-  return undefined;
+  return this._searchFallbackWindow();
 };
 
 // Customized ParentProcessTargetActor/BrowsingContextTargetActor hooks.
 
 webExtensionTargetPrototype._onDocShellDestroy = function(docShell) {
   // Stop watching this docshell (the unwatch() method will check if we
   // started watching it before).
   this._unwatchDocShell(docShell);
@@ -209,43 +197,34 @@ webExtensionTargetPrototype._onDocShellD
   // Let the _onDocShellDestroy notify that the docShell has been destroyed.
   const webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIWebProgress);
   this._notifyDocShellDestroy(webProgress);
 
   // If the destroyed docShell was the current docShell and the actor is
   // currently attached, switch to the fallback window
   if (this.attached && docShell == this.docShell) {
-    // Creates a fallback window if it doesn't exist yet.
-    this._createFallbackWindow();
-    this._changeTopLevelDocument(this.fallbackWindow);
+    this._changeTopLevelDocument(this._searchForExtensionWindow());
   }
 };
 
 webExtensionTargetPrototype._onNewExtensionWindow = function(window) {
   if (!this.window || this.window === this.fallbackWindow) {
     this._changeTopLevelDocument(window);
   }
 };
 
 webExtensionTargetPrototype._attach = function() {
   // NOTE: we need to be sure that `this.window` can return a window before calling the
   // ParentProcessTargetActor.onAttach, or the BrowsingContextTargetActor will not be
   // subscribed to the child doc shell updates.
 
   if (!this.window || this.window.document.nodePrincipal.addonId !== this.addonId) {
-    // Discovery an existent extension page to attach.
-    const extensionWindow = this._searchForExtensionWindow();
-
-    if (!extensionWindow) {
-      this._createFallbackWindow();
-      this._setWindow(this.fallbackWindow);
-    } else {
-      this._setWindow(extensionWindow);
-    }
+    // Discovery an existent extension page (or fallback window) to attach.
+    this._setWindow(this._searchForExtensionWindow());
   }
 
   // Call ParentProcessTargetActor's _attach to listen for any new/destroyed chrome
   // docshell.
   ParentProcessTargetActor.prototype._attach.apply(this);
 };
 
 webExtensionTargetPrototype._detach = function() {
--- a/devtools/server/actors/utils/accessibility.js
+++ b/devtools/server/actors/utils/accessibility.js
@@ -2,70 +2,221 @@
  * 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";
 
 loader.lazyRequireGetter(this, "Ci", "chrome", true);
 loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
 loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "InspectorActorUtils", "devtools/server/actors/inspector/utils");
+loader.lazyRequireGetter(this, "getBounds", "devtools/server/actors/highlighters/utils/accessibility", true);
+loader.lazyRequireGetter(this, "getCurrentZoom", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "Services");
+loader.lazyRequireGetter(this, "addPseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
+loader.lazyRequireGetter(this, "removePseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
+
+const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
 
 /**
- * Calculates the contrast ratio of the referenced DOM node.
- *
+ * Get text style properties for a given node, if possible.
  * @param  {DOMNode} node
- *         The node for which we want to calculate the contrast ratio.
- *
- * @return {Number|null} Contrast ratio value.
-*/
-function getContrastRatioFor(node) {
-  const backgroundColor = InspectorActorUtils.getClosestBackgroundColor(node);
-  const backgroundImage = InspectorActorUtils.getClosestBackgroundImage(node);
+ *         DOM node for which text styling information is to be calculated.
+ * @return {Object}
+ *         Color and text size information for a given DOM node.
+ */
+function getTextProperties(node) {
   const computedStyles = CssLogic.getComputedStyle(node);
   if (!computedStyles) {
     return null;
   }
 
   const { color, "font-size": fontSize, "font-weight": fontWeight } = computedStyles;
-  const isBoldText = parseInt(fontWeight, 10) >= 600;
-  const backgroundRgbaColor = new colorUtils.CssColor(backgroundColor, true);
-  const textRgbaColor = new colorUtils.CssColor(color, true);
+  const opacity = parseFloat(computedStyles.opacity);
 
+  let { r, g, b, a } = colorUtils.colorToRGBA(color, true);
+  a = opacity * a;
+  const textRgbaColor = new colorUtils.CssColor(`rgba(${r}, ${g}, ${b}, ${a})`, true);
   // TODO: For cases where text color is transparent, it likely comes from the color of
-  // the background that is underneath it (commonly from background-clip: text property).
-  // With some additional investigation it might be possible to calculate the color
-  // contrast where the color of the background is used as text color and the color of
-  // the ancestor's background is used as its background.
+  // the background that is underneath it (commonly from background-clip: text
+  // property). With some additional investigation it might be possible to calculate the
+  // color contrast where the color of the background is used as text color and the
+  // color of the ancestor's background is used as its background.
   if (textRgbaColor.isTransparent()) {
     return null;
   }
 
-  // TODO: these cases include handling gradient backgrounds and the actual image
-  // backgrounds. Each one needs to be handled individually.
-  if (backgroundImage !== "none") {
+  const isBoldText = parseInt(fontWeight, 10) >= 600;
+  const isLargeText = Math.ceil(parseFloat(fontSize) * 72) / 96 >= (isBoldText ? 14 : 18);
+
+  return {
+    // Blend text color taking its alpha into account asuming white background.
+    color: colorUtils.blendColors([r, g, b, a]),
+    isLargeText,
+  };
+}
+
+/**
+ * Get canvas rendering context for the current target window bound by the bounds of the
+ * accessible objects.
+ * @param  {Object}  win
+ *         Current target window.
+ * @param  {Object}  bounds
+ *         Bounds for the accessible object.
+ * @param  {null|DOMNode} node
+ *         If not null, a node that corresponds to the accessible object to be used to
+ *         make its text color transparent.
+ * @return {CanvasRenderingContext2D}
+ *         Canvas rendering context for the current window.
+ */
+function getImageCtx(win, bounds, node) {
+  const doc = win.document;
+  const canvas = doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+  const scale = getCurrentZoom(win);
+
+  const { left, top, width, height } = bounds;
+  canvas.width = width / scale;
+  canvas.height = height / scale;
+  const ctx = canvas.getContext("2d", { alpha: false });
+
+  // If node is passed, make its color related text properties invisible.
+  if (node) {
+    addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
+  }
+
+  ctx.drawWindow(win, left / scale, top / scale, width / scale, height / scale, "#fff",
+                 ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
+
+  // Restore all inline styling.
+  if (node) {
+    removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
+  }
+
+  return ctx;
+}
+
+/**
+ * Get RGBA or a range of RGBAs for the background pixels under the text. If luminance is
+ * uniform, only return one value of RGBA, otherwise return values that correspond to the
+ * min and max luminances.
+ * @param  {ImageData} dataText
+ *         pixel data for the accessible object with text visible.
+ * @param  {ImageData} dataBackground
+ *         pixel data for the accessible object with transparent text.
+ * @return {Object}
+ *         RGBA or a range of RGBAs with min and max values.
+ */
+function getBgRGBA(dataText, dataBackground) {
+  let min = [0, 0, 0, 1];
+  let max = [255, 255, 255, 1];
+  let minLuminance = 1;
+  let maxLuminance = 0;
+  const luminances = {};
+
+  let foundDistinctColor = false;
+  for (let i = 0; i < dataText.length; i = i + 4) {
+    const tR = dataText[i];
+    const bgR = dataBackground[i];
+    const tG = dataText[i + 1];
+    const bgG = dataBackground[i + 1];
+    const tB = dataText[i + 2];
+    const bgB = dataBackground[i + 2];
+
+    // Ignore pixels that are the same where pixels that are different between the two
+    // images are assumed to belong to the text within the node.
+    if (tR === bgR && tG === bgG && tB === bgB) {
+      continue;
+    }
+
+    foundDistinctColor = true;
+
+    const bgColor = `rgb(${bgR}, ${bgG}, ${bgB})`;
+    let luminance = luminances[bgColor];
+
+    if (!luminance) {
+      // Calculate luminance for the RGB value and store it to only measure once.
+      luminance = colorUtils.calculateLuminance([bgR, bgG, bgB]);
+      luminances[bgColor] = luminance;
+    }
+
+    if (minLuminance >= luminance) {
+      minLuminance = luminance;
+      min = [bgR, bgG, bgB, 1];
+    }
+
+    if (maxLuminance <= luminance) {
+      maxLuminance = luminance;
+      max = [bgR, bgG, bgB, 1];
+    }
+  }
+
+  if (!foundDistinctColor) {
     return null;
   }
 
-  let { r: bgR, g: bgG, b: bgB, a: bgA} = backgroundRgbaColor.getRGBATuple();
-  let { r: textR, g: textG, b: textB, a: textA } = textRgbaColor.getRGBATuple();
+  return minLuminance === maxLuminance ? { value: max } : { min, max };
+}
 
-  // If the element has opacity in addition to text and background alpha values, take it
-  // into account.
-  const opacity = parseFloat(computedStyles.opacity);
-  if (opacity < 1) {
-    bgA = opacity * bgA;
-    textA = opacity * textA;
+/**
+ * Calculates the contrast ratio of the referenced DOM node.
+ *
+ * @param  {DOMNode} node
+ *         The node for which we want to calculate the contrast ratio.
+ * @param  {Object}  options
+ *         - bounds   {Object}
+ *                    Bounds for the accessible object.
+ *         - contexts {null|Object}
+ *                    Canvas rendering contexts that have a window drawn as is and also
+ *                    with the all text made transparent for contrast comparison.
+ *         - win      {Object}
+ *                    Target window.
+ *
+ * @return {Object}
+ *         An object that may contain one or more of the following fields: error,
+ *         isLargeText, value, min, max values for contrast.
+*/
+function getContrastRatioFor(node, options = {}) {
+  const props = getTextProperties(node);
+  if (!props) {
+    return {
+      error: true,
+    };
   }
 
+  const bounds = getBounds(options.win, options.bounds);
+  const textContext = getImageCtx(options.win, bounds);
+  const backgroundContext = getImageCtx(options.win, bounds, node);
+
+  const { data: dataText } = textContext.getImageData(0, 0, bounds.width, bounds.height);
+  const { data: dataBackground } = backgroundContext.getImageData(
+    0, 0, bounds.width, bounds.height);
+
+  const rgba = getBgRGBA(dataText, dataBackground);
+  if (!rgba) {
+    return {
+      error: true,
+    };
+  }
+
+  const { color, isLargeText } = props;
+  if (rgba.value) {
+    return {
+      value: colorUtils.calculateContrastRatio(rgba.value, color),
+      isLargeText,
+    };
+  }
+
+  // calculateContrastRatio modifies the array, since we need to use color array twice,
+  // pass its copy to the method.
+  const min = colorUtils.calculateContrastRatio(rgba.min, Array.from(color));
+  const max = colorUtils.calculateContrastRatio(rgba.max, Array.from(color));
+
   return {
-    ratio: colorUtils.calculateContrastRatio([ bgR, bgG, bgB, bgA ],
-                                             [ textR, textG, textB, textA ]),
-    largeText: Math.ceil(parseFloat(fontSize) * 72) / 96 >= (isBoldText ? 14 : 18),
+    min: min < max ? min : max,
+    max: min < max ? max : min,
+    isLargeText,
   };
 }
 
 /**
  * Helper function that determines if nsIAccessible object is in defunct state.
  *
  * @param  {nsIAccessible}  accessible
  *         object to be tested.
--- a/devtools/server/tests/mochitest/test_getProcess.html
+++ b/devtools/server/tests/mochitest/test_getProcess.html
@@ -80,17 +80,18 @@ function runTests() {
     client.mainRoot.listProcesses().then(response => {
       ok(response.processes.length >= 2, "Got at least the parent process and one child");
       is(response.processes.length, processCount + 1,
          "Got one additional process on the second call to listProcesses");
 
       // Connect to the first content processe available
       const content = response.processes.filter(p => (!p.parent))[0];
 
-      client.mainRoot.getProcess(content.id).then(({form: actor}) => {
+      client.mainRoot.getProcess(content.id).then(front => {
+        const actor = front.targetForm;
         ok(actor.consoleActor, "Got the console actor");
         ok(actor.chromeDebugger, "Got the thread actor");
 
         // Ensure sending at least one request to an actor...
         client.request({
           to: actor.consoleActor,
           type: "evaluateJS",
           text: "var a = 42; a",
@@ -101,18 +102,18 @@ function runTests() {
         });
       });
     });
   }
 
   // Assert that calling client.getProcess against the same process id is
   // returning the same actor.
   function getProcessAgain(firstActor, id) {
-    client.mainRoot.getProcess(id).then(response => {
-      const actor = response.form;
+    client.mainRoot.getProcess(id).then(front => {
+      const actor = front.targetForm;
       is(actor, firstActor,
          "Second call to getProcess with the same id returns the same form");
       closeClient();
     });
   }
 
   function processScript() {
     ChromeUtils.import("resource://gre/modules/Services.jsm");
--- a/devtools/server/tests/unit/head_dbg.js
+++ b/devtools/server/tests/unit/head_dbg.js
@@ -96,19 +96,19 @@ async function createTabMemoryFront() {
 async function createFullRuntimeMemoryFront() {
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
   DebuggerServer.allowChromeProcess = true;
 
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   await client.connect();
 
-  const { form } = await client.mainRoot.getMainProcess();
+  const front = await client.mainRoot.getMainProcess();
   const options = {
-    form,
+    activeTab: front,
     client,
     chrome: true,
   };
   const target = await TargetFactory.forRemoteTab(options);
 
   const memoryFront = target.getFront("memory");
   await memoryFront.attach();
 
@@ -400,17 +400,17 @@ async function startTestDebuggerServer(t
 async function finishClient(client) {
   await client.close();
   DebuggerServer.destroy();
   do_test_finished();
 }
 
 function getParentProcessActors(client, server = DebuggerServer) {
   server.allowChromeProcess = true;
-  return client.mainRoot.getMainProcess().then(response => response.form);
+  return client.mainRoot.getMainProcess().then(response => response.targetForm);
 }
 
 /**
  * Takes a relative file path and returns the absolute file url for it.
  */
 function getFileUrl(name, allowMissing = false) {
   const file = do_get_file(name, allowMissing);
   return Services.io.newFileURI(file).spec;
--- a/devtools/server/tests/unit/test_protocol_children.js
+++ b/devtools/server/tests/unit/test_protocol_children.js
@@ -281,17 +281,18 @@ var RootActor = protocol.ActorClassWithS
       child5: this.getChild("child5"),
       more: [ this.getChild("child6"), this.getChild("child7") ],
     };
   },
 
   // This should remind you of a pause actor.
   getTemporaryChild: function(id) {
     if (!this._temporaryHolder) {
-      this._temporaryHolder = this.manage(new protocol.Actor(this.conn));
+      this._temporaryHolder = new protocol.Actor(this.conn);
+      this.manage(this._temporaryHolder);
     }
     return new ChildActor(this.conn, id);
   },
 
   clearTemporaryChildren: function(id) {
     if (!this._temporaryHolder) {
       return;
     }
@@ -310,17 +311,17 @@ var RootFront = protocol.FrontClassWithS
     // Root actor owns itself.
     this.manage(this);
   },
 
   getTemporaryChild: protocol.custom(function(id) {
     if (!this._temporaryHolder) {
       this._temporaryHolder = new protocol.Front(this.conn);
       this._temporaryHolder.actorID = this.actorID + "_temp";
-      this._temporaryHolder = this.manage(this._temporaryHolder);
+      this.manage(this._temporaryHolder);
     }
     return this._getTemporaryChild(id);
   }, {
     impl: "_getTemporaryChild",
   }),
 
   clearTemporaryChildren: protocol.custom(function() {
     if (!this._temporaryHolder) {
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_protocol_onFront.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test Front.onFront method.
+ */
+
+const protocol = require("devtools/shared/protocol");
+const {RetVal} = protocol;
+
+const childSpec = protocol.generateActorSpec({
+  typeName: "childActor",
+});
+
+const ChildActor = protocol.ActorClassWithSpec(childSpec, {
+  initialize(conn, id) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this.childID = id;
+  },
+
+  form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+    return {
+      actor: this.actorID,
+      childID: this.childID,
+    };
+  },
+});
+
+const rootSpec = protocol.generateActorSpec({
+  typeName: "root",
+
+  methods: {
+    createChild: {
+      request: {},
+      response: { actor: RetVal("childActor") },
+    },
+  },
+});
+
+const RootActor = protocol.ActorClassWithSpec(rootSpec, {
+  typeName: "root",
+
+  initialize: function(conn) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+
+    this.actorID = "root";
+
+    // Root actor owns itself.
+    this.manage(this);
+
+    this.sequence = 0;
+  },
+
+  sayHello() {
+    return {
+      from: "root",
+      applicationType: "xpcshell-tests",
+      traits: [],
+    };
+  },
+
+  createChild() {
+    return new ChildActor(this.conn, this.sequence++);
+  },
+});
+
+const ChildFront = protocol.FrontClassWithSpec(childSpec, {
+  form(form, detail) {
+    if (detail === "actorid") {
+      return;
+    }
+    this.childID = form.childID;
+  },
+});
+
+const RootFront = protocol.FrontClassWithSpec(rootSpec, {
+  initialize(client) {
+    this.actorID = "root";
+    protocol.Front.prototype.initialize.call(this, client);
+    // Root owns itself.
+    this.manage(this);
+  },
+});
+
+add_task(async function run_test() {
+  DebuggerServer.createRootActor = RootActor;
+  DebuggerServer.init();
+
+  const trace = connectPipeTracing();
+  const client = new DebuggerClient(trace);
+  await client.connect();
+
+  const rootFront = new RootFront(client);
+
+  const fronts = [];
+  rootFront.onFront("childActor", front => {
+    fronts.push(front);
+  });
+
+  const firstChild = await rootFront.createChild();
+  ok(firstChild instanceof ChildFront, "createChild returns a ChildFront instance");
+  equal(firstChild.childID, 0, "First child has ID=0");
+
+  equal(fronts.length, 1,
+    "onFront fires the callback, even if the front is created in the future");
+  equal(fronts[0], firstChild,
+    "onFront fires the callback with the right front instance");
+
+  const onFrontAfter = await new Promise(resolve => {
+    rootFront.onFront("childActor", resolve);
+  });
+  equal(onFrontAfter, firstChild,
+    "onFront fires the callback, even if the front is already created, " +
+    " with the same front instance");
+
+  equal(fronts.length, 1,
+    "There is still only one front reported from the first listener");
+
+  const secondChild = await rootFront.createChild();
+
+  equal(fronts.length, 2, "After a second call to createChild, two fronts are reported");
+  equal(fronts[1], secondChild, "And the new front is the right instance");
+
+  trace.close();
+  await client.close();
+});
--- a/devtools/server/tests/unit/test_xpcshell_debugging.js
+++ b/devtools/server/tests/unit/test_xpcshell_debugging.js
@@ -20,21 +20,19 @@ add_task(async function() {
   const client = new DebuggerClient(transport);
   await client.connect();
 
   // Ensure that global actors are available. Just test the device actor.
   const deviceFront = await client.mainRoot.getFront("device");
   const desc = await deviceFront.getDescription();
   equal(desc.geckobuildid, Services.appinfo.platformBuildID, "device actor works");
 
-  // Even though we have no tabs, getMainProcess gives us the chromeDebugger.
-  const response = await client.mainRoot.getMainProcess();
-
-  const { chromeDebugger } = response.form;
-  const [, threadClient] = await client.attachThread(chromeDebugger);
+  // Even though we have no tabs, getMainProcess gives us the chrome debugger.
+  const front = await client.mainRoot.getMainProcess();
+  const [, threadClient] = await front.attachThread();
   const onResumed = new Promise(resolve => {
     threadClient.addOneTimeListener("paused", (event, packet) => {
       equal(packet.why.type, "breakpoint",
           "yay - hit the breakpoint at the first line in our script");
       // Resume again - next stop should be our "debugger" statement.
       threadClient.addOneTimeListener("paused", (event, packet) => {
         equal(packet.why.type, "debuggerStatement",
               "yay - hit the 'debugger' statement in our script");
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -101,16 +101,17 @@ skip-if = coverage # bug 1336670
 [test_promises_object_creationtimestamp.js]
 [test_promises_object_timetosettle-01.js]
 [test_promises_object_timetosettle-02.js]
 [test_protocol_abort.js]
 [test_protocol_async.js]
 [test_protocol_children.js]
 [test_protocol_formtype.js]
 [test_protocol_longstring.js]
+[test_protocol_onFront.js]
 [test_protocol_simple.js]
 [test_protocol_stack.js]
 [test_protocol_unregister.js]
 [test_breakpoint-01.js]
 [test_register_actor.js]
 [test_breakpoint-02.js]
 [test_breakpoint-03.js]
 [test_breakpoint-04.js]
--- a/devtools/shared/client/debugger-client.js
+++ b/devtools/shared/client/debugger-client.js
@@ -20,17 +20,16 @@ const {
 loader.lazyRequireGetter(this, "Authentication", "devtools/shared/security/auth");
 loader.lazyRequireGetter(this, "DebuggerSocket", "devtools/shared/security/socket", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 
 loader.lazyRequireGetter(this, "WebConsoleClient", "devtools/shared/webconsole/client", true);
 loader.lazyRequireGetter(this, "AddonTargetFront", "devtools/shared/fronts/targets/addon", true);
 loader.lazyRequireGetter(this, "RootFront", "devtools/shared/fronts/root", true);
 loader.lazyRequireGetter(this, "BrowsingContextTargetFront", "devtools/shared/fronts/targets/browsing-context", true);
-loader.lazyRequireGetter(this, "WorkerTargetFront", "devtools/shared/fronts/targets/worker", true);
 loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
 loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/object-client");
 loader.lazyRequireGetter(this, "Pool", "devtools/shared/protocol", true);
 loader.lazyRequireGetter(this, "Front", "devtools/shared/protocol", true);
 
 // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
 const PLATFORM_MAJOR_VERSION = AppConstants.MOZ_APP_VERSION.match(/\d+/)[0];
 
@@ -55,17 +54,17 @@ function DebuggerClient(transport) {
   // To be removed once all clients are refactored to protocol.js
   this._clients = new Map();
 
   // Pool of fronts instanciated by this class.
   // This is useful for actors that have already been transitioned to protocol.js
   // Once RootClient becomes a protocol.js actor, these actors can be attached to it
   // instead of this pool.
   // This Pool will automatically be added to this._pools via addActorPool once the first
-  // Front will be added to it (in attachTarget, attachWorker,...).
+  // Front will be added to it (in attachTarget, ...).
   // And it does not need to destroyed explicitly as all Pools are destroyed on client
   // closing.
   this._frontPool = new Pool(this);
 
   this._pendingRequests = new Map();
   this._activeRequests = new Map();
   this._eventsEnabled = true;
 
@@ -376,27 +375,16 @@ DebuggerClient.prototype = {
       front = new BrowsingContextTargetFront(this, { actor: targetActor });
       this._frontPool.manage(front);
     }
 
     const response = await front.attach();
     return [response, front];
   },
 
-  attachWorker: async function(workerTargetActor) {
-    let front = this._frontPool.actor(workerTargetActor);
-    if (!front) {
-      front = new WorkerTargetFront(this, { actor: workerTargetActor });
-      this._frontPool.manage(front);
-    }
-
-    const response = await front.attach();
-    return [response, front];
-  },
-
   /**
    * Attach to an addon target actor.
    *
    * @param string addonTargetActor
    *        The actor ID for the addon to attach.
    */
   attachAddon: async function(form) {
     let front = this._frontPool.actor(form.actor);
--- a/devtools/shared/css/color.js
+++ b/devtools/shared/css/color.js
@@ -70,16 +70,18 @@ module.exports.colorUtils = {
   CssColor: CssColor,
   rgbToHsl: rgbToHsl,
   setAlpha: setAlpha,
   classifyColor: classifyColor,
   rgbToColorName: rgbToColorName,
   colorToRGBA: colorToRGBA,
   isValidCSSColor: isValidCSSColor,
   calculateContrastRatio: calculateContrastRatio,
+  calculateLuminance: calculateLuminance,
+  blendColors: blendColors,
 };
 
 /**
  * Values used in COLOR_UNIT_PREF
  */
 CssColor.COLORUNIT = {
   "authored": "authored",
   "hex": "hex",
@@ -394,17 +396,17 @@ CssColor.prototype = {
 
   /**
    * Returns a RGBA 4-Tuple representation of a color or transparent as
    * appropriate.
    */
   getRGBATuple: function() {
     const tuple = colorToRGBA(this.authored, this.cssColor4);
 
-    tuple.a = parseFloat(tuple.a.toFixed(1));
+    tuple.a = parseFloat(tuple.a.toFixed(2));
 
     return tuple;
   },
 
   /**
    * Returns a HSLA 4-Tuple representation of a color or transparent as
    * appropriate.
    */
@@ -412,17 +414,17 @@ CssColor.prototype = {
     const {r, g, b, a} = colorToRGBA(this.authored, this.cssColor4);
 
     const [h, s, l] = rgbToHsl([r, g, b]);
 
     return {
       h,
       s,
       l,
-      a: parseFloat(a.toFixed(1)),
+      a: parseFloat(a.toFixed(2)),
     };
   },
 
   _hsl: function(maybeAlpha) {
     if (this.lowerCased.startsWith("hsl(") && maybeAlpha === undefined) {
       // We can use it as-is.
       return this.authored;
     }
--- a/devtools/shared/fronts/root.js
+++ b/devtools/shared/fronts/root.js
@@ -4,16 +4,17 @@
 "use strict";
 
 const {Ci} = require("chrome");
 const {rootSpec} = require("devtools/shared/specs/root");
 const protocol = require("devtools/shared/protocol");
 const {custom} = protocol;
 
 loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);
+loader.lazyRequireGetter(this, "BrowsingContextTargetFront", "devtools/shared/fronts/targets/browsing-context", true);
 loader.lazyRequireGetter(this, "ContentProcessTargetFront", "devtools/shared/fronts/targets/content-process", true);
 
 const RootFront = protocol.FrontClassWithSpec(rootSpec, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client, { actor: form.from });
 
     this.applicationType = form.applicationType;
     this.traits = form.traits;
@@ -63,22 +64,18 @@ const RootFront = protocol.FrontClassWit
 
       // And then from the Child processes
       const { processes } = await this.listProcesses();
       for (const process of processes) {
         // Ignore parent process
         if (process.parent) {
           continue;
         }
-        const { form } = await this.getProcess(process.id);
-        const processActor = form.actor;
-        const response = await this._client.request({
-          to: processActor,
-          type: "listWorkers",
-        });
+        const front = await this.getProcess(process.id);
+        const response = await front.listWorkers();
         workers = workers.concat(response.workers);
       }
     } catch (e) {
       // Something went wrong, maybe our client is disconnected?
     }
 
     const result = {
       service: [],
@@ -93,40 +90,40 @@ const RootFront = protocol.FrontClassWit
         scope: form.scope,
         fetch: form.fetch,
         registrationActor: form.actor,
         active: form.active,
         lastUpdateTime: form.lastUpdateTime,
       });
     });
 
-    workers.forEach(form => {
+    workers.forEach(front => {
       const worker = {
-        name: form.url,
-        url: form.url,
-        workerTargetActor: form.actor,
+        name: front.url,
+        url: front.url,
+        workerTargetFront: front,
       };
-      switch (form.type) {
+      switch (front.type) {
         case Ci.nsIWorkerDebugger.TYPE_SERVICE:
-          const registration = result.service.find(r => r.scope === form.scope);
+          const registration = result.service.find(r => r.scope === front.scope);
           if (registration) {
             // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
             // have a scriptSpec, but its associated WorkerDebugger does.
             if (!registration.url) {
-              registration.name = registration.url = form.url;
+              registration.name = registration.url = front.url;
             }
-            registration.workerTargetActor = form.actor;
+            registration.workerTargetFront = front;
           } else {
-            worker.fetch = form.fetch;
+            worker.fetch = front.fetch;
 
             // If a service worker registration could not be found, this means we are in
             // e10s, and registrations are not forwarded to other processes until they
             // reach the activated state. Augment the worker as a registration worker to
             // display it in aboutdebugging.
-            worker.scope = form.scope;
+            worker.scope = front.scope;
             worker.active = false;
             result.service.push(worker);
           }
           break;
         case Ci.nsIWorkerDebugger.TYPE_SHARED:
           result.shared.push(worker);
           break;
         default:
@@ -142,16 +139,43 @@ const RootFront = protocol.FrontClassWit
    *
    * `getProcess` requests allows to fetch the target actor for any process
    * and the main process is having the process ID zero.
    */
   getMainProcess() {
     return this.getProcess(0);
   },
 
+  getProcess: custom(async function(id) {
+    // Do not use specification automatic marshalling as getProcess may return
+    // two different type: ParentProcessTargetActor or ContentProcessTargetActor.
+    // Also, we do want to memoize the fronts and return already existing ones.
+    const { form } = await this._getProcess(id);
+    let front = this.actor(form.actor);
+    if (front) {
+      return front;
+    }
+    // getProcess may return a ContentProcessTargetActor or a ParentProcessTargetActor
+    // In most cases getProcess(0) will return the main process target actor,
+    // which is a ParentProcessTargetActor, but not in xpcshell, which uses a
+    // ContentProcessTargetActor. So select the right front based on the actor ID.
+    if (form.actor.includes("contentProcessTarget")) {
+      front = new ContentProcessTargetFront(this._client, form);
+    } else {
+      // ParentProcessTargetActor doesn't have a specific front, instead it uses
+      // BrowsingContextTargetFront on the client side.
+      front = new BrowsingContextTargetFront(this._client, form);
+    }
+    this.manage(front);
+
+    return front;
+  }, {
+    impl: "_getProcess",
+  }),
+
   /**
    * Fetch the target actor for the currently selected tab, or for a specific
    * tab given as first parameter.
    *
    * @param [optional] object filter
    *        A dictionary object with following optional attributes:
    *         - outerWindowID: used to match tabs in parent process
    *         - tabId: used to match tabs in child processes
@@ -186,25 +210,16 @@ const RootFront = protocol.FrontClassWit
       }
     }
 
     return this._getTab(packet);
   }, {
     impl: "_getTab",
   }),
 
-  attachContentProcessTarget: async function(form) {
-    let front = this.actor(form.actor);
-    if (!front) {
-      front = new ContentProcessTargetFront(this._client, form);
-      this.manage(front);
-    }
-    return front;
-  },
-
   /**
    * Test request that returns the object passed as first argument.
    *
    * `echo` is special as all the property of the given object have to be passed
    * on the packet object. That's not something that can be achieve by requester helper.
    */
 
   echo(packet) {
--- a/devtools/shared/fronts/targets/browsing-context.js
+++ b/devtools/shared/fronts/targets/browsing-context.js
@@ -19,16 +19,20 @@ protocol.FrontClassWithSpec(browsingCont
     // Cache the value of some target properties that are being returned by `attach`
     // request and then keep them up-to-date in `reconfigure` request.
     this.configureOptions = {
       javascriptEnabled: null,
     };
 
     // TODO: remove once ThreadClient becomes a front
     this.client = client;
+
+    // Save the full form for Target class usage
+    // Do not use `form` name to avoid colliding with protocol.js's `form` method
+    this.targetForm = form;
   },
 
   /**
    * Attach to a thread actor.
    *
    * @param object options
    *        Configuration options.
    *        - useSourceMaps: whether to use source maps or not.
@@ -92,15 +96,11 @@ protocol.FrontClassWithSpec(browsingCont
     }
 
     this.destroy();
 
     return response;
   }, {
     impl: "_detach",
   }),
-
-  attachWorker: function(workerTargetActor) {
-    return this.client.attachWorker(workerTargetActor);
-  },
 });
 
 exports.BrowsingContextTargetFront = BrowsingContextTargetFront;
--- a/devtools/shared/fronts/targets/content-process.js
+++ b/devtools/shared/fronts/targets/content-process.js
@@ -8,16 +8,20 @@ const protocol = require("devtools/share
 
 const ContentProcessTargetFront = protocol.FrontClassWithSpec(contentProcessTargetSpec, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
 
     this.client = client;
     this.chromeDebugger = form.chromeDebugger;
 
+    // Save the full form for Target class usage
+    // Do not use `form` name to avoid colliding with protocol.js's `form` method
+    this.targetForm = form;
+
     this.traits = {};
   },
 
   attachThread() {
     return this.client.attachThread(this.chromeDebugger);
   },
 
   reconfigure: function() {
--- a/devtools/shared/fronts/targets/worker.js
+++ b/devtools/shared/fronts/targets/worker.js
@@ -5,31 +5,43 @@
 
 const {workerTargetSpec} = require("devtools/shared/specs/targets/worker");
 const protocol = require("devtools/shared/protocol");
 const {custom} = protocol;
 
 loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
 
 const WorkerTargetFront = protocol.FrontClassWithSpec(workerTargetSpec, {
-  initialize: function(client, form) {
-    protocol.Front.prototype.initialize.call(this, client, form);
+  initialize: function(client) {
+    protocol.Front.prototype.initialize.call(this, client);
 
     this.thread = null;
     this.traits = {};
 
     // TODO: remove once ThreadClient becomes a front
     this.client = client;
 
     this._isClosed = false;
 
     this.destroy = this.destroy.bind(this);
     this.on("close", this.destroy);
   },
 
+  form(json) {
+    this.actorID = json.actor;
+
+    // Save the full form for Target class usage.
+    // Do not use `form` name to avoid colliding with protocol.js's `form` method
+    this.targetForm = json;
+    this.url = json.url;
+    this.type = json.type;
+    this.scope = json.scope;
+    this.fetch = json.fetch;
+  },
+
   get isClosed() {
     return this._isClosed;
   },
 
   destroy: function() {
     this.off("close", this.destroy);
     this._isClosed = true;
 
@@ -45,17 +57,18 @@ const WorkerTargetFront = protocol.Front
   attach: custom(async function() {
     const response = await this._attach();
 
     this.url = response.url;
 
     // Immediately call `connect` in other to fetch console and thread actors
     // that will be later used by Target.
     const connectResponse = await this.connect({});
-    this.consoleActor = connectResponse.consoleActor;
+    // Set the console actor ID on the form to expose it to Target.attach's attachConsole
+    this.targetForm.consoleActor = connectResponse.consoleActor;
     this.threadActor = connectResponse.threadActor;
 
     return response;
   }, {
     impl: "_attach",
   }),
 
   detach: custom(async function() {
@@ -80,17 +93,17 @@ const WorkerTargetFront = protocol.Front
     return Promise.resolve();
   },
 
   attachThread: async function(options = {}) {
     if (this.thread) {
       const response = [{
         type: "connected",
         threadActor: this.thread._actor,
-        consoleActor: this.consoleActor,
+        consoleActor: this.targetForm.consoleActor,
       }, this.thread];
       return response;
     }
 
     const attachResponse = await this.client.request({
       to: this.threadActor,
       type: "attach",
       options,
--- a/devtools/shared/locales/en-US/accessibility.properties
+++ b/devtools/shared/locales/en-US/accessibility.properties
@@ -1,8 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # LOCALIZATION NOTE (accessibility.contrast.ratio): A title text for the color contrast
 # ratio description, used by the accessibility highlighter to display the value. %S in the
 # content will be replaced by the contrast ratio numerical value.
 accessibility.contrast.ratio=Contrast: %S
+
+# LOCALIZATION NOTE (accessibility.contrast.ratio.error): A title text for the color
+# contrast ratio, used when the tool is unable to calculate the contrast ratio value.
+accessibility.contrast.ratio.error=Unable to calculate
+
+# LOCALIZATION NOTE (accessibility.contrast.ratio.label): A title text for the color
+# contrast ratio description, used together with the actual values.
+accessibility.contrast.ratio.label=Contrast:
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -867,17 +867,16 @@ Pool.prototype = extend(EventEmitter.pro
       // TODO: not all actors have been moved to protocol.js, so they do not all have
       // a parent field. Remove the check for the parent once the conversion is finished
       const parent = this.poolFor(actor.actorID);
       if (parent) {
         parent.unmanage(actor);
       }
     }
     this._poolMap.set(actor.actorID, actor);
-    return actor;
   },
 
   /**
    * Remove an actor as a child of this pool.
    */
   unmanage: function(actor) {
     this.__poolMap && this.__poolMap.delete(actor.actorID);
   },
@@ -1281,16 +1280,20 @@ exports.ActorClassWithSpec = ActorClassW
  * @param optional form
  *   The json form provided by the server.
  * @constructor
  */
 var Front = function(conn = null, form = null, detail = null, context = null) {
   Pool.call(this, conn);
   this._requests = [];
 
+  // Front listener functions registered via `onFront` get notified
+  // of new fronts via this dedicated EventEmitter object.
+  this._frontListeners = new EventEmitter();
+
   // protocol.js no longer uses this data in the constructor, only external
   // uses do.  External usage of manually-constructed fronts will be
   // drastically reduced if we convert the root and target actors to
   // protocol.js, in which case this can probably go away.
   if (form) {
     this.actorID = form.actor;
     form = types.getType(this.typeName).formType(detail).read(form, this, detail);
     this.form(form, detail, context);
@@ -1310,24 +1313,41 @@ Front.prototype = extend(Pool.prototype,
       const { deferred, to, type, stack } = this._requests.shift();
       const msg = "Connection closed, pending request to " + to +
                 ", type " + type + " failed" +
                 "\n\nRequest stack:\n" + stack.formattedStack;
       deferred.reject(new Error(msg));
     }
     Pool.prototype.destroy.call(this);
     this.actorID = null;
+    this._frontListeners = null;
   },
 
   manage: function(front) {
     if (!front.actorID) {
       throw new Error("Can't manage front without an actor ID.\n" +
                       "Ensure server supports " + front.typeName + ".");
     }
-    return Pool.prototype.manage.call(this, front);
+    Pool.prototype.manage.call(this, front);
+
+    // Call listeners registered via `onFront` method
+    this._frontListeners.emit(front.typeName, front);
+  },
+
+  // Run callback on every front of this type that currently exists, and on every
+  // instantiation of front type in the future.
+  onFront(typeName, callback) {
+    // First fire the callback on already instantiated fronts
+    for (const front of this.poolChildren()) {
+      if (front.typeName == typeName) {
+        callback(front);
+      }
+    }
+    // Then register the callback for fronts instantiated in the future
+    this._frontListeners.on(typeName, callback);
   },
 
   toString: function() {
     return "[Front for " + this.typeName + "/" + this.actorID + "]";
   },
 
   /**
    * Update the actor from its representation.
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -267,17 +267,17 @@ const Types = exports.__TypesForTests = 
   {
     types: ["webExtensionTarget"],
     spec: "devtools/shared/specs/targets/webextension",
     front: null,
   },
   {
     types: ["workerTarget"],
     spec: "devtools/shared/specs/targets/worker",
-    front: null,
+    front: "devtools/shared/fronts/targets/worker",
   },
   {
     types: ["audionode", "webaudio"],
     spec: "devtools/shared/specs/webaudio",
     front: "devtools/shared/fronts/webaudio",
   },
   {
     types: ["console"],
--- a/devtools/shared/specs/root.js
+++ b/devtools/shared/specs/root.js
@@ -10,17 +10,17 @@ types.addDictType("root.getTab", {
 });
 types.addDictType("root.getWindow", {
   window: "json",
 });
 types.addDictType("root.listAddons", {
   addons: "array:json",
 });
 types.addDictType("root.listWorkers", {
-  workers: "array:json",
+  workers: "array:workerTarget",
 });
 types.addDictType("root.listServiceWorkerRegistrations", {
   registrations: "array:json",
 });
 types.addDictType("root.listProcesses", {
   processes: "array:json",
 });
 
--- a/devtools/shared/specs/targets/browsing-context.js
+++ b/devtools/shared/specs/targets/browsing-context.js
@@ -32,17 +32,17 @@ types.addDictType("browsingContextTarget
   parentID: "nullable:string",
   url: "nullable:string", // should be present if not destroying
   title: "nullable:string", // should be present if not destroying
   destroy: "nullable:boolean", // not present if not destroying
 });
 
 types.addDictType("browsingContextTarget.workers", {
   error: "nullable:string",
-  workers: "nullable:array:json",
+  workers: "nullable:array:workerTarget",
 });
 
 types.addDictType("browsingContextTarget.reload", {
   force: "boolean",
 });
 
 types.addDictType("browsingContextTarget.reconfigure", {
   javascriptEnabled: "nullable:boolean",
--- a/devtools/shared/specs/targets/content-process.js
+++ b/devtools/shared/specs/targets/content-process.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {types, Option, RetVal, generateActorSpec} = require("devtools/shared/protocol");
 
 types.addDictType("contentProcessTarget.workers", {
   error: "nullable:string",
-  workers: "nullable:array:json",
+  workers: "nullable:array:workerTarget",
 });
 
 const contentProcessTargetSpec = generateActorSpec({
   typeName: "contentProcessTarget",
 
   methods: {
     listWorkers: {
       request: {},
@@ -22,12 +22,15 @@ const contentProcessTargetSpec = generat
 
   events: {
     // newSource is being sent by ThreadActor in the name of its parent,
     // i.e. ContentProcessTargetActor
     newSource: {
       type: "newSource",
       source: Option(0, "json"),
     },
+    workerListChanged: {
+      type: "workerListChanged",
+    },
   },
 });
 
 exports.contentProcessTargetSpec = contentProcessTargetSpec;
--- a/devtools/shared/webconsole/test/common.js
+++ b/devtools/shared/webconsole/test/common.js
@@ -73,19 +73,19 @@ var _attachConsole = async function(
   if (response.error) {
     console.error("client.connect() failed: " + response.error + " " +
                   response.message);
     callback(state, response);
     return;
   }
 
   if (!attachToTab) {
-    response = await state.dbgClient.mainRoot.getMainProcess();
-    await state.dbgClient.attachTarget(response.form.actor);
-    const consoleActor = response.form.consoleActor;
+    const front = await state.dbgClient.mainRoot.getMainProcess();
+    await front.attach();
+    const consoleActor = front.targetForm.consoleActor;
     state.actor = consoleActor;
     state.dbgClient.attachConsole(consoleActor, listeners)
       .then(_onAttachConsole.bind(null, state), _onAttachError.bind(null, state));
     return;
   }
   response = await state.dbgClient.listTabs();
   if (response.error) {
     console.error("listTabs failed: " + response.error + " " +
@@ -100,32 +100,25 @@ var _attachConsole = async function(
     const worker = new Worker(workerName);
     // Keep a strong reference to the Worker to avoid it being
     // GCd during the test (bug 1237492).
     // eslint-disable-next-line camelcase
     state._worker_ref = worker;
     await waitForMessage(worker);
 
     const { workers } = await targetFront.listWorkers();
-    const workerTargetActor = workers.filter(w => w.url == workerName)[0].actor;
-    if (!workerTargetActor) {
-      console.error("listWorkers failed. Unable to find the " +
-                    "worker actor\n");
+    const workerTargetFront = workers.filter(w => w.url == workerName)[0];
+    if (!workerTargetFront) {
+      console.error("listWorkers failed. Unable to find the worker actor\n");
       return;
     }
-    const [workerResponse, workerTargetFront] =
-      await targetFront.attachWorker(workerTargetActor);
-    if (!workerTargetFront || workerResponse.error) {
-      console.error("attachWorker failed. No worker target front or " +
-                    " error: " + workerResponse.error);
-      return;
-    }
+    await workerTargetFront.attach();
     await workerTargetFront.attachThread({});
-    state.actor = workerTargetFront.consoleActor;
-    state.dbgClient.attachConsole(workerTargetFront.consoleActor, listeners)
+    state.actor = workerTargetFront.targetForm.consoleActor;
+    state.dbgClient.attachConsole(workerTargetFront.targetForm.consoleActor, listeners)
       .then(_onAttachConsole.bind(null, state), _onAttachError.bind(null, state));
   } else {
     state.actor = tab.consoleActor;
     state.dbgClient.attachConsole(tab.consoleActor, listeners)
       .then(_onAttachConsole.bind(null, state), _onAttachError.bind(null, state));
   }
 };
 
--- a/dom/animation/test/chrome/test_animation_performance_warning.html
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -177,24 +177,23 @@ function testBasicOperation() {
         },
         {
           property: 'opacity',
           runningOnCompositor: true
         }
       ]
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
       var animation = addDivAndAnimate(t, { class: 'compositable' },
                                        subtest.frames, 100 * MS_PER_SEC);
-      return waitForPaints().then(() => {
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          subtest.expected);
-      });
+      await waitForPaints();
+      assert_animation_property_state_equals(
+        animation.effect.getProperties(),
+        subtest.expected);
     }, subtest.desc);
   });
 }
 
 // Test adding/removing a 'width' property on the same animation object.
 function testKeyframesWithGeometricProperties() {
   [
     {
@@ -252,92 +251,91 @@ function testKeyframesWithGeometricPrope
             property: 'transform',
             runningOnCompositor: false,
             warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
           }
         ]
       }
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
       var animation = addDivAndAnimate(t, { class: 'compositable' },
                                        subtest.frames, 100 * MS_PER_SEC);
-      return waitForPaints().then(() => {
-        // First, a transform animation is running on compositor.
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          subtest.expected.withoutGeometric);
-      }).then(() => {
-        // Add a 'width' property.
-        var keyframes = animation.effect.getKeyframes();
+      await waitForPaints();
+
+      // First, a transform animation is running on compositor.
+      assert_animation_property_state_equals(
+        animation.effect.getProperties(),
+        subtest.expected.withoutGeometric);
 
-        keyframes[0].width = '100px';
-        keyframes[1].width = '200px';
+      // Add a 'width' property.
+      var keyframes = animation.effect.getKeyframes();
+
+      keyframes[0].width = '100px';
+      keyframes[1].width = '200px';
+
+      animation.effect.setKeyframes(keyframes);
+      await waitForFrame();
 
-        animation.effect.setKeyframes(keyframes);
-        return waitForFrame();
-      }).then(() => {
-        // Now the transform animation is not running on compositor because of
-        // the 'width' property.
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          subtest.expected.withGeometric);
-      }).then(() => {
-        // Remove the 'width' property.
-        var keyframes = animation.effect.getKeyframes();
+      // Now the transform animation is not running on compositor because of
+      // the 'width' property.
+      assert_animation_property_state_equals(
+        animation.effect.getProperties(),
+        subtest.expected.withGeometric);
+
+      // Remove the 'width' property.
+      var keyframes = animation.effect.getKeyframes();
 
-        delete keyframes[0].width;
-        delete keyframes[1].width;
+      delete keyframes[0].width;
+      delete keyframes[1].width;
 
-        animation.effect.setKeyframes(keyframes);
-        return waitForFrame();
-      }).then(() => {
-        // Finally, the transform animation is running on compositor.
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          subtest.expected.withoutGeometric);
-      });
+      animation.effect.setKeyframes(keyframes);
+      await waitForFrame();
+
+      // Finally, the transform animation is running on compositor.
+      assert_animation_property_state_equals(
+        animation.effect.getProperties(),
+        subtest.expected.withoutGeometric);
     }, 'An animation has: ' + subtest.desc);
   });
 }
 
 // Test that the expected set of geometric properties all block transform
 // animations.
 function testSetOfGeometricProperties() {
   const geometricProperties = [
     'width', 'height',
     'top', 'right', 'bottom', 'left',
     'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
     'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
   ];
 
   geometricProperties.forEach(property => {
-    promise_test(t => {
+    promise_test(async t => {
       const keyframes = {
         [propertyToIDL(property)]: [ '100px', '200px' ],
         transform: [ 'translate(0px)', 'translate(100px)' ]
       };
       var animation = addDivAndAnimate(t, { class: 'compositable' },
                                        keyframes, 100 * MS_PER_SEC);
 
-      return waitForPaints().then(() => {
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          [
-            {
-              property,
-              runningOnCompositor: false
-            },
-            {
-              property: 'transform',
-              runningOnCompositor: false,
-              warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
-            }
-          ]);
-      }, 'Transform animation should be run on the main thread');
+      await waitForPaints();
+      assert_animation_property_state_equals(
+        animation.effect.getProperties(),
+        [
+          {
+            property,
+            runningOnCompositor: false
+          },
+          {
+            property: 'transform',
+            runningOnCompositor: false,
+            warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+          }
+        ]);
     }, `${property} is treated as a geometric property`);
   });
 }
 
 // Performance warning tests that set and clear a style property.
 function testStyleChanges() {
   [
     {
@@ -402,36 +400,35 @@ function testStyleChanges() {
         {
           property: 'transform',
           runningOnCompositor: false,
           warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
         }
       ]
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
       var animation = addDivAndAnimate(t, { class: 'compositable' },
                                        subtest.frames, 100 * MS_PER_SEC);
-      return waitForPaints().then(() => {
-        assert_all_properties_running_on_compositor(
-          animation.effect.getProperties(),
-          subtest.expected);
-        animation.effect.target.style = subtest.style;
-        return waitForFrame();
-      }).then(() => {
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          subtest.expected);
-        animation.effect.target.style = '';
-        return waitForFrame();
-      }).then(() => {
-        assert_all_properties_running_on_compositor(
-          animation.effect.getProperties(),
-          subtest.expected);
-      });
+      await waitForPaints();
+      assert_all_properties_running_on_compositor(
+        animation.effect.getProperties(),
+        subtest.expected);
+      animation.effect.target.style = subtest.style;
+      await waitForFrame();
+
+      assert_animation_property_state_equals(
+        animation.effect.getProperties(),
+        subtest.expected);
+      animation.effect.target.style = '';
+      await waitForFrame();
+
+      assert_all_properties_running_on_compositor(
+        animation.effect.getProperties(),
+        subtest.expected);
     }, subtest.desc);
   });
 }
 
 // Performance warning tests that set and clear the id property
 function testIdChanges() {
   [
     {
@@ -445,40 +442,40 @@ function testIdChanges() {
         {
           property: 'transform',
           runningOnCompositor: false,
           warning: 'CompositorAnimationWarningHasRenderingObserver'
         }
       ]
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
       if (subtest.createelement) {
         addDiv(t, { style: subtest.createelement });
       }
 
       var animation = addDivAndAnimate(t, { class: 'compositable' },
                                        subtest.frames, 100 * MS_PER_SEC);
-      return waitForPaints().then(() => {
-        assert_all_properties_running_on_compositor(
-          animation.effect.getProperties(),
-          subtest.expected);
-        animation.effect.target.id = subtest.id;
-        return waitForFrame();
-      }).then(() => {
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          subtest.expected);
-        animation.effect.target.id = '';
-        return waitForFrame();
-      }).then(() => {
-        assert_all_properties_running_on_compositor(
-          animation.effect.getProperties(),
-          subtest.expected);
-      });
+      await waitForPaints();
+
+      assert_all_properties_running_on_compositor(
+        animation.effect.getProperties(),
+        subtest.expected);
+      animation.effect.target.id = subtest.id;
+      await waitForFrame();
+
+      assert_animation_property_state_equals(
+        animation.effect.getProperties(),
+        subtest.expected);
+      animation.effect.target.id = '';
+      await waitForFrame();
+
+      assert_all_properties_running_on_compositor(
+        animation.effect.getProperties(),
+        subtest.expected);
     }, subtest.desc);
   });
 }
 
 function testMultipleAnimations() {
   [
     {
       desc: 'opacity and transform with preserve-3d',
@@ -534,47 +531,47 @@ function testMultipleAnimations() {
               property: 'opacity',
               runningOnCompositor: true,
             }
           ]
         }
       ],
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
       var div = addDiv(t, { class: 'compositable' });
       var animations = subtest.animations.map(anim => {
         var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
 
         // Bind expected values to animation object.
         animation.expected = anim.expected;
         return animation;
       });
-      return waitForPaints().then(() => {
-        animations.forEach(anim => {
-          assert_all_properties_running_on_compositor(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
-        div.style = subtest.style;
-        return waitForFrame();
-      }).then(() => {
-        animations.forEach(anim => {
-          assert_animation_property_state_equals(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
-        div.style = '';
-        return waitForFrame();
-      }).then(() => {
-        animations.forEach(anim => {
-          assert_all_properties_running_on_compositor(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
+      await waitForPaints();
+
+      animations.forEach(anim => {
+        assert_all_properties_running_on_compositor(
+          anim.effect.getProperties(),
+          anim.expected);
+      });
+      div.style = subtest.style;
+      await waitForFrame();
+
+      animations.forEach(anim => {
+        assert_animation_property_state_equals(
+          anim.effect.getProperties(),
+          anim.expected);
+      });
+      div.style = '';
+      await waitForFrame();
+
+      animations.forEach(anim => {
+        assert_all_properties_running_on_compositor(
+          anim.effect.getProperties(),
+          anim.expected);
       });
     }, 'Multiple animations: ' + subtest.desc);
   });
 }
 
 // Test adding/removing a 'width' keyframe on the same animation object, where
 // multiple animation objects belong to the same element.
 // The 'width' property is added to animations[1].
@@ -672,65 +669,64 @@ function testMultipleAnimationsWithGeome
                 warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
               }
             ]
           }
         }
       ]
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
       var div = addDiv(t, { class: 'compositable' });
       var animations = subtest.animations.map(anim => {
         var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
 
         // Bind expected values to animation object.
         animation.expected = anim.expected;
         return animation;
       });
-      return waitForPaints().then(() => {
-        // First, all animations are running on compositor.
-        animations.forEach(anim => {
-          assert_animation_property_state_equals(
-            anim.effect.getProperties(),
-            anim.expected.withoutGeometric);
-        });
-      }).then(() => {
-        // Add a 'width' property to animations[1].
-        var keyframes = animations[1].effect.getKeyframes();
+      await waitForPaints();
+      // First, all animations are running on compositor.
+      animations.forEach(anim => {
+        assert_animation_property_state_equals(
+          anim.effect.getProperties(),
+          anim.expected.withoutGeometric);
+      });
 
-        keyframes[0].width = '100px';
-        keyframes[1].width = '200px';
+      // Add a 'width' property to animations[1].
+      var keyframes = animations[1].effect.getKeyframes();
+
+      keyframes[0].width = '100px';
+      keyframes[1].width = '200px';
+
+      animations[1].effect.setKeyframes(keyframes);
+      await waitForFrame();
 
-        animations[1].effect.setKeyframes(keyframes);
-        return waitForFrame();
-      }).then(() => {
-        // Now the transform animation is not running on compositor because of
-        // the 'width' property.
-        animations.forEach(anim => {
-          assert_animation_property_state_equals(
-            anim.effect.getProperties(),
-            anim.expected.withGeometric);
-        });
-      }).then(() => {
-        // Remove the 'width' property from animations[1].
-        var keyframes = animations[1].effect.getKeyframes();
+      // Now the transform animation is not running on compositor because of
+      // the 'width' property.
+      animations.forEach(anim => {
+        assert_animation_property_state_equals(
+          anim.effect.getProperties(),
+          anim.expected.withGeometric);
+      });
+
+      // Remove the 'width' property from animations[1].
+      var keyframes = animations[1].effect.getKeyframes();
 
-        delete keyframes[0].width;
-        delete keyframes[1].width;
+      delete keyframes[0].width;
+      delete keyframes[1].width;
+
+      animations[1].effect.setKeyframes(keyframes);
+      await waitForFrame();
 
-        animations[1].effect.setKeyframes(keyframes);
-        return waitForFrame();
-      }).then(() => {
-        // Finally, all animations are running on compositor.
-        animations.forEach(anim => {
-          assert_animation_property_state_equals(
-            anim.effect.getProperties(),
-            anim.expected.withoutGeometric);
-        });
+      // Finally, all animations are running on compositor.
+      animations.forEach(anim => {
+        assert_animation_property_state_equals(
+          anim.effect.getProperties(),
+          anim.expected.withoutGeometric);
       });
     }, 'Multiple animations with geometric property: ' + subtest.desc);
   });
 }
 
 // Tests adding/removing 'width' animation on the same element which has async
 // animations.
 function testMultipleAnimationsWithGeometricAnimations() {
@@ -792,57 +788,56 @@ function testMultipleAnimationsWithGeome
               property: 'opacity',
               runningOnCompositor: true,
             }
           ]
         }
       ],
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
       var div = addDiv(t, { class: 'compositable' });
       var animations = subtest.animations.map(anim => {
         var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
 
         // Bind expected values to animation object.
         animation.expected = anim.expected;
         return animation;
       });
 
       var widthAnimation;
 
-      return waitForPaints().then(() => {
-        animations.forEach(anim => {
-          assert_all_properties_running_on_compositor(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
-      }).then(() => {
-        // Append 'width' animation on the same element.
-        widthAnimation = div.animate({ width: ['100px', '200px'] },
-                                     100 * MS_PER_SEC);
-        return waitForFrame();
-      }).then(() => {
-        // Now transform animations are not running on compositor because of
-        // the 'width' animation.
-        animations.forEach(anim => {
-          assert_animation_property_state_equals(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
-        // Remove the 'width' animation.
-        widthAnimation.cancel();
-        return waitForFrame();
-      }).then(() => {
-        // Now all animations are running on compositor.
-        animations.forEach(anim => {
-          assert_all_properties_running_on_compositor(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
+      await waitForPaints();
+      animations.forEach(anim => {
+        assert_all_properties_running_on_compositor(
+          anim.effect.getProperties(),
+          anim.expected);
+      });
+
+      // Append 'width' animation on the same element.
+      widthAnimation = div.animate({ width: ['100px', '200px'] },
+                                   100 * MS_PER_SEC);
+      await waitForFrame();
+
+      // Now transform animations are not running on compositor because of
+      // the 'width' animation.
+      animations.forEach(anim => {
+        assert_animation_property_state_equals(
+          anim.effect.getProperties(),
+          anim.expected);
+      });
+      // Remove the 'width' animation.
+      widthAnimation.cancel();
+      await waitForFrame();
+
+      // Now all animations are running on compositor.
+      animations.forEach(anim => {
+        assert_all_properties_running_on_compositor(
+          anim.effect.getProperties(),
+          anim.expected);
       });
     }, 'Multiple async animations and geometric animation: ' + subtest.desc);
   });
 }
 
 function testSmallElements() {
   [
     {
@@ -874,74 +869,72 @@ function testSmallElements() {
       expected: [
         {
           property: 'transform',
           runningOnCompositor: true
         }
       ]
     },
   ].forEach(subtest => {
-    promise_test(t => {
+    promise_test(async t => {
     var div = addDiv(t, subtest.style);
     var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
-      return waitForPaints().then(() => {
-        assert_animation_property_state_equals(
-          animation.effect.getProperties(),
-          subtest.expected);
-      });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      subtest.expected);
     }, subtest.desc);
   });
 }
 
 function testSynchronizedAnimations() {
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ] },
                                 100 * MS_PER_SEC);
     const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
 
-    return waitForPaints()
-      .then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: false,
-              warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
-          } ]);
-      });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: false,
+          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+      } ]);
   }, 'Animations created within the same tick are synchronized'
      + ' (compositor animation created first)');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
     const animB = elemB.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ] },
                                 100 * MS_PER_SEC);
 
-    return waitForPaints()
-      .then(() => {
-        assert_animation_property_state_equals(
-          animB.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: false,
-              warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
-          } ]);
-      });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animB.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: false,
+          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+      } ]);
   }, 'Animations created within the same tick are synchronized'
      + ' (compositor animation created second)');
 
-  promise_test(t => {
+  promise_test(async t => {
     const attrs = { class: 'compositable',
                     style: 'transition: all 100s' };
     const elemA = addDiv(t, attrs);
     const elemB = addDiv(t, attrs);
     elemA.style.transform = 'translate(0px)';
     elemB.style.marginLeft = '0px';
     getComputedStyle(elemA).transform;
     getComputedStyle(elemB).marginLeft;
@@ -953,65 +946,63 @@ function testSynchronizedAnimations() {
     // In this test we want to set up two transitions during the "Events"
     // stage but only flush style for one such that the second one is actually
     // generated during the "Style" stage of the *next* tick.
     //
     // Web content often generates transitions in this way (that is, it doesn't
     // pay regard to when style is flushed and nor should it). However, we
     // still want transitions generated in this way to be synchronized.
     let timeForFirstFrame;
-    return waitForIdle()
-      .then(() => {
-        timeForFirstFrame = document.timeline.currentTime;
-        elemA.style.transform = 'translate(100px)';
-        // Flush style to trigger first transition
-        getComputedStyle(elemA).transform;
-        elemB.style.marginLeft = '100px';
-        // DON'T flush style here (this includes calling getAnimations!)
-        return waitForFrame();
-      }).then(() => {
-        assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
-                          'Should be on the other side of a tick');
-        // Wait another tick so we can let the transition be started
-        // by regular style resolution.
-        return waitForFrame();
-      }).then(() => {
-        const transitionA = elemA.getAnimations()[0];
-        assert_animation_property_state_equals(
-          transitionA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: false,
-              warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
-          } ]);
-      });
+    await waitForIdle();
+
+    timeForFirstFrame = document.timeline.currentTime;
+    elemA.style.transform = 'translate(100px)';
+    // Flush style to trigger first transition
+    getComputedStyle(elemA).transform;
+    elemB.style.marginLeft = '100px';
+    // DON'T flush style here (this includes calling getAnimations!)
+    await waitForFrame();
+
+    assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
+                      'Should be on the other side of a tick');
+    // Wait another tick so we can let the transition be started
+    // by regular style resolution.
+    await waitForFrame();
+
+    const transitionA = elemA.getAnimations()[0];
+    assert_animation_property_state_equals(
+      transitionA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: false,
+          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+      } ]);
   }, 'Transitions created before and after a tick are synchronized');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ],
                                   opacity: [ 0, 1 ] },
                                 100 * MS_PER_SEC);
     const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
 
-    return waitForPaints()
-      .then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: false,
-              warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
-            },
-            { property: 'opacity',
-              runningOnCompositor: true
-            } ]);
-      });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: false,
+          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+        },
+        { property: 'opacity',
+          runningOnCompositor: true
+        } ]);
   }, 'Opacity animations on the same element continue running on the'
      + ' compositor when transform animations are synchronized with geometric'
      + ' animations');
 
   promise_test(async t => {
     const transitionElem = addDiv(t, {
       style: 'margin-left: 0px; transition: margin-left 100s',
     });
@@ -1030,156 +1021,146 @@ function testSynchronizedAnimations() {
 
     await Promise.all([cssTransition.ready, cssAnimation.ready]);
 
     assert_animation_property_state_equals(cssAnimation.effect.getProperties(),
       [{ property: 'transform',
          runningOnCompositor: true }]);
   }, 'CSS Animations are NOT synchronized with CSS Transitions');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
-    let animB;
+    await waitForPaints();
 
-    return waitForPaints()
-      .then(() => {
-        animB = elemB.animate({ transform: [ 'translate(0px)',
+    let animB = elemB.animate({ transform: [ 'translate(0px)',
                                              'translate(100px)' ] },
-                                100 * MS_PER_SEC);
-        return animB.ready;
-      }).then(() => {
-        assert_animation_property_state_equals(
-          animB.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: true } ]);
-      });
+                              100 * MS_PER_SEC);
+    await animB.ready;
+
+    assert_animation_property_state_equals(
+      animB.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: true } ]);
   }, 'Transform animations are NOT synchronized with geometric animations'
      + ' started in the previous frame');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ] },
                                 100 * MS_PER_SEC);
-    let animB;
+    await waitForPaints();
 
-    return waitForPaints()
-      .then(() => {
-        animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+    let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                               100 * MS_PER_SEC);
-        return animB.ready;
-      }).then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: true } ]);
-      });
+    await animB.ready;
+
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: true } ]);
   }, 'Transform animations are NOT synchronized with geometric animations'
      + ' started in the next frame');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ] },
                                 100 * MS_PER_SEC);
     const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
     animB.pause();
 
-    return waitForPaints()
-      .then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform', runningOnCompositor: true } ]);
-      });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform', runningOnCompositor: true } ]);
   }, 'Paused animations are not synchronized');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ] },
                                 100 * MS_PER_SEC);
     const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
 
     // Seek one of the animations so that their start times will differ
     animA.currentTime = 5000;
 
-    return waitForPaints()
-      .then(() => {
-        assert_not_equals(animA.startTime, animB.startTime,
-                          'Animations should have different start times');
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: false,
-              warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
-          } ]);
-      });
+    await waitForPaints();
+
+    assert_not_equals(animA.startTime, animB.startTime,
+                      'Animations should have different start times');
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: false,
+          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+      } ]);
   }, 'Animations are synchronized based on when they are started'
      + ' and NOT their start time');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ] },
                                 100 * MS_PER_SEC);
     const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
 
-    return waitForPaints()
-      .then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: false } ]);
-        // Restart animation
-        animA.pause();
-        animA.play();
-        return animA.ready;
-      }).then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: true } ]);
-      });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: false } ]);
+    // Restart animation
+    animA.pause();
+    animA.play();
+    await animA.ready;
+
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: true } ]);
   }, 'An initially synchronized animation may be unsynchronized if restarted');
 
-  promise_test(t => {
+  promise_test(async t => {
     const elemA = addDiv(t, { class: 'compositable' });
     const elemB = addDiv(t, { class: 'compositable' });
 
     const animA = elemA.animate({ transform: [ 'translate(0px)',
                                                'translate(100px)' ] },
                                 100 * MS_PER_SEC);
     const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                 100 * MS_PER_SEC);
 
     // Clear target effect
     animB.effect.target = null;
 
-    return waitForPaints()
-      .then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform',
-              runningOnCompositor: true } ]);
-      });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animA.effect.getProperties(),
+      [ { property: 'transform',
+          runningOnCompositor: true } ]);
   }, 'A geometric animation with no target element is not synchronized');
 }
 
 function start() {
   var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
     .getService(SpecialPowers.Ci.nsIStringBundleService);
   gStringBundle = bundleService
     .createBundle("chrome://global/locale/layout_errors.properties");
@@ -1190,137 +1171,137 @@ function start() {
   testStyleChanges();
   testIdChanges();
   testMultipleAnimations();
   testMultipleAnimationsWithGeometricKeyframes();
   testMultipleAnimationsWithGeometricAnimations();
   testSmallElements();
   testSynchronizedAnimations();
 
-  promise_test(t => {
+  promise_test(async t => {
     var animation = addDivAndAnimate(t,
                                      { class: 'compositable' },
                                      { transform: [ 'translate(0px)',
                                                     'translate(100px)'] },
                                      100 * MS_PER_SEC);
-    return waitForPaints().then(() => {
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ { property: 'transform', runningOnCompositor: true } ]);
-      animation.effect.target.style = 'width: 10000px; height: 10000px';
-      return waitForFrame();
-    }).then(() => {
-      // viewport depends on test environment.
-      var expectedWarning = new RegExp(
-        "Animation cannot be run on the compositor because the area of the frame " +
-        "\\(\\d+\\) is too large relative to the viewport " +
-        "\\(larger than \\d+\\)");
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ {
-          property: 'transform',
-          runningOnCompositor: false,
-          warning: expectedWarning
-        } ]);
-      animation.effect.target.style = 'width: 100px; height: 100px';
-      return waitForFrame();
-    }).then(() => {
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ { property: 'transform', runningOnCompositor: true } ]);
-    });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ { property: 'transform', runningOnCompositor: true } ]);
+    animation.effect.target.style = 'width: 10000px; height: 10000px';
+    await waitForFrame();
+
+    // viewport depends on test environment.
+    var expectedWarning = new RegExp(
+      "Animation cannot be run on the compositor because the area of the frame " +
+      "\\(\\d+\\) is too large relative to the viewport " +
+      "\\(larger than \\d+\\)");
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ {
+        property: 'transform',
+        runningOnCompositor: false,
+        warning: expectedWarning
+      } ]);
+    animation.effect.target.style = 'width: 100px; height: 100px';
+    await waitForFrame();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ { property: 'transform', runningOnCompositor: true } ]);
   }, 'transform on too big element - area');
 
-  promise_test(t => {
+  promise_test(async t => {
     var animation = addDivAndAnimate(t,
                                      { class: 'compositable' },
                                      { transform: [ 'translate(0px)',
                                                     'translate(100px)'] },
                                      100 * MS_PER_SEC);
-    return waitForPaints().then(() => {
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ { property: 'transform', runningOnCompositor: true } ]);
-      animation.effect.target.style = 'width: 20000px; height: 1px';
-      return waitForFrame();
-    }).then(() => {
-      // viewport depends on test environment.
-      var expectedWarning = new RegExp(
-        "Animation cannot be run on the compositor because the frame size " +
-        "\\(20000, 1\\) is too large relative to the viewport " +
-        "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " +
-        "maximum allowed value \\(\\d+, \\d+\\)");
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ {
-          property: 'transform',
-          runningOnCompositor: false,
-          warning: expectedWarning
-        } ]);
-      animation.effect.target.style = 'width: 100px; height: 100px';
-      return waitForFrame();
-    }).then(() => {
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ { property: 'transform', runningOnCompositor: true } ]);
-    });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ { property: 'transform', runningOnCompositor: true } ]);
+    animation.effect.target.style = 'width: 20000px; height: 1px';
+    await waitForFrame();
+
+    // viewport depends on test environment.
+    var expectedWarning = new RegExp(
+      "Animation cannot be run on the compositor because the frame size " +
+      "\\(20000, 1\\) is too large relative to the viewport " +
+      "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " +
+      "maximum allowed value \\(\\d+, \\d+\\)");
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ {
+        property: 'transform',
+        runningOnCompositor: false,
+        warning: expectedWarning
+      } ]);
+    animation.effect.target.style = 'width: 100px; height: 100px';
+    await waitForFrame();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ { property: 'transform', runningOnCompositor: true } ]);
   }, 'transform on too big element - dimensions');
 
-  promise_test(t => {
+  promise_test(async t => {
     var svg  = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
     svg.setAttribute('width', '100');
     svg.setAttribute('height', '100');
     var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
     rect.setAttribute('width', '100');
     rect.setAttribute('height', '100');
     rect.setAttribute('fill', 'red');
     svg.appendChild(rect);
     document.body.appendChild(svg);
     t.add_cleanup(() => {
       svg.remove();
     });
 
     var animation = svg.animate(
       { transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
-    return waitForPaints().then(() => {
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ { property: 'transform', runningOnCompositor: true } ]);
-      svg.setAttribute('transform', 'translate(10, 20)');
-      return waitForFrame();
-    }).then(() => {
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ {
-          property: 'transform',
-          runningOnCompositor: false,
-          warning: 'CompositorAnimationWarningTransformSVG'
-        } ]);
-      svg.removeAttribute('transform');
-      return waitForFrame();
-    }).then(() => {
-      assert_animation_property_state_equals(
-        animation.effect.getProperties(),
-        [ { property: 'transform', runningOnCompositor: true } ]);
-    });
+    await waitForPaints();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ { property: 'transform', runningOnCompositor: true } ]);
+    svg.setAttribute('transform', 'translate(10, 20)');
+    await waitForFrame();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ {
+        property: 'transform',
+        runningOnCompositor: false,
+        warning: 'CompositorAnimationWarningTransformSVG'
+      } ]);
+    svg.removeAttribute('transform');
+    await waitForFrame();
+
+    assert_animation_property_state_equals(
+      animation.effect.getProperties(),
+      [ { property: 'transform', runningOnCompositor: true } ]);
   }, 'transform of nsIFrame with SVG transform');
 
-  promise_test(t => {
+  promise_test(async t => {
     var div = addDiv(t, { class: 'compositable',
                           style: 'animation: fade 100s' });
     var cssAnimation = div.getAnimations()[0];
     var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
-    return waitForPaints().then(() => {
-      assert_animation_property_state_equals(
-        cssAnimation.effect.getProperties(),
-        [ { property: 'opacity', runningOnCompositor: true } ]);
-      assert_animation_property_state_equals(
-        scriptAnimation.effect.getProperties(),
-        [ { property: 'opacity', runningOnCompositor: true } ]);
-    });
+
+    await waitForPaints();
+    assert_animation_property_state_equals(
+      cssAnimation.effect.getProperties(),
+      [ { property: 'opacity', runningOnCompositor: true } ]);
+    assert_animation_property_state_equals(
+      scriptAnimation.effect.getProperties(),
+      [ { property: 'opacity', runningOnCompositor: true } ]);
   }, 'overridden animation');
 
   done();
 }
 
 </script>
 
 </body>
--- a/dom/clients/manager/ClientManagerService.cpp
+++ b/dom/clients/manager/ClientManagerService.cpp
@@ -13,17 +13,19 @@
 #include "ClientPrincipalUtils.h"
 #include "ClientSourceParent.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/ServiceWorkerManager.h"
 #include "mozilla/dom/ServiceWorkerUtils.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/ClearOnShutdown.h"
+#include "mozilla/MozPromise.h"
 #include "mozilla/SystemGroup.h"
+#include "jsfriendapi.h"
 #include "nsIAsyncShutdown.h"
 #include "nsIXULRuntime.h"
 #include "nsProxyRelease.h"
 
 namespace mozilla {
 namespace dom {
 
 using mozilla::ipc::AssertIsOnBackgroundThread;
@@ -538,21 +540,44 @@ ClientManagerService::Claim(const Client
   promiseList->MaybeFinish();
 
   return promiseList->GetResultPromise();
 }
 
 RefPtr<ClientOpPromise>
 ClientManagerService::GetInfoAndState(const ClientGetInfoAndStateArgs& aArgs)
 {
-  RefPtr<ClientOpPromise> ref;
+  ClientSourceParent* source = FindSource(aArgs.id(), aArgs.principalInfo());
+
+  if (!source) {
+    RefPtr<ClientOpPromise> ref =
+      ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+    return ref.forget();
+  }
+
+  if (!source->ExecutionReady()) {
+    RefPtr<ClientManagerService> self = this;
 
-  ClientSourceParent* source = FindSource(aArgs.id(), aArgs.principalInfo());
-  if (!source || !source->ExecutionReady()) {
-    ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+    // rejection ultimately converted to `undefined` in Clients::Get
+    RefPtr<ClientOpPromise> ref =
+      source->ExecutionReadyPromise()
+            ->Then(GetCurrentThreadSerialEventTarget(), __func__,
+                   [self, aArgs] () -> RefPtr<ClientOpPromise> {
+                      ClientSourceParent* source = self->FindSource(aArgs.id(),
+                                                                    aArgs.principalInfo());
+
+                      if (!source) {
+                        RefPtr<ClientOpPromise> ref =
+                          ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+                        return ref.forget();
+                      }
+
+                      return source->StartOp(aArgs);
+                   });
+
     return ref.forget();
   }
 
   return source->StartOp(aArgs);
 }
 
 namespace {
 
--- a/dom/clients/manager/ClientSourceParent.cpp
+++ b/dom/clients/manager/ClientSourceParent.cpp
@@ -113,16 +113,18 @@ ClientSourceParent::RecvExecutionReady(c
   mClientInfo.SetURL(aArgs.url());
   mClientInfo.SetFrameType(aArgs.frameType());
   mExecutionReady = true;
 
   for (ClientHandleParent* handle : mHandleList) {
     Unused << handle->SendExecutionReady(mClientInfo.ToIPC());
   }
 
+  mExecutionReadyPromise.ResolveIfExists(true, __func__);
+
   return IPC_OK();
 };
 
 IPCResult
 ClientSourceParent::RecvFreeze()
 {
   MOZ_DIAGNOSTIC_ASSERT(!mFrozen);
   mFrozen = true;
@@ -223,16 +225,18 @@ ClientSourceParent::ClientSourceParent(c
   , mExecutionReady(false)
   , mFrozen(false)
 {
 }
 
 ClientSourceParent::~ClientSourceParent()
 {
   MOZ_DIAGNOSTIC_ASSERT(mHandleList.IsEmpty());
+
+  mExecutionReadyPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
 }
 
 void
 ClientSourceParent::Init()
 {
   // Ensure the principal is reasonable before adding ourself to the service.
   // Since we validate the principal on the child side as well, any failure
   // here is treated as fatal.
@@ -263,16 +267,25 @@ ClientSourceParent::IsFrozen() const
 }
 
 bool
 ClientSourceParent::ExecutionReady() const
 {
   return mExecutionReady;
 }
 
+RefPtr<GenericPromise>
+ClientSourceParent::ExecutionReadyPromise()
+{
+  // Only call if ClientSourceParent::ExecutionReady() is false; otherwise,
+  // the promise will never resolve
+  MOZ_ASSERT(!mExecutionReady);
+  return mExecutionReadyPromise.Ensure(__func__);
+}
+
 const Maybe<ServiceWorkerDescriptor>&
 ClientSourceParent::GetController() const
 {
   return mController;
 }
 
 void
 ClientSourceParent::ClearController()
--- a/dom/clients/manager/ClientSourceParent.h
+++ b/dom/clients/manager/ClientSourceParent.h
@@ -5,29 +5,31 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #ifndef _mozilla_dom_ClientSourceParent_h
 #define _mozilla_dom_ClientSourceParent_h
 
 #include "ClientInfo.h"
 #include "ClientOpPromise.h"
 #include "mozilla/dom/PClientSourceParent.h"
 #include "mozilla/dom/ServiceWorkerDescriptor.h"
+#include "mozilla/MozPromise.h"
 
 namespace mozilla {
 namespace dom {
 
 class ClientHandleParent;
 class ClientManagerService;
 
 class ClientSourceParent final : public PClientSourceParent
 {
   ClientInfo mClientInfo;
   Maybe<ServiceWorkerDescriptor> mController;
   RefPtr<ClientManagerService> mService;
   nsTArray<ClientHandleParent*> mHandleList;
+  MozPromiseHolder<GenericPromise> mExecutionReadyPromise;
   bool mExecutionReady;
   bool mFrozen;
 
   void
   KillInvalidChild();
 
   // PClientSourceParent
   mozilla::ipc::IPCResult
@@ -71,16 +73,19 @@ public:
   Info() const;
 
   bool
   IsFrozen() const;
 
   bool
   ExecutionReady() const;
 
+  RefPtr<GenericPromise>
+  ExecutionReadyPromise();
+
   const Maybe<ServiceWorkerDescriptor>&
   GetController() const;
 
   void
   ClearController();
 
   void
   AttachHandle(ClientHandleParent* aClientSource);
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -839,16 +839,19 @@ EventStateManager::PreHandleEvent(nsPres
       WidgetCompositionEvent* compositionEvent = aEvent->AsCompositionEvent();
       WidgetQueryContentEvent selectedText(true, eQuerySelectedText,
                                            compositionEvent->mWidget);
       HandleQueryContentEvent(&selectedText);
       NS_ASSERTION(selectedText.mSucceeded, "Failed to get selected text");
       compositionEvent->mData = selectedText.mReply.mString;
     }
     break;
+  case eTouchStart:
+    SetGestureDownPoint(aEvent->AsTouchEvent());
+    break;
   case eTouchEnd:
     NotifyTargetUserActivation(aEvent, aTargetContent);
     break;
   default:
     break;
   }
   return NS_OK;
 }
@@ -1683,18 +1686,17 @@ EventStateManager::BeginTrackingDragGest
                                             nsIFrame* inDownFrame)
 {
   if (!inDownEvent->mWidget) {
     return;
   }
 
   // Note that |inDownEvent| could be either a mouse down event or a
   // synthesized mouse move event.
-  mGestureDownPoint =
-    inDownEvent->mRefPoint + inDownEvent->mWidget->WidgetToScreenOffset();
+  SetGestureDownPoint(inDownEvent);
 
   if (inDownFrame) {
     inDownFrame->GetContentForEvent(inDownEvent,
                                     getter_AddRefs(mGestureDownContent));
 
     mGestureDownFrameOwner = inDownFrame->GetContent();
     if (!mGestureDownFrameOwner) {
       mGestureDownFrameOwner = mGestureDownContent;
@@ -1705,16 +1707,31 @@ EventStateManager::BeginTrackingDragGest
 
   if (inDownEvent->mMessage != eMouseTouchDrag && Prefs::ClickHoldContextMenu()) {
     // fire off a timer to track click-hold
     CreateClickHoldTimer(aPresContext, inDownFrame, inDownEvent);
   }
 }
 
 void
+EventStateManager::SetGestureDownPoint(WidgetGUIEvent* aEvent)
+{
+  mGestureDownPoint =
+    GetEventRefPoint(aEvent) + aEvent->mWidget->WidgetToScreenOffset();
+}
+
+LayoutDeviceIntPoint
+EventStateManager::GetEventRefPoint(WidgetEvent* aEvent) const
+{
+  auto touchEvent = aEvent->AsTouchEvent();
+  return (touchEvent && !touchEvent->mTouches.IsEmpty()) ?
+    aEvent->AsTouchEvent()->mTouches[0]->mRefPoint : aEvent->mRefPoint;
+}
+
+void
 EventStateManager::BeginTrackingRemoteDragGesture(nsIContent* aContent)
 {
   mGestureDownContent = aContent;
   mGestureDownFrameOwner = aContent;
 }
 
 //
 // StopTrackingDragGesture
@@ -1803,21 +1820,18 @@ EventStateManager::IsEventOutsideDragThr
     sPixelThresholdY =
       LookAndFeel::GetInt(LookAndFeel::eIntID_DragThresholdY, 0);
     if (!sPixelThresholdX)
       sPixelThresholdX = 5;
     if (!sPixelThresholdY)
       sPixelThresholdY = 5;
   }
 
-  auto touchEvent = aEvent->AsTouchEvent();
-  LayoutDeviceIntPoint pt = aEvent->mWidget->WidgetToScreenOffset() +
-    ((touchEvent && !touchEvent->mTouches.IsEmpty())
-      ? aEvent->AsTouchEvent()->mTouches[0]->mRefPoint
-      : aEvent->mRefPoint);
+  LayoutDeviceIntPoint pt =
+    aEvent->mWidget->WidgetToScreenOffset() + GetEventRefPoint(aEvent);
   LayoutDeviceIntPoint distance = pt - mGestureDownPoint;
   return
     Abs(distance.x) > AssertedCast<uint32_t>(sPixelThresholdX) ||
     Abs(distance.y) > AssertedCast<uint32_t>(sPixelThresholdY);
 }
 
 //
 // GenerateDragGesture
--- a/dom/events/EventStateManager.h
+++ b/dom/events/EventStateManager.h
@@ -1091,16 +1091,20 @@ protected:
   void DecideGestureEvent(WidgetGestureNotifyEvent* aEvent,
                           nsIFrame* targetFrame);
 
   // routines for the d&d gesture tracking state machine
   void BeginTrackingDragGesture(nsPresContext* aPresContext,
                                 WidgetMouseEvent* aDownEvent,
                                 nsIFrame* aDownFrame);
 
+  void SetGestureDownPoint(WidgetGUIEvent* aEvent);
+
+  LayoutDeviceIntPoint GetEventRefPoint(WidgetEvent* aEvent) const;
+
   friend class mozilla::dom::TabParent;
   void BeginTrackingRemoteDragGesture(nsIContent* aContent);
   void StopTrackingDragGesture();
   void GenerateDragGesture(nsPresContext* aPresContext,
                            WidgetInputEvent* aEvent);
 
   /**
    * When starting a dnd session, UA must fire a pointercancel event and stop
--- a/dom/indexedDB/IDBFactory.cpp
+++ b/dom/indexedDB/IDBFactory.cpp
@@ -928,22 +928,26 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(IDBFactory)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBFactory)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTabChild)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBFactory)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
   tmp->mOwningObject = nullptr;
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mTabChild)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBFactory)
   NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
   NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mOwningObject)
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 JSObject*
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -854,17 +854,19 @@ public:
     MOZ_ASSERT(mMaster->mLooping);
   }
 
   void Exit() override
   {
     mAudioDataRequest.DisconnectIfExists();
     mAudioSeekRequest.DisconnectIfExists();
     if (ShouldDiscardLoopedAudioData()) {
+      mMaster->mAudioDataRequest.DisconnectIfExists();
       DiscardLoopedAudioData();
+      AudioQueue().Finish();
     }
     DecodingState::Exit();
   }
 
   State GetState() const override
   {
     return DECODER_STATE_LOOPING_DECODING;
   }
@@ -890,17 +892,18 @@ public:
     // so we need to add the last ending time as the offset to correct the
     // audio data time in the next round when seamless looping is enabled.
     mAudioLoopingOffset = mMaster->mDecodedAudioEndTime;
 
     if (mMaster->mAudioDecodedDuration.isNothing()) {
       mMaster->mAudioDecodedDuration.emplace(mMaster->mDecodedAudioEndTime);
     }
 
-    SLOG("received EOS when seamless looping, starts seeking");
+    SLOG("received EOS when seamless looping, starts seeking, "
+         "AudioLoopingOffset=[%" PRId64 "]", mAudioLoopingOffset.ToMicroseconds());
     Reader()->ResetDecode(TrackInfo::kAudioTrack);
     Reader()->Seek(SeekTarget(media::TimeUnit::Zero(), SeekTarget::Accurate))
       ->Then(OwnerThread(), __func__,
               [this] () -> void {
                 mAudioSeekRequest.Complete();
                 SLOG("seeking completed, start to request first sample, "
                      "queueing audio task - queued=%zu, decoder-queued=%zu",
                      AudioQueue().GetSize(), Reader()->SizeOfAudioQueueInFrames());
@@ -950,30 +953,33 @@ private:
     return aAudio->mTime.IsValid() ?
               MediaResult(NS_OK) :
               MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
                           "Audio sample overflow during looping time adjustment");
   }
 
   bool ShouldDiscardLoopedAudioData() const
   {
+    if (!mMaster->mMediaSink->IsStarted()) {
+      return false;
+    }
     /**
      * If media cancels looping, we should check whether there are audio data
      * whose time is later than EOS. If so, we should discard them because we
      * won't have a chance to play them.
      *
      *    playback                     last decoded
      *    position          EOS        data time
      *   ----|---------------|------------|---------> (Increasing timeline)
      *    mCurrent        mLooping      mMaster's
-     * PlaybackPosition    Offset      mDecodedAudioEndTime
+     *    ClockTime        Offset      mDecodedAudioEndTime
      *
      */
     return (mAudioLoopingOffset != media::TimeUnit::Zero() &&
-            mMaster->mCurrentPosition.Ref() < mAudioLoopingOffset &&
+            mMaster->GetClock() < mAudioLoopingOffset &&
             mAudioLoopingOffset < mMaster->mDecodedAudioEndTime);
   }
 
   void DiscardLoopedAudioData()
   {
     if (mAudioLoopingOffset == media::TimeUnit::Zero()) {
         return;
     }
--- a/dom/payments/test/mochitest.ini
+++ b/dom/payments/test/mochitest.ini
@@ -21,24 +21,26 @@ support-files =
   RequestShippingChromeScript.js
   RetryPaymentChromeScript.js
   ShippingOptionsChromeScript.js
   ShowPaymentChromeScript.js
   UpdateErrorsChromeScript.js
 
 [test_abortPayment.html]
 run-if = nightly_build # Bug 1390018: Depends on the Nightly-only UI service
+skip-if = debug # Bug 1507251 - Leak
 [test_basiccard.html]
 [test_basiccarderrors.html]
 [test_block_none10s.html]
 skip-if = e10s # Bug 1408250: Don't expose PaymentRequest Constructor in non-e10s
 [test_bug1478740.html]
 [test_bug1490698.html]
 [test_canMakePayment.html]
 run-if = nightly_build # Bug 1390737: Depends on the Nightly-only UI service
+skip-if = debug # Bug 1507251 - Leak
 [test_closePayment.html]
 [test_constructor.html]
 [test_currency_amount_validation.html]
 skip-if = (verify && debug)
 [test_payerDetails.html]
 [test_payment-request-in-iframe.html]
 [test_pmi_validation.html]
 skip-if = (verify && debug)
--- a/dom/serviceworkers/ServiceWorkerEvents.cpp
+++ b/dom/serviceworkers/ServiceWorkerEvents.cpp
@@ -156,16 +156,17 @@ FetchEvent::Constructor(const GlobalObje
   MOZ_ASSERT(owner);
   RefPtr<FetchEvent> e = new FetchEvent(owner);
   bool trusted = e->Init(owner);
   e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
   e->SetTrusted(trusted);
   e->SetComposed(aOptions.mComposed);
   e->mRequest = aOptions.mRequest;
   e->mClientId = aOptions.mClientId;
+  e->mResultingClientId = aOptions.mResultingClientId;
   e->mIsReload = aOptions.mIsReload;
   return e.forget();
 }
 
 namespace {
 
 struct RespondWithClosure
 {
--- a/dom/serviceworkers/ServiceWorkerEvents.h
+++ b/dom/serviceworkers/ServiceWorkerEvents.h
@@ -118,16 +118,17 @@ public:
 class FetchEvent final : public ExtendableEvent
 {
   nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
   nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
   RefPtr<Request> mRequest;
   nsCString mScriptSpec;
   nsCString mPreventDefaultScriptSpec;
   nsString mClientId;
+  nsString mResultingClientId;
   uint32_t mPreventDefaultLineNumber;
   uint32_t mPreventDefaultColumnNumber;
   bool mIsReload;
   bool mWaitToRespond;
 protected:
   explicit FetchEvent(EventTarget* aOwner);
   ~FetchEvent();
 
@@ -164,16 +165,22 @@ public:
   }
 
   void
   GetClientId(nsAString& aClientId) const
   {
     aClientId = mClientId;
   }
 
+  void
+  GetResultingClientId(nsAString& aResultingClientId) const
+  {
+    aResultingClientId = mResultingClientId;
+  }
+
   bool
   IsReload() const
   {
     return mIsReload;
   }
 
   void
   RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv);
--- a/dom/serviceworkers/ServiceWorkerManager.cpp
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -2013,31 +2013,53 @@ public:
     nsresult status;
     rv = channel->GetStatus(&status);
     if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) {
       HandleError();
       return NS_OK;
     }
 
     nsString clientId;
+    nsString resultingClientId;
     nsCOMPtr<nsILoadInfo> loadInfo = channel->GetLoadInfo();
     if (loadInfo) {
+      char buf[NSID_LENGTH];
       Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
       if (clientInfo.isSome()) {
-        char buf[NSID_LENGTH];
         clientInfo.ref().Id().ToProvidedString(buf);
         NS_ConvertASCIItoUTF16 uuid(buf);
 
         // Remove {} and the null terminator
         clientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3));
       }
+
+      // Having an initial or reserved client are mutually exclusive events:
+      // either an initial client is used upon navigating an about:blank
+      // iframe, or a new, reserved environment/client is created (e.g.
+      // upon a top-level navigation). See step 4 of
+      // https://html.spec.whatwg.org/#process-a-navigate-fetch as well as
+      // https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444
+      Maybe<ClientInfo> resulting = loadInfo->GetInitialClientInfo();
+
+      if (resulting.isNothing()) {
+        resulting = loadInfo->GetReservedClientInfo();
+      } else {
+        MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing());
+      }
+
+      if (resulting.isSome()) {
+        resulting.ref().Id().ToProvidedString(buf);
+        NS_ConvertASCIItoUTF16 uuid(buf);
+
+        resultingClientId.Assign(Substring(uuid, 1, NSID_LENGTH - 3));
+      }
     }
 
     rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId,
-                                               mIsReload);
+                                               resultingClientId, mIsReload);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       HandleError();
     }
 
     return NS_OK;
   }
 };
 
--- a/dom/serviceworkers/ServiceWorkerPrivate.cpp
+++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp
@@ -1304,56 +1304,62 @@ class FetchEventRunnable : public Extend
   nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
   const nsCString mScriptSpec;
   nsTArray<nsCString> mHeaderNames;
   nsTArray<nsCString> mHeaderValues;
   nsCString mSpec;
   nsCString mFragment;
   nsCString mMethod;
   nsString mClientId;
+  nsString mResultingClientId;
   bool mIsReload;
   bool mMarkLaunchServiceWorkerEnd;
   RequestCache mCacheMode;
   RequestMode mRequestMode;
   RequestRedirect mRequestRedirect;
   RequestCredentials mRequestCredentials;
   nsContentPolicyType mContentPolicyType;
   nsCOMPtr<nsIInputStream> mUploadStream;
   int64_t mUploadStreamContentLength;
   nsCString mReferrer;
   ReferrerPolicy mReferrerPolicy;
   nsString mIntegrity;
+  const bool mIsNonSubresourceRequest;
 public:
   FetchEventRunnable(WorkerPrivate* aWorkerPrivate,
                      KeepAliveToken* aKeepAliveToken,
                      nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
                      // CSP checks might require the worker script spec
                      // later on.
                      const nsACString& aScriptSpec,
                      nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
                      const nsAString& aClientId,
+                     const nsAString& aResultingClientId,
                      bool aIsReload,
-                     bool aMarkLaunchServiceWorkerEnd)
+                     bool aMarkLaunchServiceWorkerEnd,
+                     bool aIsNonSubresourceRequest)
     : ExtendableFunctionalEventWorkerRunnable(
         aWorkerPrivate, aKeepAliveToken, aRegistration)
     , mInterceptedChannel(aChannel)
     , mScriptSpec(aScriptSpec)
     , mClientId(aClientId)
+    , mResultingClientId(aResultingClientId)
     , mIsReload(aIsReload)
     , mMarkLaunchServiceWorkerEnd(aMarkLaunchServiceWorkerEnd)
     , mCacheMode(RequestCache::Default)
     , mRequestMode(RequestMode::No_cors)
     , mRequestRedirect(RequestRedirect::Follow)
     // By default we set it to same-origin since normal HTTP fetches always
     // send credentials to same-origin websites unless explicitly forbidden.
     , mRequestCredentials(RequestCredentials::Same_origin)
     , mContentPolicyType(nsIContentPolicy::TYPE_INVALID)
     , mUploadStreamContentLength(-1)
     , mReferrer(kFETCH_CLIENT_REFERRER_STR)
     , mReferrerPolicy(ReferrerPolicy::_empty)
+    , mIsNonSubresourceRequest(aIsNonSubresourceRequest)
   {
     MOZ_ASSERT(aWorkerPrivate);
   }
 
   NS_DECL_ISUPPORTS_INHERITED
 
   NS_IMETHOD
   VisitHeader(const nsACString& aHeader, const nsACString& aValue) override
@@ -1619,22 +1625,36 @@ private:
     MOZ_ASSERT_IF(internalReq->IsNavigationRequest(),
                   request->Redirect() == RequestRedirect::Manual);
 
     RootedDictionary<FetchEventInit> init(aCx);
     init.mRequest = request;
     init.mBubbles = false;
     init.mCancelable = true;
     // Only expose the FetchEvent.clientId on subresource requests for now.
-    // Once we implement .resultingClientId and .targetClientId we can then
-    // start exposing .clientId on non-subresource requests as well.  See
-    // bug 1264177.
+    // Once we implement .targetClientId we can then start exposing .clientId
+    // on non-subresource requests as well.  See bug 1487534.
     if (!mClientId.IsEmpty() && !internalReq->IsNavigationRequest()) {
       init.mClientId = mClientId;
     }
+
+    /*
+     * https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm
+     *
+     * "If request is a non-subresource request and request’s
+     * destination is not "report", initialize e’s resultingClientId attribute
+     * to reservedClient’s [resultingClient's] id, and to the empty string
+     * otherwise." (Step 18.8)
+     */
+    if (!mResultingClientId.IsEmpty() &&
+        mIsNonSubresourceRequest &&
+        internalReq->Destination() != RequestDestination::Report) {
+      init.mResultingClientId = mResultingClientId;
+    }
+
     init.mIsReload = mIsReload;
     RefPtr<FetchEvent> event =
       FetchEvent::Constructor(globalObj, NS_LITERAL_STRING("fetch"), init, result);
     if (NS_WARN_IF(result.Failed())) {
       result.SuppressException();
       return false;
     }
 
@@ -1668,17 +1688,19 @@ private:
 
 NS_IMPL_ISUPPORTS_INHERITED(FetchEventRunnable, WorkerRunnable, nsIHttpHeaderVisitor)
 
 } // anonymous namespace
 
 nsresult
 ServiceWorkerPrivate::SendFetchEvent(nsIInterceptedChannel* aChannel,
                                      nsILoadGroup* aLoadGroup,
-                                     const nsAString& aClientId, bool aIsReload)
+                                     const nsAString& aClientId,
+                                     const nsAString& aResultingClientId,
+                                     bool aIsReload)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
   if (NS_WARN_IF(!mInfo || !swm)) {
     return NS_ERROR_FAILURE;
   }
 
@@ -1733,21 +1755,27 @@ ServiceWorkerPrivate::SendFetchEvent(nsI
       "nsIInterceptedChannel", aChannel, false));
 
   nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> regInfo(
     new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(
       "ServiceWorkerRegistrationInfoProxy", registration, false));
 
   RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken();
 
+  nsCOMPtr<nsIChannel> channel;
+  rv = aChannel->GetChannel(getter_AddRefs(channel));
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isNonSubresourceRequest = nsContentUtils::IsNonSubresourceRequest(channel);
 
   RefPtr<FetchEventRunnable> r =
     new FetchEventRunnable(mWorkerPrivate, token, handle,
                            mInfo->ScriptSpec(), regInfo,
-                           aClientId, aIsReload, newWorkerCreated);
+                           aClientId, aResultingClientId,
+                           aIsReload, newWorkerCreated,
+                           isNonSubresourceRequest);
   rv = r->Init();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (mInfo->State() == ServiceWorkerState::Activating) {
     mPendingFunctionalEvents.AppendElement(r.forget());
     return NS_OK;
--- a/dom/serviceworkers/ServiceWorkerPrivate.h
+++ b/dom/serviceworkers/ServiceWorkerPrivate.h
@@ -120,18 +120,21 @@ public:
                         const nsAString& aBody,
                         const nsAString& aTag,
                         const nsAString& aIcon,
                         const nsAString& aData,
                         const nsAString& aBehavior,
                         const nsAString& aScope);
 
   nsresult
-  SendFetchEvent(nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup,
-                 const nsAString& aClientId, bool aIsReload);
+  SendFetchEvent(nsIInterceptedChannel* aChannel,
+                 nsILoadGroup* aLoadGroup,
+                 const nsAString& aClientId,
+                 const nsAString& aResultingClientId,
+                 bool aIsReload);
 
   bool
   MaybeStoreISupports(nsISupports* aSupports);
 
   void
   RemoveISupports(nsISupports* aSupports);
 
   // This will terminate the current running worker thread and drop the
--- a/dom/webidl/FetchEvent.webidl
+++ b/dom/webidl/FetchEvent.webidl
@@ -8,19 +8,21 @@
  */
 
 [Constructor(DOMString type, FetchEventInit eventInitDict),
  Func="ServiceWorkerVisible",
  Exposed=(ServiceWorker)]
 interface FetchEvent : ExtendableEvent {
   [SameObject] readonly attribute Request request;
   readonly attribute DOMString clientId;
+  readonly attribute DOMString resultingClientId;
   readonly attribute boolean isReload;
 
   [Throws]
   void respondWith(Promise<Response> r);
 };
 
 dictionary FetchEventInit : EventInit {
   required Request request;
   DOMString clientId = "";
+  DOMString resultingClientId = "";
   boolean isReload = false;
 };
--- a/embedding/ios/confvars.sh
+++ b/embedding/ios/confvars.sh
@@ -1,9 +1,8 @@
 #! /bin/sh
 # 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/.
 
 MOZ_APP_NAME=geckoembed
 MOZ_APP_DISPLAYNAME=GeckoEmbed
 MOZ_UPDATER=
-MOZ_APP_VERSION=$MOZILLA_VERSION
--- a/extensions/confvars.sh
+++ b/extensions/confvars.sh
@@ -1,8 +1,7 @@
 #! /bin/sh
 # 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/.
 
 MOZ_APP_NAME=mozilla
 MOZ_APP_DISPLAYNAME=Mozilla
-MOZ_APP_VERSION=$MOZILLA_VERSION
--- a/gfx/layers/apz/test/mochitest/helper_basic_doubletap_zoom.html
+++ b/gfx/layers/apz/test/mochitest/helper_basic_doubletap_zoom.html
@@ -4,52 +4,64 @@
   <meta charset="utf-8">
   <meta name="viewport" content="width=2100"/>
   <title>Sanity check for double-tap zooming</title>
   <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
   <script type="application/javascript" src="apz_test_utils.js"></script>
   <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
   <script type="application/javascript">
 
-function* test(testDriver) {
-  var initial_resolution = getResolution();
-  ok(initial_resolution > 0,
-      'The initial_resolution is ' + initial_resolution + ', which is some sane value');
-
+function* doubleTapOn(element, x, y, testDriver) {
   // This listener will trigger the test to continue once APZ is done with
   // processing the scroll.
   SpecialPowers.Services.obs.addObserver(testDriver, "APZ:TransformEnd");
 
-  synthesizeNativeTap(document.getElementById('target'), 10, 10);
-  synthesizeNativeTap(document.getElementById('target'), 10, 10);
+  synthesizeNativeTap(element, x, y);
+  synthesizeNativeTap(element, x, y);
 
   // Wait for the APZ:TransformEnd to fire
   yield true;
 
   // We get here once the APZ:TransformEnd has fired, so we don't need that
   // observer any more.
   SpecialPowers.Services.obs.removeObserver(testDriver, "APZ:TransformEnd", false);
 
-  // Flush state and get the resolution we're at now
+  // Flush state so we can query an accurate resolution
   yield flushApzRepaints(testDriver);
-  let final_resolution = getResolution();
-  ok(final_resolution > initial_resolution, 'The final resolution (' + final_resolution + ') is greater after zooming in');
+}
+
+function* test(testDriver) {
+  var resolution = getResolution();
+  ok(resolution > 0,
+     'The initial_resolution is ' + resolution + ', which is some sane value');
+
+  // Check that double-tapping once zooms in
+  yield* doubleTapOn(document.getElementById('target'), 10, 10, testDriver);
+  var prev_resolution = resolution;
+  resolution = getResolution();
+  ok(resolution > prev_resolution, 'The first double-tap has increased the resolution to ' + resolution);
+
+  // Check that double-tapping again on the same spot zooms out
+  yield* doubleTapOn(document.getElementById('target'), 10, 10, testDriver);
+  prev_resolution = resolution;
+  resolution = getResolution();
+  ok(resolution < prev_resolution, 'The second double-tap has decreased the resolution to ' + resolution);
 }
 
 waitUntilApzStable()
 .then(runContinuation(test))
 .then(subtestDone);
 
   </script>
   <style type="text/css">
     .box {
         width: 800px;
-        height: 200px;
+        height: 500px;
         margin: 0 auto;
     }
 </style>
 </head>
 <body>
 <div class="box">Text before the div.</div>
-<div id="target" style="width:400px; height: 2000px; background-image: linear-gradient(blue,red)">
-</div>
+<div id="target" style="margin-left: 100px; width:900px; height: 400px; background-image: linear-gradient(blue,red)"></div>
+<div class="box">Text after the div.</div>
 </body>
 </html>
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -2203,17 +2203,18 @@ CompositorBridgeParent::DidComposite(Tim
     mPendingTransaction = TransactionId{0};
   }
 }
 
 void
 CompositorBridgeParent::NotifyPipelineRendered(const wr::PipelineId& aPipelineId,
                                                const wr::Epoch& aEpoch,
                                                TimeStamp& aCompositeStart,
-                                               TimeStamp& aCompositeEnd)
+                                               TimeStamp& aCompositeEnd,
+                                               wr::RendererStats* aStats)
 {
   if (!mWrBridge) {
     return;
   }
 
   RefPtr<UiCompositorControllerParent> uiController =
     UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayerTreeID);
 
@@ -2238,17 +2239,17 @@ CompositorBridgeParent::NotifyPipelineRe
     if (lts->mCrossProcessParent &&
         lts->mWrBridge &&
         lts->mWrBridge->PipelineId() == aPipelineId) {
 
       lts->mWrBridge->RemoveEpochDataPriorTo(aEpoch);
 
       if (!mPaused) {
         CrossProcessCompositorBridgeParent* cpcp = lts->mCrossProcessParent;
-        TransactionId transactionId = lts->mWrBridge->FlushTransactionIdsForEpoch(aEpoch, aCompositeEnd, uiController);
+        TransactionId transactionId = lts->mWrBridge->FlushTransactionIdsForEpoch(aEpoch, aCompositeEnd, uiController, aStats);
         Unused << cpcp->SendDidComposite(aLayersId, transactionId, aCompositeStart, aCompositeEnd);
       }
     }
   });
 }
 
 RefPtr<AsyncImagePipelineManager>
 CompositorBridgeParent::GetAsyncImagePipelineManager() const
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -279,17 +279,18 @@ public:
 
   bool IsSameProcess() const override;
 
   void NotifyWebRenderError(wr::WebRenderError aError);
   void NotifyWebRenderContextPurge();
   void NotifyPipelineRendered(const wr::PipelineId& aPipelineId,
                               const wr::Epoch& aEpoch,
                               TimeStamp& aCompositeStart,
-                              TimeStamp& aCompositeEnd);
+                              TimeStamp& aCompositeEnd,
+                              wr::RendererStats* aStats = nullptr);
   RefPtr<AsyncImagePipelineManager> GetAsyncImagePipelineManager() const;
 
   PCompositorWidgetParent* AllocPCompositorWidgetParent(const CompositorWidgetInitData& aInitData) override;
   bool DeallocPCompositorWidgetParent(PCompositorWidgetParent* aActor) override;
 
   void ObserveLayersUpdate(LayersId aLayersId, LayersObserverEpoch aEpoch, bool aActive) override { }
 
   /**
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -1806,17 +1806,18 @@ WebRenderBridgeParent::LastPendingTransa
   if (!mPendingTransactionIds.empty()) {
     id = mPendingTransactionIds.back().mId;
   }
   return id;
 }
 
 TransactionId
 WebRenderBridgeParent::FlushTransactionIdsForEpoch(const wr::Epoch& aEpoch, const TimeStamp& aEndTime,
-                                                   UiCompositorControllerParent* aUiController)
+                                                   UiCompositorControllerParent* aUiController,
+                                                   wr::RendererStats* aStats)
 {
   TransactionId id{0};
   while (!mPendingTransactionIds.empty()) {
     const auto& transactionId = mPendingTransactionIds.front();
 
     if (aEpoch.mHandle < transactionId.mEpoch.mHandle) {
       break;
     }
@@ -1844,16 +1845,30 @@ WebRenderBridgeParent::FlushTransactionI
 
       Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME, fracLatencyNorm);
       if (fracLatencyNorm > 200) {
         wr::RenderThread::Get()->NotifySlowFrame(mApi->GetId());
       }
       if (transactionId.mContainsSVGGroup) {
         Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITH_SVG, fracLatencyNorm);
       }
+
+      if (aStats) {
+        latencyMs -= (double(aStats->resource_upload_time) / 1000000.0);
+        latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
+        fracLatencyNorm = lround(latencyNorm * 100.0);
+      }
+      Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITHOUT_RESOURCE_UPLOAD, fracLatencyNorm);
+
+      if (aStats) {
+        latencyMs -= (double(aStats->gpu_cache_upload_time) / 1000000.0);
+        latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
+        fracLatencyNorm = lround(latencyNorm * 100.0);
+      }
+      Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITHOUT_UPLOAD, fracLatencyNorm);
     }
 
 #if defined(ENABLE_FRAME_LATENCY_LOG)
     if (transactionId.mRefreshStartTime) {
       int32_t latencyMs = lround((aEndTime - transactionId.mRefreshStartTime).ToMilliseconds());
       printf_stderr("From transaction start to end of generate frame latencyMs %d this %p\n", latencyMs, this);
     }
     if (transactionId.mFwdTime) {
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -164,17 +164,18 @@ public:
                                 bool aContainsSVGGroup,
                                 const TimeStamp& aRefreshStartTime,
                                 const TimeStamp& aTxnStartTime,
                                 const TimeStamp& aFwdTime,
                                 const bool aIsFirstPaint,
                                 const bool aUseForTelemetry = true);
   TransactionId LastPendingTransactionId();
   TransactionId FlushTransactionIdsForEpoch(const wr::Epoch& aEpoch, const TimeStamp& aEndTime,
-                                            UiCompositorControllerParent* aUiController);
+                                            UiCompositorControllerParent* aUiController,
+                                            wr::RendererStats* aStats = nullptr);
 
   TextureFactoryIdentifier GetTextureFactoryIdentifier();
 
   void ExtractImageCompositeNotifications(nsTArray<ImageCompositeNotificationInfo>* aNotifications);
 
   wr::Epoch GetCurrentEpoch() const { return mWrEpoch; }
   wr::IdNamespace GetIdNamespace()
   {
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -339,33 +339,35 @@ RenderThread::RunEvent(wr::WindowId aWin
   aEvent = nullptr;
 }
 
 static void
 NotifyDidRender(layers::CompositorBridgeParent* aBridge,
                 RefPtr<WebRenderPipelineInfo> aInfo,
                 TimeStamp aStart,
                 TimeStamp aEnd,
-                bool aRender)
+                bool aRender,
+                RendererStats aStats)
 {
   if (aRender && aBridge->GetWrBridge()) {
     // We call this here to mimic the behavior in LayerManagerComposite, as to
     // not change what Talos measures. That is, we do not record an empty frame
     // as a frame.
     aBridge->GetWrBridge()->RecordFrame();
   }
 
   auto info = aInfo->Raw();
 
   for (uintptr_t i = 0; i < info.epochs.length; i++) {
     aBridge->NotifyPipelineRendered(
         info.epochs.data[i].pipeline_id,
         info.epochs.data[i].epoch,
         aStart,
-        aEnd);
+        aEnd,
+        &aStats);
   }
 }
 
 void
 RenderThread::UpdateAndRender(wr::WindowId aWindowId,
                               const TimeStamp& aStartTime,
                               bool aRender,
                               const Maybe<gfx::IntSize>& aReadbackSize,
@@ -379,34 +381,36 @@ RenderThread::UpdateAndRender(wr::Window
   auto it = mRenderers.find(aWindowId);
   MOZ_ASSERT(it != mRenderers.end());
   if (it == mRenderers.end()) {
     return;
   }
 
   auto& renderer = it->second;
   bool rendered = false;
+  RendererStats stats = { 0 };
   if (aRender) {
-    rendered = renderer->UpdateAndRender(aReadbackSize, aReadbackBuffer, aHadSlowFrame);
+    rendered = renderer->UpdateAndRender(aReadbackSize, aReadbackBuffer, aHadSlowFrame, &stats);
   } else {
     renderer->Update();
   }
   // Check graphics reset status even when rendering is skipped.
   renderer->CheckGraphicsResetStatus();
 
   TimeStamp end = TimeStamp::Now();
   auto info = renderer->FlushPipelineInfo();
 
   layers::CompositorThreadHolder::Loop()->PostTask(NewRunnableFunction(
     "NotifyDidRenderRunnable",
     &NotifyDidRender,
     renderer->GetCompositorBridge(),
     info,
     aStartTime, end,
-    aRender
+    aRender,
+    stats
   ));
 
   if (rendered) {
     // Wait for GPU after posting NotifyDidRender, since the wait is not
     // necessary for the NotifyDidRender.
     // The wait is necessary for Textures recycling of AsyncImagePipelineManager
     // and for avoiding GPU queue is filled with too much tasks.
     // WaitForGPU's implementation is different for each platform.
--- a/gfx/webrender_bindings/RendererOGL.cpp
+++ b/gfx/webrender_bindings/RendererOGL.cpp
@@ -102,17 +102,20 @@ RendererOGL::Update()
 
 static void
 DoNotifyWebRenderContextPurge(layers::CompositorBridgeParent* aBridge)
 {
   aBridge->NotifyWebRenderContextPurge();
 }
 
 bool
-RendererOGL::UpdateAndRender(const Maybe<gfx::IntSize>& aReadbackSize, const Maybe<Range<uint8_t>>& aReadbackBuffer, bool aHadSlowFrame)
+RendererOGL::UpdateAndRender(const Maybe<gfx::IntSize>& aReadbackSize,
+                             const Maybe<Range<uint8_t>>& aReadbackBuffer,
+                             bool aHadSlowFrame,
+                             RendererStats* aOutStats)
 {
   uint32_t flags = gfx::gfxVars::WebRenderDebugFlags();
   // Disable debug flags during readback
   if (aReadbackBuffer.isSome()) {
     flags = 0;
   }
 
   if (mDebugFlags.mBits != flags) {
@@ -139,17 +142,17 @@ RendererOGL::UpdateAndRender(const Maybe
   if (!mCompositor->BeginFrame()) {
     return false;
   }
 
   wr_renderer_update(mRenderer);
 
   auto size = mCompositor->GetBufferSize();
 
-  if (!wr_renderer_render(mRenderer, size.width, size.height, aHadSlowFrame)) {
+  if (!wr_renderer_render(mRenderer, size.width, size.height, aHadSlowFrame, aOutStats)) {
     NotifyWebRenderError(WebRenderError::RENDER);
   }
 
   if (aReadbackBuffer.isSome()) {