merge mozilla-central to mozilla-beta. a=merge l10n=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sat, 20 Oct 2018 00:34:26 +0300
changeset 497869 01378c910610cd214b2838650d0d2b7218fa8b5d
parent 497648 2122185b0f1d102c1458170361788e805e13b1bb (current diff)
parent 497868 4888629028695c78b394cef273c00e70ae4b87df (diff)
child 497870 865c05d39687a7d957354d46280b8c91612ee041
push id10002
push userarchaeopteryx@coole-files.de
push dateFri, 19 Oct 2018 23:09:29 +0000
treeherdermozilla-beta@01378c910610 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0
merge mozilla-central to mozilla-beta. a=merge l10n=merge
browser/extensions/webcompat/bootstrap.js
browser/extensions/webcompat/content/data/ua_overrides.jsm
browser/extensions/webcompat/content/lib/ua_overrider.jsm
browser/extensions/webcompat/injections/css/bug0000000-dummy-css-injection.css
browser/extensions/webcompat/injections/js/bug0000000-dummy-js-injection.js
browser/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js
browser/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js
browser/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js
browser/extensions/webcompat/injections/js/bug1472081-election.gov.np-window.sidebar-shim.js
browser/extensions/webcompat/injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js
browser/extensions/webcompat/install.rdf.in
browser/extensions/webcompat/jar.mn
browser/extensions/webcompat/test/.eslintrc.js
browser/extensions/webcompat/test/browser.ini
browser/extensions/webcompat/test/browser_check_installed.js
browser/extensions/webcompat/test/browser_overrider.js
browser/extensions/webcompat/webextension/background.js
browser/extensions/webcompat/webextension/injections/css/bug0000000-dummy-css-injection.css
browser/extensions/webcompat/webextension/injections/js/bug0000000-dummy-js-injection.js
browser/extensions/webcompat/webextension/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js
browser/extensions/webcompat/webextension/injections/js/bug1457335-histography.io-ua-change.js
browser/extensions/webcompat/webextension/injections/js/bug1472075-bankofamerica.com-ua-change.js
browser/extensions/webcompat/webextension/injections/js/bug1472081-election.gov.np-window.sidebar-shim.js
browser/extensions/webcompat/webextension/injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js
browser/extensions/webcompat/webextension/manifest.json
build/build-clang/clang-7-mingw.json
build/build-clang/clang-trunk-mingw.json
devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
devtools/client/aboutdebugging/test/addons/test-devtools-webextension-noid/manifest.json
devtools/client/aboutdebugging/test/addons/test-devtools-webextension-unknown-prop/manifest.json
devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
devtools/client/webconsole/test/mochitest/browser_jsterm_await_concurrent.js
docshell/base/nsDocShellLoadInfo.cpp
docshell/base/nsDocShellLoadInfo.h
dom/chrome-webidl/Flex.webidl
dom/flex/FlexItem.cpp
dom/flex/FlexItem.h
dom/flex/FlexItemValues.cpp
dom/flex/FlexItemValues.h
dom/flex/FlexLine.cpp
dom/flex/FlexLine.h
dom/flex/FlexLineValues.cpp
dom/flex/FlexLineValues.h
dom/tests/html/jshandlers.html
dom/tests/js/DumpHTML.js
dom/tests/js/DumpTree.js
dom/tests/js/HTMLString.js
dom/tests/js/attributes.html
dom/tests/js/class.html
dom/tests/js/clone.html
dom/tests/js/docfrag.html
dom/tests/js/flamer.gif
dom/tests/js/id.html
dom/tests/js/lists.html
dom/tests/js/simple.js
dom/tests/js/ssheets.js
dom/tests/js/style1.html
dom/tests/js/style2.html
dom/tests/js/tables/changeCaption.js
dom/tests/js/tables/changeCell.js
dom/tests/js/timer.js
dom/tests/js/write.html
dom/tests/js/write2.html
dom/webidl/Flex.webidl
js/src/jit-test/tests/cacheir/bug1494537.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-0-3.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-16.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-17.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-18.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-19.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-20.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-21.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-22.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-23.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-24.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-25.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3-11-26.js
js/src/tests/test262/built-ins/JSON/stringify/15.12.3_4-1-3.js
js/src/tests/test262/intl402/NumberFormat/prototype/formatToParts/formatToParts.js
js/src/tests/test262/intl402/NumberFormat/prototype/formatToParts/return-abrupt-tonumber.js
js/src/tests/test262/intl402/NumberFormat/prototype/formatToParts/this-has-not-internal-throws.js
js/src/tests/test262/intl402/NumberFormat/prototype/formatToParts/this-is-not-object-throws.js
js/src/tests/test262/intl402/NumberFormat/prototype/this-value-not-numberformat.js
js/src/tests/test262/language/expressions/async-generator/args-trailing-comma-multiple.js
js/src/tests/test262/language/expressions/async-generator/args-trailing-comma-null.js
js/src/tests/test262/language/expressions/async-generator/args-trailing-comma-single-args.js
js/src/tests/test262/language/expressions/async-generator/args-trailing-comma-undefined.js
js/src/tests/test262/language/expressions/async-generator/named-args-trailing-comma-multiple.js
js/src/tests/test262/language/expressions/async-generator/named-args-trailing-comma-null.js
js/src/tests/test262/language/expressions/async-generator/named-args-trailing-comma-single-args.js
js/src/tests/test262/language/expressions/async-generator/named-args-trailing-comma-undefined.js
js/src/tests/test262/language/expressions/class/async-gen-meth-args-trailing-comma-multiple.js
js/src/tests/test262/language/expressions/class/async-gen-meth-args-trailing-comma-null.js
js/src/tests/test262/language/expressions/class/async-gen-meth-args-trailing-comma-single-args.js
js/src/tests/test262/language/expressions/class/async-gen-meth-args-trailing-comma-undefined.js
js/src/tests/test262/language/expressions/class/async-gen-meth-static-args-trailing-comma-multiple.js
js/src/tests/test262/language/expressions/class/async-gen-meth-static-args-trailing-comma-null.js
js/src/tests/test262/language/expressions/class/async-gen-meth-static-args-trailing-comma-single-args.js
js/src/tests/test262/language/expressions/class/async-gen-meth-static-args-trailing-comma-undefined.js
js/src/tests/test262/language/expressions/class/err-field-delete-call-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-field-delete-covered-call-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-field-delete-covered-member-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-field-delete-member-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-field-delete-twice-covered-call-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-field-delete-twice-covered-member-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-method-delete-call-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-method-delete-covered-call-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-method-delete-covered-member-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-method-delete-member-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-method-delete-twice-covered-call-expression-privatename.js
js/src/tests/test262/language/expressions/class/err-method-delete-twice-covered-member-expression-privatename.js
js/src/tests/test262/language/expressions/class/fields-computed-name-propname-constructor.js
js/src/tests/test262/language/expressions/class/fields-computed-variable-name-propname-constructor.js
js/src/tests/test262/language/expressions/class/fields-privatename-constructor-err.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-1-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-10-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-11-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-12-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-13-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-14-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-15-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-16-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-17-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-18-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-19-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-2-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-20-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-21-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-22-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-3-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-4-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-5-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-6-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-7-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-8-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/11.13.2-6-9-s-strict.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T1.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T10.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T11.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T2.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T3.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T4.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T5.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T6.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T7.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T8.js
js/src/tests/test262/language/expressions/compound-assignment/S11.13.2_A1_T9.js
js/src/tests/test262/language/expressions/delete/11.4.1-2-1.js
js/src/tests/test262/language/expressions/delete/11.4.1-2-3.js
js/src/tests/test262/language/expressions/delete/11.4.1-2-4.js
js/src/tests/test262/language/expressions/delete/11.4.1-2-5.js
js/src/tests/test262/language/expressions/delete/11.4.1-2-6.js
js/src/tests/test262/language/expressions/object/method-definition/async-gen-meth-args-trailing-comma-multiple.js
js/src/tests/test262/language/expressions/object/method-definition/async-gen-meth-args-trailing-comma-null.js
js/src/tests/test262/language/expressions/object/method-definition/async-gen-meth-args-trailing-comma-single-args.js
js/src/tests/test262/language/expressions/object/method-definition/async-gen-meth-args-trailing-comma-undefined.js
js/src/tests/test262/language/expressions/postfix-decrement/11.3.2-2-1-s-strict.js
js/src/tests/test262/language/expressions/postfix-decrement/11.3.2-2-2-s-strict.js
js/src/tests/test262/language/expressions/postfix-decrement/S11.3.2_A1.1_T1.js
js/src/tests/test262/language/expressions/postfix-decrement/S11.3.2_A1.1_T2.js
js/src/tests/test262/language/expressions/postfix-decrement/S11.3.2_A1.1_T3.js
js/src/tests/test262/language/expressions/postfix-decrement/S11.3.2_A1.1_T4.js
js/src/tests/test262/language/expressions/postfix-decrement/S11.3.2_A1.2_T1.js
js/src/tests/test262/language/expressions/postfix-increment/11.3.1-2-1-s-strict.js
js/src/tests/test262/language/expressions/postfix-increment/11.3.1-2-2-s-strict.js
js/src/tests/test262/language/expressions/postfix-increment/S11.3.1_A1.1_T1.js
js/src/tests/test262/language/expressions/postfix-increment/S11.3.1_A1.1_T2.js
js/src/tests/test262/language/expressions/postfix-increment/S11.3.1_A1.1_T3.js
js/src/tests/test262/language/expressions/postfix-increment/S11.3.1_A1.1_T4.js
js/src/tests/test262/language/expressions/postfix-increment/S11.3.1_A1.2_T1.js
js/src/tests/test262/language/expressions/prefix-decrement/11.4.5-2-1-s-strict.js
js/src/tests/test262/language/expressions/prefix-decrement/11.4.5-2-2-s-strict.js
js/src/tests/test262/language/expressions/prefix-decrement/S11.4.5_A1.js
js/src/tests/test262/language/expressions/prefix-increment/11.4.4-2-1-s-strict.js
js/src/tests/test262/language/expressions/prefix-increment/11.4.4-2-2-s-strict.js
js/src/tests/test262/language/expressions/prefix-increment/S11.4.4_A1.js
js/src/tests/test262/language/statements/async-generator/args-trailing-comma-multiple.js
js/src/tests/test262/language/statements/async-generator/args-trailing-comma-null.js
js/src/tests/test262/language/statements/async-generator/args-trailing-comma-single-args.js
js/src/tests/test262/language/statements/async-generator/args-trailing-comma-undefined.js
js/src/tests/test262/language/statements/class/async-gen-meth-args-trailing-comma-multiple.js
js/src/tests/test262/language/statements/class/async-gen-meth-args-trailing-comma-null.js
js/src/tests/test262/language/statements/class/async-gen-meth-args-trailing-comma-single-args.js
js/src/tests/test262/language/statements/class/async-gen-meth-args-trailing-comma-undefined.js
js/src/tests/test262/language/statements/class/async-gen-meth-static-args-trailing-comma-multiple.js
js/src/tests/test262/language/statements/class/async-gen-meth-static-args-trailing-comma-null.js
js/src/tests/test262/language/statements/class/async-gen-meth-static-args-trailing-comma-single-args.js
js/src/tests/test262/language/statements/class/async-gen-meth-static-args-trailing-comma-undefined.js
js/src/tests/test262/language/statements/class/err-field-delete-call-expression-privatename.js
js/src/tests/test262/language/statements/class/err-field-delete-covered-call-expression-privatename.js
js/src/tests/test262/language/statements/class/err-field-delete-covered-member-expression-privatename.js
js/src/tests/test262/language/statements/class/err-field-delete-member-expression-privatename.js
js/src/tests/test262/language/statements/class/err-field-delete-twice-covered-call-expression-privatename.js
js/src/tests/test262/language/statements/class/err-field-delete-twice-covered-member-expression-privatename.js
js/src/tests/test262/language/statements/class/err-method-delete-call-expression-privatename.js
js/src/tests/test262/language/statements/class/err-method-delete-covered-call-expression-privatename.js
js/src/tests/test262/language/statements/class/err-method-delete-covered-member-expression-privatename.js
js/src/tests/test262/language/statements/class/err-method-delete-member-expression-privatename.js
js/src/tests/test262/language/statements/class/err-method-delete-twice-covered-call-expression-privatename.js
js/src/tests/test262/language/statements/class/err-method-delete-twice-covered-member-expression-privatename.js
js/src/tests/test262/language/statements/class/fields-computed-variable-name-propname-constructor.js
js/src/tests/test262/language/statements/class/fields-privatename-constructor-err.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-constructor-empty-missing-class-heritage.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-contains-multiple-constructor.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-has-direct-super-missing-class-heritage.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-method-contains-direct-super.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-special-method-generator-contains-direct-super.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-special-method-generator-propname-constructor.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-special-method-get-contains-direct-super.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-special-method-get-propname-constructor.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-special-method-set-contains-direct-super.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-special-method-set-propname-constructor.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-static-method-contains-direct-super.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-static-method-get-contains-direct-super.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-static-method-get-propname-prototype.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-static-method-propname-prototype.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-static-method-set-contains-direct-super.js
js/src/tests/test262/language/statements/class/syntax/early-errors/class-body-static-method-set-propname-prototype.js
js/src/vm/Interpreter.cpp
mobile/android/extensions/webcompat/content/lib/ua_overrider.jsm
mobile/android/extensions/webcompat/webextension/background.js
mobile/android/extensions/webcompat/webextension/injections/css/bug0000000-dummy-css-injection.css
mobile/android/extensions/webcompat/webextension/injections/js/bug0000000-dummy-js-injection.js
mobile/android/extensions/webcompat/webextension/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js
mobile/android/extensions/webcompat/webextension/injections/js/bug1457335-histography.io-ua-change.js
mobile/android/extensions/webcompat/webextension/injections/js/bug1472075-bankofamerica.com-ua-change.js
mobile/android/extensions/webcompat/webextension/injections/js/bug1472081-election.gov.np-window.sidebar-shim.js
mobile/android/extensions/webcompat/webextension/injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js
taskcluster/docs/kinds.rst
taskcluster/scripts/misc/build-clang-7-mingw.sh
taskcluster/scripts/misc/build-clang-trunk-mingw.sh
testing/web-platform/meta/fetch/api/request/destination/fetch-destination.https.html.ini
testing/web-platform/meta/html/rendering/non-replaced-elements/tables/table-cell-width-s.html.ini
testing/web-platform/meta/html/rendering/non-replaced-elements/tables/table-width-s.html.ini
testing/web-platform/meta/html/rendering/non-replaced-elements/the-hr-element-0/width.html.ini
testing/web-platform/meta/html/rendering/replaced-elements/embedded-content-rendering-rules/canvas_without_context_a.html.ini
testing/web-platform/meta/quirks/historical/list-item-bullet-size.html.ini
testing/web-platform/meta/svg/shapes/rect-01.svg.ini
testing/web-platform/meta/svg/shapes/rect-02.svg.ini
tools/lint/wpt/wpt_manifest.py
tools/lint/wpt_manifest.yml
--- a/accessible/android/AccessibleWrap.cpp
+++ b/accessible/android/AccessibleWrap.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "AccessibleWrap.h"
 
 #include "Accessible-inl.h"
+#include "AndroidInputType.h"
 #include "DocAccessibleWrap.h"
 #include "IDSet.h"
 #include "JavaBuiltins.h"
 #include "SessionAccessibility.h"
 #include "nsAccessibilityService.h"
 #include "nsIPersistentProperties2.h"
 #include "nsIStringBundle.h"
 #include "nsAccUtils.h"
@@ -281,20 +282,21 @@ AccessibleWrap::CreateBundle(int32_t aPa
     if (!IsNaN(aMaxVal)) {
       GECKOBUNDLE_PUT(rangeInfo, "max", java::sdk::Double::New(aMaxVal));
     }
 
     GECKOBUNDLE_FINISH(rangeInfo);
     GECKOBUNDLE_PUT(nodeInfo, "rangeInfo", rangeInfo);
   }
 
-  nsString inputType;
-  nsAccUtils::GetAccAttr(aAttributes, nsGkAtoms::textInputType, inputType);
-  if (!inputType.IsEmpty()) {
-    GECKOBUNDLE_PUT(nodeInfo, "inputType", jni::StringParam(inputType));
+  nsString inputTypeAttr;
+  nsAccUtils::GetAccAttr(aAttributes, nsGkAtoms::textInputType, inputTypeAttr);
+  int32_t inputType = GetInputType(inputTypeAttr);
+  if (inputType) {
+    GECKOBUNDLE_PUT(nodeInfo, "inputType", java::sdk::Integer::ValueOf(inputType));
   }
 
   nsString posinset;
   nsresult rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("posinset"), posinset);
   if (NS_SUCCEEDED(rv)) {
     int32_t rowIndex;
     if (sscanf(NS_ConvertUTF16toUTF8(posinset).get(), "%d", &rowIndex) > 0) {
       GECKOBUNDLE_START(collectionItemInfo);
@@ -457,16 +459,46 @@ AccessibleWrap::GetAndroidClass(role aRo
 #include "RoleMap.h"
     default:
       return java::SessionAccessibility::CLASSNAME_VIEW;
   }
 
 #undef ROLE
 }
 
+int32_t
+AccessibleWrap::GetInputType(const nsString& aInputTypeAttr)
+{
+  if (aInputTypeAttr.EqualsIgnoreCase("email")) {
+    return java::sdk::InputType::TYPE_CLASS_TEXT | java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
+  }
+
+  if (aInputTypeAttr.EqualsIgnoreCase("number")) {
+    return java::sdk::InputType::TYPE_CLASS_NUMBER;
+  }
+
+  if (aInputTypeAttr.EqualsIgnoreCase("password")) {
+    return java::sdk::InputType::TYPE_CLASS_TEXT | java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
+  }
+
+  if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
+    return java::sdk::InputType::TYPE_CLASS_PHONE;
+  }
+
+  if (aInputTypeAttr.EqualsIgnoreCase("text")) {
+    return java::sdk::InputType::TYPE_CLASS_TEXT | java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
+  }
+
+  if (aInputTypeAttr.EqualsIgnoreCase("url")) {
+    return java::sdk::InputType::TYPE_CLASS_TEXT | java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
+  }
+
+  return 0;
+}
+
 void
 AccessibleWrap::DOMNodeID(nsString& aDOMNodeID)
 {
   if (mContent) {
     nsAtom* id = mContent->GetID();
     if (id) {
       id->ToString(aDOMNodeID);
     }
--- a/accessible/android/AccessibleWrap.h
+++ b/accessible/android/AccessibleWrap.h
@@ -52,16 +52,18 @@ protected:
     const nsTArray<int32_t>& aChildren) const;
 
   // IDs should be a positive 32bit integer.
   static int32_t AcquireID();
   static void ReleaseID(int32_t aID);
 
   static int32_t GetAndroidClass(role aRole);
 
+  static int32_t GetInputType(const nsString& aInputTypeAttr);
+
   int32_t mID;
 
 private:
   void DOMNodeID(nsString& aDOMNodeID);
 
   static void GetRoleDescription(role aRole,
                                  nsAString& aGeckoRole,
                                  nsAString& aRoleDescription);
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1514,25 +1514,21 @@ pref("network.cookie.cookieBehavior", 4 
 pref("browser.contentblocking.allowlist.storage.enabled", true);
 
 #ifdef NIGHTLY_BUILD
 pref("browser.contentblocking.global-toggle.enabled", true);
 #else
 pref("browser.contentblocking.global-toggle.enabled", false);
 #endif
 
-// Define a set of default features for the Content Blocking UI
-#ifdef EARLY_BETA_OR_EARLIER
-pref("browser.contentblocking.fastblock.ui.enabled", true);
-pref("browser.contentblocking.fastblock.control-center.ui.enabled", true);
-#else
+// Disable the UI for FastBlock in product.
 pref("browser.contentblocking.fastblock.ui.enabled", false);
 pref("browser.contentblocking.fastblock.control-center.ui.enabled", false);
-#endif
 
+// Define a set of default features for the Content Blocking UI.
 pref("browser.contentblocking.trackingprotection.ui.enabled", true);
 pref("browser.contentblocking.trackingprotection.control-center.ui.enabled", true);
 pref("browser.contentblocking.rejecttrackers.ui.enabled", true);
 pref("browser.contentblocking.rejecttrackers.ui.recommended", true);
 pref("browser.contentblocking.rejecttrackers.control-center.ui.enabled", true);
 pref("browser.contentblocking.cookies-site-data.ui.reject-trackers.recommended", true);
 pref("browser.contentblocking.cookies-site-data.ui.reject-trackers.enabled", true);
 
@@ -1774,13 +1770,8 @@ pref("browser.fission.simulate", false);
 pref("prio.publicKeyA", "35AC1C7576C7C6EDD7FED6BCFC337B34D48CB4EE45C86BEEFB40BD8875707733");
 pref("prio.publicKeyB", "26E6674E65425B823F1F1D5F96E3BB3EF9E406EC7FBA7DEF8B08A35DD135AF50");
 #endif
 
 // Whether or not Prio-encoded Telemetry will be sent along with the main ping.
 #if defined(NIGHTLY_BUILD) && defined(MOZ_LIBPRIO)
 pref("prio.enabled", true);
 #endif
-
-#ifdef NIGHTLY_BUILD
-pref("browser.fastblock.enabled", true);
-#endif
-
--- a/browser/base/content/browser-contentblocking.js
+++ b/browser/base/content/browser-contentblocking.js
@@ -121,17 +121,16 @@ var TrackingProtection = {
   },
 
   isBlockerActivated(state) {
     return state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT;
   },
 };
 
 var ThirdPartyCookies = {
-  reportBreakageLabel: "cookierestrictions",
   telemetryIdentifier: "cr",
   PREF_ENABLED: "network.cookie.cookieBehavior",
   PREF_REPORT_BREAKAGE_ENABLED: "browser.contentblocking.rejecttrackers.reportBreakage.enabled",
   PREF_ENABLED_VALUES: [
     // These values match the ones exposed under the Content Blocking section
     // of the Preferences UI.
     Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,  // Block all third-party cookies
     Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,  // Block third-party cookies from trackers
@@ -139,22 +138,59 @@ var ThirdPartyCookies = {
   PREF_UI_ENABLED: "browser.contentblocking.rejecttrackers.control-center.ui.enabled",
 
   get categoryItem() {
     delete this.categoryItem;
     return this.categoryItem =
       document.getElementById("identity-popup-content-blocking-category-3rdpartycookies");
   },
 
+  get reportBreakageLabel() {
+    switch (this.behaviorPref) {
+    case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+      return "nocookiesblocked";
+    case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+      return "allthirdpartycookiesblocked";
+    case Ci.nsICookieService.BEHAVIOR_REJECT:
+      return "allcookiesblocked";
+    case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+      return "cookiesfromunvisitedsitesblocked";
+    default:
+      Cu.reportError(`Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}`);
+      // fall through
+    case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+      return "cookierestrictions";
+    }
+  },
+
+  get categoryLabelDefault() {
+    delete this.categoryLabelDefault;
+    return this.categoryLabelDefault =
+      document.getElementById("identity-popup-content-blocking-category-label-default");
+  },
+
+  get categoryLabelTrackers() {
+    delete this.categoryLabelTrackers;
+    return this.categoryLabelTrackers =
+      document.getElementById("identity-popup-content-blocking-category-label-trackers");
+  },
+
+  updateCategoryLabel() {
+    let rejectTrackers = this.behaviorPref == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
+    this.categoryLabelDefault.hidden = rejectTrackers;
+    this.categoryLabelTrackers.hidden = !rejectTrackers;
+  },
+
   init() {
     XPCOMUtils.defineLazyPreferenceGetter(this, "behaviorPref", this.PREF_ENABLED,
-                                          Ci.nsICookieService.BEHAVIOR_ACCEPT);
+      Ci.nsICookieService.BEHAVIOR_ACCEPT, this.updateCategoryLabel.bind(this));
     XPCOMUtils.defineLazyPreferenceGetter(this, "visible", this.PREF_UI_ENABLED, false);
     XPCOMUtils.defineLazyPreferenceGetter(this, "reportBreakageEnabled",
       this.PREF_REPORT_BREAKAGE_ENABLED, false);
+    this.updateCategoryLabel();
   },
   get enabled() {
     return this.PREF_ENABLED_VALUES.includes(this.behaviorPref);
   },
 
   isBlockerActivated(state) {
     return (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER) != 0 ||
            (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN) != 0;
--- a/browser/base/content/browser-customization.js
+++ b/browser/base/content/browser-customization.js
@@ -61,8 +61,120 @@ var CustomizationHandler = {
     for (let childNode of menubar.children)
       childNode.setAttribute("disabled", false);
     let cmd = document.getElementById("cmd_CustomizeToolbars");
     cmd.removeAttribute("disabled");
 
     gBrowser.selectedBrowser.focus();
   },
 };
+
+var AutoHideMenubar = {
+  get _node() {
+    delete this._node;
+    return this._node = document.getElementById("toolbar-menubar");
+  },
+
+  _contextMenuListener: {
+    contextMenu: null,
+
+    get active() {
+      return !!this.contextMenu;
+    },
+
+    init(event) {
+      // Ignore mousedowns in <menupopup>s.
+      if (event.target.closest("menupopup")) {
+        return;
+      }
+
+      let contextMenuId = AutoHideMenubar._node.getAttribute("context");
+      this.contextMenu = document.getElementById(contextMenuId);
+      this.contextMenu.addEventListener("popupshown", this);
+      this.contextMenu.addEventListener("popuphiding", this);
+      AutoHideMenubar._node.addEventListener("mousemove", this);
+    },
+    handleEvent(event) {
+      switch (event.type) {
+        case "popupshown":
+          AutoHideMenubar._node.removeEventListener("mousemove", this);
+          break;
+        case "popuphiding":
+        case "mousemove":
+          AutoHideMenubar._setInactiveAsync();
+          AutoHideMenubar._node.removeEventListener("mousemove", this);
+          this.contextMenu.removeEventListener("popuphiding", this);
+          this.contextMenu.removeEventListener("popupshown", this);
+          this.contextMenu = null;
+          break;
+      }
+    },
+  },
+
+  init() {
+    this._node.addEventListener("toolbarvisibilitychange", this);
+    if (this._node.getAttribute("autohide") == "true") {
+      this._enable();
+    }
+  },
+
+  _updateState() {
+    if (this._node.getAttribute("autohide") == "true") {
+      this._enable();
+    } else {
+      this._disable();
+    }
+  },
+
+  _events: ["DOMMenuBarInactive", "DOMMenuBarActive", "popupshowing", "mousedown"],
+  _enable() {
+    this._node.setAttribute("inactive", "true");
+    for (let event of this._events) {
+      this._node.addEventListener(event, this);
+    }
+  },
+
+  _disable() {
+    this._setActive();
+    for (let event of this._events) {
+      this._node.removeEventListener(event, this);
+    }
+  },
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "toolbarvisibilitychange":
+        this._updateState();
+        break;
+      case "popupshowing":
+        // fall through
+      case "DOMMenuBarActive":
+        this._setActive();
+        break;
+      case "mousedown":
+        if (event.button == 2) {
+          this._contextMenuListener.init(event);
+        }
+        break;
+      case "DOMMenuBarInactive":
+        if (!this._contextMenuListener.active)
+          this._setInactiveAsync();
+        break;
+    }
+  },
+
+  _setInactiveAsync() {
+    this._inactiveTimeout = setTimeout(() => {
+      if (this._node.getAttribute("autohide") == "true") {
+        this._inactiveTimeout = null;
+        this._node.setAttribute("inactive", "true");
+      }
+    }, 0);
+  },
+
+  _setActive() {
+    if (this._inactiveTimeout) {
+      clearTimeout(this._inactiveTimeout);
+      this._inactiveTimeout = null;
+    }
+    this._node.removeAttribute("inactive");
+  },
+};
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -71,20 +71,16 @@ toolbar[customizable="true"] {
 }
 
 %ifdef XP_MACOSX
 #toolbar-menubar {
   -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar-menubar-stub");
 }
 %endif
 
-#toolbar-menubar[autohide="true"] {
-  -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar-menubar-autohide");
-}
-
 panelmultiview {
   -moz-box-align: start;
 }
 
 panelmultiview[transitioning] {
   pointer-events: none;
 }
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -102,17 +102,17 @@ XPCOMUtils.defineLazyScriptGetter(this, 
 XPCOMUtils.defineLazyScriptGetter(this, "gTabsPanel",
                                   "chrome://browser/content/browser-allTabsMenu.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["LightWeightThemeWebInstaller",
                                          "gExtensionsNotifications",
                                          "gXPInstallObserver"],
                                   "chrome://browser/content/browser-addons.js");
 XPCOMUtils.defineLazyScriptGetter(this, "ctrlTab",
                                   "chrome://browser/content/browser-ctrlTab.js");
-XPCOMUtils.defineLazyScriptGetter(this, "CustomizationHandler",
+XPCOMUtils.defineLazyScriptGetter(this, ["CustomizationHandler", "AutoHideMenubar"],
                                   "chrome://browser/content/browser-customization.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["PointerLock", "FullScreen"],
                                   "chrome://browser/content/browser-fullScreenAndPointerLock.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["gGestureSupport", "gHistorySwipeAnimation"],
                                   "chrome://browser/content/browser-gestureSupport.js");
 XPCOMUtils.defineLazyScriptGetter(this, "gSafeBrowsing",
                                   "chrome://browser/content/browser-safebrowsing.js");
 XPCOMUtils.defineLazyScriptGetter(this, "gSync",
@@ -1237,16 +1237,19 @@ var gBrowserInit = {
       document.documentElement.setAttribute("width", width);
       document.documentElement.setAttribute("height", height);
 
       if (width < TARGET_WIDTH && height < TARGET_HEIGHT) {
         document.documentElement.setAttribute("sizemode", "maximized");
       }
     }
 
+    // Run menubar initialization first, to avoid TabsInTitlebar code picking
+    // up mutations from it and causing a reflow.
+    AutoHideMenubar.init();
     // Update the chromemargin attribute so the window can be sized correctly.
     window.TabBarVisibility.update();
     TabsInTitlebar.init();
 
     new LightweightThemeConsumer(document);
     CompactTheme.init();
 
     if (AppConstants.platform == "win") {
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -58,17 +58,17 @@
         <textbox id="password-notification-password" type="password" show-content=""/>
         <checkbox id="password-notification-visibilityToggle" hidden="true"/>
       </popupnotificationcontent>
     </popupnotification>
 
 
     <popupnotification id="addon-progress-notification" hidden="true">
       <popupnotificationcontent orient="vertical">
-        <progressmeter id="addon-progress-notification-progressmeter"/>
+        <html:progress id="addon-progress-notification-progressmeter" max="100"/>
         <label id="addon-progress-notification-progresstext" crop="end"/>
       </popupnotificationcontent>
     </popupnotification>
 
     <popupnotification id="addon-install-confirmation-notification" hidden="true">
       <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
     </popupnotification>
 
--- a/browser/base/content/test/trackingUI/browser_trackingUI_state.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_state.js
@@ -172,16 +172,28 @@ function testTrackingPage(window) {
                    "#identity-popup-content-blocking-category-3rdpartycookies" :
                    "#identity-popup-content-blocking-category-tracking-protection";
     }
     is(hidden(category + " > .identity-popup-content-blocking-category-add-blocking"), blockedByTP,
       "Category item is" + (blockedByTP ? " not" : "") + " showing add blocking");
     is(hidden(category + " > .identity-popup-content-blocking-category-state-label"), !blockedByTP,
       "Category item is" + (blockedByTP ? "" : " not") + " set to blocked");
   }
+
+  if (Services.prefs.getIntPref(TPC_PREF) == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER) {
+    ok(hidden("#identity-popup-content-blocking-category-label-default"),
+      "Not showing default cookie restrictions label.");
+    ok(!hidden("#identity-popup-content-blocking-category-label-trackers"),
+      "Showing trackers cookie restrictions label.");
+  } else {
+    ok(hidden("#identity-popup-content-blocking-category-label-trackers"),
+      "Not showing trackers cookie restrictions label.");
+    ok(!hidden("#identity-popup-content-blocking-category-label-default"),
+      "Showing default cookie restrictions label.");
+  }
 }
 
 function testTrackingPageUnblocked(blockedByTP, window) {
   info("Tracking content must be white-listed and not blocked");
   ok(ContentBlocking.content.hasAttribute("detected"), "trackers are detected");
   ok(ContentBlocking.content.hasAttribute("hasException"), "content shows exception");
 
   let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
--- a/browser/base/content/test/trackingUI/browser_trackingUI_telemetry.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_telemetry.js
@@ -34,19 +34,20 @@ add_task(async function setup() {
 
   let enabledCounts =
     Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED").snapshot().counts;
   is(enabledCounts[0], 1, "TP was not enabled on start up");
 
   let scalars = Services.telemetry.snapshotScalars(
     Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT, false).parent;
 
-  is(scalars["contentblocking.enabled"], true, "CB was enabled at startup");
-  is(scalars["contentblocking.fastblock_enabled"], AppConstants.NIGHTLY_BUILD,
-     "FB is enabled in Nightly");
+  is(scalars["contentblocking.enabled"], Services.prefs.getBoolPref("browser.contentblocking.enabled"),
+    "CB enabled status was recorded at startup");
+  is(scalars["contentblocking.fastblock_enabled"], Services.prefs.getBoolPref("browser.fastblock.enabled"),
+    "FB enabled status was recorded at startup");
   is(scalars["contentblocking.exceptions"], 0, "no CB exceptions at startup");
 });
 
 
 add_task(async function testShieldHistogram() {
   Services.prefs.setBoolPref(PREF, true);
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
 
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -2756,19 +2756,18 @@ file, You can obtain one at http://mozil
         ]]></body>
       </method>
 
       <method name="setProgress">
         <parameter name="aProgress"/>
         <parameter name="aMaxProgress"/>
         <body><![CDATA[
           if (aMaxProgress == -1) {
-            this.progressmeter.setAttribute("mode", "undetermined");
+            this.progressmeter.removeAttribute("value");
           } else {
-            this.progressmeter.setAttribute("mode", "determined");
             this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress);
           }
 
           let now = Date.now();
 
           if (!this.notification.lastUpdate) {
             this.notification.lastUpdate = now;
             this.notification.lastProgress = aProgress;
@@ -2828,17 +2827,17 @@ file, You can obtain one at http://mozil
             if (maxProgress >= 0)
               maxProgress += aInstall.maxProgress;
             if (aInstall.state < AddonManager.STATE_DOWNLOADED)
               downloadingCount++;
           });
 
           if (downloadingCount == 0) {
             this.destroy();
-            this.progressmeter.setAttribute("mode", "undetermined");
+            this.progressmeter.removeAttribute("value");
             let status = gNavigatorBundle.getString("addonDownloadVerifying");
             this.progresstext.setAttribute("value", status);
             this.progresstext.setAttribute("tooltiptext", status);
           } else {
             this.setProgress(progress, maxProgress);
           }
         ]]></body>
       </method>
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -100,17 +100,20 @@
               <label flex="1" class="identity-popup-content-blocking-category-state-label">&contentBlocking.trackingProtection.blocking.label;</label>
               <label flex="1" class="identity-popup-content-blocking-category-add-blocking text-link"
                      id="identity-popup-tracking-protection-add-blocking"
                      onclick="ContentBlocking.openPreferences('identityPopup-CB-tracking-protection'); gIdentityHandler.recordClick('tp_add_blocking');">&contentBlocking.trackingProtection.add.label;</label>
             </hbox>
             <hbox id="identity-popup-content-blocking-category-3rdpartycookies"
                   class="identity-popup-content-blocking-category" align="center" role="group">
               <image class="identity-popup-content-blocking-category-icon thirdpartycookies-icon"/>
-              <label flex="1" class="identity-popup-content-blocking-category-label">&contentBlocking.3rdPartyCookies.label;</label>
+              <label flex="1" id="identity-popup-content-blocking-category-label-default"
+                     class="identity-popup-content-blocking-category-label">&contentBlocking.3rdPartyCookies.label;</label>
+              <label flex="1" id="identity-popup-content-blocking-category-label-trackers"
+                     hidden="true" class="identity-popup-content-blocking-category-label">&contentBlocking.3rdPartyCookies.trackers.label;</label>
               <label flex="1" class="identity-popup-content-blocking-category-state-label">&contentBlocking.3rdPartyCookies.blocking.label;</label>
               <label flex="1" class="identity-popup-content-blocking-category-add-blocking text-link"
                      id="identity-popup-3rdpartycookies-add-blocking"
                      onclick="ContentBlocking.openPreferences('identityPopup-CB-3rdpartycookies'); gIdentityHandler.recordClick('cookies_add_blocking');">&contentBlocking.3rdPartyCookies.add.label;</label>
             </hbox>
           </vbox>
 
           <button id="tracking-action-unblock"
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -628,16 +628,20 @@
         </vbox>
         <button id="PanelUI-panic-view-button"
                 label="&panicButton.view.forgetButton;"/>
       </vbox>
     </panelview>
 
     <panelview id="appMenu-moreView" title="&moreMenu.label;" class="PanelUI-subView">
       <vbox class="panel-subview-body">
+        <toolbarbutton id="appMenu-taskmanager-button"
+                       class="subviewbutton subviewbutton-iconic"
+                       label="&taskManagerCmd.label;"
+                       oncommand="switchToTabHavingURI('about:performance', true)"/>
         <toolbarbutton id="appMenu-characterencoding-button"
                        class="subviewbutton subviewbutton-nav"
                        label="&charsetMenu2.label;"
                        closemenu="none"
                        oncommand="PanelUI.showSubView('PanelUI-characterEncodingView', this)"/>
         <toolbarbutton id="appMenu-workoffline-button"
                        class="subviewbutton"
                        label="&goOfflineCmd.label;"
--- a/browser/components/customizableui/content/toolbar.xml
+++ b/browser/components/customizableui/content/toolbar.xml
@@ -156,118 +156,20 @@
       <method name="insertItem">
         <body><![CDATA[
           return null;
         ]]></body>
       </method>
     </implementation>
   </binding>
 
-  <!-- The toolbar-menubar-autohide and toolbar-drag bindings are almost
-       verbatim copies of their toolkit counterparts - they just inherit from
-       the customizableui's toolbar binding instead of toolkit's. We're currently
-       OK with the maintainance burden of having two copies of a binding, since
-       the long term goal is to move the customization framework into toolkit. -->
-
-  <binding id="toolbar-menubar-autohide"
-           extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
-    <implementation>
-      <constructor>
-        this._setInactive();
-      </constructor>
-      <destructor>
-        this._setActive();
-      </destructor>
-
-      <field name="_inactiveTimeout">null</field>
-
-      <field name="_contextMenuListener"><![CDATA[({
-        toolbar: this,
-        contextMenu: null,
-
-        get active() {
-          return !!this.contextMenu;
-        },
-
-        init(event) {
-          let node = event.target;
-          while (node != this.toolbar) {
-            if (node.localName == "menupopup")
-              return;
-            node = node.parentNode;
-          }
-
-          let contextMenuId = this.toolbar.getAttribute("context");
-          if (!contextMenuId)
-            return;
-
-          this.contextMenu = document.getElementById(contextMenuId);
-          if (!this.contextMenu)
-            return;
-
-          this.contextMenu.addEventListener("popupshown", this);
-          this.contextMenu.addEventListener("popuphiding", this);
-          this.toolbar.addEventListener("mousemove", this);
-        },
-        handleEvent(event) {
-          switch (event.type) {
-            case "popupshown":
-              this.toolbar.removeEventListener("mousemove", this);
-              break;
-            case "popuphiding":
-            case "mousemove":
-              this.toolbar._setInactiveAsync();
-              this.toolbar.removeEventListener("mousemove", this);
-              this.contextMenu.removeEventListener("popuphiding", this);
-              this.contextMenu.removeEventListener("popupshown", this);
-              this.contextMenu = null;
-              break;
-          }
-        },
-      })]]></field>
-
-      <method name="_setInactive">
-        <body><![CDATA[
-          this.setAttribute("inactive", "true");
-        ]]></body>
-      </method>
-
-      <method name="_setInactiveAsync">
-        <body><![CDATA[
-          this._inactiveTimeout = setTimeout(function(self) {
-            if (self.getAttribute("autohide") == "true") {
-              self._inactiveTimeout = null;
-              self._setInactive();
-            }
-          }, 0, this);
-        ]]></body>
-      </method>
-
-      <method name="_setActive">
-        <body><![CDATA[
-          if (this._inactiveTimeout) {
-            clearTimeout(this._inactiveTimeout);
-            this._inactiveTimeout = null;
-          }
-          this.removeAttribute("inactive");
-        ]]></body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="DOMMenuBarActive"     action="this._setActive();"/>
-      <handler event="popupshowing"         action="this._setActive();"/>
-      <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/>
-      <handler event="DOMMenuBarInactive"><![CDATA[
-        if (!this._contextMenuListener.active)
-          this._setInactiveAsync();
-      ]]></handler>
-    </handlers>
-  </binding>
-
+  <!-- The toolbar-drag binding is almost a verbatim copy of its toolkit counterpart,
+       but it inherits from the customizableui's toolbar binding instead of toolkit's.
+       This functionality will move into CustomizableUI proper as part of our move
+       away from XBL. -->
   <binding id="toolbar-drag"
            extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
     <implementation>
       <field name="_dragBindingAlive">true</field>
       <constructor><![CDATA[
         if (!this._draggableStarted) {
           this._draggableStarted = true;
           try {
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -9,27 +9,28 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "DownloadsViewUI",
 ];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   Downloads: "resource://gre/modules/Downloads.jsm",
   DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
 });
 
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
 var gDownloadElementButtons = {
   cancel: {
     commandName: "downloadsCmd_cancel",
     l10nId: "download-cancel",
     descriptionL10nId: "download-cancel-description",
     iconClass: "downloadIconCancel",
   },
   retry: {
@@ -172,17 +173,16 @@ this.DownloadsViewUI.DownloadElementShel
       downloadListItemFragment = MozXULElement.parseXULToFragment(`
         <hbox class="downloadMainArea" flex="1" align="center">
           <stack>
             <image class="downloadTypeIcon" validate="always"/>
             <image class="downloadBlockedBadge" />
           </stack>
           <vbox class="downloadContainer" flex="1" pack="center">
             <description class="downloadTarget" crop="center"/>
-            <progressmeter class="downloadProgress" min="0" max="100"/>
             <description class="downloadDetails downloadDetailsNormal"
                          crop="end"/>
             <description class="downloadDetails downloadDetailsHover"
                          crop="end"/>
             <description class="downloadDetails downloadDetailsButtonHover"
                          crop="end"/>
           </vbox>
         </hbox>
@@ -196,24 +196,30 @@ this.DownloadsViewUI.DownloadElementShel
     this.element.setAttribute("orient", "horizontal");
     this.element.setAttribute("onclick",
                               "DownloadsView.onDownloadClick(event);");
     this.element.appendChild(document.importNode(downloadListItemFragment,
                                                  true));
     for (let [propertyName, selector] of [
       ["_downloadTypeIcon", ".downloadTypeIcon"],
       ["_downloadTarget", ".downloadTarget"],
-      ["_downloadProgress", ".downloadProgress"],
       ["_downloadDetailsNormal", ".downloadDetailsNormal"],
       ["_downloadDetailsHover", ".downloadDetailsHover"],
       ["_downloadDetailsButtonHover", ".downloadDetailsButtonHover"],
       ["_downloadButton", ".downloadButton"],
     ]) {
       this[propertyName] = this.element.querySelector(selector);
     }
+
+    // HTML elements can be created directly without using parseXULToFragment.
+    let progress = this._downloadProgress =
+                   document.createElementNS(HTML_NS, "progress");
+    progress.className = "downloadProgress";
+    progress.setAttribute("max", "100");
+    this._downloadTarget.insertAdjacentElement("afterend", progress);
   },
 
   /**
    * Returns a string from the downloads stringbundleset, which contains legacy
    * strings that are loaded from DTD files instead of properties files. This
    * won't be necessary once localization is converted to Fluent (bug 1452637).
    */
   string(l10nId) {
@@ -265,38 +271,22 @@ this.DownloadsViewUI.DownloadElementShel
    * @param mode
    *        Either "normal" or "undetermined".
    * @param value
    *        Percentage of the progress bar to display, from 0 to 100.
    * @param paused
    *        True to display the progress bar style for paused downloads.
    */
   showProgress(mode, value, paused) {
-    // The "undetermined" mode of the progressmeter is implemented with a
-    // different element structure, and with support from platform code as well.
-    // On Linux only, this mode isn't compatible with the custom styling that we
-    // apply separately with the "progress-undetermined" attribute.
-    this._downloadProgress.setAttribute("mode",
-      AppConstants.platform == "linux" ? "normal" : mode);
     if (mode == "undetermined") {
-      this._downloadProgress.setAttribute("progress-undetermined", "true");
+      this._downloadProgress.removeAttribute("value");
     } else {
-      this._downloadProgress.removeAttribute("progress-undetermined");
+      this._downloadProgress.setAttribute("value", value);
     }
-    this._downloadProgress.setAttribute("value", value);
-    if (paused) {
-      this._downloadProgress.setAttribute("paused", "true");
-    } else {
-      this._downloadProgress.removeAttribute("paused");
-    }
-
-    // Dispatch the ValueChange event for accessibility.
-    let event = this.element.ownerDocument.createEvent("Events");
-    event.initEvent("ValueChange", true, true);
-    this._downloadProgress.dispatchEvent(event);
+    this._downloadProgress.toggleAttribute("paused", !!paused);
   },
 
   /**
    * Updates the full status line.
    *
    * @param status
    *        Status line of the Downloads Panel or the Downloads View.
    * @param hoverStatus
--- a/browser/components/downloads/content/downloadsPanel.inc.xul
+++ b/browser/components/downloads/content/downloadsPanel.inc.xul
@@ -123,21 +123,19 @@
                 orient="horizontal"
                 onkeydown="DownloadsSummary.onKeyDown(event);"
                 onclick="DownloadsSummary.onClick(event);">
             <image class="downloadTypeIcon" />
             <vbox pack="center"
                   flex="1"
                   class="downloadContainer">
               <description id="downloadsSummaryDescription"/>
-              <progressmeter id="downloadsSummaryProgress"
+              <html:progress id="downloadsSummaryProgress"
                              class="downloadProgress"
-                             min="0"
-                             max="100"
-                             mode="normal" />
+                             max="100"/>
               <description id="downloadsSummaryDetails"
                            crop="end"/>
             </vbox>
           </hbox>
           <hbox id="downloadsFooterButtons">
             <button id="downloadsHistory"
                     class="downloadsPanelFooterButton"
                     label="&downloadsHistory.label;"
--- a/browser/components/pocket/content/Pocket.jsm
+++ b/browser/components/pocket/content/Pocket.jsm
@@ -29,23 +29,18 @@ var Pocket = {
    * Functions related to the Pocket panel UI.
    */
   onShownInPhotonPageActionPanel(panel, iframe) {
     let window = panel.ownerGlobal;
     window.pktUI.setPhotonPageActionPanelFrame(iframe);
     Pocket._initPanelView(window);
   },
 
-  onPanelViewShowing(event) {
-    Pocket._initPanelView(event.target.ownerGlobal);
-  },
-
   _initPanelView(window) {
     let document = window.document;
-    let iframe = window.pktUI.getPanelFrame();
 
     let libraryButton = document.getElementById("library-button");
     if (libraryButton) {
       BrowserUtils.setToolbarButtonHeightProperty(libraryButton);
     }
 
     let urlToSave = Pocket._urlToSave;
     let titleToSave = Pocket._titleToSave;
@@ -54,46 +49,19 @@ var Pocket = {
     // ViewShowing fires immediately before it creates the contents,
     // in lieu of an AfterViewShowing event, just spin the event loop.
     window.setTimeout(function() {
       if (urlToSave) {
         window.pktUI.tryToSaveUrl(urlToSave, titleToSave);
       } else {
         window.pktUI.tryToSaveCurrentPage();
       }
-
-      // pocketPanelDidHide in main.js set iframe to about:blank when it was
-      // hidden, make sure we're loading the save panel.
-      if (iframe.contentDocument &&
-          iframe.contentDocument.readyState == "complete" &&
-          iframe.contentDocument.documentURI != "about:blank") {
-        window.pktUI.pocketPanelDidShow();
-      } else {
-        // iframe didn't load yet. This seems to always be the case when in
-        // the toolbar panel, but never the case for a subview.
-        // XXX this only being fired when it's a _capturing_ listener!
-        iframe.addEventListener("load", Pocket.onFrameLoaded, true);
-      }
     }, 0);
   },
 
-  onFrameLoaded(event) {
-    let document = event.currentTarget.ownerDocument;
-    let window = document.defaultView;
-    let iframe = window.pktUI.getPanelFrame();
-
-    iframe.removeEventListener("load", Pocket.onFrameLoaded, true);
-    window.pktUI.pocketPanelDidShow();
-  },
-
-  onPanelViewHiding(event) {
-    let window = event.target.ownerGlobal;
-    window.pktUI.pocketPanelDidHide(event);
-  },
-
   _urlToSave: null,
   _titleToSave: null,
   savePage(browser, url, title) {
     if (this.pageAction) {
       this._urlToSave = url;
       this._titleToSave = title;
       this.pageAction.doCommand(browser.ownerGlobal);
     }
--- a/browser/components/pocket/content/main.js
+++ b/browser/components/pocket/content/main.js
@@ -50,90 +50,37 @@ ChromeUtils.defineModuleGetter(this, "Pr
 ChromeUtils.defineModuleGetter(this, "ReaderMode",
   "resource://gre/modules/ReaderMode.jsm");
 ChromeUtils.defineModuleGetter(this, "pktApi",
   "chrome://pocket/content/pktApi.jsm");
 
 var pktUI = (function() {
 
     // -- Initialization (on startup and new windows) -- //
-    var _currentPanelDidShow;
-    var _currentPanelDidHide;
 
     // Init panel id at 0. The first actual panel id will have the number 1 so
     // in case at some point any panel has the id 0 we know there is something
     // wrong
     var _panelId = 0;
 
     var overflowMenuWidth = 230;
     var overflowMenuHeight = 475;
     var savePanelWidth = 350;
     var savePanelHeights = {collapsed: 153, expanded: 272};
 
-    var _lastAddSucceeded = false;
-
-    // -- Event Handling -- //
-
-    /**
-     * Event handler when Pocket toolbar button is pressed
-     */
-
-    function pocketPanelDidShow(event) {
-        if (_currentPanelDidShow) {
-            _currentPanelDidShow(event);
-        }
-
-    }
-
-    function pocketPanelDidHide(event) {
-        if (_currentPanelDidHide) {
-            _currentPanelDidHide(event);
-        }
-
-        // clear the panel
-        getPanelFrame().setAttribute("src", "about:blank");
-
-        if (_lastAddSucceeded) {
-            var libraryButton = document.getElementById("library-button");
-            if (!Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled") ||
-                !libraryButton ||
-                libraryButton.getAttribute("cui-areatype") == "menu-panel" ||
-                libraryButton.getAttribute("overflowedItem") == "true" ||
-                !libraryButton.closest("#nav-bar")) {
-                return;
-            }
-            libraryButton.removeAttribute("fade");
-            libraryButton.setAttribute("animate", "pocket");
-            libraryButton.addEventListener("animationend", onLibraryButtonAnimationEnd);
-        }
-    }
-
-    function onLibraryButtonAnimationEnd(event) {
-        let doc = event.target.ownerDocument;
-        let libraryButton = doc.getElementById("library-button");
-        if (event.animationName.startsWith("library-pocket-animation")) {
-            libraryButton.setAttribute("fade", "true");
-        } else if (event.animationName == "library-pocket-fade") {
-            libraryButton.removeEventListener("animationend", onLibraryButtonAnimationEnd);
-            libraryButton.removeAttribute("animate");
-            libraryButton.removeAttribute("fade");
-        }
-    }
-
     // -- Communication to API -- //
 
     /**
      * Either save or attempt to log the user in
      */
     function tryToSaveCurrentPage() {
         tryToSaveUrl(getCurrentUrl(), getCurrentTitle());
     }
 
     function tryToSaveUrl(url, title) {
-
         // If the user is logged in, go ahead and save the current page
         if (pktApi.isUserLoggedIn()) {
             saveAndShowConfirmation(url, title);
             return;
         }
 
         // If the user is not logged in, show the logged-out state to prompt them to authenticate
         showSignUp();
@@ -190,19 +137,16 @@ var pktUI = (function() {
                 + "&variant="
                 + variant
                 + "&controlvariant="
                 + controlvariant
                 + "&inoverflowmenu="
                 + inOverflowMenu
                 + "&locale="
                 + getUILocale(), {
-                    onShow() {
-                    },
-                    onHide: panelDidHide,
                     width: inOverflowMenu ? overflowMenuWidth : 300,
                     height: startheight,
             });
         });
     }
 
     /**
      * Show the logged-out state / sign-up panel
@@ -226,17 +170,16 @@ var pktUI = (function() {
             var panelId = showPanel("about:pocket-saved?pockethost="
                 + Services.prefs.getCharPref("extensions.pocket.site")
                 + "&premiumStatus=" + (pktApi.isPremiumUser() ? "1" : "0")
                 + "&fxasignedin=" + ((typeof userdata == "object" && userdata !== null) ? "1" : "0")
                 + "&inoverflowmenu=" + inOverflowMenu
                 + "&locale=" + getUILocale(), {
                 onShow() {
                     var saveLinkMessageId = "saveLink";
-                    _lastAddSucceeded = false;
                     getPanelFrame().setAttribute("itemAdded", "false");
 
                     // Send error message for invalid url
                     if (!isValidURL) {
                         // TODO: Pass key for localized error in error object
                         let error = {
                             message: "Only links can be saved",
                             localizedKey: "onlylinkssaved",
@@ -265,17 +208,16 @@ var pktUI = (function() {
                             var successResponse = {
                                 status: "success",
                                 accountState,
                                 displayName,
                                 item,
                                 ho2,
                             };
                             pktUIMessaging.sendMessageToPanel(panelId, saveLinkMessageId, successResponse);
-                            _lastAddSucceeded = true;
                             getPanelFrame().setAttribute("itemAdded", "true");
                         },
                         error(error, request) {
                             // If user is not authorized show singup page
                             if (request.status === 401) {
                                 showSignUp();
                                 return;
                             }
@@ -293,52 +235,52 @@ var pktUI = (function() {
                     // Add title if given
                     if (typeof title !== "undefined") {
                         options.title = title;
                     }
 
                     // Send the link
                     pktApi.addLink(url, options);
                 },
-                onHide: panelDidHide,
                 width: inOverflowMenu ? overflowMenuWidth : savePanelWidth,
                 height: startheight,
             });
         });
     }
 
     /**
      * Open a generic panel
      */
     function showPanel(url, options) {
-
         // Add new panel id
         _panelId += 1;
         url += ("&panelId=" + _panelId);
 
         // We don't have to hide and show the panel again if it's already shown
         // as if the user tries to click again on the toolbar button the overlay
         // will close instead of the button will be clicked
         var iframe = getPanelFrame();
+        options.onShow = options.onShow || (() => {});
 
         // Register event handlers
         registerEventMessages();
 
         // Load the iframe
         iframe.setAttribute("src", url);
 
-        // Uncomment to leave panel open -- for debugging
-        // panel.setAttribute('noautohide', true);
-        // panel.setAttribute('consumeoutsideclicks', false);
-        //
-
-        // For some reason setting onpopupshown and onpopuphidden on the panel directly didn't work, so
-        // do it this hacky way for now
-        _currentPanelDidShow = options.onShow;
-        _currentPanelDidHide = options.onHide;
+        if (iframe.contentDocument &&
+            iframe.contentDocument.readyState == "complete" &&
+            iframe.contentDocument.documentURI != "about:blank") {
+          options.onShow();
+        } else {
+          // iframe didn't load yet. This seems to always be the case when in
+          // the toolbar panel, but never the case for a subview.
+          // XXX this only being fired when it's a _capturing_ listener!
+          iframe.addEventListener("load", options.onShow, { once: true, capture: true });
+        }
 
         resizePanel({
             width: options.width,
             height: options.height,
         });
         return _panelId;
     }
 
@@ -354,25 +296,16 @@ var pktUI = (function() {
         var iframe = getPanelFrame();
 
         // Set an explicit size, panel will adapt.
         iframe.style.width  = options.width + "px";
         iframe.style.height = options.height + "px";
     }
 
     /**
-     * Called when the signup and saved panel was hidden
-     */
-    function panelDidHide() {
-        // clear the onShow and onHide values
-        _currentPanelDidShow = null;
-        _currentPanelDidHide = null;
-    }
-
-    /**
      * Register all of the messages needed for the panels
      */
     function registerEventMessages() {
         var iframe = getPanelFrame();
 
         // Only register the messages once
         var didInitAttributeKey = "did_init";
         var didInitMessageListener = iframe.getAttribute(didInitAttributeKey);
@@ -512,17 +445,16 @@ var pktUI = (function() {
 
         // Based on clicking "remove page" CTA, and passed unique item id, remove the item
         var _deleteItemMessageId = "deleteItem";
         pktUIMessaging.addMessageListener(iframe, _deleteItemMessageId, function(panelId, data) {
             pktApi.deleteItem(data.itemId, {
                 success(data, response) {
                     var successResponse = {status: "success"};
                     pktUIMessaging.sendResponseMessageToPanel(panelId, _deleteItemMessageId, successResponse);
-                    _lastAddSucceeded = false;
                     getPanelFrame().setAttribute("itemAdded", "false");
                 },
                 error(error, response) {
                     pktUIMessaging.sendErrorResponseMessageToPanel(panelId, _deleteItemMessageId, error);
                 },
             });
         });
 
@@ -631,19 +563,16 @@ var pktUI = (function() {
      * Public functions
      */
     return {
         setPhotonPageActionPanelFrame,
         getPanelFrame,
 
         openTabWithUrl,
 
-        pocketPanelDidShow,
-        pocketPanelDidHide,
-
         tryToSaveUrl,
         tryToSaveCurrentPage,
     };
 }());
 
 // -- Communication to Background -- //
 // https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Interaction_between_privileged_and_non-privileged_pages
 var pktUIMessaging = (function() {
--- a/browser/components/preferences/browserLanguages.js
+++ b/browser/components/preferences/browserLanguages.js
@@ -5,16 +5,34 @@
 /* import-globals-from ../../../toolkit/content/preferencesBindings.js */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "AddonManager",
                                "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "AddonRepository",
                                "resource://gre/modules/addons/AddonRepository.jsm");
+ChromeUtils.defineModuleGetter(this, "RemoteSettings",
+                               "resource://services-settings/remote-settings.js");
+
+async function installFromUrl(url, hash) {
+  let install = await AddonManager.getInstallForURL(
+    url, "application/x-xpinstall", hash);
+  return install.install();
+}
+
+async function dictionaryIdsForLocale(locale) {
+  let entries = await RemoteSettings("language-dictionaries").get({
+    filters: {id: locale},
+  });
+  if (entries.length > 0) {
+    return entries[0].dictionaries;
+  }
+  return [];
+}
 
 class OrderedListBox {
   constructor({richlistbox, upButton, downButton, removeButton, onRemove}) {
     this.richlistbox = richlistbox;
     this.upButton = upButton;
     this.downButton = downButton;
     this.removeButton = removeButton;
     this.onRemove = onRemove;
@@ -389,47 +407,71 @@ var gBrowserLanguagesDialog = {
     items.push({
       label: await document.l10n.formatValue("browser-languages-search"),
       value: "search",
     });
     this._availableLocales.setItems(items);
   },
 
   async availableLanguageSelected(item) {
-    let available = new Set(Services.locale.availableLocales);
-
-    if (available.has(item.value)) {
-      this._requestedLocales.addItem(item);
-      if (available.size == this._requestedLocales.items.length) {
-        // Remove the installed label, they're all installed.
-        this._availableLocales.items.shift();
-        this._availableLocales.setItems(this._availableLocales.items);
-      }
+    if (Services.locale.availableLocales.includes(item.value)) {
+      this.requestLocalLanguage(item);
     } else if (this.availableLangpacks.has(item.value)) {
-      this._availableLocales.disableWithMessageId("browser-languages-downloading");
-
-      let {url, hash} = this.availableLangpacks.get(item.value);
-      let install = await AddonManager.getInstallForURL(
-        url, "application/x-xpinstall", hash);
-
-      try {
-        await install.install();
-      } catch (e) {
-        this.showError();
-        return;
-      }
-
-      item.installed = true;
-      this._requestedLocales.addItem(item);
-      this._availableLocales.enableWithMessageId("browser-languages-select-language");
+      await this.requestRemoteLanguage(item);
     } else {
       this.showError();
     }
   },
 
+  requestLocalLanguage(item, available) {
+    this._requestedLocales.addItem(item);
+    let requestedCount = this._requestedLocales.items.length;
+    let availableCount = Services.locale.availableLocales.length;
+    if (requestedCount == availableCount) {
+      // Remove the installed label, they're all installed.
+      this._availableLocales.items.shift();
+      this._availableLocales.setItems(this._availableLocales.items);
+    }
+  },
+
+  async requestRemoteLanguage(item) {
+    this._availableLocales.disableWithMessageId(
+      "browser-languages-downloading");
+
+    let {url, hash} = this.availableLangpacks.get(item.value);
+
+    try {
+      await installFromUrl(url, hash);
+    } catch (e) {
+      this.showError();
+      return;
+    }
+
+    item.installed = true;
+    this._requestedLocales.addItem(item);
+    this._availableLocales.enableWithMessageId(
+      "browser-languages-select-language");
+
+    // This is an async task that will install the recommended dictionaries for
+    // this locale. This will fail silently at least until a management UI is
+    // added in bug 1493705.
+    this.installDictionariesForLanguage(item.value);
+  },
+
+  async installDictionariesForLanguage(locale) {
+    try {
+      let ids = await dictionaryIdsForLocale(locale);
+      let addonInfos = await AddonRepository.getAddonsByIDs(ids);
+      await Promise.all(addonInfos.map(
+        info => installFromUrl(info.sourceURI.spec)));
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+
   showError() {
     document.querySelectorAll(".warning-message-separator")
       .forEach(separator => separator.classList.add("thin"));
     document.getElementById("warning-message").hidden = false;
     this._availableLocales.enableWithMessageId("browser-languages-select-language");
   },
 
   hideError() {
--- a/browser/components/preferences/in-content/extensionControlled.js
+++ b/browser/components/preferences/in-content/extensionControlled.js
@@ -39,16 +39,17 @@ const API_PROXY_PREFS = [
   "network.proxy.autoconfig_url",
   "signon.autologin.proxy",
 ];
 
 let extensionControlledContentIds = {
   "privacy.containers": "browserContainersExtensionContent",
   "homepage_override": "browserHomePageExtensionContent",
   "newTabURL": "browserNewTabExtensionContent",
+  "webNotificationsDisabled": "browserNotificationsPermissionExtensionContent",
   "defaultSearch": "browserDefaultSearchExtensionContent",
   "proxy.settings": "proxyExtensionContent",
   get "websites.trackingProtectionMode"() {
     return {
       button: contentBlockingUiEnabled ?
         "contentBlockingDisableTrackingProtectionExtension" :
         "trackingProtectionExtensionContentButton",
       section: contentBlockingUiEnabled ?
@@ -56,16 +57,17 @@ let extensionControlledContentIds = {
         "trackingProtectionExtensionContentLabel",
     };
   },
 };
 
 const extensionControlledL10nKeys = {
   "homepage_override": "homepage-override",
   "newTabURL": "new-tab-url",
+  "webNotificationsDisabled": "web-notifications",
   "defaultSearch": "default-search",
   "privacy.containers": "privacy-containers",
   "websites.trackingProtectionMode": contentBlockingUiEnabled ?
                                        "websites-content-blocking-all-trackers" :
                                        "websites-tracking-protection-mode",
   "proxy.settings": "proxy-config",
 };
 
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3113d1fa5da0482256cef2fd3edf78ad2f6e7bc2
GIT binary patch
literal 963
zc$^FHW@Zs#U|`^25U%v`)pC+`E?{I}U}t7v;AW6v$jmD)NzBR7D@sWV4dG;9cKd%I
zx)g*fxEUB(z5-Q(HRn$9FK#jrIQqVhtLyoM56iW9j+Gtzc<1H9@1jj<T%40zR=xeQ
zTYRxg&gstD_umh_YpW^E$$H<cd3I^UuSADGqH~|Df8Bds+DZTKUc=nBj}Hqt_zD%S
z^0jBqN)mdg;k0Va1UpV<ca<+!=2tvF&Sre=PO9|Jldq-TFbXPirk%C4{hBF%>bj43
z*Y;)khnm;EIdkd#loRU~_HgGeSQp6L6z(G}{VI9Q$qLWgzg9juYRTPYEb9>IweWcS
znUAd{D_<YjGT*$z!$LW5%Z3XH7Mp~1Mc!Xo(HHq3%KHACD7W;^uWxo2Zwr#j71^@=
zgKyE4hPI31k;QwrcdVTot-<D|wQJ&~UCwVCK5g6aQ8T%+s8L?vlUdA0FFD&5H|B<0
zhM(_jGu1z^<K}z))C+gc?T^@~V7T(xiVltiDbqqU?h3UXl1TE1wCWc>`Tg#__L9R#
zZ<n6#XWV$gW0(6wix(%867p|d(D--x<x{JFv)_o%dHgSY`DFP3V5D&|fPgYE(pqfH
zLL-4}5EceTT1sYeNoIatVo_#lv3^00USb*~@{)iNmx392!LA_+p27YW7N(ZEMqD95
zkqU{K`KeWTMditr#RVnVxhb20v9&QhDdW|)XP@_{ecV%4SmEgD?;7P76z&`u91!B|
z>#{i1$7`XxlgFuL$1bmTySOGSFoFx=N)4bZ*%!XL%LHUI;cz984|ZiX(1qEUu1o|P
z$H*kdj4Q25fGq-Y8J0AHSSTr%6`FEUjf5D8D;$vx+zO<yhAJy0RPh*$>2+j-qnR)b
Z1_uNyBp@&hW@Q6uW(Gl^;3|-b3;>KuQZoPm
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 prefs =
   extensions.formautofill.available='on'
   extensions.formautofill.creditCards.available=true
 support-files =
   head.js
   privacypane_tests_perwindow.js
+  addons/pl-dictionary.xpi
   addons/set_homepage.xpi
   addons/set_newtab.xpi
 
 [browser_applications_selection.js]
 [browser_advanced_update.js]
 skip-if = !updater
 [browser_basic_rebuild_fonts_test.js]
 [browser_bug410900.js]
--- a/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
+++ b/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
@@ -1,17 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+
 AddonTestUtils.initMochitest(this);
 
 const BROWSER_LANGUAGES_URL = "chrome://browser/content/preferences/browserLanguages.xul";
+const DICTIONARY_ID_PL = "pl@dictionaries.addons.mozilla.org";
 
 function getManifestData(locale) {
   return {
     langpack_id: locale,
     name: `${locale} Language Pack`,
     description: `${locale} Language pack`,
     languages: {
       [locale]: {
@@ -74,16 +76,55 @@ async function createLanguageToolsFile()
   let files = {[filename]: {results}};
   let tempdir = AddonTestUtils.tempDir.clone();
   let dir = await AddonTestUtils.promiseWriteFilesToDir(tempdir.path, files);
   dir.append(filename);
 
   return dir;
 }
 
+async function createDictionaryBrowseResults() {
+  let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+  let dictionaryPath = testDir + "/addons/pl-dictionary.xpi";
+  let filename = "dictionaries.json";
+  let response = {
+    page_size: 25,
+    page_count: 1,
+    count: 1,
+    results: [{
+      current_version: {
+        id: 1823648,
+        compatibility: {
+          firefox: {max: "9999", min: "4.0"},
+        },
+        files: [{
+          platform: "all",
+          url: dictionaryPath,
+        }],
+        version: "1.0.20160228",
+      },
+      default_locale: "pl",
+      description: "Polish spell-check",
+      guid: DICTIONARY_ID_PL,
+      name: "Polish Dictionary",
+      slug: "polish-spellchecker-dictionary",
+      status: "public",
+      summary: "Polish dictionary",
+      type: "dictionary",
+    }],
+  };
+
+  let files = {[filename]: response};
+  let dir = await AddonTestUtils.promiseWriteFilesToDir(
+    AddonTestUtils.tempDir.path, files);
+  dir.append(filename);
+
+  return dir;
+}
+
 function assertLocaleOrder(list, locales) {
   is(list.itemCount, locales.split(",").length,
      "The right number of locales are requested");
   is(Array.from(list.children).map(child => child.value).join(","),
      locales, "The requested locales are in order");
 }
 
 function assertAvailableLocales(list, locales) {
@@ -240,22 +281,26 @@ add_task(async function testAddAndRemove
 });
 
 add_task(async function testInstallFromAMO() {
   let langpacks = await AddonManager.getAddonsByTypes(["locale"]);
   is(langpacks.length, 0, "There are no langpacks installed");
 
   let langpacksFile = await createLanguageToolsFile();
   let langpacksUrl = Services.io.newFileURI(langpacksFile).spec;
+  let dictionaryBrowseFile = await createDictionaryBrowseResults();
+  let browseApiEndpoint = Services.io.newFileURI(dictionaryBrowseFile).spec;
+
   await SpecialPowers.pushPrefEnv({
     set: [
       ["intl.multilingual.enabled", true],
       ["intl.locale.requested", "en-US"],
       ["extensions.getAddons.langpacks.url", langpacksUrl],
       ["extensions.langpacks.signatures.required", false],
+      ["extensions.getAddons.get.url", browseApiEndpoint],
     ],
   });
 
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
 
   let doc = gBrowser.contentDocument;
   let messageBar = doc.getElementById("confirmBrowserLanguage");
   is(messageBar.hidden, true, "The message bar is hidden at first");
@@ -272,31 +317,46 @@ add_task(async function testInstallFromA
   }
 
   // The initial order is set by the pref.
   assertLocaleOrder(requested, "en-US");
   assertAvailableLocales(available, ["fr", "he", "pl"]);
   is(Services.locale.availableLocales.join(","),
      "en-US", "There is only one installed locale");
 
+  // Verify that there are no extra dictionaries.
+  let dicts = await AddonManager.getAddonsByTypes(["dictionary"]);
+  is(dicts.length, 0, "There are no installed dictionaries");
+
   // Add Polish, this will install the langpack.
   requestLocale("pl", available, dialogDoc);
 
   // Wait for the langpack to install and be added to the list.
   let requestedLocales = dialogDoc.getElementById("requestedLocales");
   await waitForMutation(
     requestedLocales,
     {childList: true},
     target => requestedLocales.itemCount == 2);
 
   // Verify the list is correct.
   assertLocaleOrder(requested, "pl,en-US");
   assertAvailableLocales(available, ["fr", "he"]);
   is(Services.locale.availableLocales.sort().join(","),
      "en-US,pl", "Polish is now installed");
 
-  // Uninstall the langpack.
-  langpacks = await AddonManager.getAddonsByTypes(["locale"]);
-  is(langpacks.length, 1, "There is one langpacks installed");
-  await Promise.all(langpacks.map(pack => pack.uninstall()));
+  await BrowserTestUtils.waitForCondition(async () => {
+    let newDicts = await AddonManager.getAddonsByTypes(["dictionary"]);
+    let done = newDicts.length != 0;
+
+    if (done) {
+      is(newDicts[0].id, DICTIONARY_ID_PL, "The polish dictionary was installed");
+    }
+
+    return done;
+  });
+
+  // Uninstall the langpack and dictionary.
+  let installs = await AddonManager.getAddonsByTypes(["locale", "dictionary"]);
+  is(installs.length, 2, "There is one langpack and one dictionary installed");
+  await Promise.all(installs.map(item => item.uninstall()));
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -6,16 +6,18 @@ ChromeUtils.defineModuleGetter(this, "Ex
                                "resource://gre/modules/ExtensionSettingsStore.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 XPCOMUtils.defineLazyPreferenceGetter(this, "proxyType", PROXY_PREF);
 
 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
 const CHROME_URL_ROOT = TEST_DIR + "/";
+const PERMISSIONS_URL = "chrome://browser/content/preferences/sitePermissions.xul";
+let sitePermissionsDialog;
 
 function getSupportsFile(path) {
   let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
     .getService(Ci.nsIChromeRegistry);
   let uri = Services.io.newURI(CHROME_URL_ROOT + path);
   let fileurl = cr.convertChromeURL(uri);
   return fileurl.QueryInterface(Ci.nsIFileURL);
 }
@@ -63,16 +65,29 @@ function waitForEnableMessage(messageId,
 
 function waitForMessageContent(messageId, l10nId, doc) {
   return waitForMessageChange(
     getElement(messageId, doc),
     target => doc.l10n.getAttributes(target).id === l10nId,
     { childList: true });
 }
 
+async function openNotificationsPermissionDialog() {
+  let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    let doc = content.document;
+    let settingsButton = doc.getElementById("notificationSettingsButton");
+    settingsButton.click();
+  });
+
+  sitePermissionsDialog = await dialogOpened;
+  await sitePermissionsDialog.document.mozSubdialogReady;
+}
+
 add_task(async function testExtensionControlledHomepage() {
   await openPreferencesViaOpenPreferencesAPI("paneHome", {leaveOpen: true});
   // eslint-disable-next-line mozilla/no-cpows-in-tests
   let doc = gBrowser.contentDocument;
   is(gBrowser.currentURI.spec, "about:preferences#home",
      "#home should be in the URI for about:preferences");
   let homepagePref = () => Services.prefs.getCharPref("browser.startup.homepage");
   let originalHomepagePref = homepagePref();
@@ -334,16 +349,85 @@ add_task(async function testExtensionCon
   is(controlledContent.hidden, true, "The extension controlled row is shown");
 
   // Cleanup the tab and add-on.
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
   let addon = await AddonManager.getAddonByID("@set_newtab");
   await addon.uninstall();
 });
 
+add_task(async function testExtensionControlledWebNotificationsPermission() {
+  let manifest = {
+    manifest_version: 2,
+    name: "TestExtension",
+    version: "1.0",
+    description: "Testing WebNotificationsDisable",
+    applications: {gecko: {id: "@web_notifications_disable"}},
+    permissions: [
+      "browserSettings",
+    ],
+    browser_action: {
+      default_title: "Testing",
+    },
+  };
+
+  await openPreferencesViaOpenPreferencesAPI("privacy", {leaveOpen: true});
+  await openNotificationsPermissionDialog();
+
+  let doc = sitePermissionsDialog.document;
+  let extensionControlledContent = doc.getElementById("browserNotificationsPermissionExtensionContent");
+
+  // Test that extension content is initially hidden.
+  ok(extensionControlledContent.hidden, "Extension content is initially hidden");
+
+  // Install an extension that will disable web notifications permission.
+  let messageShown = waitForMessageShown("browserNotificationsPermissionExtensionContent", doc);
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest,
+    useAddonManager: "permanent",
+    background() {
+      browser.browserSettings.webNotificationsDisabled.set({value: true});
+      browser.test.sendMessage("load-extension");
+    },
+  });
+  await extension.startup();
+  await extension.awaitMessage("load-extension");
+  await messageShown;
+
+  let controlledDesc = extensionControlledContent.querySelector("description");
+  Assert.deepEqual(doc.l10n.getAttributes(controlledDesc), {
+    id: "extension-controlled-web-notifications",
+    args: {
+      name: "TestExtension",
+    },
+  }, "The user is notified that an extension is controlling the web notifications permission");
+  is(extensionControlledContent.hidden, false, "The extension controlled row is not hidden");
+
+  // Disable the extension.
+  doc.getElementById("disableNotificationsPermissionExtension").click();
+
+  // Verify the user is notified how to enable the extension.
+  await waitForEnableMessage(extensionControlledContent.id, doc);
+  is(doc.l10n.getAttributes(controlledDesc.querySelector("label")).id,
+    "extension-controlled-enable",
+    "The user is notified of how to enable the extension again");
+
+  // Verify the enable message can be dismissed.
+  let hidden = waitForMessageHidden(extensionControlledContent.id, doc);
+  let dismissButton = controlledDesc.querySelector("image:last-of-type");
+  dismissButton.click();
+  await hidden;
+
+  // Verify that the extension controlled content in hidden again.
+  is(extensionControlledContent.hidden, true, "The extension controlled row is now hidden");
+
+  await extension.unload();
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
 add_task(async function testExtensionControlledDefaultSearch() {
   await openPreferencesViaOpenPreferencesAPI("paneSearch", {leaveOpen: true});
   let doc = gBrowser.contentDocument;
   let extensionId = "@set_default_search";
   let manifest = {
     manifest_version: 2,
     name: "set_default_search",
     applications: {gecko: {id: extensionId}},
--- a/browser/components/preferences/sitePermissions.css
+++ b/browser/components/preferences/sitePermissions.css
@@ -18,18 +18,22 @@
   min-height: 35px;
 }
 
 .website-status {
   margin: 1px;
   margin-inline-end: 5px;
 }
 
+#browserNotificationsPermissionExtensionContent,
 #permissionsDisableDescription {
   margin-inline-start: 32px;
+}
+
+#permissionsDisableDescription {
   color: #737373;
   line-height: 110%;
 }
 
 #permissionsDisableCheckbox {
   margin-inline-start: 4px;
   padding-top: 10px;
 }
--- a/browser/components/preferences/sitePermissions.js
+++ b/browser/components/preferences/sitePermissions.js
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+/* import-globals-from in-content/extensionControlled.js */
+
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource:///modules/SitePermissions.jsm");
 
 const sitePermissionsL10n = {
   "desktop-notification": {
     window: "permissions-site-notification-window",
     description: "permissions-site-notification-desc",
@@ -37,16 +39,18 @@ function Permission(principal, type, cap
   this.principal = principal;
   this.origin = principal.origin;
   this.type = type;
   this.capability = capability;
   this.l10nId = l10nId;
 }
 
 const PERMISSION_STATES = [SitePermissions.ALLOW, SitePermissions.BLOCK, SitePermissions.PROMPT];
+const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled";
+const NOTIFICATIONS_PERMISSION_PREF = "permissions.default.desktop-notification";
 
 var gSitePermissionsManager = {
   _type: "",
   _isObserving: false,
   _permissions: new Map(),
   _permissionsToChange: new Map(),
   _permissionsToDelete: new Map(),
   _list: null,
@@ -69,52 +73,38 @@ var gSitePermissionsManager = {
     }
 
     this._type = params.permissionType;
     this._list = document.getElementById("permissionsBox");
     this._removeButton = document.getElementById("removePermission");
     this._removeAllButton = document.getElementById("removeAllPermissions");
     this._searchBox = document.getElementById("searchBox");
     this._checkbox = document.getElementById("permissionsDisableCheckbox");
+    this._disableExtensionButton = document.getElementById("disableNotificationsPermissionExtension");
+    this._permissionsDisableDescription = document.getElementById("permissionsDisableDescription");
 
-    let permissionsDisableDescription = document.getElementById("permissionsDisableDescription");
     let permissionsText = document.getElementById("permissionsText");
 
     let l10n = sitePermissionsL10n[this._type];
     document.l10n.setAttributes(permissionsText, l10n.description);
     document.l10n.setAttributes(this._checkbox, l10n.disableLabel);
-    document.l10n.setAttributes(permissionsDisableDescription, l10n.disableDescription);
+    document.l10n.setAttributes(this._permissionsDisableDescription, l10n.disableDescription);
     document.l10n.setAttributes(document.documentElement, l10n.window);
 
     await document.l10n.translateElements([
       permissionsText,
       this._checkbox,
-      permissionsDisableDescription,
+      this._permissionsDisableDescription,
       document.documentElement,
     ]);
 
-    // Initialize the checkbox state.
+    // Initialize the checkbox state and handle showing notification permission UI
+    // when it is disabled by an extension.
     this._defaultPermissionStatePrefName = "permissions.default." + this._type;
-    let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName);
-    if (pref != Services.prefs.PREF_INVALID) {
-      this._currentDefaultPermissionsState = Services.prefs.getIntPref(this._defaultPermissionStatePrefName);
-    }
-
-    if (this._currentDefaultPermissionsState === null) {
-      this._checkbox.setAttribute("hidden", true);
-      permissionsDisableDescription.setAttribute("hidden", true);
-    } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
-      this._checkbox.checked = true;
-    } else {
-      this._checkbox.checked = false;
-    }
-
-    if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) {
-      this._checkbox.disabled = true;
-    }
+    this._watchPermissionPrefChange();
 
     this._loadPermissions();
     this.buildPermissionsList();
 
     this._searchBox.focus();
   },
 
   uninit() {
@@ -150,16 +140,78 @@ var gSitePermissionsManager = {
 
   _handleCapabilityChange(perm) {
     let permissionlistitem = document.getElementsByAttribute("origin", perm.origin)[0];
     let menulist = permissionlistitem.getElementsByTagName("menulist")[0];
     menulist.selectedItem =
       menulist.getElementsByAttribute("value", perm.capability)[0];
   },
 
+  _handleCheckboxUIUpdates() {
+    let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName);
+    if (pref != Services.prefs.PREF_INVALID) {
+      this._currentDefaultPermissionsState = Services.prefs.getIntPref(this._defaultPermissionStatePrefName);
+    }
+
+    if (this._currentDefaultPermissionsState === null) {
+      this._checkbox.setAttribute("hidden", true);
+      this._permissionsDisableDescription.setAttribute("hidden", true);
+    } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
+      this._checkbox.checked = true;
+    } else {
+      this._checkbox.checked = false;
+    }
+
+    if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) {
+      this._checkbox.disabled = true;
+    }
+  },
+
+  /**
+  * Listen for changes to the permissions.default.* pref and make
+  * necessary changes to the UI.
+  */
+  _watchPermissionPrefChange() {
+    this._handleCheckboxUIUpdates();
+
+    if (this._type == "desktop-notification") {
+      this._handleWebNotificationsDisable();
+
+      this._disableExtensionButton.addEventListener(
+        "command",
+        makeDisableControllingExtension(PREF_SETTING_TYPE, NOTIFICATIONS_PERMISSION_OVERRIDE_KEY)
+      );
+    }
+
+    let observer = () => {
+      this._handleCheckboxUIUpdates();
+      if (this._type == "desktop-notification") {
+        this._handleWebNotificationsDisable();
+      }
+    };
+    Services.prefs.addObserver(this._defaultPermissionStatePrefName, observer);
+    window.addEventListener("unload", () => {
+      Services.prefs.removeObserver(this._defaultPermissionStatePrefName, observer);
+    });
+  },
+
+  /**
+  * Handles the UI update for web notifications disable by extensions.
+  */
+  async _handleWebNotificationsDisable() {
+    let prefLocked = Services.prefs.prefIsLocked(NOTIFICATIONS_PERMISSION_PREF);
+    if (prefLocked) {
+      // An extension can't control these settings if they're locked.
+      hideControllingExtension(NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
+    } else {
+      let isControlled = await handleControllingExtension(PREF_SETTING_TYPE, NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
+      this._checkbox.disabled = isControlled;
+    }
+  },
+
   _getCapabilityString(capability) {
     let stringKey = null;
     switch (capability) {
     case Services.perms.ALLOW_ACTION:
       stringKey = "permissions-capabilities-allow";
       break;
     case Services.perms.DENY_ACTION:
       stringKey = "permissions-capabilities-block";
--- a/browser/components/preferences/sitePermissions.xul
+++ b/browser/components/preferences/sitePermissions.xul
@@ -14,20 +14,22 @@
         data-l10n-id="permissions-window"
         data-l10n-attrs="title, style"
         onload="gSitePermissionsManager.onLoad();"
         onunload="gSitePermissionsManager.uninit();"
         persist="screenX screenY width height"
         onkeypress="gSitePermissionsManager.onWindowKeyPress(event);">
 
   <linkset>
+    <link rel="localization" href="browser/preferences/preferences.ftl"/>
     <link rel="localization" href="browser/preferences/permissions.ftl"/>
   </linkset>
 
   <script src="chrome://browser/content/preferences/sitePermissions.js"/>
+  <script type="application/javascript" src="chrome://browser/content/preferences/in-content/extensionControlled.js"/>
 
   <keyset>
     <key data-l10n-id="permissions-close-key" modifiers="accel" oncommand="window.close();"/>
   </keyset>
 
   <vbox class="contentPane largeDialogContainer" flex="1">
     <description id="permissionsText" control="url"/>
     <separator class="thin"/>
@@ -58,16 +60,23 @@
               data-l10n-id="permissions-remove-all"
               icon="clear"
               oncommand="gSitePermissionsManager.onAllPermissionsDelete();"/>
     </hbox>
     <spacer flex="1"/>
     <checkbox id="permissionsDisableCheckbox"/>
     <description id="permissionsDisableDescription"/>
     <spacer flex="1"/>
+    <hbox id="browserNotificationsPermissionExtensionContent" 
+          class="extension-controlled" align="center" hidden="true">
+      <description control="disableNotificationsPermissionExtension" flex="1"/>
+      <button id="disableNotificationsPermissionExtension"
+              class="extension-controlled-button accessory-button"
+              data-l10n-id="disable-extension"/>
+    </hbox>
     <hbox class="actionButtons" align="right" flex="1">
       <button oncommand="close();" icon="close" id="cancel"
               data-l10n-id="permissions-button-cancel" />
       <button id="btnApplyChanges" oncommand="gSitePermissionsManager.onApplyChanges();" icon="save"
               data-l10n-id="permissions-button-ok" />
     </hbox>
   </vbox>
 </window>
--- a/browser/config/mozconfigs/win32/mingwclang
+++ b/browser/config/mozconfigs/win32/mingwclang
@@ -49,17 +49,17 @@ HOST_CXX="$TOOLTOOL_DIR/clang/bin/clang+
 CC="$TOOLTOOL_DIR/clang/bin/i686-w64-mingw32-clang"
 CXX="$TOOLTOOL_DIR/clang/bin/i686-w64-mingw32-clang++"
 CXXFLAGS="-fms-extensions"
 CPP="$TOOLTOOL_DIR/clang/bin/i686-w64-mingw32-clang -E"
 AR=llvm-ar
 RANLIB=llvm-ranlib
 
 # For Stylo
-BINDGEN_CFLAGS="-I$TOOLTOOL_DIR/clang/i686-w64-mingw32/include/c++/v1 -I$TOOLTOOL_DIR/clang/lib/clang/7.0.0/include -I$TOOLTOOL_DIR/clang/i686-w64-mingw32/include"
+BINDGEN_CFLAGS="-I$TOOLTOOL_DIR/clang/i686-w64-mingw32/include/c++/v1 -I$TOOLTOOL_DIR/clang/lib/clang/8.0.0/include -I$TOOLTOOL_DIR/clang/i686-w64-mingw32/include"
 
 # Since we use windres from binutils without the rest of tools (like cpp), we need to
 # explicitly specify clang as a preprocessor.
 WINDRES="i686-w64-mingw32-windres --preprocessor=\"$CPP -xc\" -DRC_INVOKED"
 
 # Bug 1471698 - Work around binutils corrupting mingw clang binaries.
 LDFLAGS="-Wl,-S"
 STRIP=/bin/true
--- a/browser/config/mozconfigs/win64/mingwclang
+++ b/browser/config/mozconfigs/win64/mingwclang
@@ -49,17 +49,17 @@ HOST_CXX="$TOOLTOOL_DIR/clang/bin/clang+
 CC="$TOOLTOOL_DIR/clang/bin/x86_64-w64-mingw32-clang"
 CXX="$TOOLTOOL_DIR/clang/bin/x86_64-w64-mingw32-clang++"
 CXXFLAGS="-fms-extensions"
 CPP="$TOOLTOOL_DIR/clang/bin/x86_64-w64-mingw32-clang -E"
 AR=llvm-ar
 RANLIB=llvm-ranlib
 
 # For Stylo
-BINDGEN_CFLAGS="-I$TOOLTOOL_DIR/clang/x86_64-w64-mingw32/include/c++/v1 -I$TOOLTOOL_DIR/clang/lib/clang/7.0.0/include -I$TOOLTOOL_DIR/clang/x86_64-w64-mingw32/include"
+BINDGEN_CFLAGS="-I$TOOLTOOL_DIR/clang/x86_64-w64-mingw32/include/c++/v1 -I$TOOLTOOL_DIR/clang/lib/clang/8.0.0/include -I$TOOLTOOL_DIR/clang/x86_64-w64-mingw32/include"
 
 # Since we use windres from binutils without the rest of tools (like cpp), we need to
 # explicitly specify clang as a preprocessor.
 WINDRES="x86_64-w64-mingw32-windres --preprocessor=\"$CPP -xc\" -DRC_INVOKED"
 
 # Bug 1471698 - Work around binutils corrupting mingw clang binaries.
 LDFLAGS="-Wl,-S"
 STRIP=/bin/true
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutConfigPrefs.js
@@ -0,0 +1,46 @@
+/* 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";
+
+/* global ExtensionAPI, ExtensionCommon */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+this.aboutConfigPrefs = class extends ExtensionAPI {
+  getAPI(context) {
+    const EventManager = ExtensionCommon.EventManager;
+    const extensionIDBase = context.extension.id.split("@")[0];
+    const extensionPrefNameBase = `extensions.${extensionIDBase}.`;
+
+    return {
+      aboutConfigPrefs: {
+        onPrefChange: new EventManager({
+          context,
+          name: "aboutConfigPrefs.onUAOverridesPrefChange",
+          register: (fire, name) => {
+            const prefName = `${extensionPrefNameBase}${name}`;
+            const callback = () => {
+              fire.async(name).catch(() => {}); // ignore Message Manager disconnects
+            };
+            Services.prefs.addObserver(prefName, callback);
+            return () => {
+              Services.prefs.removeObserver(prefName, callback);
+            };
+          },
+        }).api(),
+        async getPref(name) {
+          try {
+            return Services.prefs.getBoolPref(`${extensionPrefNameBase}${name}`);
+          } catch (_) {
+            return undefined;
+          }
+        },
+        async setPref(name, value) {
+          Services.prefs.setBoolPref(`${extensionPrefNameBase}${name}`, value);
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutConfigPrefs.json
@@ -0,0 +1,53 @@
+[
+  {
+    "namespace": "aboutConfigPrefs",
+    "description": "experimental API extension to allow access to about:config preferences",
+    "events": [
+      {
+        "name": "onPrefChange",
+        "type": "function",
+        "parameters": [{
+          "name": "name",
+          "type": "string",
+          "description": "The preference which changed"
+        }],
+        "extraParameters": [{
+          "name": "name",
+          "type": "string",
+          "description": "The preference to monitor"
+        }]
+      }
+    ],
+    "functions": [
+      {
+        "name": "getPref",
+        "type": "function",
+        "description": "Get a preference's value",
+        "parameters": [{
+          "name": "name",
+          "type": "string",
+          "description": "The preference name"
+        }],
+        "async": true
+      },
+      {
+        "name": "setPref",
+        "type": "function",
+        "description": "Set a preference's value",
+        "parameters": [
+          {
+            "name": "name",
+            "type": "string",
+            "description": "The preference name"
+          },
+          {
+            "name": "value",
+            "type": "boolean",
+            "description": "The new value"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
deleted file mode 100644
--- a/browser/extensions/webcompat/bootstrap.js
+++ /dev/null
@@ -1,116 +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/. */
-
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-const PREF_BRANCH = "extensions.webcompat.";
-const PREF_DEFAULTS = {
-  perform_injections: true,
-  perform_ua_overrides: true,
-};
-
-const INJECTIONS_ENABLE_PREF_NAME = "extensions.webcompat.perform_injections";
-
-const BROWSER_STARTUP_FINISHED_TOPIC = "browser-delayed-startup-finished";
-
-const UA_OVERRIDES_INIT_TOPIC = "useragentoverrides-initialized";
-const UA_ENABLE_PREF_NAME = "extensions.webcompat.perform_ua_overrides";
-
-ChromeUtils.defineModuleGetter(this, "UAOverrider", "chrome://webcompat/content/lib/ua_overrider.jsm");
-ChromeUtils.defineModuleGetter(this, "UAOverrides", "chrome://webcompat/content/data/ua_overrides.jsm");
-
-let overrider;
-let webextensionPort;
-
-function InjectionsEnablePrefObserver() {
-  let isEnabled = Services.prefs.getBoolPref(INJECTIONS_ENABLE_PREF_NAME);
-  webextensionPort.postMessage({
-    type: "injection-pref-changed",
-    prefState: isEnabled,
-  });
-}
-
-function UAEnablePrefObserver() {
-  let isEnabled = Services.prefs.getBoolPref(UA_ENABLE_PREF_NAME);
-  overrider.setShouldOverride(isEnabled);
-}
-
-function setDefaultPrefs() {
-  const branch = Services.prefs.getDefaultBranch(PREF_BRANCH);
-  for (const [key, val] of Object.entries(PREF_DEFAULTS)) {
-    // If someone beat us to setting a default, don't overwrite it.
-    if (branch.getPrefType(key) !== branch.PREF_INVALID) {
-      continue;
-    }
-
-    switch (typeof val) {
-      case "boolean":
-        branch.setBoolPref(key, val);
-        break;
-      case "number":
-        branch.setIntPref(key, val);
-        break;
-      case "string":
-        branch.setCharPref(key, val);
-        break;
-    }
-  }
-}
-
-this.install = function() {};
-this.uninstall = function() {};
-
-this.startup = function({webExtension}) {
-  setDefaultPrefs();
-
-  // Intentionally reset the preference on every browser restart to avoid site
-  // breakage by accidentally toggled preferences or by leaving it off after
-  // debugging a site.
-  Services.prefs.clearUserPref(INJECTIONS_ENABLE_PREF_NAME);
-  Services.prefs.addObserver(INJECTIONS_ENABLE_PREF_NAME, InjectionsEnablePrefObserver);
-
-  Services.prefs.clearUserPref(UA_ENABLE_PREF_NAME);
-  Services.prefs.addObserver(UA_ENABLE_PREF_NAME, UAEnablePrefObserver);
-
-  // Listen to the useragentoverrides-initialized notification we get and
-  // initialize our overrider there. This is done to avoid slowing down the
-  // apparent startup process, since we avoid loading anything before the first
-  // window is visible to the user. See bug 1371442 for details.
-  let uaStartupObserver = {
-    observe(aSubject, aTopic, aData) {
-      if (aTopic !== UA_OVERRIDES_INIT_TOPIC) {
-        return;
-      }
-
-      Services.obs.removeObserver(this, UA_OVERRIDES_INIT_TOPIC);
-      overrider = new UAOverrider(UAOverrides);
-      overrider.init();
-    },
-  };
-  Services.obs.addObserver(uaStartupObserver, UA_OVERRIDES_INIT_TOPIC);
-
-  // Observe browser-delayed-startup-finished and only initialize our embedded
-  // WebExtension after that. Otherwise, we'd try to initialize as soon as the
-  // browser starts up, which adds a heavy startup penalty.
-  let appStartupObserver = {
-    observe(aSubject, aTopic, aData) {
-      webExtension.startup().then((api) => {
-        api.browser.runtime.onConnect.addListener((port) => {
-          webextensionPort = port;
-        });
-
-        return Promise.resolve();
-      }).catch((ex) => {
-        console.error(ex);
-      });
-      Services.obs.removeObserver(this, BROWSER_STARTUP_FINISHED_TOPIC);
-    },
-  };
-  Services.obs.addObserver(appStartupObserver, BROWSER_STARTUP_FINISHED_TOPIC);
-};
-
-this.shutdown = function() {
-  Services.prefs.removeObserver(INJECTIONS_ENABLE_PREF_NAME, InjectionsEnablePrefObserver);
-  Services.prefs.removeObserver(UA_ENABLE_PREF_NAME, UAEnablePrefObserver);
-};
deleted file mode 100644
--- a/browser/extensions/webcompat/content/data/ua_overrides.jsm
+++ /dev/null
@@ -1,65 +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/. */
-
-/**
- * For detailed information on our policies, and a documention on this format
- * and its possibilites, please check the Mozilla-Wiki at
- *
- * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
- */
-const UAOverrides = [
-
-  /*
-   * This is a dummy override that applies a Chrome UA to a dummy site that
-   * blocks all browsers but Chrome.
-   *
-   * This was only put in place to allow QA to test this system addon on an
-   * actual site, since we were not able to find a proper override in time.
-   */
-  {
-    baseDomain: "schub.io",
-    applications: ["firefox", "fennec"],
-    uriMatcher: (uri) => uri.includes("webcompat-addon-testcases.schub.io"),
-    uaTransformer: (originalUA) => {
-      let prefix = originalUA.substr(0, originalUA.indexOf(")") + 1);
-      return `${prefix} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36`;
-    },
-  },
-
-  /*
-   * Bug 1480710 - m.imgur.com - Build UA override
-   * WebCompat issue #13154 - https://webcompat.com/issues/13154
-   *
-   * imgur returns a 404 for requests to CSS and JS file if requested with a Fennec
-   * User Agent. By removing the Fennec identifies and adding Chrome Mobile's, we
-   * receive the correct CSS and JS files.
-   */
-  {
-    baseDomain: "imgur.com",
-    applications: ["fennec"],
-    uriMatcher: (uri) => uri.includes("m.imgur.com"),
-    uaTransformer: (originalUA) => {
-      let prefix = originalUA.substr(0, originalUA.indexOf(")") + 1);
-      return prefix + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36";
-    },
-  },
-
-  /*
-   * Bug 755590 - sites.google.com - top bar doesn't show up in Firefox for Android
-   *
-   * Google Sites does show a different top bar template based on the User Agent.
-   * For Fennec, this results in a broken top bar. Appending Chrome and Mobile Safari
-   * identifiers to the UA results in a correct rendering.
-   */
-  {
-    baseDomain: "google.com",
-    applications: ["fennec"],
-    uriMatcher: (uri) => uri.includes("sites.google.com"),
-    uaTransformer: (originalUA) => {
-      return originalUA + " Chrome/68.0.3440.85 Mobile Safari/537.366";
-    },
-  },
-];
-
-var EXPORTED_SYMBOLS = ["UAOverrides"]; /* exported UAOverrides */
deleted file mode 100644
--- a/browser/extensions/webcompat/content/lib/ua_overrider.jsm
+++ /dev/null
@@ -1,126 +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/. */
-
-ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
-ChromeUtils.defineModuleGetter(this, "UserAgentOverrides", "resource://gre/modules/UserAgentOverrides.jsm");
-
-class UAOverrider {
-  constructor(overrides) {
-    this._overrides = {};
-    this._shouldOverride = true;
-
-    this.initOverrides(overrides);
-  }
-
-  initOverrides(overrides) {
-    // on xpcshell tests, there is no implementation for nsIXULAppInfo, so this
-    // might fail there. To have all of our test cases running at all times,
-    // assume they are on Desktop for now.
-    let currentApplication = "firefox";
-    try {
-      currentApplication = Services.appinfo.name.toLowerCase();
-    } catch (ex) {
-      console.warn("Getting appinfo.name failed, assuming we run on Desktop.", ex);
-    }
-
-    for (let override of overrides) {
-      // Firefox for Desktop is the default application for all overrides.
-      if (!override.applications) {
-        override.applications = ["firefox"];
-      }
-
-      // If the current application is not targeted by the override in question,
-      // we can skip adding the override to our checks entirely.
-      if (!override.applications.includes(currentApplication)) {
-        continue;
-      }
-
-      if (!this._overrides[override.baseDomain]) {
-        this._overrides[override.baseDomain] = [];
-      }
-
-      if (!override.uriMatcher) {
-        override.uriMatcher = () => true;
-      }
-
-      this._overrides[override.baseDomain].push(override);
-    }
-  }
-
-  /**
-   * Used for disabling overrides when the pref has been flipped to false.
-   *
-   * Since we no longer use our own event handlers, we check this bool in our
-   * override callback and simply return early if we are not supposed to do
-   * anything.
-   */
-  setShouldOverride(newState) {
-    this._shouldOverride = newState;
-  }
-
-  init() {
-    UserAgentOverrides.addComplexOverride(this.overrideCallback.bind(this));
-  }
-
-  overrideCallback(channel, defaultUA) {
-    if (!this._shouldOverride) {
-      return false;
-    }
-
-    let uaOverride = this.lookupUAOverride(channel.URI, defaultUA);
-    if (uaOverride) {
-      console.log("The user agent has been overridden for compatibility reasons.");
-      return uaOverride;
-    }
-
-    return false;
-  }
-
-  /**
-   * Try to use the eTLDService to get the base domain (will return example.com
-   * for http://foo.bar.example.com/foo/bar).
-   *
-   * However, the eTLDService is a bit picky and throws whenever we pass a
-   * blank host name or an IP into it, see bug 1337785. Since we do not plan on
-   * override UAs for such cases, we simply catch everything and return false.
-   */
-  getBaseDomainFromURI(uri) {
-    try {
-      return Services.eTLD.getBaseDomain(uri);
-    } catch (_) {
-      return false;
-    }
-  }
-
-  /**
-   * This function returns a User Agent based on the URI passed into. All
-   * override rules are defined in data/ua_overrides.jsm and the required format
-   * is explained there.
-   *
-   * Since it is expected and designed to have more than one override per base
-   * domain, we have to loop over this._overrides[baseDomain], which contains
-   * all available overrides.
-   *
-   * If the uriMatcher function returns true, the uaTransformer function gets
-   * called and its result will be used as the Use Agent for the current
-   * request.
-   *
-   * If there are more than one possible overrides, that is if two or more
-   * uriMatchers would return true, the first one gets applied.
-   */
-  lookupUAOverride(uri, defaultUA) {
-    let baseDomain = this.getBaseDomainFromURI(uri);
-    if (baseDomain && this._overrides[baseDomain]) {
-      for (let uaOverride of this._overrides[baseDomain]) {
-        if (uaOverride.uriMatcher(uri.specIgnoringRef)) {
-          return uaOverride.uaTransformer(defaultUA);
-        }
-      }
-    }
-
-    return false;
-  }
-}
-
-var EXPORTED_SYMBOLS = ["UAOverrider"]; /* exported UAOverrider */
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/injections.js
@@ -0,0 +1,102 @@
+/**
+ * For detailed information on our policies, and a documention on this format
+ * and its possibilites, please check the Mozilla-Wiki at
+ *
+ * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
+ */
+const contentScripts = {
+  universal: [
+    {
+      matches: ["*://webcompat-addon-testcases.schub.io/*"],
+      css: [{file: "injections/css/bug0000000-dummy-css-injection.css"}],
+      js: [{file: "injections/js/bug0000000-dummy-js-injection.js"}],
+      runAt: "document_start",
+    },
+  ],
+  desktop: [
+    {
+      matches: ["https://ib.absa.co.za/*"],
+      js: [{file: "injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js"}],
+      runAt: "document_start",
+    },
+    {
+      matches: ["http://histography.io/*"],
+      js: [{file: "injections/js/bug1457335-histography.io-ua-change.js"}],
+      runAt: "document_start",
+    },
+    {
+      matches: ["*://*.bankofamerica.com/*"],
+      js: [{file: "injections/js/bug1472075-bankofamerica.com-ua-change.js"}],
+      runAt: "document_start",
+    },
+    {
+      matches: ["http://202.166.205.141/bbvrs/*"],
+      js: [{file: "injections/js/bug1472081-election.gov.np-window.sidebar-shim.js"}],
+      runAt: "document_start",
+      allFrames: true,
+    },
+    {
+      matches: ["*://portalminasnet.com/*"],
+      js: [{file: "injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js"}],
+      runAt: "document_start",
+      allFrames: true,
+    },
+  ],
+  android: [],
+};
+
+/* globals browser */
+
+let port = browser.runtime.connect();
+let registeredContentScripts = [];
+
+async function registerContentScripts() {
+  let platform = "desktop";
+  let platformInfo = await browser.runtime.getPlatformInfo();
+  if (platformInfo.os == "android") {
+    platform = "android";
+  }
+
+  let targetContentScripts = contentScripts.universal.concat(contentScripts[platform]);
+  targetContentScripts.forEach(async (contentScript) => {
+    try {
+      let handle = await browser.contentScripts.register(contentScript);
+      registeredContentScripts.push(handle);
+    } catch (ex) {
+      console.error("Registering WebCompat GoFaster content scripts failed: ", ex);
+    }
+  });
+}
+
+function unregisterContentScripts() {
+  registeredContentScripts.forEach((contentScript) => {
+    contentScript.unregister();
+  });
+}
+
+port.onMessage.addListener((message) => {
+  switch (message.type) {
+    case "injection-pref-changed":
+      if (message.prefState) {
+        registerContentScripts();
+      } else {
+        unregisterContentScripts();
+      }
+      break;
+  }
+});
+
+const INJECTION_PREF = "perform_injections";
+function checkInjectionPref() {
+  browser.aboutConfigPrefs.getPref(INJECTION_PREF).then(value => {
+    if (value === undefined) {
+      browser.aboutConfigPrefs.setPref(INJECTION_PREF, true);
+    } else if (value === false) {
+      unregisterContentScripts();
+    } else {
+      registerContentScripts();
+    }
+  });
+}
+browser.aboutConfigPrefs.onPrefChange.addListener(checkInjectionPref, INJECTION_PREF);
+checkInjectionPref();
rename from browser/extensions/webcompat/webextension/injections/css/bug0000000-dummy-css-injection.css
rename to browser/extensions/webcompat/injections/css/bug0000000-dummy-css-injection.css
rename from browser/extensions/webcompat/webextension/injections/js/bug0000000-dummy-js-injection.js
rename to browser/extensions/webcompat/injections/js/bug0000000-dummy-js-injection.js
rename from browser/extensions/webcompat/webextension/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js
rename to browser/extensions/webcompat/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js
rename from browser/extensions/webcompat/webextension/injections/js/bug1457335-histography.io-ua-change.js
rename to browser/extensions/webcompat/injections/js/bug1457335-histography.io-ua-change.js
rename from browser/extensions/webcompat/webextension/injections/js/bug1472075-bankofamerica.com-ua-change.js
rename to browser/extensions/webcompat/injections/js/bug1472075-bankofamerica.com-ua-change.js
rename from browser/extensions/webcompat/webextension/injections/js/bug1472081-election.gov.np-window.sidebar-shim.js
rename to browser/extensions/webcompat/injections/js/bug1472081-election.gov.np-window.sidebar-shim.js
rename from browser/extensions/webcompat/webextension/injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js
rename to browser/extensions/webcompat/injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js
deleted file mode 100644
--- a/browser/extensions/webcompat/install.rdf.in
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.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/. -->
-
-#filter substitution
-
-<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-
-  <Description about="urn:mozilla:install-manifest">
-    <em:id>webcompat@mozilla.org</em:id>
-    <em:version>2.0.1</em:version>
-    <em:type>2</em:type>
-    <em:bootstrap>true</em:bootstrap>
-    <em:multiprocessCompatible>true</em:multiprocessCompatible>
-    <em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
-
-    <!-- Firefox Desktop -->
-    <em:targetApplication>
-      <Description>
-        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
-        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
-        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-
-    <!-- Firefox for Android -->
-    <em:targetApplication>
-      <Description>
-        <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
-        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
-        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-
-    <!-- Front End MetaData -->
-    <em:name>Web Compat</em:name>
-    <em:description>Urgent post-release fixes for web compatibility.</em:description>
-  </Description>
-</RDF>
deleted file mode 100644
--- a/browser/extensions/webcompat/jar.mn
+++ /dev/null
@@ -1,3 +0,0 @@
-[features/webcompat@mozilla.org] chrome.jar:
-% content webcompat %content/
-  content/ (content/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/manifest.json
@@ -0,0 +1,37 @@
+{
+  "manifest_version": 2,
+  "name": "Web Compat",
+  "description": "Urgent post-release fixes for web compatibility.",
+  "version": "3.0.0",
+
+  "applications": {
+    "gecko": {
+      "id": "webcompat@mozilla.org",
+      "strict_min_version": "59.0b5"
+    }
+  },
+
+  "experiment_apis": {
+    "aboutConfigPrefs": {
+      "schema": "aboutConfigPrefs.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "aboutConfigPrefs.js",
+        "paths": [["aboutConfigPrefs"]]
+      }
+    }
+  },
+
+  "permissions": [
+    "webRequest",
+    "webRequestBlocking",
+    "<all_urls>"
+  ],
+
+  "background": {
+    "scripts": [
+      "injections.js",
+      "ua_overrides.js"
+    ]
+  }
+}
--- a/browser/extensions/webcompat/moz.build
+++ b/browser/extensions/webcompat/moz.build
@@ -3,38 +3,30 @@
 # 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['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org'] += [
-  'bootstrap.js'
-]
-
-FINAL_TARGET_FILES.features['webcompat@mozilla.org']['webextension'] += [
-  'webextension/background.js',
-  'webextension/manifest.json'
-]
-
-FINAL_TARGET_FILES.features['webcompat@mozilla.org']['webextension']['injections']['css'] += [
-  'webextension/injections/css/bug0000000-dummy-css-injection.css'
+  'aboutConfigPrefs.js',
+  'aboutConfigPrefs.json',
+  'injections.js',
+  'manifest.json',
+  'ua_overrides.js'
 ]
 
-FINAL_TARGET_FILES.features['webcompat@mozilla.org']['webextension']['injections']['js'] += [
-  'webextension/injections/js/bug0000000-dummy-js-injection.js',
-  'webextension/injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js',
-  'webextension/injections/js/bug1457335-histography.io-ua-change.js',
-  'webextension/injections/js/bug1472075-bankofamerica.com-ua-change.js',
-  'webextension/injections/js/bug1472081-election.gov.np-window.sidebar-shim.js',
-  'webextension/injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js'
+FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['css'] += [
+  'injections/css/bug0000000-dummy-css-injection.css'
 ]
 
-FINAL_TARGET_PP_FILES.features['webcompat@mozilla.org'] += [
-  'install.rdf.in'
+FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['js'] += [
+  'injections/js/bug0000000-dummy-js-injection.js',
+  'injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js',
+  'injections/js/bug1457335-histography.io-ua-change.js',
+  'injections/js/bug1472075-bankofamerica.com-ua-change.js',
+  'injections/js/bug1472081-election.gov.np-window.sidebar-shim.js',
+  'injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js'
 ]
 
-BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
-JAR_MANIFESTS += ['jar.mn']
-
 with Files('**'):
   BUG_COMPONENT = ('Web Compatibility Tools', 'Go Faster')
deleted file mode 100644
--- a/browser/extensions/webcompat/test/.eslintrc.js
+++ /dev/null
@@ -1,7 +0,0 @@
-"use strict";
-
-module.exports = {
-  "extends": [
-    "plugin:mozilla/browser-test"
-  ]
-};
deleted file mode 100644
--- a/browser/extensions/webcompat/test/browser.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[DEFAULT]
-
-[browser_check_installed.js]
-[browser_overrider.js]
deleted file mode 100644
--- a/browser/extensions/webcompat/test/browser_check_installed.js
+++ /dev/null
@@ -1,15 +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/. */
-
-"use strict";
-
-add_task(async function installed() {
-  let addon = await AddonManager.getAddonByID("webcompat@mozilla.org");
-  isnot(addon, null, "Webcompat addon should exist");
-  is(addon.name, "Web Compat");
-  ok(addon.isCompatible, "Webcompat addon is compatible with Firefox");
-  ok(!addon.appDisabled, "Webcompat addon is not app disabled");
-  ok(addon.isActive, "Webcompat addon is active");
-  is(addon.type, "extension", "Webcompat addon is type extension");
-});
deleted file mode 100644
--- a/browser/extensions/webcompat/test/browser_overrider.js
+++ /dev/null
@@ -1,38 +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/. */
-
-"use strict";
-
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "UAOverrider", "chrome://webcompat/content/lib/ua_overrider.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "IOService", "@mozilla.org/network/io-service;1", "nsIIOService");
-
-function getnsIURI(uri) {
-  return IOService.newURI(uri, "utf-8");
-}
-
-add_task(function test() {
-  let overrider = new UAOverrider([
-    {
-      baseDomain: "example.org",
-      uaTransformer: () => "Test UA",
-    },
-  ]);
-
-  let finalUA = overrider.lookupUAOverride(getnsIURI("http://www.example.org/foobar/"));
-  is(finalUA, "Test UA", "Overrides the UA without a matcher function");
-});
-
-add_task(function test() {
-  let overrider = new UAOverrider([
-    {
-      baseDomain: "example.org",
-      uriMatcher: () => false,
-      uaTransformer: () => "Test UA",
-    },
-  ]);
-
-  let finalUA = overrider.lookupUAOverride(getnsIURI("http://www.example.org/foobar/"));
-  isnot(finalUA, "Test UA", "Does not override the UA with the matcher returning false");
-});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/ua_overrides.js
@@ -0,0 +1,129 @@
+/**
+ * For detailed information on our policies, and a documention on this format
+ * and its possibilites, please check the Mozilla-Wiki at
+ *
+ * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
+ */
+const UAOverrides = {
+  universal: [
+    /*
+     * This is a dummy override that applies a Chrome UA to a dummy site that
+     * blocks all browsers but Chrome.
+     *
+     * This was only put in place to allow QA to test this system addon on an
+     * actual site, since we were not able to find a proper override in time.
+     */
+    {
+      matches: ["*://webcompat-addon-testcases.schub.io/*"],
+      uaTransformer: (originalUA) => {
+        let prefix = originalUA.substr(0, originalUA.indexOf(")") + 1);
+        return `${prefix} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36`;
+      },
+    },
+  ],
+  desktop: [],
+  android: [
+    /*
+     * Bug 1480710 - m.imgur.com - Build UA override
+     * WebCompat issue #13154 - https://webcompat.com/issues/13154
+     *
+     * imgur returns a 404 for requests to CSS and JS file if requested with a Fennec
+     * User Agent. By removing the Fennec identifies and adding Chrome Mobile's, we
+     * receive the correct CSS and JS files.
+     */
+    {
+      matches: ["*://m.imgur.com/*"],
+      uaTransformer: (originalUA) => {
+        let prefix = originalUA.substr(0, originalUA.indexOf(")") + 1);
+        return prefix + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36";
+      },
+    },
+
+    /*
+     * Bug 755590 - sites.google.com - top bar doesn't show up in Firefox for Android
+     *
+     * Google Sites does show a different top bar template based on the User Agent.
+     * For Fennec, this results in a broken top bar. Appending Chrome and Mobile Safari
+     * identifiers to the UA results in a correct rendering.
+     */
+    {
+      matches: ["*://sites.google.com/*"],
+      uaTransformer: (originalUA) => {
+        return originalUA + " Chrome/68.0.3440.85 Mobile Safari/537.366";
+      },
+    },
+
+    /*
+     * Bug 945963 - tieba.baidu.com serves simplified mobile content to Firefox Android
+     * WebCompat issue #18455 - https://webcompat.com/issues/18455
+     *
+     * tieba.baidu.com and tiebac.baidu.com serve a heavily simplified and less functional
+     * mobile experience to Firefox for Android users. Adding the AppleWebKit indicator
+     * to the User Agent gets us the same experience.
+     */
+    {
+      matches: ["*://tieba.baidu.com/*", "*://tiebac.baidu.com/*"],
+      uaTransformer: (originalUA) => {
+        return originalUA + " AppleWebKit/537.36 (KHTML, like Gecko)";
+      },
+    },
+  ],
+};
+
+/* globals browser */
+
+let activeListeners = [];
+function buildAndRegisterListener(matches, transformer) {
+  let listener = (details) => {
+    for (var header of details.requestHeaders) {
+      if (header.name.toLowerCase() === "user-agent") {
+        header.value = transformer(header.value);
+      }
+    }
+    return {requestHeaders: details.requestHeaders};
+  };
+
+  browser.webRequest.onBeforeSendHeaders.addListener(
+    listener,
+    {urls: matches},
+    ["blocking", "requestHeaders"]
+  );
+
+  activeListeners.push(listener);
+}
+
+async function registerUAOverrides() {
+  let platform = "desktop";
+  let platformInfo = await browser.runtime.getPlatformInfo();
+  if (platformInfo.os == "android") {
+    platform = "android";
+  }
+
+  let targetOverrides = UAOverrides.universal.concat(UAOverrides[platform]);
+  targetOverrides.forEach((override) => {
+    buildAndRegisterListener(override.matches, override.uaTransformer);
+  });
+}
+
+function unregisterUAOverrides() {
+  activeListeners.forEach((listener) => {
+    browser.webRequest.onBeforeSendHeaders.removeListener(listener);
+  });
+
+  activeListeners = [];
+}
+
+const OVERRIDE_PREF = "perform_ua_overrides";
+function checkOverridePref() {
+  browser.aboutConfigPrefs.getPref(OVERRIDE_PREF).then(value => {
+    if (value === undefined) {
+      browser.aboutConfigPrefs.setPref(OVERRIDE_PREF, true);
+    } else if (value === false) {
+      unregisterUAOverrides();
+    } else {
+      registerUAOverrides();
+    }
+  });
+}
+browser.aboutConfigPrefs.onPrefChange.addListener(checkOverridePref, OVERRIDE_PREF);
+checkOverridePref();
deleted file mode 100644
--- a/browser/extensions/webcompat/webextension/background.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * For detailed information on our policies, and a documention on this format
- * and its possibilites, please check the Mozilla-Wiki at
- *
- * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
- */
-const contentScripts = {
-  universal: [
-    {
-      matches: ["*://webcompat-addon-testcases.schub.io/*"],
-      css: [{file: "injections/css/bug0000000-dummy-css-injection.css"}],
-      js: [{file: "injections/js/bug0000000-dummy-js-injection.js"}],
-      runAt: "document_start",
-    },
-  ],
-  desktop: [
-    {
-      matches: ["https://ib.absa.co.za/*"],
-      js: [{file: "injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js"}],
-      runAt: "document_start",
-    },
-    {
-      matches: ["http://histography.io/*"],
-      js: [{file: "injections/js/bug1457335-histography.io-ua-change.js"}],
-      runAt: "document_start",
-    },
-    {
-      matches: ["*://*.bankofamerica.com/*"],
-      js: [{file: "injections/js/bug1472075-bankofamerica.com-ua-change.js"}],
-      runAt: "document_start",
-    },
-    {
-      matches: ["http://202.166.205.141/bbvrs/*"],
-      js: [{file: "injections/js/bug1472081-election.gov.np-window.sidebar-shim.js"}],
-      runAt: "document_start",
-      allFrames: true,
-    },
-    {
-      matches: ["*://portalminasnet.com/*"],
-      js: [{file: "injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js"}],
-      runAt: "document_start",
-      allFrames: true,
-    },
-  ],
-  android: [],
-};
-
-/* globals browser */
-
-let port = browser.runtime.connect();
-let registeredContentScripts = [];
-
-async function registerContentScripts() {
-  let platform = "desktop";
-  let platformInfo = await browser.runtime.getPlatformInfo();
-  if (platformInfo.os == "android") {
-    platform = "android";
-  }
-
-  let targetContentScripts = contentScripts.universal.concat(contentScripts[platform]);
-  targetContentScripts.forEach(async (contentScript) => {
-    try {
-      let handle = await browser.contentScripts.register(contentScript);
-      registeredContentScripts.push(handle);
-    } catch (ex) {
-      console.error("Registering WebCompat GoFaster content scripts failed: ", ex);
-    }
-  });
-}
-
-function unregisterContentScripts() {
-  registeredContentScripts.forEach((contentScript) => {
-    contentScript.unregister();
-  });
-}
-
-port.onMessage.addListener((message) => {
-  switch (message.type) {
-    case "injection-pref-changed":
-      if (message.prefState) {
-        registerContentScripts();
-      } else {
-        unregisterContentScripts();
-      }
-      break;
-  }
-});
-
-/**
- * Note that we reset all preferences on extension startup, so the injections will
- * never be disabled when this loads up. Because of that, we can simply register
- * right away.
- */
-registerContentScripts();
deleted file mode 100644
--- a/browser/extensions/webcompat/webextension/manifest.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "manifest_version": 2,
-  "name": "Web Compat",
-  "description": "Urgent post-release fixes for web compatibility.",
-  "version": "2.0",
-
-  "applications": {
-    "gecko": {
-      "id": "webcompat@mozilla.org",
-      "strict_min_version": "59.0b5"
-    }
-  },
-
-  "permissions": [
-    "<all_urls>"
-  ],
-
-  "background": {
-    "scripts": [
-      "background.js"
-    ]
-  }
-}
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -92,16 +92,20 @@ restart-later = Restart Later
 # This string is shown to notify the user that their home page
 # is being controlled by an extension.
 extension-controlled-homepage-override = An extension, <img data-l10n-name="icon"/> { $name }, is controlling your home page.
 
 # This string is shown to notify the user that their new tab page
 # is being controlled by an extension.
 extension-controlled-new-tab-url = An extension, <img data-l10n-name="icon"/> { $name }, is controlling your New Tab page.
 
+# This string is shown to notify the user that their notifications permission
+# is being controlled by an extension.
+extension-controlled-web-notifications= An extension, <img data-l10n-name="icon"/> { $name }, is controlling this setting.
+
 # This string is shown to notify the user that the default search engine
 # is being controlled by an extension.
 extension-controlled-default-search = An extension, <img data-l10n-name="icon"/> { $name }, has set your default search engine.
 
 # This string is shown to notify the user that Container Tabs
 # are being enabled by an extension.
 extension-controlled-privacy-containers = An extension, <img data-l10n-name="icon"/> { $name }, requires Container Tabs.
 
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -116,16 +116,18 @@ when there are no windows but Firefox is
 <!ENTITY printSetupCmd.label "Page Setup…">
 <!ENTITY printSetupCmd.accesskey "u">
 <!ENTITY printPreviewCmd.label "Print Preview">
 <!ENTITY printPreviewCmd.accesskey "v">
 <!ENTITY printCmd.label "Print…">
 <!ENTITY printCmd.accesskey "P">
 <!ENTITY printCmd.commandkey "p">
 
+<!ENTITY taskManagerCmd.label "Task Manager">
+
 <!ENTITY goOfflineCmd.label "Work Offline">
 <!ENTITY goOfflineCmd.accesskey "k">
 
 <!ENTITY menubarCmd.label "Menu Bar">
 <!ENTITY menubarCmd.accesskey "M">
 <!ENTITY navbarCmd.label "Navigation Toolbar">
 <!ENTITY personalbarCmd.label "Bookmarks Toolbar">
 <!ENTITY personalbarCmd.accesskey "B">
@@ -995,16 +997,17 @@ you can use these alternative items. Oth
 <!ENTITY contentBlocking.trackingProtection.blocking.label "Blocking">
 <!-- LOCALIZATION NOTE (contentBlocking.trackingProtection.add.label):
      This is displayed as a link to preferences, where the user can add
      this specific type of content blocking. When this text is shown
      the type of content blocking is currently not enabled. -->
 <!ENTITY contentBlocking.trackingProtection.add.label "Add Blocking…">
 
 <!ENTITY contentBlocking.3rdPartyCookies.label "Third-Party Cookies">
+<!ENTITY contentBlocking.3rdPartyCookies.trackers.label "Tracking Cookies">
 <!-- LOCALIZATION NOTE (contentBlocking.3rdPartyCookies.blocked.label):
      This label signals that this type of content blocking is turned
      ON and is successfully blocking third-party cookies, so this is
      a positive thing. It forms the end of the (imaginary) sentence
      "Third-Party Cookies [are] Blocked"-->
 <!ENTITY contentBlocking.3rdPartyCookies.blocked.label "Blocked">
 <!-- LOCALIZATION NOTE (contentBlocking.tranckingProtection.blocking.label):
      This label signals that this type of content blocking is turned
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -706,24 +706,16 @@ async function sanitizeOnShutdown(progre
 }
 
 async function sanitizeSessionPrincipals() {
   if (Services.prefs.getIntPref(PREF_COOKIE_LIFETIME,
                                 Ci.nsICookieService.ACCEPT_NORMALLY) != Ci.nsICookieService.ACCEPT_SESSION) {
     return;
   }
 
-  // When PREF_COOKIE_LIFETIME is set to ACCEPT_SESSION, any new cookie will be
-  // marked as session only. But we don't touch the existing ones. For this
-  // reason, here we delete any existing cookie, at shutdown.
-  await new Promise(resolve => {
-    Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_COOKIES,
-                                  resolve);
-  });
-
   let principals = await new Promise(resolve => {
     quotaManagerService.getUsage(request => {
       if (request.resultCode != Cr.NS_OK) {
         // We are probably shutting down. We don't want to propagate the
         // error, rejecting the promise.
         resolve([]);
         return;
       }
--- a/browser/themes/osx/downloads/allDownloadsView.css
+++ b/browser/themes/osx/downloads/allDownloadsView.css
@@ -5,15 +5,15 @@
 %include ../../shared/downloads/allDownloadsView.inc.css
 
 /*** List items ***/
 
 :root {
   --downloads-item-height: 6em;
 }
 
-.downloadProgress > .progress-bar {
+.downloadProgress::-moz-progress-bar {
   background-color: #3c9af8;
 }
 
-.downloadProgress[paused="true"] > .progress-bar {
+.downloadProgress[paused]::-moz-progress-bar {
   background-color: #a6a6a6;
 }
--- a/browser/themes/osx/downloads/downloads.css
+++ b/browser/themes/osx/downloads/downloads.css
@@ -28,21 +28,21 @@
 @item@[verdict="Malware"]:not(:hover) {
   color: #aa1b08;
 }
 
 :root[lwt-popup-brighttext] @item@[verdict="Malware"]:not(:hover) {
   color: #ff0039;
 }
 
-.downloadProgress > .progress-bar {
+.downloadProgress::-moz-progress-bar {
   background-color: #3c9af8;
 }
 
-.downloadProgress[paused="true"] > .progress-bar {
+.downloadProgress[paused]::-moz-progress-bar {
   background-color: #a6a6a6;
 }
 
 /*** Highlighted list items ***/
 
 @keyfocus@ @itemFocused@ {
   outline: 2px -moz-mac-focusring solid;
   outline-offset: -2px;
--- a/browser/themes/shared/browser.inc.css
+++ b/browser/themes/shared/browser.inc.css
@@ -167,16 +167,20 @@
   margin-left: 0;
   margin-right: 0;
 }
 
 #widget-overflow .webextension-popup-browser {
   background: #fff;
 }
 
+#addon-progress-notification-progressmeter {
+  margin: 2px 4px;
+}
+
 /* Contextual Feature Recommendation popup-notification */
 
 :root {
   --cfr-notification-header-image: url(resource://activity-stream/data/content/assets/glyph-help-24.svg);
   --cfr-notification-footer-star: url(resource://activity-stream/data/content/assets/glyph-star-17.svg);
 }
 
 #cfr-notification-header {
--- a/browser/themes/shared/downloads/progressmeter.inc.css
+++ b/browser/themes/shared/downloads/progressmeter.inc.css
@@ -1,65 +1,58 @@
 /*** Common-styled progressmeter ***/
+
+/*
+ * Styling "html:progress" is limited by the fact that a number of properties
+ * are intentionally locked at the UA stylesheet level. We have to use a border
+ * instead of an outline because the latter would be drawn over the progress
+ * bar and we cannot change its z-index. This means we have to use a negative
+ * margin, except when the value is zero, and adjust the width calculation for
+ * the indeterminate state.
+ */
+
 .downloadProgress {
-  height: 8px;
-  border-radius: 1px;
+  -moz-appearance: none;
+  display: -moz-box;
   margin: 4px 0 0;
   margin-inline-end: 12px;
-
-  /* for overriding rules in progressmeter.css */
-  -moz-appearance: none;
-  border-style: none;
-  background-color: transparent;
-  min-width: initial;
-  min-height: initial;
+  border: 1px solid ButtonShadow;
+  height: 6px;
+  background-color: ButtonFace;
 }
 
-.downloadProgress > .progress-bar {
+.downloadProgress::-moz-progress-bar {
+  -moz-appearance: none;
   background-color: Highlight;
-
-  /* for overriding rules in progressmeter.css */
-  -moz-appearance: none;
 }
 
-.downloadProgress[paused="true"] > .progress-bar {
+.downloadProgress[paused]::-moz-progress-bar {
   background-color: GrayText;
 }
 
-.downloadProgress[progress-undetermined] > .progress-bar {
+.downloadProgress:not([value="0"])::-moz-progress-bar {
+  margin: -1px;
+  height: 8px;
+}
+
+.downloadProgress:indeterminate::-moz-progress-bar {
+  width: calc(100% + 2px);
   /* Make a white reflecting animation.
      Create a gradient with 2 identical pattern, and enlarge the size to 200%.
      This allows us to animate background-position with percentage. */
   background-image: linear-gradient(90deg, transparent 0%,
                                            rgba(255,255,255,0.5) 25%,
                                            transparent 50%,
                                            rgba(255,255,255,0.5) 75%,
                                            transparent 100%);
   background-blend-mode: lighten;
   background-size: 200% 100%;
   animation: downloadProgressSlideX 1.5s linear infinite;
 }
 
-.downloadProgress > .progress-remainder {
-  border: solid ButtonShadow;
-  border-block-start-width: 1px;
-  border-block-end-width: 1px;
-  border-inline-start-width: 0;
-  border-inline-end-width: 1px;
-  background-color: ButtonFace;
-}
-
-.downloadProgress[value="0"] > .progress-remainder {
-  border-width: 1px;
-}
-
-.downloadProgress[progress-undetermined] > .progress-remainder {
-  border: none;
-}
-
 @keyframes downloadProgressSlideX {
   0% {
     background-position: 0 0;
   }
   100% {
     background-position: -100% 0;
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/icons/performance.svg
@@ -0,0 +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">
+  <path fill="context-fill" d="M8 1a8 8 0 0 0-8 8 7.89 7.89 0 0 0 .78 3.43 1 1 0 0 0 .9.57.94.94 0 0 0 .43-.1 1 1 0 0 0 .47-1.33A6.07 6.07 0 0 1 2 9a6 6 0 0 1 12 0 5.93 5.93 0 0 1-.59 2.57 1 1 0 0 0 1.81.86A7.89 7.89 0 0 0 16 9a8 8 0 0 0-8-8z"/>
+  <path fill="context-fill" d="M11.77 7.08a.5.5 0 0 0-.69.15L8.62 11.1A2.12 2.12 0 0 0 8 11a2 2 0 0 0 0 4 2.05 2.05 0 0 0 1.12-.34 2.31 2.31 0 0 0 .54-.54 2 2 0 0 0 0-2.24 2.31 2.31 0 0 0-.2-.24l2.46-3.87a.5.5 0 0 0-.15-.69z"/>
+</svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -159,16 +159,17 @@
   skin/classic/browser/link.svg                       (../shared/icons/link.svg)
   skin/classic/browser/mail.svg                       (../shared/icons/mail.svg)
   skin/classic/browser/menu.svg                       (../shared/icons/menu.svg)
   skin/classic/browser/menu-badged.svg                (../shared/icons/menu-badged.svg)
   skin/classic/browser/new-tab.svg                    (../shared/icons/new-tab.svg)
   skin/classic/browser/new-window.svg                 (../shared/icons/new-window.svg)
   skin/classic/browser/open.svg                       (../shared/icons/open.svg)
   skin/classic/browser/page-action.svg                (../shared/icons/page-action.svg)
+  skin/classic/browser/performance.svg                (../shared/icons/performance.svg)
   skin/classic/browser/print.svg                      (../shared/icons/print.svg)
   skin/classic/browser/private-browsing.svg           (../shared/icons/private-browsing.svg)
   skin/classic/browser/privateBrowsing.svg            (../shared/icons/privateBrowsing.svg)
   skin/classic/browser/restore-session.svg            (../shared/icons/restore-session.svg)
   skin/classic/browser/quit.svg                       (../shared/icons/quit.svg)
   skin/classic/browser/reload.svg                     (../shared/icons/reload.svg)
   skin/classic/browser/reload-to-stop.svg             (../shared/icons/reload-to-stop.svg)
   skin/classic/browser/save.svg                       (../shared/icons/save.svg)
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -28,16 +28,20 @@
   list-style-image: url(chrome://browser/skin/customize.svg);
 }
 
 #appMenu-find-button,
 #panelMenu_searchBookmarks {
   list-style-image: url(chrome://browser/skin/search-glass.svg);
 }
 
+#appMenu-taskmanager-button {
+  list-style-image: url("chrome://browser/skin/performance.svg");
+}
+
 #appMenu-help-button {
   list-style-image: url(chrome://global/skin/icons/help.svg);
 }
 
 #appMenu-cut-button {
   list-style-image: url(chrome://browser/skin/edit-cut.svg);
 }
 
--- a/browser/themes/windows/downloads/allDownloadsView.css
+++ b/browser/themes/windows/downloads/allDownloadsView.css
@@ -6,19 +6,23 @@
 
 /*** List items ***/
 
 :root {
   --downloads-item-height: 6em;
 }
 
 @media (-moz-windows-default-theme) {
-  .downloadProgress > .progress-bar {
+  .downloadProgress::-moz-progress-bar {
     background-color: #3c9af8;
   }
+
+  .downloadProgress[paused]::-moz-progress-bar {
+    background-color: #a6a6a6;
+  }
 }
 
 /*** Highlighted list items ***/
 
 @media (-moz-windows-default-theme) {
   /*
   -moz-appearance: menuitem is almost right, but the hover effect is not
   transparent and is lighter than desired.
--- a/browser/themes/windows/downloads/downloads.css
+++ b/browser/themes/windows/downloads/downloads.css
@@ -32,24 +32,23 @@
     color: #aa1b08;
   }
 
   :root[lwt-popup-brighttext] @item@[verdict="Malware"]:not(:hover) {
     color: #ff0039;
   }
 
   /* Use unified color for the progressbar on default theme */
-  .downloadProgress > .progress-bar {
+  .downloadProgress::-moz-progress-bar {
     background-color: #3c9af8;
   }
 
-  .downloadProgress[paused="true"] > .progress-bar {
+  .downloadProgress[paused]::-moz-progress-bar {
     background-color: #a6a6a6;
   }
-
 }
 
 /*** Highlighted list items ***/
 
 @keyfocus@ @itemFocused@ {
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -1px;
 }
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm
@@ -29,213 +29,210 @@ var ControlCenter = {
   init(libDir) {
     // Disable the FTU tours.
     Services.prefs.setIntPref("privacy.trackingprotection.introCount", 20);
     Services.prefs.setIntPref("browser.contentblocking.introCount", 20);
   },
 
   configurations: {
     about: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage("about:rights");
         await openIdentityPopup();
       },
     },
 
     localFile: {
-      selectors: ["#identity-popup"],
+      // This selector is different so we can exclude the changing file: path
+      selectors: ["#identity-popup-security"],
       async applyConfig() {
         let channel = NetUtil.newChannel({
             uri: "resource://mozscreenshots/lib/mozscreenshots.html",
             loadUsingSystemPrincipal: true,
         });
         channel = channel.QueryInterface(Ci.nsIFileChannel);
         let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
         let gBrowser = browserWindow.gBrowser;
         BrowserTestUtils.loadURI(gBrowser.selectedBrowser, channel.file.path);
         await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
         await openIdentityPopup();
       },
-
-      async verifyConfig() {
-        return { todo: "Bug 1373563: intermittent controlCenter_localFile on Taskcluster" };
-      },
     },
 
     http: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(HTTP_PAGE);
         await openIdentityPopup();
       },
     },
 
     httpSubView: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(HTTP_PAGE);
         await openIdentityPopup(true);
       },
     },
 
     https: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(HTTPS_PAGE);
         await openIdentityPopup();
       },
     },
 
     httpsSubView: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(HTTPS_PAGE);
         await openIdentityPopup(true);
       },
     },
 
     singlePermission: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         let uri = Services.io.newURI(PERMISSIONS_PAGE);
         SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
 
         await loadPage(PERMISSIONS_PAGE);
         await openIdentityPopup();
       },
     },
 
     allPermissions: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         // TODO: (Bug 1330601) Rewrite this to consider temporary (TAB) permission states.
         // There are 2 possible non-default permission states, so we alternate between them.
         let states = [SitePermissions.ALLOW, SitePermissions.BLOCK];
         let uri = Services.io.newURI(PERMISSIONS_PAGE);
         SitePermissions.listPermissions().forEach(function(permission, index) {
           SitePermissions.set(uri, permission, states[index % 2]);
         });
 
         await loadPage(PERMISSIONS_PAGE);
         await openIdentityPopup();
       },
     },
 
     mixed: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(MIXED_CONTENT_URL);
         await openIdentityPopup();
       },
     },
 
     mixedSubView: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(MIXED_CONTENT_URL);
         await openIdentityPopup(true);
       },
     },
 
     mixedPassive: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(MIXED_PASSIVE_CONTENT_URL);
         await openIdentityPopup();
       },
     },
 
     mixedPassiveSubView: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(MIXED_PASSIVE_CONTENT_URL);
         await openIdentityPopup(true);
       },
     },
 
     mixedActive: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(MIXED_ACTIVE_CONTENT_URL);
         await openIdentityPopup();
       },
     },
 
     mixedActiveSubView: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(MIXED_ACTIVE_CONTENT_URL);
         await openIdentityPopup(true);
       },
     },
 
     mixedActiveUnblocked: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
         let gBrowser = browserWindow.gBrowser;
         await loadPage(MIXED_ACTIVE_CONTENT_URL);
         gBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
         await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, MIXED_ACTIVE_CONTENT_URL);
         await openIdentityPopup();
       },
     },
 
     mixedActiveUnblockedSubView: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
         let gBrowser = browserWindow.gBrowser;
         await loadPage(MIXED_ACTIVE_CONTENT_URL);
         gBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
         await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, MIXED_ACTIVE_CONTENT_URL);
         await openIdentityPopup(true);
       },
     },
 
     httpPassword: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(HTTP_PASSWORD_PAGE);
         await openIdentityPopup();
       },
     },
 
     httpPasswordSubView: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         await loadPage(HTTP_PASSWORD_PAGE);
         await openIdentityPopup(true);
       },
     },
 
     trackingProtectionNoElements: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
 
         await loadPage(HTTP_PAGE);
         await openIdentityPopup();
       },
     },
 
     trackingProtectionEnabled: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
         await UrlClassifierTestUtils.addTestTrackers();
 
         await loadPage(TRACKING_PAGE);
         await openIdentityPopup();
       },
     },
 
     trackingProtectionDisabled: {
-      selectors: ["#identity-popup"],
+      selectors: ["#navigator-toolbox", "#identity-popup"],
       async applyConfig() {
         let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
         let gBrowser = browserWindow.gBrowser;
         Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
         await UrlClassifierTestUtils.addTestTrackers();
 
         await loadPage(TRACKING_PAGE);
         await openIdentityPopup();
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm
@@ -20,68 +20,68 @@ var LightweightThemes = {
     // convert -size 3000x200 canvas:#eee white_theme.png
     let whiteImage = libDir.clone();
     whiteImage.append("white_theme.png");
     this._whiteImageURL = Services.io.newFileURI(whiteImage).spec;
   },
 
   configurations: {
     noLWT: {
-      selectors: ["#navigator-toolbox"],
+      selectors: [],
       async applyConfig() {
         LightweightThemeManager.currentTheme = null;
       },
     },
 
     darkLWT: {
-      selectors: ["#navigator-toolbox"],
+      selectors: [],
       applyConfig() {
         LightweightThemeManager.setLocalTheme({
           id:          "black",
           name:        "black",
           headerURL:   LightweightThemes._blackImageURL,
           textcolor:   "#eeeeee",
           accentcolor: "#111111",
         });
 
         // Wait for LWT listener
         return new Promise(resolve => {
           setTimeout(() => {
-            resolve("darkLWT");
+            resolve();
           }, 500);
         });
       },
     },
 
     lightLWT: {
-      selectors: ["#navigator-toolbox"],
+      selectors: [],
       applyConfig() {
         LightweightThemeManager.setLocalTheme({
           id:          "white",
           name:        "white",
           headerURL:   LightweightThemes._whiteImageURL,
           textcolor:   "#111111",
           accentcolor: "#eeeeee",
         });
         // Wait for LWT listener
         return new Promise(resolve => {
           setTimeout(() => {
-            resolve("lightLWT");
+            resolve();
           }, 500);
         });
       },
     },
 
     compactLight: {
-      selectors: ["#navigator-toolbox"],
+      selectors: [],
       applyConfig() {
         LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme("firefox-compact-light@mozilla.org");
       },
     },
 
     compactDark: {
-      selectors: ["#navigator-toolbox"],
+      selectors: [],
       applyConfig() {
         LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme("firefox-compact-dark@mozilla.org");
       },
     },
   },
 };
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm
@@ -18,91 +18,91 @@ var PermissionPrompts = {
   init(libDir) {
     Services.prefs.setBoolPref("media.navigator.permission.fake", true);
     Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false);
     Services.prefs.setBoolPref("signon.rememberSignons", true);
   },
 
   configurations: {
     shareDevices: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#webRTC-shareDevices");
       },
     },
 
     shareMicrophone: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#webRTC-shareMicrophone");
       },
     },
 
     shareVideoAndMicrophone: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#webRTC-shareDevices2");
       },
     },
 
     shareScreen: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#webRTC-shareScreen");
       },
     },
 
     geo: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#geo");
       },
     },
 
     persistentStorage: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#persistent-storage");
       },
     },
 
     loginCapture: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#login-capture");
       },
     },
 
     notifications: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         await closeLastTab();
         await clickOn("#web-notifications");
       },
     },
 
     addons: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         Services.prefs.setBoolPref("xpinstall.whitelist.required", true);
 
         await closeLastTab();
         await clickOn("#addons");
       },
     },
 
     addonsNoWhitelist: {
-      selectors: ["#notification-popup"],
+      selectors: ["#notification-popup", "#identity-box"],
       async applyConfig() {
         Services.prefs.setBoolPref("xpinstall.whitelist.required", false);
 
         let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
         let notification = browserWindow.document.getElementById("addon-install-confirmation-notification");
 
         await closeLastTab();
         await clickOn("#addons");
rename from build/build-clang/clang-7-mingw.json
rename to build/build-clang/clang-trunk-mingw.json
--- a/build/build-clang/clang-7-mingw.json
+++ b/build/build-clang/clang-trunk-mingw.json
@@ -1,18 +1,18 @@
 {
     "llvm_revision": "342383",
     "stages": "3",
     "build_libcxx": true,
     "build_type": "Release",
     "assertions": false,
-    "llvm_repo": "https://llvm.org/svn/llvm-project/llvm/tags/RELEASE_700/final",
-    "clang_repo": "https://llvm.org/svn/llvm-project/cfe/tags/RELEASE_700/final",
-    "lld_repo": "https://llvm.org/svn/llvm-project/lld/tags/RELEASE_700/final",
-    "compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/tags/RELEASE_700/final",
-    "libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/tags/RELEASE_700/final",
-    "libcxxabi_repo": "https://llvm.org/svn/llvm-project/libcxxabi/tags/RELEASE_700/final",
+    "llvm_repo": "https://llvm.org/svn/llvm-project/llvm/trunk",
+    "clang_repo": "https://llvm.org/svn/llvm-project/cfe/trunk",
+    "lld_repo": "https://llvm.org/svn/llvm-project/lld/trunk",
+    "compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/trunk",
+    "libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/trunk",
+    "libcxxabi_repo": "https://llvm.org/svn/llvm-project/libcxxabi/trunk",
     "python_path": "/usr/bin/python2.7",
     "gcc_dir": "/builds/worker/workspace/build/src/gcc",
     "cc": "/builds/worker/workspace/build/src/gcc/bin/gcc",
     "cxx": "/builds/worker/workspace/build/src/gcc/bin/g++",
     "as": "/builds/worker/workspace/build/src/gcc/bin/gcc"
 }
--- a/devtools/client/aboutdebugging-new/aboutdebugging.css
+++ b/devtools/client/aboutdebugging-new/aboutdebugging.css
@@ -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/. */
 
 @import "chrome://global/skin/in-content/common.css";
 @import "resource://devtools/client/themes/variables.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/App.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/RuntimeInfo.css";
+@import "resource://devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/connect/ConnectSteps.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/connect/NetworkLocationsForm.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/connect/NetworkLocationsList.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetItem.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetList.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetPane.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/ExtensionDetail.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/WorkerDetail.css";
--- a/devtools/client/aboutdebugging-new/index.html
+++ b/devtools/client/aboutdebugging-new/index.html
@@ -2,24 +2,14 @@
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
     <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://devtools/content/aboutdebugging-new/aboutdebugging.css"/>
-    <script>
-    "use strict";
-
-    const { BrowserLoader } =
-      ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
-    const { require } = BrowserLoader({
-      baseURI: "resource://devtools/client/aboutdebugging-new/",
-      window,
-    });
-    require("./aboutdebugging");
-    </script>
+    <script type="application/javascript" src="resource://devtools/client/aboutdebugging-new/initializer.js"></script>
   </head>
   <body>
     <div id="mount"></div>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/initializer.js
@@ -0,0 +1,22 @@
+/* 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 { BrowserLoader } =
+  ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
+const { require } = BrowserLoader({
+  baseURI: "resource://devtools/client/aboutdebugging-new/",
+  window,
+});
+
+// The only purpose of this module is to load the real aboutdebugging module via the
+// BrowserLoader.
+// This cannot be done using an inline script tag in index.html because we are applying
+// CSP for about: pages in Bug 1492063.
+// And this module cannot be merged with aboutdebugging.js because modules loaded with
+// script tags are using Promises bound to the lifecycle of the document, while modules
+// loaded with a devtools loader use Promises that will still resolve if the document is
+// destroyed. This is particularly useful to ensure asynchronous destroy() calls succeed.
+require("./aboutdebugging");
--- a/devtools/client/aboutdebugging-new/moz.build
+++ b/devtools/client/aboutdebugging-new/moz.build
@@ -1,14 +1,15 @@
 # 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/.
 
 DevToolsModules(
     'aboutdebugging.js',
+    'initializer.js',
 )
 
 DIRS += [
     'src',
 ]
 
 XPCSHELL_TESTS_MANIFESTS += [
     'test/unit/xpcshell.ini'
--- a/devtools/client/aboutdebugging-new/src/actions/ui.js
+++ b/devtools/client/aboutdebugging-new/src/actions/ui.js
@@ -1,23 +1,30 @@
 /* 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 {
+  ADB_ADDON_INSTALL_START,
+  ADB_ADDON_INSTALL_SUCCESS,
+  ADB_ADDON_INSTALL_FAILURE,
+  ADB_ADDON_UNINSTALL_START,
+  ADB_ADDON_UNINSTALL_SUCCESS,
+  ADB_ADDON_UNINSTALL_FAILURE,
   ADB_ADDON_STATUS_UPDATED,
   DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
   NETWORK_LOCATIONS_UPDATED,
   PAGE_SELECTED,
   PAGES,
 } = require("../constants");
 
 const NetworkLocationsModule = require("../modules/network-locations");
+const { adbAddon } = require("devtools/shared/adb/adb-addon");
 
 const Actions = require("./index");
 
 // XXX: Isolating the code here, because it feels wrong to rely solely on the page "not"
 // being CONNECT to decide what to do. Should we have a page "type" on top of page "id"?
 function _isRuntimePage(page) {
   return page && page !== PAGES.CONNECT;
 }
@@ -64,16 +71,44 @@ function removeNetworkLocation(location)
 function updateAdbAddonStatus(adbAddonStatus) {
   return { type: ADB_ADDON_STATUS_UPDATED, adbAddonStatus };
 }
 
 function updateNetworkLocations(locations) {
   return { type: NETWORK_LOCATIONS_UPDATED, locations };
 }
 
+function installAdbAddon() {
+  return async (dispatch, getState) => {
+    dispatch({ type: ADB_ADDON_INSTALL_START });
+
+    try {
+      await adbAddon.install();
+      dispatch({ type: ADB_ADDON_INSTALL_SUCCESS });
+    } catch (e) {
+      dispatch({ type: ADB_ADDON_INSTALL_FAILURE, error: e.message });
+    }
+  };
+}
+
+function uninstallAdbAddon() {
+  return async (dispatch, getState) => {
+    dispatch({ type: ADB_ADDON_UNINSTALL_START });
+
+    try {
+      await adbAddon.uninstall();
+      dispatch({ type: ADB_ADDON_UNINSTALL_SUCCESS });
+    } catch (e) {
+      dispatch({ type: ADB_ADDON_UNINSTALL_FAILURE, error: e.message });
+    }
+  };
+}
+
 module.exports = {
   addNetworkLocation,
+  installAdbAddon,
   removeNetworkLocation,
   selectPage,
+  uninstallAdbAddon,
   updateAdbAddonStatus,
   updateDebugTargetCollapsibility,
   updateNetworkLocations,
 };
--- a/devtools/client/aboutdebugging-new/src/components/App.js
+++ b/devtools/client/aboutdebugging-new/src/components/App.js
@@ -31,26 +31,35 @@ class App extends PureComponent {
       networkLocations: PropTypes.arrayOf(PropTypes.string).isRequired,
       networkRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
       selectedPage: PropTypes.string,
       usbRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
     };
   }
 
   getSelectedPageComponent() {
-    const { dispatch, networkLocations, selectedPage } = this.props;
+    const {
+      adbAddonStatus,
+      dispatch,
+      networkLocations,
+      selectedPage
+    } = this.props;
 
     if (!selectedPage) {
       // No page selected.
       return null;
     }
 
     switch (selectedPage) {
       case PAGES.CONNECT:
-        return ConnectPage({ dispatch, networkLocations });
+        return ConnectPage({
+          adbAddonStatus,
+          dispatch,
+          networkLocations
+        });
       default:
         // All pages except for the CONNECT page are RUNTIME pages.
         return RuntimePage({ dispatch });
     }
   }
 
   render() {
     const {
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.css
@@ -0,0 +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/. */
+
+.connect-page__usb__toggle-button {
+  margin-top: calc(var(--base-distance) * 4);
+}
--- a/devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.js
+++ b/devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.js
@@ -6,28 +6,37 @@
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 
+const {
+  USB_STATES,
+} = require("../../constants");
+
+const Actions = require("../../actions/index");
+
+loader.lazyRequireGetter(this, "ADB_ADDON_STATES", "devtools/shared/adb/adb-addon", true);
+
 const ConnectSection = createFactory(require("./ConnectSection"));
 const ConnectSteps = createFactory(require("./ConnectSteps"));
 const NetworkLocationsForm = createFactory(require("./NetworkLocationsForm"));
 const NetworkLocationsList = createFactory(require("./NetworkLocationsList"));
 
 const USB_ICON_SRC = "chrome://devtools/skin/images/aboutdebugging-connect-icon.svg";
 const WIFI_ICON_SRC = "chrome://devtools/skin/images/aboutdebugging-connect-icon.svg";
 const GLOBE_ICON_SRC = "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg";
 
 class ConnectPage extends PureComponent {
   static get propTypes() {
     return {
+      adbAddonStatus: PropTypes.string,
       dispatch: PropTypes.func.isRequired,
       // Provided by wrapping the component with FluentReact.withLocalization.
       getString: PropTypes.func.isRequired,
       networkLocations: PropTypes.arrayOf(PropTypes.string).isRequired,
     };
   }
 
   renderWifi() {
@@ -49,35 +58,101 @@ class ConnectPage extends PureComponent 
             getString("about-debugging-connect-wifi-step-open-options"),
             getString("about-debugging-connect-wifi-step-enable-debug"),
           ]
         })
       )
     );
   }
 
+  onToggleUSBClick() {
+    const { adbAddonStatus } = this.props;
+    const isAddonInstalled = adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
+    if (isAddonInstalled) {
+      this.props.dispatch(Actions.uninstallAdbAddon());
+    } else {
+      this.props.dispatch(Actions.installAdbAddon());
+    }
+  }
+
+  getUsbStatus() {
+    switch (this.props.adbAddonStatus) {
+      case ADB_ADDON_STATES.INSTALLED:
+        return USB_STATES.ENABLED_USB;
+      case ADB_ADDON_STATES.UNINSTALLED:
+        return USB_STATES.DISABLED_USB;
+      default:
+        return USB_STATES.UPDATING_USB;
+    }
+  }
+
+  renderUsbToggleButton() {
+    const usbStatus = this.getUsbStatus();
+
+    const localizedStates = {
+      [USB_STATES.ENABLED_USB]: "about-debugging-connect-usb-disable-button",
+      [USB_STATES.DISABLED_USB]: "about-debugging-connect-usb-enable-button",
+      [USB_STATES.UPDATING_USB]: "about-debugging-connect-usb-updating-button",
+    };
+    const localizedState = localizedStates[usbStatus];
+
+    // Disable the button while the USB status is updating.
+    const disabled = usbStatus === USB_STATES.UPDATING_USB;
+
+    return Localized(
+      {
+        id: localizedState
+      },
+      dom.button(
+        {
+          className: "std-button connect-page__usb__toggle-button " +
+                     "js-connect-usb-toggle-button",
+          disabled,
+          onClick: () => this.onToggleUSBClick(),
+        },
+        localizedState
+      )
+    );
+  }
+
   renderUsb() {
-    const { getString } = this.props;
+    const { adbAddonStatus, getString } = this.props;
+    const isAddonInstalled = adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
     return Localized(
       {
         id: "about-debugging-connect-usb",
         attrs: { title: true }
       },
       ConnectSection(
         {
           icon: USB_ICON_SRC,
           title: "Via USB",
         },
-        ConnectSteps({
-          steps: [
-            getString("about-debugging-connect-usb-step-enable-dev-menu"),
-            getString("about-debugging-connect-usb-step-enable-debug"),
-            getString("about-debugging-connect-usb-step-plug-device"),
-          ]
-        })
+        (isAddonInstalled ?
+          ConnectSteps({
+            steps: [
+              getString("about-debugging-connect-usb-step-enable-dev-menu"),
+              getString("about-debugging-connect-usb-step-enable-debug"),
+              getString("about-debugging-connect-usb-step-plug-device"),
+            ]
+          }) :
+          Localized(
+            {
+              id: "about-debugging-connect-usb-disabled",
+            },
+            dom.aside(
+              {
+                className: "js-connect-usb-disabled-message"
+              },
+              "Enabling this will download and add the required Android USB debugging " +
+              "components to Firefox."
+            )
+          )
+        ),
+        this.renderUsbToggleButton()
       )
     );
   }
 
   renderNetwork() {
     const { dispatch, networkLocations } = this.props;
     return Localized(
       {
--- a/devtools/client/aboutdebugging-new/src/components/connect/moz.build
+++ b/devtools/client/aboutdebugging-new/src/components/connect/moz.build
@@ -1,13 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'ConnectPage.css',
     'ConnectPage.js',
     'ConnectSection.js',
     'ConnectSteps.css',
     'ConnectSteps.js',
     'NetworkLocationsForm.css',
     'NetworkLocationsForm.js',
     'NetworkLocationsList.css',
     'NetworkLocationsList.js',
--- a/devtools/client/aboutdebugging-new/src/constants.js
+++ b/devtools/client/aboutdebugging-new/src/constants.js
@@ -1,15 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const actionTypes = {
+  ADB_ADDON_INSTALL_START: "ADB_ADDON_INSTALL_START",
+  ADB_ADDON_INSTALL_SUCCESS: "ADB_ADDON_INSTALL_SUCCESS",
+  ADB_ADDON_INSTALL_FAILURE: "ADB_ADDON_INSTALL_FAILURE",
+  ADB_ADDON_UNINSTALL_START: "ADB_ADDON_UNINSTALL_START",
+  ADB_ADDON_UNINSTALL_SUCCESS: "ADB_ADDON_UNINSTALL_SUCCESS",
+  ADB_ADDON_UNINSTALL_FAILURE: "ADB_ADDON_UNINSTALL_FAILURE",
   ADB_ADDON_STATUS_UPDATED: "ADB_ADDON_STATUS_UPDATED",
   CONNECT_RUNTIME_FAILURE: "CONNECT_RUNTIME_FAILURE",
   CONNECT_RUNTIME_START: "CONNECT_RUNTIME_START",
   CONNECT_RUNTIME_SUCCESS: "CONNECT_RUNTIME_SUCCESS",
   DEBUG_TARGET_COLLAPSIBILITY_UPDATED: "DEBUG_TARGET_COLLAPSIBILITY_UPDATED",
   DISCONNECT_RUNTIME_FAILURE: "DISCONNECT_RUNTIME_FAILURE",
   DISCONNECT_RUNTIME_START: "DISCONNECT_RUNTIME_START",
   DISCONNECT_RUNTIME_SUCCESS: "DISCONNECT_RUNTIME_SUCCESS",
@@ -65,17 +71,24 @@ const SERVICE_WORKER_FETCH_STATES = {
 };
 
 const SERVICE_WORKER_STATUSES = {
   RUNNING: "RUNNING",
   REGISTERING: "REGISTERING",
   STOPPED: "STOPPED",
 };
 
+const USB_STATES = {
+  DISABLED_USB: "DISABLED_USB",
+  ENABLED_USB: "ENABLED_USB",
+  UPDATING_USB: "UPDATING_USB",
+};
+
 // flatten constants
 module.exports = Object.assign({}, {
   DEBUG_TARGETS,
   DEBUG_TARGET_PANE,
   PAGES,
   RUNTIMES,
   SERVICE_WORKER_FETCH_STATES,
   SERVICE_WORKER_STATUSES,
+  USB_STATES,
 }, actionTypes);
--- a/devtools/client/aboutdebugging-new/test/browser/browser.ini
+++ b/devtools/client/aboutdebugging-new/test/browser/browser.ini
@@ -6,16 +6,18 @@ support-files =
   head-addons-script.js
   head.js
   resources/test-adb-extension/*
   resources/test-temporary-extension/*
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
 [browser_aboutdebugging_connect_networklocations.js]
+[browser_aboutdebugging_connect_toggle_usb_devices.js]
+skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
 [browser_aboutdebugging_debug-target-pane_collapsibilities_interaction.js]
 [browser_aboutdebugging_debug-target-pane_collapsibilities_preference.js]
 [browser_aboutdebugging_debug-target-pane_empty.js]
 [browser_aboutdebugging_navigate.js]
 [browser_aboutdebugging_sidebar_network_runtimes.js]
 [browser_aboutdebugging_sidebar_usb_status.js]
 skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
 [browser_aboutdebugging_thisfirefox.js]
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_connect_networklocations.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_connect_networklocations.js
@@ -8,27 +8,17 @@
  * Check that a network location can be added and removed.
  */
 
 const TEST_NETWORK_LOCATION = "localhost:1111";
 
 add_task(async function() {
   const { document, tab } = await openAboutDebugging();
 
-  const sidebarItems = document.querySelectorAll(".js-sidebar-item");
-  const connectSidebarItem = [...sidebarItems].find(element => {
-    return element.textContent === "Connect";
-  });
-  ok(connectSidebarItem, "Sidebar contains a Connect item");
-
-  info("Click on the Connect item in the sidebar");
-  connectSidebarItem.click();
-
-  info("Wait until Connect page is displayed");
-  await waitUntil(() => document.querySelector(".js-connect-page"));
+  await selectConnectPage(document);
 
   let networkLocations = document.querySelectorAll(".js-network-location");
   is(networkLocations.length, 0, "By default, no network locations are displayed");
 
   addNetworkLocation(TEST_NETWORK_LOCATION, document);
 
   info("Wait until the new network location is visible in the list");
   await waitUntil(() => document.querySelectorAll(".js-network-location").length === 1);
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_connect_toggle_usb_devices.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ADB } = require("devtools/shared/adb/adb");
+
+/**
+ * Check that USB Devices scanning can be enabled and disabled from the connect page.
+ */
+add_task(async function() {
+  await pushPref("devtools.remote.adb.extensionURL",
+                 CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi");
+
+  const { document, tab } = await openAboutDebugging();
+
+  await selectConnectPage(document);
+
+  info("Wait until Connect page is displayed");
+  await waitUntil(() => document.querySelector(".js-connect-page"));
+
+  info("Check that by default USB devices are disabled");
+  const usbDisabledMessage = document.querySelector(".js-connect-usb-disabled-message");
+  ok(usbDisabledMessage, "A message about enabling USB devices is rendered");
+
+  const usbToggleButton = document.querySelector(".js-connect-usb-toggle-button");
+  ok(usbToggleButton, "The button to toggle USB devices debugging is rendered");
+  ok(usbToggleButton.textContent.includes("Enable"),
+    "The text of the toggle USB button is correct");
+
+  info("Click on the toggle button");
+  usbToggleButton.click();
+
+  info("Wait until the toggle button text is updated");
+  await waitUntil(() => usbToggleButton.textContent.includes("Disable"));
+  ok(!document.querySelector(".js-connect-usb-disabled-message"),
+    "The message about enabling USB devices is no longer rendered");
+
+  // Right now we are resuming as soon as "USB devices enabled" is displayed, but ADB
+  // might still be starting up. If we move to uninstall directly, the ADB startup will
+  // fail and we will have an unhandled promise rejection.
+  // See Bug 1498469.
+  info("Wait until ADB has started.");
+  await waitUntil(() => ADB.ready);
+
+  info("Click on the toggle button");
+  usbToggleButton.click();
+
+  info("Wait until the toggle button text is updated");
+  await waitUntil(() => usbToggleButton.textContent.includes("Enable"));
+  ok(document.querySelector(".js-connect-usb-disabled-message"),
+    "The message about enabling USB devices is rendered again");
+
+  await removeTab(tab);
+});
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_sidebar_usb_status.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_sidebar_usb_status.js
@@ -6,27 +6,16 @@
 const { adbAddon } = require("devtools/shared/adb/adb-addon");
 const { ADB } = require("devtools/shared/adb/adb");
 
 /**
  * This test asserts that the sidebar shows a message describing the status of the USB
  * devices scanning.
  */
 add_task(async function() {
-  // Make sure the ADB addon is removed and ADB is stopped when the test ends.
-  registerCleanupFunction(async function() {
-    try {
-      await adbAddon.uninstall();
-    } catch (e) {
-      // Will throw if the addon is already uninstalled, ignore exceptions here.
-    }
-
-    await ADB.kill();
-  });
-
   await pushPref("devtools.remote.adb.extensionURL",
                  CHROME_URL_ROOT + "resources/test-adb-extension/adb-extension-#OS#.xpi");
 
   const { document, tab } = await openAboutDebugging();
 
   const usbStatusElement = document.querySelector(".js-sidebar-usb-status");
   ok(usbStatusElement, "Sidebar shows the USB status element");
   ok(usbStatusElement.textContent.includes("USB devices disabled"),
--- a/devtools/client/aboutdebugging-new/test/browser/head.js
+++ b/devtools/client/aboutdebugging-new/test/browser/head.js
@@ -12,16 +12,28 @@
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
   this);
 
 // Load collapsibilities helpers
 Services.scriptloader.loadSubScript(
   CHROME_URL_ROOT + "debug-target-pane_collapsibilities_head.js", this);
 
+// Make sure the ADB addon is removed and ADB is stopped when the test ends.
+registerCleanupFunction(async function() {
+  try {
+    const { adbAddon } = require("devtools/shared/adb/adb-addon");
+    await adbAddon.uninstall();
+  } catch (e) {
+    // Will throw if the addon is already uninstalled, ignore exceptions here.
+  }
+  const { ADB } = require("devtools/shared/adb/adb");
+  await ADB.kill();
+});
+
 /**
  * Enable the new about:debugging panel.
  */
 async function enableNewAboutDebugging() {
   await pushPref("devtools.aboutdebugging.new-enabled", true);
 }
 
 async function openAboutDebugging(page, win) {
@@ -37,14 +49,31 @@ async function openAboutDebugging(page, 
   await waitUntil(() => document.querySelector(".app"));
 
   info("Wait until the client connection was established");
   await waitUntil(() => document.querySelector(".js-runtime-page"));
 
   return { tab, document, window };
 }
 
+/**
+ * Navigate to the Connect page. Resolves when the Connect page is rendered.
+ */
+async function selectConnectPage(doc) {
+  const sidebarItems = doc.querySelectorAll(".js-sidebar-item");
+  const connectSidebarItem = [...sidebarItems].find(element => {
+    return element.textContent === "Connect";
+  });
+  ok(connectSidebarItem, "Sidebar contains a Connect item");
+
+  info("Click on the Connect item in the sidebar");
+  connectSidebarItem.click();
+
+  info("Wait until Connect page is displayed");
+  await waitUntil(() => doc.querySelector(".js-connect-page"));
+}
+
 function findSidebarItemByText(text, document) {
   const sidebarItems = document.querySelectorAll(".js-sidebar-item");
   return [...sidebarItems].find(element => {
     return element.textContent.includes(text);
   });
 }
--- a/devtools/client/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.notftl
+++ b/devtools/client/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.notftl
@@ -54,16 +54,21 @@ about-debugging-connect-wifi-step-open-o
 
 # WiFi section step by step guide
 about-debugging-connect-wifi-step-enable-debug = Enable Remote Debugging via WiFi in the Developer Tools section
 
 # USB section of the Connect page
 about-debugging-connect-usb
   .title = Via USB
 
+about-debugging-connect-usb-disabled = Enabling this will download and add the required Android USB debugging components to Firefox.
+about-debugging-connect-usb-enable-button = Enable USB Devices
+about-debugging-connect-usb-disable-button = Disable USB Devices
+about-debugging-connect-usb-updating-button = Updating…
+
 # USB section step by step guide
 about-debugging-connect-usb-step-enable-dev-menu = Enable Developer menu on your Android device
 
 # USB section step by step guide
 about-debugging-connect-usb-step-enable-debug = Enable USB Debugging on the Android Developer Menu
 
 # USB section step by step guide
 about-debugging-connect-usb-step-plug-device = Connect the USB Device to your computer
deleted file mode 100644
--- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "manifest_version": 2,
-  "name": "test-devtools-webextension-nobg",
-  "version": "1.0",
-  "applications": {
-    "gecko": {
-      "id": "test-devtools-webextension-nobg@mozilla.org"
-    }
-  }
-}
deleted file mode 100644
--- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-noid/manifest.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "manifest_version": 2,
-  "name": "test-devtools-webextension-noid",
-  "version": "1.0"
-}
deleted file mode 100644
--- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-unknown-prop/manifest.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "manifest_version": 2,
-  "name": "test-devtools-webextension-unknown-prop",
-  "version": "1.0",
-  "applications": {
-    "gecko": {
-      "id": "test-devtools-webextension-unknown-prop@mozilla.org"
-    }
-  },
-  "browser_actions": {
-    "default_title": "WebExtension Popup Debugging",
-    "default_popup": "popup.html"
-  }
-}
deleted file mode 100644
--- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/* eslint-env browser */
-/* global browser */
-
-"use strict";
-
-document.body.innerText = "Background Page Body Test Content";
-
-// These functions are called from the following about:debugging tests:
-// - browser_addons_debug_webextension.js
-// - browser_addons_debug_webextension_popup.js
-
-// eslint-disable-next-line no-unused-vars
-function myWebExtensionAddonFunction() {
-  console.log("Background page function called", browser.runtime.getManifest());
-}
-
-// eslint-disable-next-line no-unused-vars
-function myWebExtensionShowPopup() {
-  browser.test.sendMessage("readyForOpenPopup");
-}
deleted file mode 100644
--- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "manifest_version": 2,
-  "name": "test-devtools-webextension",
-  "version": "1.0",
-  "applications": {
-    "gecko": {
-      "id": "test-devtools-webextension@mozilla.org"
-    }
-  },
-  "background": {
-    "scripts": ["bg.js"]
-  },
-  "browser_action": {
-    "default_title": "WebExtension Popup Debugging",
-    "default_popup": "popup.html"
-  }
-}
deleted file mode 100644
--- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <script src="popup.js"></script>
-  </head>
-  <body>
-    Background Page Body Test Content
-  </body>
-</html>
deleted file mode 100644
--- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/* eslint-env browser */
-/* global browser */
-
-"use strict";
-
-// This function is called from the following about:debugging test:
-// browser_addons_debug_webextension.js
-//
-// eslint-disable-next-line no-unused-vars
-function myWebExtensionPopupAddonFunction() {
-  browser.test.sendMessage("popupPageFunctionCalled", browser.runtime.getManifest());
-}
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_info.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_info.js
@@ -34,25 +34,37 @@ add_task(async function testLegacyAddon(
 });
 
 add_task(async function testWebExtension() {
   const addonId = "test-devtools-webextension-nobg@mozilla.org";
   const addonName = "test-devtools-webextension-nobg";
   const { tab, document } = await openAboutDebugging("addons");
 
   await waitForInitialAddonList(document);
+
+  const addonFile = ExtensionTestCommon.generateXPI({
+    manifest: {
+      name: addonName,
+      applications: {
+        gecko: {id: addonId},
+      },
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   await installAddon({
     document,
-    path: "addons/test-devtools-webextension-nobg/manifest.json",
+    file: addonFile,
     name: addonName,
     isWebExtension: true
   });
 
   const container = document.querySelector(`[data-addon-id="${addonId}"]`);
-  testFilePath(container, "/test/addons/test-devtools-webextension-nobg/");
+
+  testFilePath(container, addonFile.leafName);
 
   const extensionID = container.querySelector(".extension-id span");
   ok(extensionID.textContent === "test-devtools-webextension-nobg@mozilla.org");
 
   const internalUUID = container.querySelector(".internal-uuid span");
   ok(internalUUID.textContent.match(UUID_REGEX), "internalUUID is correct");
 
   const manifestURL = container.querySelector(".manifest-url");
@@ -63,19 +75,27 @@ add_task(async function testWebExtension
   await closeAboutDebugging(tab);
 });
 
 add_task(async function testTemporaryWebExtension() {
   const addonName = "test-devtools-webextension-noid";
   const { tab, document } = await openAboutDebugging("addons");
 
   await waitForInitialAddonList(document);
+
+  const addonFile = ExtensionTestCommon.generateXPI({
+    manifest: {
+      name: addonName,
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   await installAddon({
     document,
-    path: "addons/test-devtools-webextension-noid/manifest.json",
+    file: addonFile,
     name: addonName,
     isWebExtension: true
   });
 
   const addons =
     document.querySelectorAll("#temporary-extensions .addon-target-container");
   // Assuming that our temporary add-on is now at the top.
   const container = addons[addons.length - 1];
@@ -93,32 +113,45 @@ add_task(async function testTemporaryWeb
 });
 
 add_task(async function testUnknownManifestProperty() {
   const addonId = "test-devtools-webextension-unknown-prop@mozilla.org";
   const addonName = "test-devtools-webextension-unknown-prop";
   const { tab, document } = await openAboutDebugging("addons");
 
   await waitForInitialAddonList(document);
+
+  const addonFile = ExtensionTestCommon.generateXPI({
+    manifest: {
+      name: addonName,
+      applications: {
+        gecko: {id: addonId},
+      },
+      wrong_manifest_property_name: {
+      }
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   await installAddon({
     document,
-    path: "addons/test-devtools-webextension-unknown-prop/manifest.json",
+    file: addonFile,
     name: addonName,
     isWebExtension: true
   });
 
   info("Wait until the addon appears in about:debugging");
   const container = await waitUntilAddonContainer(addonName, document);
 
   info("Wait until the installation message appears for the new addon");
   await waitUntilElement(".addon-target-messages", container);
 
   const messages = container.querySelectorAll(".addon-target-message");
   ok(messages.length === 1, "there is one message");
-  ok(messages[0].textContent.match(/Error processing browser_actions/),
+  ok(messages[0].textContent.match(/Error processing wrong_manifest_property_name/),
      "the message is helpful");
   ok(messages[0].classList.contains("addon-target-warning-message"),
      "the message is a warning");
 
   await uninstallAddon({document, id: addonId, name: addonName});
 
   await closeAboutDebugging(tab);
 });
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
@@ -8,31 +8,46 @@
 const { PromiseTestUtils } = scopedCuImport("resource://testing-common/PromiseTestUtils.jsm");
 PromiseTestUtils.whitelistRejectionsGlobally(/File closed/);
 
 // Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
 requestLongerTimeout(2);
 
 const ADDON_ID = "test-devtools-webextension@mozilla.org";
 const ADDON_NAME = "test-devtools-webextension";
-const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
 
 const {
   BrowserToolboxProcess
 } = ChromeUtils.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
 
 /**
  * This test file ensures that the webextension addon developer toolbox:
  * - when the debug button is clicked on a webextension, the opened toolbox
  *   has a working webconsole with the background page as default target;
  */
 add_task(async function testWebExtensionsToolboxWebConsole() {
+  const addonFile = ExtensionTestCommon.generateXPI({
+    background: function() {
+      window.myWebExtensionAddonFunction = function() {
+        console.log("Background page function called",
+                    this.browser.runtime.getManifest());
+      };
+    },
+    manifest: {
+      name: ADDON_NAME,
+      applications: {
+        gecko: {id: ADDON_ID},
+      },
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   const {
     tab, document, debugBtn,
-  } = await setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+  } = await setupTestAboutDebuggingWebExtension(ADDON_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 */
     function findMessages(hud, text, selector = ".message") {
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
@@ -7,31 +7,43 @@
 const { PromiseTestUtils } = scopedCuImport("resource://testing-common/PromiseTestUtils.jsm");
 PromiseTestUtils.whitelistRejectionsGlobally(/File closed/);
 
 // Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
 requestLongerTimeout(2);
 
 const ADDON_ID = "test-devtools-webextension@mozilla.org";
 const ADDON_NAME = "test-devtools-webextension";
-const ADDON_PATH = "addons/test-devtools-webextension/manifest.json";
 
 const {
   BrowserToolboxProcess
 } = ChromeUtils.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
 
 /**
  * This test file ensures that the webextension addon developer toolbox:
  * - the webextension developer toolbox has a working Inspector panel, with the
  *   background page as default target;
  */
 add_task(async function testWebExtensionsToolboxInspector() {
+  const addonFile = ExtensionTestCommon.generateXPI({
+    background: function() {
+      document.body.innerText = "Background Page Body Test Content";
+    },
+    manifest: {
+      name: ADDON_NAME,
+      applications: {
+        gecko: {id: ADDON_ID},
+      },
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   const {
     tab, document, debugBtn,
-  } = await setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_PATH);
+  } = await setupTestAboutDebuggingWebExtension(ADDON_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")
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
@@ -7,75 +7,80 @@
 const { PromiseTestUtils } = scopedCuImport("resource://testing-common/PromiseTestUtils.jsm");
 PromiseTestUtils.whitelistRejectionsGlobally(/File closed/);
 
 // Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
 requestLongerTimeout(2);
 
 const ADDON_NOBG_ID = "test-devtools-webextension-nobg@mozilla.org";
 const ADDON_NOBG_NAME = "test-devtools-webextension-nobg";
-const ADDON_NOBG_PATH = "addons/test-devtools-webextension-nobg/manifest.json";
 
 const {
   BrowserToolboxProcess
 } = ChromeUtils.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
 
 /**
  * This test file ensures that the webextension addon developer toolbox:
  * - the webextension developer toolbox is connected to a fallback page when the
  *   background page is not available (and in the fallback page document body contains
  *   the expected message, which warns the user that the current page is not a real
  *   webextension context);
  */
 add_task(async function testWebExtensionsToolboxNoBackgroundPage() {
+  const addonFile = ExtensionTestCommon.generateXPI({
+    manifest: {
+      name: ADDON_NOBG_NAME,
+      applications: {
+        gecko: {id: ADDON_NOBG_ID},
+      },
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   const {
     tab, document, debugBtn,
-  } = await setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, ADDON_NOBG_PATH);
+  } = 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(inspector => {
-        return inspector.walker.querySelector(inspector.walker.rootNode, "body");
-      })
-      .then((nodeActor) => {
-        if (!nodeActor) {
-          throw new Error("nodeActor not found");
-        }
+    toolbox.selectTool("inspector").then(async inspector => {
+      const nodeActor = await inspector.walker.querySelector(
+        inspector.walker.rootNode, "body");
 
-        dump("Got a nodeActor\n");
+      if (!nodeActor) {
+        throw new Error("nodeActor not found");
+      }
 
-        if (!(nodeActor.inlineTextChild)) {
-          throw new Error("inlineTextChild not found");
-        }
+      if (!(nodeActor.inlineTextChild)) {
+        throw new Error("inlineTextChild not found");
+      }
 
-        dump("Got a nodeActor with an inline text child\n");
+      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;
+      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}"`
-          );
-        }
+      if (actualValue !== expectedValue) {
+        throw new Error(
+          `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+        );
+      }
 
-        dump("Got the expected inline text content in the selected node\n");
-        return Promise.resolve();
-      })
-      .then(() => toolbox.destroy())
-      .catch((error) => {
-        dump("Error while running code in the browser toolbox process:\n");
-        dump(error + "\n");
-        dump("stack:\n" + error.stack + "\n");
-      });
+      dump("Got the expected inline text content in the selected node\n");
+
+      await toolbox.destroy();
+    }).catch((error) => {
+      dump("Error while running code in the browser toolbox process:\n");
+      dump(error + "\n");
+      dump("stack:\n" + error.stack + "\n");
+    });
     /* eslint-enable no-undef */
   };
   env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
   registerCleanupFunction(() => {
     env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
   });
 
   const onToolboxClose = BrowserToolboxProcess.once("close");
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
@@ -7,17 +7,16 @@
 const { PromiseTestUtils } = scopedCuImport("resource://testing-common/PromiseTestUtils.jsm");
 PromiseTestUtils.whitelistRejectionsGlobally(/File closed/);
 
 // Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
 requestLongerTimeout(2);
 
 const ADDON_ID = "test-devtools-webextension@mozilla.org";
 const ADDON_NAME = "test-devtools-webextension";
-const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
 
 const {
   BrowserToolboxProcess
 } = ChromeUtils.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
 
 /**
  * This test file ensures that the webextension addon developer toolbox:
  * - when the debug button is clicked on a webextension, the opened toolbox
@@ -37,16 +36,56 @@ const {
  * Returns the widget id for an extension with the passed id.
  */
 function makeWidgetId(id) {
   id = id.toLowerCase();
   return id.replace(/[^a-z0-9_-]/g, "_");
 }
 
 add_task(async function testWebExtensionsToolboxSwitchToPopup() {
+  const addonFile = ExtensionTestCommon.generateXPI({
+    background: function() {
+      const {browser} = this;
+      window.myWebExtensionShowPopup = function() {
+        browser.test.sendMessage("readyForOpenPopup");
+      };
+    },
+    manifest: {
+      name: ADDON_NAME,
+      applications: {
+        gecko: {id: ADDON_ID},
+      },
+      browser_action: {
+        default_title: "WebExtension Popup Debugging",
+        default_popup: "popup.html",
+      },
+    },
+    files: {
+      "popup.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <script src="popup.js"></script>
+          </head>
+          <body>
+            Background Page Body Test Content
+          </body>
+        </html>
+      `,
+      "popup.js": function() {
+        const {browser} = this;
+        window.myWebExtensionPopupAddonFunction = function() {
+          browser.test.sendMessage("popupPageFunctionCalled",
+                                   browser.runtime.getManifest());
+        };
+      },
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   let onReadyForOpenPopup;
   let onPopupCustomMessage;
 
   is(Services.prefs.getBoolPref("ui.popup.disable_autohide"), false,
      "disable_autohide shoult be initially false");
 
   Management.on("startup", function listener(event, extension) {
     if (extension.name != ADDON_NAME) {
@@ -76,17 +115,17 @@ add_task(async function testWebExtension
 
     // Wait for a notification sent by a script evaluated the test addon via
     // the web console.
     onPopupCustomMessage = waitForExtensionTestMessage("popupPageFunctionCalled");
   });
 
   const {
     tab, document, debugBtn,
-  } = await setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+  } = await setupTestAboutDebuggingWebExtension(ADDON_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 */
 
--- a/devtools/client/aboutdebugging/test/browser_addons_remove.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_remove.js
@@ -39,20 +39,30 @@ add_task(async function removeLegacyExte
 
 add_task(async function removeWebextension() {
   const addonID = "test-devtools-webextension@mozilla.org";
   const addonName = "test-devtools-webextension";
 
   const { tab, document } = await openAboutDebugging("addons");
   await waitForInitialAddonList(document);
 
+  const addonFile = ExtensionTestCommon.generateXPI({
+    manifest: {
+      name: addonName,
+      applications: {
+        gecko: {id: addonID},
+      },
+    },
+  });
+  registerCleanupFunction(() => addonFile.remove(false));
+
   // Install this add-on, and verify that it appears in the about:debugging UI
   await installAddon({
     document,
-    path: "addons/test-devtools-webextension/manifest.json",
+    file: addonFile,
     name: addonName,
     isWebExtension: true,
   });
 
   ok(getTargetEl(document, addonID), "add-on is shown");
 
   info("Click on the remove button and wait until the addon container is removed");
   getRemoveButton(document, addonID).click();
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -9,16 +9,17 @@
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
   this);
 
 const { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm", {});
 const { Management } = ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
+const { ExtensionTestCommon } = ChromeUtils.import("resource://testing-common/ExtensionTestCommon.jsm", {});
 
 async function openAboutDebugging(page, win) {
   info("opening about:debugging");
   let url = "about:debugging";
   if (page) {
     url += "#" + page;
   }
 
@@ -160,22 +161,26 @@ async function waitUntilElement(selector
  * @param  {DOMDocument}  document   #tabs section container document
  * @return {DOMNode}                 target list or container element
  */
 function getTabList(document) {
   return document.querySelector("#tabs .target-list") ||
     document.querySelector("#tabs.targets");
 }
 
-async function installAddon({document, path, name, isWebExtension}) {
+async function installAddon({document, path, file, name, isWebExtension}) {
   // Mock the file picker to select a test addon
   const MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(window);
-  const file = getSupportsFile(path);
-  MockFilePicker.setFiles([file.file]);
+  if (path) {
+    file = getSupportsFile(path);
+    MockFilePicker.setFiles([file.file]);
+  } else {
+    MockFilePicker.setFiles([file]);
+  }
 
   let onAddonInstalled;
 
   if (isWebExtension) {
     onAddonInstalled = new Promise(done => {
       Management.on("startup", function listener(event, extension) {
         if (extension.name != name) {
           return;
@@ -336,17 +341,17 @@ function waitForDelayedStartupFinished(w
       }
     }, "browser-delayed-startup-finished");
   });
 }
 
 /**
  * open the about:debugging page and install an addon
  */
-async function setupTestAboutDebuggingWebExtension(name, path) {
+async function setupTestAboutDebuggingWebExtension(name, file) {
   await new Promise(resolve => {
     const options = {"set": [
       // Force enabling of addons debugging
       ["devtools.chrome.enabled", true],
       ["devtools.debugger.remote-enabled", true],
       // Disable security prompt
       ["devtools.debugger.prompt-connection", false],
       // Enable Browser toolbox test script execution via env variable
@@ -355,17 +360,17 @@ async function setupTestAboutDebuggingWe
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   const { tab, document } = await openAboutDebugging("addons");
   await waitForInitialAddonList(document);
 
   await installAddon({
     document,
-    path,
+    file,
     name,
     isWebExtension: true,
   });
 
   // Retrieve the DEBUG button for the addon
   const names = getInstalledAddonNames(document);
   const nameEl = names.filter(element => element.textContent === name)[0];
   ok(name, "Found the addon in the list");
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -474,18 +474,18 @@ Workers.prototype = {
 
   connect: function () {
     if (!Prefs.workersEnabled) {
       return;
     }
 
     this._updateWorkerList();
 
-    // `_targetFront` can be BrowsingContextTargetFront (protocol.js front) or
-    // WorkerClient/DebuggerClient (old fashion client)
+    // `_targetFront` can be BrowsingContextTargetFront/WorkerTargetFront (protocol.js
+    // front) or DebuggerClient (old fashion client)
     if (typeof(this._targetFront.on) == "function") {
       this._targetFront.on("workerListChanged", this._onWorkerListChanged);
     } else {
       this._targetFront.addListener("workerListChanged", this._onWorkerListChanged);
     }
   },
 
   disconnect: function () {
@@ -524,18 +524,18 @@ Workers.prototype = {
     });
   },
 
   _onWorkerListChanged: function () {
     this._updateWorkerList();
   },
 
   _onWorkerSelect: function (workerForm) {
-    DebuggerController.client.attachWorker(workerForm.actor).then(([response, workerClient]) => {
-      let toolbox = gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+    DebuggerController.client.attachWorker(workerForm.actor).then(([response, workerTargetFront]) => {
+      let toolbox = gDevTools.showToolbox(TargetFactory.forWorker(workerTargetFront),
                                           "jsdebugger", Toolbox.HostType.WINDOW);
       window.emit(EVENTS.WORKER_SELECTED, toolbox);
     });
   }
 };
 
 /**
  * ThreadState keeps the UI up to date with the state of the
--- a/devtools/client/debugger/new/panel.js
+++ b/devtools/client/debugger/new/panel.js
@@ -59,21 +59,21 @@ DebuggerPanel.prototype = {
     return this._store.getState();
   },
 
   openLink: function(url) {
     openContentLink(url);
   },
 
   openWorkerToolbox: async function(worker) {
-    const [response, workerClient] =
+    const [response, workerTargetFront] =
       await this.toolbox.target.client.attachWorker(worker.actor);
-    const workerTarget = TargetFactory.forWorker(workerClient);
+    const workerTarget = TargetFactory.forWorker(workerTargetFront);
     const toolbox = await gDevTools.showToolbox(workerTarget, "jsdebugger", Toolbox.HostType.WINDOW);
-    toolbox.once("destroy", () => workerClient.detach());
+    toolbox.once("destroy", () => workerTargetFront.detach());
   },
 
   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/client/firefox/events.js
+++ b/devtools/client/debugger/new/src/client/firefox/events.js
@@ -36,18 +36,18 @@ function setupEvents(dependencies) {
   });
 
   if (threadClient) {
     Object.keys(clientEvents).forEach(eventName => {
       threadClient.addListener(eventName, clientEvents[eventName]);
     });
 
     if (threadClient._parent) {
-      // Parent may be BrowsingContextTargetFront and be protocol.js.
-      // Or DebuggerClient/WorkerClient and still be old fashion actor.
+      // Parent may be BrowsingContextTargetFront/WorkerTargetFront and be protocol.js.
+      // Or DebuggerClient and still be old fashion actor.
       if (threadClient._parent.on) {
         threadClient._parent.on("workerListChanged", workerListChanged);
       } else {
         threadClient._parent.addListener("workerListChanged", workerListChanged);
       }
     }
   }
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -23,20 +23,20 @@ add_task(async function() {
   let tab = await addTab(TAB_URL);
   let { tabs } = await listTabs(client);
   let [, targetFront] = await attachTarget(client, findTab(tabs, TAB_URL));
 
   await listWorkers(targetFront);
   await createWorkerInTab(tab, WORKER_URL);
 
   let { workers } = await listWorkers(targetFront);
-  let [, workerClient] = await attachWorker(targetFront,
+  let [, workerTargetFront] = await attachWorker(targetFront,
                                              findWorker(workers, WORKER_URL));
 
-  let toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+  let toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerTargetFront),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   is(toolbox.hostType, "window", "correct host");
 
   await new Promise(done => {
     toolbox.win.parent.addEventListener("message", function onmessage(event) {
       if (event.data.name == "set-host-title") {
@@ -50,13 +50,13 @@ add_task(async function() {
 
   let toolTabs = toolbox.doc.querySelectorAll(".devtools-tab");
   let activeTools = [...toolTabs].map(tab=>tab.getAttribute("data-id"));
 
   is(activeTools.join(","), "webconsole,jsdebugger,scratchpad",
     "Correct set of tools supported by worker");
 
   terminateWorkerInTab(tab, WORKER_URL);
-  await waitForWorkerClose(workerClient);
+  await waitForWorkerClose(workerTargetFront);
   await close(client);
 
   await toolbox.destroy();
 });
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -1109,24 +1109,24 @@ function attachWorker(targetFront, worke
   return targetFront.attachWorker(worker.actor);
 }
 
 function waitForWorkerListChanged(targetFront) {
   info("Waiting for worker list to change.");
   return targetFront.once("workerListChanged");
 }
 
-function attachThread(workerClient, options) {
+function attachThread(workerTargetFront, options) {
   info("Attaching to thread.");
-  return workerClient.attachThread(options);
+  return workerTargetFront.attachThread(options);
 }
 
-async function waitForWorkerClose(workerClient) {
+async function waitForWorkerClose(workerTargetFront) {
   info("Waiting for worker to close.");
-  await workerClient.once("close");
+  await workerTargetFront.once("close");
   info("Worker did close.");
 }
 
 function resume(threadClient) {
   info("Resuming thread.");
   return threadClient.resume();
 }
 
@@ -1283,20 +1283,20 @@ 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 [, workerClient] = await attachWorker(targetFront,
+  let [, workerTargetFront] = await attachWorker(targetFront,
                                              findWorker(workers, WORKER_URL));
 
-  let toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+  let toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerTargetFront),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   let debuggerPanel = toolbox.getCurrentPanel();
   let gDebugger = debuggerPanel.panelWin;
 
-  return {client, tab, targetFront, workerClient, toolbox, gDebugger};
+  return {client, tab, targetFront, workerTargetFront, toolbox, gDebugger};
 }
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -370,20 +370,20 @@ var gDevToolsBrowser = exports.gDevTools
    * 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
    */
   async openWorkerToolbox(client, workerTargetActor) {
-    const [, workerClient] = await client.attachWorker(workerTargetActor);
-    const workerTarget = TargetFactory.forWorker(workerClient);
+    const [, workerTargetFront] = await client.attachWorker(workerTargetActor);
+    const workerTarget = TargetFactory.forWorker(workerTargetFront);
     const toolbox = await gDevTools.showToolbox(workerTarget, null, Toolbox.HostType.WINDOW);
-    toolbox.once("destroy", () => workerClient.detach());
+    toolbox.once("destroy", () => workerTargetFront.detach());
   },
 
   /**
    * Install WebIDE widget
    */
   // Used by itself
   installWebIDEWidget() {
     if (this.isWebIDEWidgetInstalled()) {
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -1,15 +1,18 @@
 # -*- 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']
+BROWSER_CHROME_MANIFESTS += [
+    'test/browser.ini',
+    'test/metrics/browser_metrics_inspector.ini'
+]
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 DIRS += [
     'components',
 ]
 
 DevToolsModules(
     'attach-thread.js',
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -115,21 +115,21 @@ const TargetFactory = exports.TargetFact
     if (targetPromise == null) {
       const target = new TabTarget(options);
       targetPromise = target.attach().then(() => target);
       promiseTargets.set(options, targetPromise);
     }
     return targetPromise;
   },
 
-  forWorker: function(workerClient) {
-    let target = targets.get(workerClient);
+  forWorker: function(workerTargetFront) {
+    let target = targets.get(workerTargetFront);
     if (target == null) {
-      target = new WorkerTarget(workerClient);
-      targets.set(workerClient, target);
+      target = new WorkerTarget(workerTargetFront);
+      targets.set(workerTargetFront, target);
     }
     return target;
   },
 
   /**
    * Creating a target for a tab that is being closed is a problem because it
    * allows a leak as a result of coming after the close event which normally
    * clears things up. This function allows us to ask if there is a known
@@ -843,29 +843,29 @@ TabTarget.prototype = {
   logWarningInPage: function(text, category) {
     if (this.activeTab && this.activeTab.traits.logInPage) {
       const warningFlag = 1;
       this.activeTab.logInPage({ text, category, flags: warningFlag });
     }
   },
 };
 
-function WorkerTarget(workerClient) {
+function WorkerTarget(workerTargetFront) {
   EventEmitter.decorate(this);
-  this._workerClient = workerClient;
+  this._workerTargetFront = workerTargetFront;
 }
 
 /**
  * A WorkerTarget represents a worker. Unlike TabTarget, which can represent
  * either a local or remote tab, WorkerTarget always represents a remote worker.
  * Moreover, unlike TabTarget, which is constructed with a placeholder object
  * for remote tabs (from which a TargetFront can then be lazily obtained),
- * WorkerTarget is constructed with a WorkerClient directly.
+ * WorkerTarget is constructed with a WorkerTargetFront directly.
  *
- * WorkerClient is designed to mimic the interface of TargetFront as closely as
+ * WorkerTargetFront is designed to mimic the interface of TargetFront as closely as
  * possible. This allows us to debug workers as if they were ordinary tabs,
  * requiring only minimal changes to the rest of the frontend.
  */
 WorkerTarget.prototype = {
   get isRemote() {
     return true;
   },
 
@@ -873,47 +873,47 @@ WorkerTarget.prototype = {
     return true;
   },
 
   get name() {
     return "Worker";
   },
 
   get url() {
-    return this._workerClient.url;
+    return this._workerTargetFront.url;
   },
 
   get isWorkerTarget() {
     return true;
   },
 
   get form() {
     return {
-      consoleActor: this._workerClient.consoleActor
+      consoleActor: this._workerTargetFront.consoleActor
     };
   },
 
   get activeTab() {
-    return this._workerClient;
+    return this._workerTargetFront;
   },
 
   get activeConsole() {
     return this.client._clients.get(this.form.consoleActor);
   },
 
   get client() {
-    return this._workerClient.client;
+    return this._workerTargetFront.client;
   },
 
   get canRewind() {
     return false;
   },
 
   destroy: function() {
-    this._workerClient.detach();
+    this._workerTargetFront.detach();
   },
 
   hasActor: function(name) {
     // console is the only one actor implemented by WorkerTargetActor
     if (name == "console") {
       return true;
     }
     return false;
--- a/devtools/client/framework/test/browser_toolbox_window_reload_target.js
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
@@ -1,98 +1,103 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Test that pressing various page reload keyboard shortcuts always works when devtools
+// has focus, no matter if it's undocked or docked, and whatever the tool selected (this
+// is to avoid tools from overriding the page reload shortcuts).
+// This test also serves as a safety net checking that tools just don't explode when the
+// page is reloaded.
+// It is therefore quite long to run.
+
 requestLongerTimeout(10);
 
 const TEST_URL = "data:text/html;charset=utf-8," +
                  "<html><head><title>Test reload</title></head>" +
                  "<body><h1>Testing reload from devtools</h1></body></html>";
 
-var {Toolbox} = require("devtools/client/framework/toolbox");
-
-const {LocalizationHelper} = require("devtools/shared/l10n");
+const { Toolbox } = require("devtools/client/framework/toolbox");
+const { LocalizationHelper } = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
 
-var target, toolbox, description, reloadsSent, toolIDs;
+// Track how many page reloads we've sent to the page.
+var reloadsSent = 0;
 
-function test() {
-  addTab(TEST_URL).then(async () => {
-    target = await TargetFactory.forTab(gBrowser.selectedTab);
+add_task(async function() {
+  await addTab(TEST_URL);
+  const target = await TargetFactory.forTab(gBrowser.selectedTab);
+  // Load the frame-script-utils into the child.
+  loadFrameScriptUtils();
 
-    toolIDs = gDevTools.getToolDefinitionArray()
-                .filter(def => def.isTargetSupported(target))
-                .map(def => def.id);
-    gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM)
-             .then(startReloadTest);
-  });
-}
+  info("Getting the entire list of tools supported in this tab");
+  const toolIDs = gDevTools.getToolDefinitionArray()
+                           .filter(def => def.isTargetSupported(target))
+                           .map(def => def.id);
 
-function startReloadTest(aToolbox) {
-  loadFrameScriptUtils(); // causes frame-script-utils to be loaded into the child.
-  toolbox = aToolbox;
+  info("Display the toolbox, docked at the bottom, with the first tool selected");
+  const toolbox = await gDevTools.showToolbox(target, toolIDs[0],
+    Toolbox.HostType.BOTTOM);
 
-  reloadsSent = 0;
-  let reloads = 0;
-  const reloadCounter = (msg) => {
-    reloads++;
-    info("Detected reload #" + reloads);
-    is(reloads, reloadsSent, "Reloaded from devtools window once and only for " + description + "");
+  info("Listen to page reloads to check that they are indeed sent by the toolbox");
+  let reloadDetected = 0;
+  const reloadCounter = msg => {
+    reloadDetected++;
+    info("Detected reload #" + reloadDetected);
+    is(reloadDetected, reloadsSent, "Detected the right number of reloads in the page");
   };
   gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", reloadCounter);
 
-  testAllTheTools("docked", () => {
-    const origHostType = toolbox.hostType;
-    toolbox.switchHost(Toolbox.HostType.WINDOW).then(() => {
-      toolbox.win.focus();
-      testAllTheTools("undocked", () => {
-        toolbox.switchHost(origHostType).then(() => {
-          gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", reloadCounter);
-          // If we finish too early, the inspector breaks promises:
-          toolbox.getPanel("inspector").once("new-root", finishUp);
-        });
-      });
-    });
-  }, toolIDs.length - 1 /* only test 1 tool in docked mode, to cut down test time */);
+  info("Start testing with the toolbox docked");
+  // Note that we actually only test 1 tool in docked mode, to cut down on test time.
+  await testOneTool(toolbox, toolIDs[toolIDs.length - 1]);
+
+  info("Switch to undocked mode");
+  await toolbox.switchHost(Toolbox.HostType.WINDOW);
+  toolbox.win.focus();
+
+  info("Now test with the toolbox undocked");
+  for (const toolID of toolIDs) {
+    await testOneTool(toolbox, toolID);
+  }
+
+  info("Switch back to docked mode");
+  await toolbox.switchHost(Toolbox.HostType.BOTTOM);
+
+  gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", reloadCounter);
+
+  await toolbox.destroy();
+  gBrowser.removeCurrentTab();
+});
+
+async function testOneTool(toolbox, toolID) {
+  info(`Select tool ${toolID}`);
+  await toolbox.selectTool(toolID);
+
+  await testReload("toolbox.reload.key", toolbox, toolID);
+  await testReload("toolbox.reload2.key", toolbox, toolID);
+  await testReload("toolbox.forceReload.key", toolbox, toolID);
+  await testReload("toolbox.forceReload2.key", toolbox, toolID);
 }
 
-function testAllTheTools(docked, callback, toolNum = 0) {
-  if (toolNum >= toolIDs.length) {
-    return callback();
-  }
-  toolbox.selectTool(toolIDs[toolNum]).then(() => {
-    testReload("toolbox.reload.key", docked, toolIDs[toolNum], () => {
-      testReload("toolbox.reload2.key", docked, toolIDs[toolNum], () => {
-        testReload("toolbox.forceReload.key", docked, toolIDs[toolNum], () => {
-          testReload("toolbox.forceReload2.key", docked, toolIDs[toolNum], () => {
-            testAllTheTools(docked, callback, toolNum + 1);
-          });
-        });
-      });
-    });
+function testReload(shortcut, toolbox, toolID) {
+  info(`Reload with ${shortcut}`);
+
+  const mm = gBrowser.selectedBrowser.messageManager;
+
+  return new Promise(resolve => {
+    // The inspector needs some special care.
+    const toolUpdated = toolID === "inspector"
+      ? toolbox.getPanel("inspector").once("new-root")
+      : Promise.resolve();
+
+    const complete = () => {
+      mm.removeMessageListener("devtools:test:load", complete);
+      toolUpdated.then(resolve);
+    };
+    mm.addMessageListener("devtools:test:load", complete);
+
+    toolbox.win.focus();
+    synthesizeKeyShortcut(L10N.getStr(shortcut), toolbox.win);
+    reloadsSent++;
   });
 }
-
-function testReload(shortcut, docked, toolID, callback) {
-  const complete = () => {
-    gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", complete);
-    return callback();
-  };
-  gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", complete);
-
-  description = docked + " devtools with tool " + toolID + ", shortcut #" + shortcut;
-  info("Testing reload in " + description);
-  toolbox.win.focus();
-  synthesizeKeyShortcut(L10N.getStr(shortcut), toolbox.win);
-  reloadsSent++;
-}
-
-function finishUp() {
-  toolbox.destroy().then(() => {
-    gBrowser.removeCurrentTab();
-
-    target = toolbox = description = reloadsSent = toolIDs = null;
-
-    finish();
-  });
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_inspector.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  head.js
+  !/devtools/client/shared/test/shared-head.js
+  !/devtools/client/shared/test/telemetry-test-helpers.js
+
+# Each metrics tests is loaded in a separate .ini file. This way the test is executed
+# individually, without any other test being executed before or after.
+[browser_metrics_inspector.js]
+skip-if = os != 'linux' || debug || asan # Results should be platform agnostic - only run on linux64-opt
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/metrics/browser_metrics_inspector.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../../shared/test/shared-head.js */
+
+/**
+ * This test records the number of modules loaded by DevTools, as well as the total count
+ * of characters in those modules, when opening the inspector. These metrics are retrieved
+ * by perfherder via logs.
+ */
+
+const TEST_URL = "data:text/html;charset=UTF-8,<div>Inspector modules load test</div>";
+
+add_task(async function() {
+  await openNewTabAndToolbox(TEST_URL, "inspector");
+
+  const allModules = getFilteredModules("");
+  const inspectorModules = getFilteredModules("devtools/client/inspector");
+
+  const allModulesCount = allModules.length;
+  const inspectorModulesCount = inspectorModules.length;
+
+  const allModulesChars = countCharsInModules(allModules);
+  const inspectorModulesChars = countCharsInModules(inspectorModules);
+
+  const PERFHERDER_DATA = {
+    framework: {
+      name: "devtools"
+    },
+    suites: [{
+      name: "inspector-metrics",
+      value: allModulesChars,
+      subtests: [
+        {
+          name: "inspector-modules",
+          value: inspectorModulesCount
+        },
+        {
+          name: "inspector-chars",
+          value: inspectorModulesChars
+        },
+        {
+          name: "all-modules",
+          value: allModulesCount
+        },
+        {
+          name: "all-chars",
+          value: allModulesChars
+        },
+      ],
+    }]
+  };
+  info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA));
+
+  // Simply check that we found valid values.
+  ok(allModulesCount > inspectorModulesCount &&
+     inspectorModulesCount > 0, "Successfully recorded module count for Inspector");
+  ok(allModulesChars > inspectorModulesChars &&
+     inspectorModulesChars > 0, "Successfully recorded char count for Inspector");
+});
+
+function getFilteredModules(filter) {
+  const modules = Object.keys(loader.provider.loader.modules);
+  return modules.filter(url => url.includes(filter));
+}
+
+function countCharsInModules(modules) {
+  return modules.reduce((sum, uri) => {
+    try {
+      return sum + require("raw!" + uri).length;
+    } catch (e) {
+      // Ignore failures
+      return sum;
+    }
+  }, 0);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/metrics/head.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../../shared/test/shared-head.js */
+/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", this);
+
+// So that PERFHERDER data can be extracted from the logs.
+SimpleTest.requestCompleteLog();
--- a/devtools/client/framework/toolbox-highlighter-utils.js
+++ b/devtools/client/framework/toolbox-highlighter-utils.js
@@ -23,48 +23,35 @@ const flags = require("devtools/shared/f
  * This should be done once only by the toolbox itself and stored there so that
  * panels can get it from there. That's because the API returned has a stateful
  * scope that would be different for another instance returned by this function.
  *
  * @param {Toolbox} toolbox
  * @return {Object} the highlighterUtils public API
  */
 exports.getHighlighterUtils = function(toolbox) {
-  if (!toolbox || !toolbox.target) {
+  if (!toolbox) {
     throw new Error("Missing or invalid toolbox passed to getHighlighterUtils");
   }
 
   // Exported API properties will go here
   const exported = {};
 
-  // The current toolbox target
-  let target = toolbox.target;
-
   // Is the highlighter currently in pick mode
   let isPicking = false;
 
   // Is the box model already displayed, used to prevent dispatching
   // unnecessary requests, especially during toolbox shutdown
   let isNodeFrontHighlighted = false;
 
   /**
    * Release this utils, nullifying the references to the toolbox
    */
   exported.release = function() {
-    toolbox = target = null;
-  };
-
-  /**
-   * Does the target have the highlighter actor.
-   * The devtools must be backwards compatible with at least B2G 1.3 (28),
-   * which doesn't have the highlighter actor. This can be removed as soon as
-   * the minimal supported version becomes 1.4 (29)
-   */
-  const isRemoteHighlightable = exported.isRemoteHighlightable = function() {
-    return target.client.traits.highlightable;
+    toolbox = null;
   };
 
   /**
    * Make a function that initializes the inspector before it runs.
    * Since the init of the inspector is asynchronous, the return value will be
    * produced by Task.async and the argument should be a generator
    * @param {Function*} generator A generator function
    * @return {Function} A function
@@ -110,59 +97,44 @@ exports.getHighlighterUtils = function(t
         return;
       }
       isPicking = true;
 
       toolbox.pickerButton.isChecked = true;
       await toolbox.selectTool("inspector", "inspect_dom");
       toolbox.on("select", cancelPicker);
 
-      if (isRemoteHighlightable()) {
-        toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
-        toolbox.walker.on("picker-node-picked", onPickerNodePicked);
-        toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed);
-        toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
+      toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
+      toolbox.walker.on("picker-node-picked", onPickerNodePicked);
+      toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed);
+      toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
 
-        await toolbox.highlighter.pick(doFocus);
-        toolbox.emit("picker-started");
-      } else {
-        // If the target doesn't have the highlighter actor, we can use the
-        // walker's pick method instead, knowing that it only responds when a node
-        // is picked (instead of emitting events)
-        toolbox.emit("picker-started");
-        const node = await toolbox.walker.pick();
-        onPickerNodePicked({node: node});
-      }
+      await toolbox.highlighter.pick(doFocus);
+      toolbox.emit("picker-started");
     });
 
   /**
    * Stop the element picker. Note that the picker is automatically stopped when
    * an element is picked
    * @return A promise that resolves when the picker has stopped or immediately
    * if it is already stopped
    */
   const stopPicker = exported.stopPicker = requireInspector(async function() {
     if (!isPicking) {
       return;
     }
     isPicking = false;
 
     toolbox.pickerButton.isChecked = false;
 
-    if (isRemoteHighlightable()) {
-      await toolbox.highlighter.cancelPick();
-      toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
-      toolbox.walker.off("picker-node-picked", onPickerNodePicked);
-      toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed);
-      toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
-    } else {
-      // If the target doesn't have the highlighter actor, use the walker's
-      // cancelPick method instead
-      await toolbox.walker.cancelPick();
-    }
+    await toolbox.highlighter.cancelPick();
+    toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
+    toolbox.walker.off("picker-node-picked", onPickerNodePicked);
+    toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed);
+    toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
 
     toolbox.off("select", cancelPicker);
     toolbox.emit("picker-stopped");
   });
 
   /**
    * Stop the picker, but also emit an event that the picker was canceled.
    */
@@ -216,23 +188,17 @@ exports.getHighlighterUtils = function(t
    */
   const highlightNodeFront = exported.highlightNodeFront = requireInspector(
   async function(nodeFront, options = {}) {
     if (!nodeFront) {
       return;
     }
 
     isNodeFrontHighlighted = true;
-    if (isRemoteHighlightable()) {
-      await toolbox.highlighter.showBoxModel(nodeFront, options);
-    } else {
-      // If the target doesn't have the highlighter actor, revert to the
-      // walker's highlight method, which draws a simple outline
-      await toolbox.walker.highlight(nodeFront);
-    }
+    await toolbox.highlighter.showBoxModel(nodeFront, options);
 
     toolbox.emit("node-highlight", nodeFront);
   });
 
   /**
    * This is a convenience method in case you don't have a nodeFront but a
    * valueGrip. This is often the case with VariablesView properties.
    * This method will simply translate the grip into a nodeFront and call
@@ -266,20 +232,17 @@ exports.getHighlighterUtils = function(t
    * in the markup view doesn't hide/show the highlighter to ease testing. The
    * highlighter stays visible at all times, except when the mouse leaves the
    * markup view, which is when this param is passed to true
    * @return a promise that resolves when the highlighter is hidden
    */
   exported.unhighlight = async function(forceHide = false) {
     forceHide = forceHide || !flags.testing;
 
-    // Note that if isRemoteHighlightable is true, there's no need to hide the
-    // highlighter as the walker uses setTimeout to hide it after some time
-    if (isNodeFrontHighlighted && forceHide && toolbox.highlighter &&
-        isRemoteHighlightable()) {
+    if (isNodeFrontHighlighted && forceHide && toolbox.highlighter) {
       isNodeFrontHighlighted = false;
       await toolbox.highlighter.hideBoxModel();
     }
 
     // unhighlight is called when destroying the toolbox, which means that by
     // now, the toolbox reference might have been nullified already.
     if (toolbox) {
       toolbox.emit("node-unhighlight");
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -2340,17 +2340,17 @@ Toolbox.prototype = {
    * Highlight a frame in the page
    */
   onHighlightFrame: async function(frameId) {
     // Need to initInspector to check presence of getNodeActorFromWindowID
     // and use the highlighter later
     await this.initInspector();
 
     // Only enable frame highlighting when the top level document is targeted
-    if (this._supportsFrameHighlight && this.rootFrameSelected) {
+    if (this.rootFrameSelected) {
       const frameActor = await this.walker.getNodeActorFromWindowID(frameId);
       this.highlighterUtils.highlightNodeFront(frameActor);
     }
   },
 
   /**
    * A handler for 'frameUpdate' packets received from the backend.
    * Following properties might be set on the packet:
@@ -2675,28 +2675,21 @@ Toolbox.prototype = {
         // TODO: replace with getFront once inspector is separated from the toolbox
         this._inspector = this.target.getInspector();
         const pref = "devtools.inspector.showAllAnonymousContent";
         const showAllAnonymousContent = Services.prefs.getBoolPref(pref);
         this._walker = await this._inspector.getWalker({ showAllAnonymousContent });
         this._selection = new Selection(this._walker);
         this._selection.on("new-node-front", this._onNewSelectedNodeFront);
 
-        if (this.highlighterUtils.isRemoteHighlightable()) {
-          this.walker.on("highlighter-ready", this._highlighterReady);
-          this.walker.on("highlighter-hide", this._highlighterHidden);
-
-          const autohide = !flags.testing;
-          this._highlighter = await this._inspector.getHighlighter(autohide);
-        }
-        if (!("_supportsFrameHighlight" in this)) {
-          // Only works with FF58+ targets
-          this._supportsFrameHighlight =
-            await this.target.actorHasMethod("domwalker", "getNodeActorFromWindowID");
-        }
+        this.walker.on("highlighter-ready", this._highlighterReady);
+        this.walker.on("highlighter-hide", this._highlighterHidden);
+
+        const autohide = !flags.testing;
+        this._highlighter = await this._inspector.getHighlighter(autohide);
       }.bind(this))();
     }
     return this._initInspector;
   },
 
   _onNewSelectedNodeFront: function() {
     // Emit a "selection-changed" event when the toolbox.selection has been set
     // to a new node (or cleared). Currently used in the WebExtensions APIs (to
@@ -2764,24 +2757,16 @@ Toolbox.prototype = {
       } else {
         await this.highlighterUtils.stopPicker();
       }
       // Temporary fix for bug #1493131 - inspector has a different life cycle
       // than most other fronts because it is closely related to the toolbox.
       this._inspector.destroy();
 
       if (this._highlighter) {
-        // Note that if the toolbox is closed, this will work fine, but will fail
-        // in case the browser is closed and will trigger a noSuchActor message.
-        // We ignore the promise that |_hideBoxModel| returns, since we should still
-        // proceed with the rest of destruction if it fails.
-        // FF42+ now does the cleanup from the actor.
-        if (!this.highlighter.traits.autoHideOnDestroy) {
-          this.highlighterUtils.unhighlight();
-        }
         await this._highlighter.destroy();
       }
       if (this._selection) {
         this._selection.off("new-node-front", this._onNewSelectedNodeFront);
         this._selection.destroy();
       }
 
       if (this.walker) {
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -1,15 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { AnimationsFront } = require("devtools/shared/fronts/animation");
 const { createElement, createFactory } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const App = createFactory(require("./components/App"));
 const CurrentTimeTimer = require("./current-time-timer");
 
@@ -97,17 +96,17 @@ class AnimationInspector {
       setSelectedNode,
       simulateAnimation,
       simulateAnimationForKeyframesProgressBar,
       toggleElementPicker,
     } = this;
 
     const target = this.inspector.target;
     const direction = this.win.document.dir;
-    this.animationsFront = new AnimationsFront(target.client, target.form);
+    this.animationsFront = target.getFront("animations");
     this.animationsFront.setWalkerActor(this.inspector.walker);
 
     this.animationsCurrentTimeListeners = [];
     this.isCurrentTimeSet = false;
 
     const provider = createElement(Provider,
       {
         id: "animationinspector",
@@ -321,17 +320,22 @@ class AnimationInspector {
         animation.off("changed", this.onAnimationStateChanged);
       }
     }
 
     // Update existing other animations as well since the currentTime would be proceeded
     // sice the scrubber position is related the currentTime.
     // Also, don't update the state of removed animations since React components
     // may refer to the same instance still.
-    animations = await this.updateAnimations(animations);
+    try {
+      animations = await this.updateAnimations(animations);
+    } catch (_) {
+      console.error(`Updating Animations failed`);
+      return;
+    }
 
     this.updateState(animations.concat(addedAnimations));
   }
 
   onElementPickerStarted() {
     this.inspector.store.dispatch(updateElementPickerEnabled(true));
   }
 
--- a/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js
@@ -5,18 +5,28 @@
 
 // Test whether the animation inspector will not crash when remove/add animations faster.
 
 add_task(async function() {
   const tab = await addTab(URL_ROOT + "doc_mutations_fast.html");
   const { inspector } = await openAnimationInspector();
 
   info("Check state of the animation inspector after fast mutations");
+  const animationsFinished = waitForAnimations(inspector);
   await startFastMutations(tab);
   ok(inspector.panelWin.document.getElementById("animation-container"),
     "Animation inspector should be live");
+  await animationsFinished;
 });
 
 async function startFastMutations(tab) {
   await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
     await content.wrappedJSObject.startFastMutations();
   });
 }
+
+function waitForAnimations(inspector) {
+  // wait at least once
+  let count = 1;
+  // queue any further waits
+  inspector.animationinspector.animationsFront.on("mutations", () => count++);
+  return waitForDispatch(inspector, "UPDATE_ANIMATIONS", () => count);
+}
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -549,16 +549,61 @@ const setStyles = async function(animati
 const waitForRendering = async function(animationInspector) {
   await Promise.all([
     waitForAllAnimationTargets(animationInspector),
     waitForAllSummaryGraph(animationInspector),
     waitForAnimationDetail(animationInspector),
   ]);
 };
 
+// Wait until an action of `type` is dispatched. If it's part of an
+// async operation, wait until the `status` field is "done" or "error"
+function _afterDispatchDone(store, type) {
+  return new Promise(resolve => {
+    store.dispatch({
+      // Normally we would use `services.WAIT_UNTIL`, but use the
+      // internal name here so tests aren't forced to always pass it
+      // in
+      type: "@@service/waitUntil",
+      predicate: action => {
+        if (action.type === type) {
+          return true;
+        }
+        return false;
+      },
+      run: (dispatch, getState, action) => {
+        resolve(action);
+      }
+    });
+  });
+}
+
+/**
+ * Wait for a specific action type to be dispatch.
+ * If an async action, will wait for it to be done.
+ * This is a custom waitForDispatch, and rather than having a number to wait on
+ * the function has a callback, that returns a number. This allows us to wait for
+ * an unknown number of dispatches.
+ *
+ * @memberof mochitest/waits
+ * @param {Object} inspector
+ * @param {String} type
+ * @param {Function} repeat
+ * @return {Promise}
+ * @static
+ */
+async function waitForDispatch(inspector, type, repeat) {
+  let count = 0;
+
+  while (count < repeat()) {
+    await _afterDispatchDone(inspector.store, type);
+    count++;
+  }
+}
+
 /**
  * Wait for rendering of animation keyframes.
  *
  * @param {AnimationInspector} inspector
  */
 
 const waitForAnimationDetail = async function(animationInspector) {
   if (animationInspector.state.selectedAnimation &&
--- a/devtools/client/inspector/flexbox/test/browser.ini
+++ b/devtools/client/inspector/flexbox/test/browser.ini
@@ -1,23 +1,26 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   doc_flexbox_simple.html
   doc_flexbox_pseudos.html
+  doc_flexbox_text_nodes.html
   head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_flexbox_highlighter_color_picker_on_ESC.js]
 [browser_flexbox_highlighter_color_picker_on_RETURN.js]
 [browser_flexbox_item_outline_exists.js]
 [browser_flexbox_item_outline_has_correct_layout.js]
 [browser_flexbox_item_outline_rotates_for_column.js]
 [browser_flexbox_pseudo_elements_are_listed.js]
 [browser_flexbox_sizing_info_exists.js]
 [browser_flexbox_sizing_info_for_pseudos.js]
+[browser_flexbox_sizing_info_for_text_nodes.js]
 [browser_flexbox_sizing_info_has_correct_sections.js]
+[browser_flexbox_text_nodes_are_listed.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js
@@ -0,0 +1,38 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the flex item sizing UI also appears for text nodes.
+
+const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html";
+
+add_task(async function() {
+  await addTab(TEST_URI);
+  const { inspector, flexboxInspector } = await openLayoutView();
+  const { document: doc } = flexboxInspector;
+
+  info("Select the first text node in the flex container");
+  const containerNode = await getNodeFront(".container", inspector);
+  const { nodes } = await inspector.walker.children(containerNode);
+  const firstTextNode = nodes[0];
+
+  const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing");
+  const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container");
+  await selectNode(firstTextNode, inspector);
+  const [flexSizingContainer] = await onFlexItemSizingRendered;
+  const [flexOutlineContainer] = await onFlexItemOutlineRendered;
+
+  ok(flexSizingContainer, "The flex sizing exists in the DOM");
+  ok(flexOutlineContainer, "The flex outline exists in the DOM");
+
+  info("Check that the various sizing sections are displayed");
+  const allSections = [...flexSizingContainer.querySelectorAll(".section")];
+  ok(allSections.length, "Sizing sections are displayed");
+
+  info("Check that the various parts of the outline are displayed");
+  const [basis, final] = [...flexOutlineContainer.querySelectorAll(
+    ".flex-outline-basis, .flex-outline-final")];
+  ok(basis && final, "The final and basis parts of the outline exist");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js
@@ -0,0 +1,27 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that text nodes that are flex items do appear in the list of items.
+
+const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html";
+
+add_task(async function() {
+  await addTab(TEST_URI);
+  const { inspector, flexboxInspector } = await openLayoutView();
+  const { document: doc } = flexboxInspector;
+
+  // Select the flex container in the inspector.
+  const onItemsListRendered = waitForDOM(doc,
+    "#layout-flexbox-container .flex-item-list");
+  await selectNode(".container", inspector);
+  const [flexItemList] = await onItemsListRendered;
+
+  const items = [...flexItemList.querySelectorAll("li")];
+  is(items.length, 3, "There are 3 items displayed in the list");
+
+  is(items[0].textContent, "#text", "The first item is a text node");
+  is(items[2].textContent, "#text", "The third item is a text node");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+.container {
+  width: 400px;
+  display: flex;
+}
+.container div {
+  flex-basis: 100px;
+  flex-shrink: 0;
+  background: #f06;
+  align-self: stretch;
+}
+</style>
+<div class="container">
+  A text node will be wrapped into an anonymous block container
+  <div></div>
+  Here is yet another text node
+</div>
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -92,31 +92,29 @@ const INSET_POINT_TYPES = ["top", "right
  * @param {Inspector} inspector
  *        Inspector toolbox panel
  * @param {Document} document
  *        The document that will contain the rule view.
  * @param {Object} store
  *        The CSS rule view can use this object to store metadata
  *        that might outlast the rule view, particularly the current
  *        set of disabled properties.
- * @param {PageStyleFront} pageStyle
- *        The PageStyleFront for communicating with the remote server.
  */
-function CssRuleView(inspector, document, store, pageStyle) {
+function CssRuleView(inspector, document, store) {
   EventEmitter.decorate(this);
 
   this.inspector = inspector;
   this.cssProperties = inspector.cssProperties;
   this.styleDocument = document;
   this.styleWindow = this.styleDocument.defaultView;
   this.store = store || {};
   // References to rules marked by various editors where they intend to write changes.
   // @see selectRule(), unselectRule()
   this.selectedRules = new Map();
-  this.pageStyle = pageStyle;
+  this.pageStyle = inspector.pageStyle;
 
   // Allow tests to override debouncing behavior, as this can cause intermittents.
   this.debounce = debounce;
 
   this._outputParser = new OutputParser(document, this.cssProperties);
 
   this._onAddRule = this._onAddRule.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -263,14 +263,13 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_rules_shapes-toggle_06.js]
 [browser_rules_shapes-toggle_07.js]
 [browser_rules_shorthand-overridden-lists.js]
 [browser_rules_strict-search-filter-computed-list_01.js]
 [browser_rules_strict-search-filter_01.js]
 [browser_rules_strict-search-filter_02.js]
 [browser_rules_strict-search-filter_03.js]
 [browser_rules_style-editor-link.js]
-skip-if = true # Bug 1309759
 [browser_rules_url-click-opens-new-tab.js]
 [browser_rules_urls-clickable.js]
 [browser_rules_user-agent-styles.js]
 [browser_rules_user-agent-styles-uneditable.js]
 [browser_rules_user-property-reset.js]
--- a/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js
+++ b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js
@@ -5,18 +5,17 @@
 "use strict";
 
 // Test the links from the rule-view to the styleeditor
 
 const STYLESHEET_DATA_URL_CONTENTS = ["#first {",
                                       "color: blue",
                                       "}"].join("\n");
 const STYLESHEET_DATA_URL =
-      `data:text/css,${encodeURIComponent(STYLESHEET_DATA_URL_CONTENTS)}`;
-const STYLESHEET_DECODED_DATA_URL = `data:text/css,${STYLESHEET_DATA_URL_CONTENTS}`;
+  `data:text/css,${encodeURIComponent(STYLESHEET_DATA_URL_CONTENTS)}`;
 
 const EXTERNAL_STYLESHEET_FILE_NAME = "doc_style_editor_link.css";
 const EXTERNAL_STYLESHEET_URL = URL_ROOT + EXTERNAL_STYLESHEET_FILE_NAME;
 
 const DOCUMENT_URL = "data:text/html;charset=utf-8," + encodeURIComponent(`
   <html>
   <head>
   <title>Rule view style editor link test</title>
@@ -50,38 +49,23 @@ const DOCUMENT_URL = "data:text/html;cha
   </html>
 `);
 
 add_task(async function() {
   await addTab(DOCUMENT_URL);
   const {toolbox, inspector, view, testActor} = await openRuleView();
   await selectNode("div", inspector);
 
-  await testInlineStyle(view);
+  testRuleViewLinkLabel(view);
   await testFirstInlineStyleSheet(view, toolbox, testActor);
   await testSecondInlineStyleSheet(view, toolbox, testActor);
   await testExternalStyleSheet(view, toolbox, testActor);
   await testDisabledStyleEditor(view, toolbox);
 });
 
-async function testInlineStyle(view) {
-  info("Testing inline style");
-
-  const onTab = waitForTab();
-  info("Clicking on the first link in the rule-view");
-  clickLinkByIndex(view, 0);
-
-  const tab = await onTab;
-
-  const tabURI = tab.linkedBrowser.documentURI.spec;
-  ok(tabURI.startsWith("view-source:"), "View source tab is open");
-  info("Closing tab");
-  gBrowser.removeTab(tab);
-}
-
 async function testFirstInlineStyleSheet(view, toolbox, testActor) {
   info("Testing inline stylesheet");
 
   info("Listening for toolbox switch to the styleeditor");
   const onSwitch = waitForStyleEditor(toolbox);
 
   info("Clicking an inline stylesheet");
   clickLinkByIndex(view, 4);
@@ -98,17 +82,16 @@ async function testSecondInlineStyleShee
   info("Waiting for the stylesheet editor to be selected");
   const panel = toolbox.getCurrentPanel();
   const onSelected = panel.UI.once("editor-selected");
 
   info("Switching back to the inspector panel in the toolbox");
   await toolbox.selectTool("inspector");
 
   info("Clicking on second inline stylesheet link");
-  testRuleViewLinkLabel(view);
   clickLinkByIndex(view, 3);
   const editor = await onSelected;
 
   is(toolbox.currentToolId, "styleeditor",
     "The style editor is selected again");
   await validateStyleEditorSheet(editor, 1, testActor);
 }
 
@@ -118,35 +101,34 @@ async function testExternalStyleSheet(vi
   info("Waiting for the stylesheet editor to be selected");
   const panel = toolbox.getCurrentPanel();
   const onSelected = panel.UI.once("editor-selected");
 
   info("Switching back to the inspector panel in the toolbox");
   await toolbox.selectTool("inspector");
 
   info("Clicking on an external stylesheet link");
-  testRuleViewLinkLabel(view);
   clickLinkByIndex(view, 1);
   const editor = await onSelected;
 
   is(toolbox.currentToolId, "styleeditor",
     "The style editor is selected again");
   await validateStyleEditorSheet(editor, 2, testActor);
 }
 
 async function validateStyleEditorSheet(editor, expectedSheetIndex, testActor) {
   info("validating style editor stylesheet");
   is(editor.styleSheet.styleSheetIndex, expectedSheetIndex,
      "loaded stylesheet index matches document stylesheet");
 
   const href = editor.styleSheet.href || editor.styleSheet.nodeHref;
 
   const expectedHref = await testActor.eval(
-    `content.document.styleSheets[${expectedSheetIndex}].href ||
-     content.document.location.href`);
+    `document.styleSheets[${expectedSheetIndex}].href ||
+     document.location.href`);
 
   is(href, expectedHref, "loaded stylesheet href matches document stylesheet");
 }
 
 async function testDisabledStyleEditor(view, toolbox) {
   info("Testing with the style editor disabled");
 
   info("Switching to the inspector panel in the toolbox");
@@ -178,19 +160,19 @@ async function testDisabledStyleEditor(v
 function testRuleViewLinkLabel(view) {
   info("Checking the data URL link label");
 
   let link = getRuleViewLinkByIndex(view, 1);
   let labelElem = link.querySelector(".ruleview-rule-source-label");
   let value = labelElem.textContent;
   let tooltipText = labelElem.getAttribute("title");
 
-  is(value, `${STYLESHEET_DATA_URL_CONTENTS}:1`,
+  is(value, encodeURIComponent(STYLESHEET_DATA_URL_CONTENTS) + ":1",
     "Rule view data URL stylesheet display value matches contents");
-  is(tooltipText, `${STYLESHEET_DECODED_DATA_URL}:1`,
+  is(tooltipText, STYLESHEET_DATA_URL + ":1",
     "Rule view data URL stylesheet tooltip text matches the full URI path");
 
   info("Checking the external link label");
   link = getRuleViewLinkByIndex(view, 2);
   labelElem = link.querySelector(".ruleview-rule-source-label");
   value = labelElem.textContent;
   tooltipText = labelElem.getAttribute("title");
 
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -278,17 +278,22 @@ RuleEditor.prototype = {
     if (this.source.hasAttribute("unselectable") || !this._currentLocation) {
       return;
     }
 
     const target = this.ruleView.inspector.target;
     if (Tools.styleEditor.isTargetSupported(target)) {
       gDevTools.showToolbox(target, "styleeditor").then(toolbox => {
         const {url, line, column} = this._currentLocation;
-        toolbox.getCurrentPanel().selectStyleSheet(url, line, column);
+
+        if (!this.rule.sheet.href && this.rule.sheet.nodeHref) {
+          toolbox.getCurrentPanel().selectStyleSheet(this.rule.sheet, line, column);
+        } else {
+          toolbox.getCurrentPanel().selectStyleSheet(url, line, column);
+        }
       });
     }
   },
 
   /**
    * Update the text of the source link to reflect whether we're showing
    * original sources or not.  This is a callback for
    * SourceMapURLService.subscribe, which see.
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -353,13 +353,9 @@ pref("devtools.aboutdebugging.collapsibi
 // about:debugging: only show system add-ons in local builds by default.
 #ifdef MOZILLA_OFFICIAL
   pref("devtools.aboutdebugging.showSystemAddons", false);
 #else
   pref("devtools.aboutdebugging.showSystemAddons", true);
 #endif
 
 // Map top-level await expressions in the console
-#if defined(NIGHTLY_BUILD)
 pref("devtools.debugger.features.map-await-expression", true);
-#else
-pref("devtools.debugger.features.map-await-expression", false);
-#endif
--- 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 [, workerClient1] = yield attachWorker(targetFront,
+    let [, workerTargetFront1] = yield attachWorker(targetFront,
                                                findWorker(workers, WORKER1_URL));
-    is(workerClient1.isClosed, false, "worker in tab 1 should not be closed");
+    is(workerTargetFront1.isClosed, false, "worker in tab 1 should not be closed");
 
     executeSoon(() => {
       BrowserTestUtils.loadURI(tab.linkedBrowser, TAB2_URL);
     });
-    yield waitForWorkerClose(workerClient1);
-    is(workerClient1.isClosed, true, "worker in tab 1 should be closed");
+    yield waitForWorkerClose(workerTargetFront1);
+    is(workerTargetFront1.isClosed, true, "worker in tab 1 should be closed");
 
     yield createWorkerInTab(tab, WORKER2_URL);
     ({ workers } = yield listWorkers(targetFront));
-    const [, workerClient2] = yield attachWorker(targetFront,
+    const [, workerTargetFront2] = yield attachWorker(targetFront,
                                                findWorker(workers, WORKER2_URL));
-    is(workerClient2.isClosed, false, "worker in tab 2 should not be closed");
+    is(workerTargetFront2.isClosed, false, "worker in tab 2 should not be closed");
 
     executeSoon(() => {
       tab.linkedBrowser.goBack();
     });
-    yield waitForWorkerClose(workerClient2);
-    is(workerClient2.isClosed, true, "worker in tab 2 should be closed");
+    yield waitForWorkerClose(workerTargetFront2);
+    is(workerTargetFront2.isClosed, true, "worker in tab 2 should be closed");
 
     ({ workers } = yield listWorkers(targetFront));
-    [, workerClient1] = yield attachWorker(targetFront,
+    [, workerTargetFront1] = yield attachWorker(targetFront,
                                            findWorker(workers, WORKER1_URL));
-    is(workerClient1.isClosed, false, "worker in tab 1 should not be closed");
+    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-console-01.js
+++ b/devtools/client/shared/test/browser_dbg_worker-console-01.js
@@ -13,22 +13,22 @@
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/helper_workers.js",
   this);
 
 var TAB_URL = EXAMPLE_URL + "doc_WorkerTargetActor.attachThread-tab.html";
 var WORKER_URL = "code_WorkerTargetActor.attachThread-worker.js";
 
 add_task(async function testNormalExecution() {
-  const {client, tab, workerClient, toolbox} =
+  const {client, tab, workerTargetFront, toolbox} =
     await initWorkerDebugger(TAB_URL, WORKER_URL);
 
   const jsterm = await getSplitConsole(toolbox);
   const executed = await jsterm.execute("this.location.toString()");
   ok(executed.textContent.includes(WORKER_URL),
       "Evaluating the global's location works");
 
   terminateWorkerInTab(tab, WORKER_URL);
-  await waitForWorkerClose(workerClient);
-  await gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+  await waitForWorkerClose(workerTargetFront);
+  await gDevTools.closeToolbox(TargetFactory.forWorker(workerTargetFront));
   await close(client);
   await removeTab(tab);
 });
--- a/devtools/client/shared/test/browser_dbg_worker-console-02.js
+++ b/devtools/client/shared/test/browser_dbg_worker-console-02.js
@@ -14,17 +14,17 @@ Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/helper_workers.js",
   this);
 
 var TAB_URL = EXAMPLE_URL + "doc_WorkerTargetActor.attachThread-tab.html";
 var WORKER_URL = "code_WorkerTargetActor.attachThread-worker.js";
 
 add_task(async function testWhilePaused() {
   const dbg = await initWorkerDebugger(TAB_URL, WORKER_URL);
-  const {client, tab, workerClient, toolbox} = dbg;
+  const {client, tab, workerTargetFront, toolbox} = dbg;
 
   // Execute some basic math to make sure evaluations are working.
   const jsterm = await getSplitConsole(toolbox);
   let executed = await jsterm.execute("10000+1");
   ok(executed.textContent.includes("10001"), "Text for message appeared correct");
 
   await clickElement(dbg, "pause");
   once(dbg.client, "willInterrupt").then(() => {
@@ -50,13 +50,13 @@ add_task(async function testWhilePaused(
   info("Trying to get the result of command3");
   executed = await command3;
   ok(executed.textContent.includes("ReferenceError: foobar is not defined"),
      "command3 executed successfully");
 
   await resume(dbg);
 
   terminateWorkerInTab(tab, WORKER_URL);
-  await waitForWorkerClose(workerClient);
-  await gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+  await waitForWorkerClose(workerTargetFront);
+  await gDevTools.closeToolbox(TargetFactory.forWorker(workerTargetFront));
   await close(client);
   await removeTab(tab);
 });
--- a/devtools/client/shared/test/browser_dbg_worker-console-03.js
+++ b/devtools/client/shared/test/browser_dbg_worker-console-03.js
@@ -15,17 +15,17 @@ Services.scriptloader.loadSubScript(
   this);
 
 var TAB_URL = EXAMPLE_URL + "doc_WorkerTargetActor.attachThread-tab.html";
 var WORKER_URL = "code_WorkerTargetActor.attachThread-worker.js";
 
 // Test to see if creating the pause from the console works.
 add_task(async function testPausedByConsole() {
   const dbg = await initWorkerDebugger(TAB_URL, WORKER_URL);
-  const {client, tab, workerClient, toolbox} = dbg;
+  const {client, tab, workerTargetFront, toolbox} = dbg;
 
   const jsterm = await getSplitConsole(toolbox);
   let executed = await jsterm.execute("10000+1");
   ok(executed.textContent.includes("10001"),
       "Text for message appeared correct");
 
   await clickElement(dbg, "pause");
 
@@ -41,13 +41,13 @@ add_task(async function testPausedByCons
   info("Waiting for a resume");
   await clickElement(dbg, "resume");
 
   executed = await pausedExecution;
   ok(executed.textContent.includes("10002"),
       "Text for message appeared correct");
 
   terminateWorkerInTab(tab, WORKER_URL);
-  await waitForWorkerClose(workerClient);
-  await gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+  await waitForWorkerClose(workerTargetFront);
+  await gDevTools.closeToolbox(TargetFactory.forWorker(workerTargetFront));
   await close(client);
   await removeTab(tab);
 });
--- a/devtools/client/shared/test/browser_dbg_worker-console-04.js
+++ b/devtools/client/shared/test/browser_dbg_worker-console-04.js
@@ -22,28 +22,28 @@ Services.scriptloader.loadSubScript(
 // See bug 1018184 for resolving these issues.
 const { PromiseTestUtils } = scopedCuImport("resource://testing-common/PromiseTestUtils.jsm");
 PromiseTestUtils.whitelistRejectionsGlobally(/connection just closed/);
 
 const TAB_URL = EXAMPLE_URL + "doc_WorkerTargetActor.attachThread-tab.html";
 const WORKER_URL = "code_WorkerTargetActor.attachThread-worker.js";
 
 add_task(async function testPausedByConsole() {
-  const {client, tab, workerClient, toolbox} =
+  const {client, tab, workerTargetFront, toolbox} =
     await initWorkerDebugger(TAB_URL, WORKER_URL);
 
   info("Check Date objects can be used in the console");
   const jsterm = await getSplitConsole(toolbox);
   let executed = await jsterm.execute("new Date(0)");
   ok(executed.textContent.includes("1970-01-01T00:00:00.000Z"),
       "Text for message appeared correct");
 
   info("Check RegExp objects can be used in the console");
   executed = await jsterm.execute("new RegExp('.*')");
   ok(executed.textContent.includes("/.*/"),
       "Text for message appeared correct");
 
   terminateWorkerInTab(tab, WORKER_URL);
-  await waitForWorkerClose(workerClient);
-  await gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+  await waitForWorkerClose(workerTargetFront);
+  await gDevTools.closeToolbox(TargetFactory.forWorker(workerTargetFront));
   await close(client);
   await removeTab(tab);
 });
--- a/devtools/client/shared/test/helper_workers.js
+++ b/devtools/client/shared/test/helper_workers.js
@@ -113,24 +113,24 @@ function findWorker(workers, url) {
   return null;
 }
 
 function attachWorker(targetFront, worker) {
   info("Attaching to worker with url '" + worker.url + "'.");
   return targetFront.attachWorker(worker.actor);
 }
 
-function attachThread(workerClient, options) {
+function attachThread(workerTargetFront, options) {
   info("Attaching to thread.");
-  return workerClient.attachThread(options);
+  return workerTargetFront.attachThread(options);
 }
 
-async function waitForWorkerClose(workerClient) {
+async function waitForWorkerClose(workerTargetFront) {
   info("Waiting for worker to close.");
-  await workerClient.once("close");
+  await workerTargetFront.once("close");
   info("Worker did close.");
 }
 
 // Return a promise with a reference to jsterm, opening the split
 // console if necessary.  This cleans up the split console pref so
 // it won't pollute other tests.
 function getSplitConsole(toolbox, win) {
   if (!win) {
@@ -159,30 +159,30 @@ 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 [, workerClient] = await attachWorker(targetFront,
+  const [, workerTargetFront] = await attachWorker(targetFront,
                                              findWorker(workers, WORKER_URL));
 
-  const toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+  const toolbox = await gDevTools.showToolbox(TargetFactory.forWorker(workerTargetFront),
                                             "jsdebugger",
                                             Toolbox.HostType.WINDOW);
 
   const debuggerPanel = toolbox.getCurrentPanel();
 
   const gDebugger = debuggerPanel.panelWin;
 
   const context = createDebuggerContext(toolbox);
 
-  return { ...context, client, tab, targetFront, workerClient, toolbox, gDebugger};
+  return { ...context, client, tab, targetFront, workerTargetFront, toolbox, gDebugger};
 }
 
 // Override addTab/removeTab as defined by shared-head, since these have
 // an extra window parameter and add a frame script
 this.addTab = function addTab(url, win) {
   info("Adding tab: " + url);
 
   const deferred = getDeferredPromise().defer();
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -545,16 +545,17 @@ class JSTerm extends Component {
     const inspectorSelection = this.hud.owner.getInspectorSelection();
     if (inspectorSelection && inspectorSelection.nodeFront) {
       selectedNodeActor = inspectorSelection.nodeFront.actorID;
     }
 
     const { ConsoleCommand } = require("devtools/client/webconsole/types");
     const cmdMessage = new ConsoleCommand({
       messageText: executeString,
+      timeStamp: Date.now(),
     });
     this.hud.proxy.dispatchMessageAdd(cmdMessage);
 
     let mappedExpressionRes = null;
     try {
       mappedExpressionRes = await this.hud.owner.getMappedExpression(executeString);
     } catch (e) {
       console.warn("Error when calling getMappedExpression", e);
--- a/devtools/client/webconsole/components/message-types/ConsoleCommand.js
+++ b/devtools/client/webconsole/components/message-types/ConsoleCommand.js
@@ -30,26 +30,28 @@ function ConsoleCommand(props) {
   } = props;
 
   const {
     indent,
     source,
     type,
     level,
     messageText,
+    timeStamp,
   } = message;
 
   // This uses a Custom Element to syntax highlight when possible. If it's not
   // (no CodeMirror editor), then it will just render text.
   const messageBody = createElement("syntax-highlighted", null, messageText);
   return Message({
     source,
     type,
     level,
     topLevelClasses: [],
     messageBody,
     serviceContainer,
     indent,
+    timeStamp,
     timestampsVisible,
   });
 }
 
 module.exports = ConsoleCommand;
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -200,16 +200,17 @@ skip-if = verify
 [browser_jsterm_autocomplete_inside_text.js]
 [browser_jsterm_autocomplete_native_getters.js]
 [browser_jsterm_autocomplete_nav_and_tab_key.js]
 [browser_jsterm_autocomplete_paste_undo.js]
 [browser_jsterm_autocomplete_return_key_no_selection.js]
 [browser_jsterm_autocomplete_return_key.js]
 [browser_jsterm_autocomplete_width.js]
 [browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js]
+[browser_jsterm_await_concurrent.js]
 [browser_jsterm_await_error.js]
 [browser_jsterm_await_helper_dollar_underscore.js]
 [browser_jsterm_await_paused.js]
 [browser_jsterm_await.js]
 [browser_jsterm_completion_bracket_cached_results.js]
 [browser_jsterm_completion_bracket.js]
 [browser_jsterm_completion_case_sensitivity.js]
 [browser_jsterm_completion.js]
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_await.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_await.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Test that top-level await expression work as expected.
+// Test that top-level await expressions work as expected.
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf-8,Web Console test top-level await";
 
 add_task(async function() {
   // Enable await mapping.
   await pushPref("devtools.debugger.features.map-await-expression", true);
@@ -16,70 +16,55 @@ add_task(async function() {
   await performTests();
   // And then run it with the CodeMirror-powered one.
   await pushPref("devtools.webconsole.jsterm.codeMirror", true);
   await performTests();
 });
 
 async function performTests() {
   const hud = await openNewTabAndConsole(TEST_URI);
-  const {jsterm} = hud;
 
   const executeAndWaitForResultMessage = (input, expectedOutput) =>
     executeAndWaitForMessage(hud, input, expectedOutput, ".result");
 
   info("Evaluate a top-level await expression");
   const simpleAwait = `await new Promise(r => setTimeout(() => r(["await1"]), 500))`;
   await executeAndWaitForResultMessage(
     simpleAwait,
     `Array [ "await1" ]`,
   );
 
   // Check that the resulting promise of the async iife is not displayed.
-  let messages = hud.ui.outputNode.querySelectorAll(".message .message-body");
-  let messagesText = Array.from(messages).map(n => n.textContent).join(" - ");
+  const messages = hud.ui.outputNode.querySelectorAll(".message .message-body");
+  const messagesText = Array.from(messages).map(n => n.textContent).join(" - ");
   is(messagesText, `${simpleAwait} - Array [ "await1" ]`,
     "The output contains the the expected messages");
 
+  // Check that the timestamp of the result is accurate
+  const {
+    visibleMessages,
+    messagesById
+  } = hud.ui.consoleOutput.getStore().getState().messages;
+  const [commandId, resultId] = visibleMessages;
+  const delta = messagesById.get(resultId).timeStamp -
+    messagesById.get(commandId).timeStamp;
+  ok(delta >= 500,
+    `The result has a timestamp at least 500ms (${delta}ms) older than the command`);
+
   info("Check that assigning the result of a top-level await expression works");
   await executeAndWaitForResultMessage(
     `x = await new Promise(r => setTimeout(() => r("await2"), 500))`,
     `await2`,
   );
 
   let message = await executeAndWaitForResultMessage(
     `"-" + x + "-"`,
     `"-await2-"`,
   );
   ok(message.node, "`x` was assigned as expected");
 
-  info("Check that concurrent await expression work fine");
-  hud.ui.clearOutput();
-  const delays = [1000, 500, 2000, 1500];
-  const inputs = delays.map(delay => `await new Promise(
-    r => setTimeout(() => r("await-concurrent-" + ${delay}), ${delay}))`);
-
-  // Let's wait for the message that sould be displayed last.
-  const onMessage = waitForMessage(hud, "await-concurrent-2000", ".message.result");
-  for (const input of inputs) {
-    jsterm.execute(input);
-  }
-  await onMessage;
-
-  messages = hud.ui.outputNode.querySelectorAll(".message .message-body");
-  messagesText = Array.from(messages).map(n => n.textContent);
-  const expectedMessages = [
-    ...inputs,
-    `"await-concurrent-500"`,
-    `"await-concurrent-1000"`,
-    `"await-concurrent-1500"`,
-    `"await-concurrent-2000"`,
-  ];
-  is(JSON.stringify(messagesText, null, 2), JSON.stringify(expectedMessages, null, 2),
-    "The output contains the the expected messages, in the expected order");
-
   info("Check that a logged promise is still displayed as a promise");
   message = await executeAndWaitForResultMessage(
     `new Promise(r => setTimeout(() => r(1), 1000))`,
     `Promise {`,
   );
   ok(message, "Promise are displayed as expected");
 }
copy from devtools/client/webconsole/test/mochitest/browser_jsterm_await.js
copy to devtools/client/webconsole/test/mochitest/browser_jsterm_await_concurrent.js
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_await.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_await_concurrent.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Test that top-level await expression work as expected.
+// Test that multiple concurrent top-level await expressions work as expected.
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf-8,Web Console test top-level await";
 
 add_task(async function() {
   // Enable await mapping.
   await pushPref("devtools.debugger.features.map-await-expression", true);
@@ -18,68 +18,32 @@ add_task(async function() {
   await pushPref("devtools.webconsole.jsterm.codeMirror", true);
   await performTests();
 });
 
 async function performTests() {
   const hud = await openNewTabAndConsole(TEST_URI);
   const {jsterm} = hud;
 
-  const executeAndWaitForResultMessage = (input, expectedOutput) =>
-    executeAndWaitForMessage(hud, input, expectedOutput, ".result");
-
-  info("Evaluate a top-level await expression");
-  const simpleAwait = `await new Promise(r => setTimeout(() => r(["await1"]), 500))`;
-  await executeAndWaitForResultMessage(
-    simpleAwait,
-    `Array [ "await1" ]`,
-  );
-
-  // Check that the resulting promise of the async iife is not displayed.
-  let messages = hud.ui.outputNode.querySelectorAll(".message .message-body");
-  let messagesText = Array.from(messages).map(n => n.textContent).join(" - ");
-  is(messagesText, `${simpleAwait} - Array [ "await1" ]`,
-    "The output contains the the expected messages");
-
-  info("Check that assigning the result of a top-level await expression works");
-  await executeAndWaitForResultMessage(
-    `x = await new Promise(r => setTimeout(() => r("await2"), 500))`,
-    `await2`,
-  );
-
-  let message = await executeAndWaitForResultMessage(
-    `"-" + x + "-"`,
-    `"-await2-"`,
-  );
-  ok(message.node, "`x` was assigned as expected");
-
-  info("Check that concurrent await expression work fine");
   hud.ui.clearOutput();
-  const delays = [1000, 500, 2000, 1500];
+  const delays = [3000, 500, 9000, 6000];
   const inputs = delays.map(delay => `await new Promise(
     r => setTimeout(() => r("await-concurrent-" + ${delay}), ${delay}))`);
 
   // Let's wait for the message that sould be displayed last.
-  const onMessage = waitForMessage(hud, "await-concurrent-2000", ".message.result");
+  const onMessage = waitForMessage(hud, "await-concurrent-9000", ".message.result");
   for (const input of inputs) {
     jsterm.execute(input);
   }
   await onMessage;
 
-  messages = hud.ui.outputNode.querySelectorAll(".message .message-body");
-  messagesText = Array.from(messages).map(n => n.textContent);
+  const messages = hud.ui.outputNode.querySelectorAll(".message .message-body");
+  const messagesText = Array.from(messages).map(n => n.textContent);
   const expectedMessages = [
     ...inputs,
     `"await-concurrent-500"`,
-    `"await-concurrent-1000"`,
-    `"await-concurrent-1500"`,
-    `"await-concurrent-2000"`,
+    `"await-concurrent-3000"`,
+    `"await-concurrent-6000"`,
+    `"await-concurrent-9000"`,
   ];
   is(JSON.stringify(messagesText, null, 2), JSON.stringify(expectedMessages, null, 2),
     "The output contains the the expected messages, in the expected order");
-
-  info("Check that a logged promise is still displayed as a promise");
-  message = await executeAndWaitForResultMessage(
-    `new Promise(r => setTimeout(() => r(1), 1000))`,
-    `Promise {`,
-  );
-  ok(message, "Promise are displayed as expected");
 }
--- a/devtools/client/webconsole/types.js
+++ b/devtools/client/webconsole/types.js
@@ -17,16 +17,17 @@ exports.ConsoleCommand = function(props)
     allowRepeating: false,
     messageText: null,
     source: MESSAGE_SOURCE.JAVASCRIPT,
     type: MESSAGE_TYPE.COMMAND,
     level: MESSAGE_LEVEL.LOG,
     groupId: null,
     indent: 0,
     private: false,
+    timeStamp: null,
   }, props);
 };
 
 exports.ConsoleMessage = function(props) {
   return Object.assign({
     id: null,
     allowRepeating: true,
     source: null,
--- a/devtools/client/webconsole/webconsole-output-wrapper.js
+++ b/devtools/client/webconsole/webconsole-output-wrapper.js
@@ -61,17 +61,17 @@ WebConsoleOutputWrapper.prototype = {
             timeStamp,
           }]));
         },
         hudProxy: hud.proxy,
         openLink: (url, e) => {
           hud.owner.openLink(url, e);
         },
         canRewind: () => {
-          if (!hud.owner.target.activeTab) {
+          if (!(hud.owner && hud.owner.target && hud.owner.target.activeTab)) {
             return false;
           }
 
           return hud.owner.target.activeTab.traits.canRewind;
         },
         createElement: nodename => {
           return this.document.createElement(nodename);
         },
--- a/devtools/client/webide/content/webide.js
+++ b/devtools/client/webide/content/webide.js
@@ -227,18 +227,16 @@ var UI = {
 
   cancelBusyTimeout: function() {
     clearTimeout(this._busyTimeout);
   },
 
   busyWithProgressUntil: function(promise, operationDescription) {
     const busy = this.busyUntil(promise, operationDescription);
     const win = document.querySelector("window");
-    const progress = document.querySelector("#action-busy-determined");
-    progress.mode = "undetermined";
     win.classList.add("busy-determined");
     win.classList.remove("busy-undetermined");
     return busy;
   },
 
   busyUntil: function(promise, operationDescription) {
     // Freeze the UI until the promise is resolved. A timeout will unfreeze the
     // UI, just in case the promise never gets resolved.
--- a/devtools/client/webide/content/webide.xul
+++ b/devtools/client/webide/content/webide.xul
@@ -119,17 +119,17 @@
 
     <vbox flex="1">
       <hbox id="action-buttons-container" class="busy">
         <toolbarbutton id="action-button-play"  class="action-button" command="cmd_play" tooltiptext="&projectMenu_play_label;"/>
         <toolbarbutton id="action-button-stop"  class="action-button" command="cmd_stop" tooltiptext="&projectMenu_stop_label;"/>
         <toolbarbutton id="action-button-debug" class="action-button" command="cmd_toggleToolbox" tooltiptext="&projectMenu_debug_label;"/>
         <hbox id="action-busy" align="center">
           <html:img id="action-busy-undetermined" src="chrome://webide/skin/throbber.svg"/>
-          <progressmeter id="action-busy-determined"/>
+          <html:progress id="action-busy-determined"/>
         </hbox>
       </hbox>
 
       <hbox id="panel-buttons-container">
         <spacer flex="1"/>
         <toolbarbutton id="runtime-panel-button" class="panel-button">
           <image class="panel-button-image"/>
           <label class="panel-button-label" value="&runtimeButton_label;"/>
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -108,19 +108,16 @@ exports.HighlighterActor = protocol.Acto
 
   get conn() {
     return this._inspector && this._inspector.conn;
   },
 
   form: function() {
     return {
       actor: this.actorID,
-      traits: {
-        autoHideOnDestroy: true
-      }
     };
   },
 
   _createHighlighter: function() {
     this._isPreviousWindowXUL = isXUL(this._targetActor.window);
 
     if (!this._isPreviousWindowXUL) {
       this._highlighter = new BoxModelHighlighter(this._highlighterEnv,
--- a/devtools/server/actors/layout.js
+++ b/devtools/server/actors/layout.js
@@ -310,33 +310,39 @@ const LayoutActor = ActorClassWithSpec(l
     if (node.rawNode) {
       node = node.rawNode;
     }
 
     const treeWalker = this.walker.getDocumentWalker(node, SHOW_ELEMENT);
     let currentNode = treeWalker.currentNode;
     let displayType = this.walker.getNode(currentNode).displayType;
 
-    if (!displayType) {
-      return null;
-    }
-
-    if (type == "flex") {
-      if (displayType == "inline-flex" || displayType == "flex") {
-        return new FlexboxActor(this, currentNode);
-      } else if (onlyLookAtCurrentNode) {
+    // If the node is an element, check first if it is itself a flex or a grid.
+    if (currentNode.nodeType === currentNode.ELEMENT_NODE) {
+      if (!displayType) {
         return null;
       }
-    } else if (type == "grid" &&
-               (displayType == "inline-grid" || displayType == "grid")) {
-      return new GridActor(this, currentNode);
+
+      if (type == "flex") {
+        if (displayType == "inline-flex" || displayType == "flex") {
+          return new FlexboxActor(this, currentNode);
+        } else if (onlyLookAtCurrentNode) {
+          return null;
+        }
+      } else if (type == "grid" &&
+                 (displayType == "inline-grid" || displayType == "grid")) {
+        return new GridActor(this, currentNode);
+      }
     }
 
     // Otherwise, check if this is a flex/grid item or the parent node is a flex/grid
     // container.
+    // Note that text nodes that are children of flex/grid containers are wrapped in
+    // anonymous containers, so even if their displayType getter returns null we still
+    // want to walk up the chain to find their container.
     while ((currentNode = treeWalker.parentNode())) {
       if (!currentNode) {
         break;
       }
 
       displayType = this.walker.getNode(currentNode).displayType;
 
       if (type == "flex" &&
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -57,17 +57,21 @@ ReplayDebugger.prototype = {
   /////////////////////////////////////////////////////////
 
   replaying: true,
 
   canRewind: RecordReplayControl.canRewind,
   replayResumeBackward() { RecordReplayControl.resume(/* forward = */ false); },
   replayResumeForward() { RecordReplayControl.resume(/* forward = */ true); },
   replayTimeWarp: RecordReplayControl.timeWarp,
-  replayPause: RecordReplayControl.pause,
+
+  replayPause() {
+    RecordReplayControl.pause();
+    this._repaint();
+  },
 
   addDebuggee() {},
   removeAllDebuggees() {},
 
   replayingContent(url) {
     return this._sendRequest({ type: "getContent", url });
   },
 
@@ -85,16 +89,28 @@ ReplayDebugger.prototype = {
   // diverge from the recording. In such cases we want to be interacting with a
   // replaying process (if there is one), as recording child processes won't
   // provide useful responses to such requests.
   _sendRequestAllowDiverge(request) {
     RecordReplayControl.maybeSwitchToReplayingChild();
     return this._sendRequest(request);
   },
 
+  // Update graphics according to the current state of the child process. This
+  // should be done anytime we pause and allow the user to interact with the
+  // debugger.
+  _repaint() {
+    const rv = this._sendRequestAllowDiverge({ type: "repaint" });
+    if ("width" in rv && "height" in rv) {
+      RecordReplayControl.hadRepaint(rv.width, rv.height);
+    } else {
+      RecordReplayControl.hadRepaintFailure();
+    }
+  },
+
   _setBreakpoint(handler, position, data) {
     const id = RecordReplayControl.setBreakpoint(handler, position);
     this._breakpoints.push({id, position, data});
   },
 
   _clearMatchingBreakpoints(callback) {
     this._breakpoints = this._breakpoints.filter(breakpoint => {
       if (callback(breakpoint)) {
@@ -158,20 +174,34 @@ ReplayDebugger.prototype = {
 
   _addScript(data) {
     if (!this._scripts[data.id]) {
       this._scripts[data.id] = new ReplayDebuggerScript(this, data);
     }
     return this._scripts[data.id];
   },
 
-  findScripts() {
-    // Note: Debugger's findScripts() method takes a query argument, which
-    // we ignore here.
-    const data = this._sendRequest({ type: "findScripts" });
+  _convertScriptQuery(query) {
+    // Make a copy of the query, converting properties referring to debugger
+    // things into their associated ids.
+    const rv = Object.assign({}, query);
+    if ("global" in query) {
+      rv.global = query.global._data.id;
+    }
+    if ("source" in query) {
+      rv.source = query.source._data.id;
+    }
+    return rv;
+  },
+
+  findScripts(query) {
+    const data = this._sendRequest({
+      type: "findScripts",
+      query: this._convertScriptQuery(query)
+    });
     return data.map(script => this._addScript(script));
   },
 
   findAllConsoleMessages() {
     const messages = this._sendRequest({ type: "findConsoleMessages" });
     return messages.map(this._convertConsoleMessage.bind(this));
   },
 
@@ -310,42 +340,45 @@ ReplayDebugger.prototype = {
   set onNewScript(handler) {
     this._breakpointKindSetter("NewScript", handler,
                                () => handler.call(this, this._getNewScript()));
   },
 
   get onEnterFrame() { return this._breakpointKindGetter("EnterFrame"); },
   set onEnterFrame(handler) {
     this._breakpointKindSetter("EnterFrame", handler,
-                               () => handler.call(this, this.getNewestFrame()));
+                               () => { this._repaint();
+                                       handler.call(this, this.getNewestFrame()); });
   },
 
   get replayingOnPopFrame() {
     return this._searchBreakpoints(({position, data}) => {
       return (position.kind == "OnPop" && !position.script) ? data : null;
     });
   },
 
   set replayingOnPopFrame(handler) {
     if (handler) {
-      this._setBreakpoint(() => handler.call(this, this.getNewestFrame()),
+      this._setBreakpoint(() => { this._repaint();
+                                  handler.call(this, this.getNewestFrame()); },
                           { kind: "OnPop" }, handler);
     } else {
       this._clearMatchingBreakpoints(({position}) => {
         return position.kind == "OnPop" && !position.script;
       });
     }
   },
 
   get replayingOnForcedPause() {
     return this._breakpointKindGetter("ForcedPause");
   },
   set replayingOnForcedPause(handler) {
     this._breakpointKindSetter("ForcedPause", handler,
-                               () => handler.call(this, this.getNewestFrame()));
+                               () => { this._repaint();
+                                       handler.call(this, this.getNewestFrame()); });
   },
 
   getNewConsoleMessage() {
     const message = this._sendRequest({ type: "getNewConsoleMessage" });
     return this._convertConsoleMessage(message);
   },
 
   get onConsoleMessage() {
@@ -383,17 +416,18 @@ ReplayDebuggerScript.prototype = {
   },
 
   getLineOffsets(line) { return this._forward("getLineOffsets", line); },
   getOffsetLocation(pc) { return this._forward("getOffsetLocation", pc); },
   getSuccessorOffsets(pc) { return this._forward("getSuccessorOffsets", pc); },
   getPredecessorOffsets(pc) { return this._forward("getPredecessorOffsets", pc); },
 
   setBreakpoint(offset, handler) {
-    this._dbg._setBreakpoint(() => { handler.hit(this._dbg.getNewestFrame()); },
+    this._dbg._setBreakpoint(() => { this._dbg._repaint();
+                                     handler.hit(this._dbg.getNewestFrame()); },
                              { kind: "Break", script: this._data.id, offset },
                              handler);
   },
 
   clearBreakpoint(handler) {
     this._dbg._clearMatchingBreakpoints(({position, data}) => {
       return position.script == this._data.id && handler == data;
     });
@@ -500,17 +534,18 @@ ReplayDebuggerFrame.prototype = {
       ({position}) => this._positionMatches(position, "OnStep")
     );
   },
 
   setReplayingOnStep(handler, offsets) {
     this._clearOnStepBreakpoints();
     offsets.forEach(offset => {
       this._dbg._setBreakpoint(
-        () => handler.call(this._dbg.getNewestFrame()),
+        () => { this._dbg._repaint();
+                handler.call(this._dbg.getNewestFrame()); },
         { kind: "OnStep",
           script: this._data.script,
           offset,
           frameIndex: this._data.index },
         handler);
     });
   },
 
@@ -518,16 +553,17 @@ ReplayDebuggerFrame.prototype = {
     return this._dbg._searchBreakpoints(({position, data}) => {
       return this._positionMatches(position, "OnPop") ? data : null;
     });
   },
 
   set onPop(handler) {
     if (handler) {
       this._dbg._setBreakpoint(() => {
+          this._dbg._repaint();
           const result = this._dbg._sendRequest({ type: "popFrameResult" });
           handler.call(this._dbg.getNewestFrame(),
                        this._dbg._convertCompletionValue(result));
         },
         { kind: "OnPop", script: this._data.script, frameIndex: this._data.index },
         handler);
     } else {
       this._dbg._clearMatchingBreakpoints(
@@ -599,41 +635,45 @@ ReplayDebuggerObject.prototype = {
 
   getOwnPropertySymbols() {
     // Symbol properties are not handled yet.
     return [];
   },
 
   getOwnPropertyDescriptor(name) {
     this._ensureProperties();
-    return this._properties[name];
+    const desc = this._properties[name];
+    return desc ? this._convertPropertyDescriptor(desc) : null;
   },
 
   _ensureProperties() {
     if (!this._properties) {
       const properties = this._dbg._sendRequestAllowDiverge({
         type: "getObjectProperties",
         id: this._data.id
       });
       this._properties = {};
-      properties.forEach(({name, desc}) => {
-        if ("value" in desc) {
-          desc.value = this._dbg._convertValue(desc.value);
-        }
-        if ("get" in desc) {
-          desc.get = this._dbg._getObject(desc.get);
-        }
-        if ("set" in desc) {
-          desc.set = this._dbg._getObject(desc.set);
-        }
-        this._properties[name] = desc;
-      });
+      properties.forEach(({name, desc}) => { this._properties[name] = desc; });
     }
   },
 
+  _convertPropertyDescriptor(desc) {
+    const rv = Object.assign({}, desc);
+    if ("value" in desc) {
+      rv.value = this._dbg._convertValue(desc.value);
+    }
+    if ("get" in desc) {
+      rv.get = this._dbg._getObject(desc.get);
+    }
+    if ("set" in desc) {
+      rv.set = this._dbg._getObject(desc.set);
+    }
+    return rv;
+  },
+
   get allocationSite() { NYI(); },
   get errorMessageName() { NYI(); },
   get errorNotes() { NYI(); },
   get errorLineNumber() { NYI(); },
   get errorColumnNumber() { NYI(); },
   get proxyTarget() { NYI(); },
   get proxyHandler() { NYI(); },
   get isPromise() { NYI(); },
--- a/devtools/server/actors/replay/graphics.js
+++ b/devtools/server/actors/replay/graphics.js
@@ -9,17 +9,17 @@
 // which are connected to the compositor in the UI process in the usual way.
 // We need to update the contents of the document to draw the raw graphics data
 // provided by the child process.
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-function updateWindow(window, buffer, width, height) {
+function updateWindow(window, buffer, width, height, hadFailure) {
   // Make sure the window has a canvas filling the screen.
   let canvas = window.middlemanCanvas;
   if (!canvas) {
     canvas = window.document.createElement("canvas");
     window.document.body.style.margin = "0px";
     window.document.body.insertBefore(canvas, window.document.body.firstChild);
     window.middlemanCanvas = canvas;
   }
@@ -31,33 +31,43 @@ function updateWindow(window, buffer, wi
   // been scaled in the child process. To avoid scaling the graphics twice,
   // transform the canvas to undo the scaling.
   const scale = window.devicePixelRatio;
   if (scale != 1) {
     canvas.style.transform =
       `scale(${ 1 / scale }) translate(-${ width / scale }px, -${ height / scale }px)`;
   }
 
+  const cx = canvas.getContext("2d");
+
   const graphicsData = new Uint8Array(buffer);
-  const imageData = canvas.getContext("2d").getImageData(0, 0, width, height);
+  const imageData = cx.getImageData(0, 0, width, height);
   imageData.data.set(graphicsData);
-  canvas.getContext("2d").putImageData(imageData, 0, 0);
+  cx.putImageData(imageData, 0, 0);
+
+  // Indicate to the user when repainting failed and we are showing old painted
+  // graphics instead of the most up-to-date graphics.
+  if (hadFailure) {
+    cx.fillStyle = "red";
+    cx.font = "48px serif";
+    cx.fillText("PAINT FAILURE", 10, 50);
+  }
 
   // Make recording/replaying tabs easier to differentiate from other tabs.
   window.document.title = "RECORD/REPLAY";
 }
 
 // Entry point for when we have some new graphics data from the child process
 // to draw.
 // eslint-disable-next-line no-unused-vars
-function Update(buffer, width, height) {
+function Update(buffer, width, height, hadFailure) {
   try {
     // Paint to all windows we can find. Hopefully there is only one.
     for (const window of Services.ww.getWindowEnumerator()) {
-      updateWindow(window, buffer, width, height);
+      updateWindow(window, buffer, width, height, hadFailure);
     }
   } catch (e) {
     dump("Middleman Graphics Update Exception: " + e + "\n");
   }
 }
 
 // eslint-disable-next-line no-unused-vars
 var EXPORTED_SYMBOLS = [
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -518,20 +518,40 @@ function forwardToScript(name) {
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Handlers
 ///////////////////////////////////////////////////////////////////////////////
 
 const gRequestHandlers = {
 
+  repaint() {
+    if (!RecordReplayControl.maybeDivergeFromRecording()) {
+      return {};
+    }
+    return RecordReplayControl.repaint();
+  },
+
   findScripts(request) {
+    const query = Object.assign({}, request.query);
+    if ("global" in query) {
+      query.global = gPausedObjects.getObject(query.global);
+    }
+    if ("source" in query) {
+      query.source = gScriptSources.getObject(query.source);
+      if (!query.source) {
+        return [];
+      }
+    }
+    const scripts = dbg.findScripts(query);
     const rv = [];
-    gScripts.forEach((id) => {
-      rv.push(getScriptData(id));
+    scripts.forEach(script => {
+      if (considerScript(script)) {
+        rv.push(getScriptData(gScripts.getId(script)));
+      }
     });
     return rv;
   },
 
   getScript(request) {
     return getScriptData(request.id);
   },
 
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -111,19 +111,16 @@ function RootActor(connection, parameter
 }
 
 RootActor.prototype = {
   constructor: RootActor,
   applicationType: "browser",
 
   traits: {
     sources: true,
-    // Whether the server-side highlighter actor exists and can be used to
-    // remotely highlight nodes (see server/actors/highlighters.js)
-    highlightable: true,
     networkMonitor: true,
     // Whether the storage inspector actor to inspect cookies, etc.
     storageInspector: true,
     // Whether storage inspector is read only
     storageInspectorReadOnly: true,
     // Whether conditional breakpoints are supported
     conditionalBreakpoints: true,
     // Whether the server supports full source actors (breakpoints on
--- a/devtools/server/actors/targets/addon.js
+++ b/devtools/server/actors/targets/addon.js
@@ -91,17 +91,16 @@ AddonTargetActor.prototype = {
       debuggable: this._addon.isDebuggable,
       temporarilyInstalled: this._addon.temporarilyInstalled,
       type: this._addon.type,
       isWebExtension: this._addon.isWebExtension,
       isAPIExtension: this._addon.isAPIExtension,
       consoleActor: this._consoleActor.actorID,
 
       traits: {
-        highlightable: false,
         networkMonitor: false,
       },
     };
   },
 
   destroy() {
     this.conn.removeActorPool(this._contextPool);
     this._contextPool = null;
--- a/devtools/server/actors/targets/content-process.js
+++ b/devtools/server/actors/targets/content-process.js
@@ -114,17 +114,16 @@ ContentProcessTargetActor.prototype = {
       name: "Content process",
 
       consoleActor: this._consoleActor.actorID,
       chromeDebugger: this.threadActor.actorID,
       memoryActor: this.memoryActor.actorID,
       promisesActor: this._promisesActor.actorID,
 
       traits: {
-        highlightable: false,
         networkMonitor: false,
       },
     };
   },
 
   onListWorkers: function() {
     if (!this._workerList) {
       this._workerList = new WorkerTargetActorList(this.conn, {});
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -971,21 +971,24 @@ WebConsoleActor.prototype =
    *         `resultID` field, and potentially a promise in the `helperResult` or in the
    *         `awaitResult` field.
    *
    * @return object
    *         The response packet to send to with the unique id in the
    *         `resultID` field, with a sanitized helperResult field.
    */
   _waitForResultAndSend: async function(response) {
+    let updateTimestamp = false;
+
     // Wait for asynchronous command completion before sending back the response
     if (
       response.helperResult && typeof response.helperResult.then == "function"
     ) {
       response.helperResult = await response.helperResult;
+      updateTimestamp = true;
     } else if (response.awaitResult && typeof response.awaitResult.then === "function") {
       let result;
       try {
         result = await response.awaitResult;
       } catch (e) {
         // The promise was rejected. We let the engine handle this as it will report a
         // `uncaught exception` error.
         response.topLevelAwaitRejected = true;
@@ -995,16 +998,24 @@ WebConsoleActor.prototype =
         // `createValueGrip` expect a debuggee value, while here we have the raw object.
         // We need to call `makeDebuggeeValue` on it to make it work.
         const dbgResult = this.makeDebuggeeValue(result);
         response.result = this.createValueGrip(dbgResult);
       }
 
       // Remove the promise from the response object.
       delete response.awaitResult;
+
+      updateTimestamp = true;
+    }
+
+    if (updateTimestamp) {
+      // We need to compute the timestamp again as the one in the response was set before
+      // doing the evaluation, which is now innacurate since we waited for a bit.
+      response.timestamp = Date.now();
     }
 
     // Finally, send an unsolicited evaluationResult packet with
     // the normal return value
     this.conn.sendActorEvent(this.actorID, "evaluationResult", response);
   },
 
   /**
--- a/devtools/shared/client/thread-client.js
+++ b/devtools/shared/client/thread-client.js
@@ -20,17 +20,17 @@ loader.lazyRequireGetter(this, "SourceCl
 
 const noop = () => {};
 
 /**
  * Creates a thread client for the remote debugging protocol server. This client
  * is a front to the thread actor created in the server side, hiding the
  * protocol details in a traditional JavaScript API.
  *
- * @param client DebuggerClient, WorkerClient or BrowsingContextFront
+ * @param client DebuggerClient, WorkerTargetFront or BrowsingContextFront
  *        The parent of the thread (tab for target-scoped debuggers,
  *        DebuggerClient for chrome debuggers).
  * @param actor string
  *        The actor ID for this thread.
  */
 function ThreadClient(client, actor) {
   this._parent = client;
   this.client = client instanceof DebuggerClient ? client : client.client;
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -1599,17 +1599,23 @@ var generateRequestMethods = function(ac
  * @param object proto
  *    The object prototype.  Must have a 'typeName' property,
  *    should have method definitions, can have event definitions.
  */
 var FrontClassWithSpec = function(actorSpec, frontProto) {
   // Existing Fronts are relying on the initialize instead of constructor methods.
   const cls = function() {
     const instance = Object.create(cls.prototype);
-    instance.initialize.apply(instance, arguments);
+    const initializer = instance.initialize.apply(instance, arguments);
+
+    // Async Initialization
+    // return a promise that resolves with the instance if the initializer is async
+    if (initializer && typeof initializer.then === "function") {
+      return initializer.then(resolve => instance);
+    }
     return instance;
   };
   cls.prototype = extend(Front.prototype, generateRequestMethods(actorSpec, frontProto));
 
   if (!registeredTypes.has(actorSpec.typeName)) {
     types.addActorType(actorSpec.typeName);
   }
   registeredTypes.get(actorSpec.typeName).frontClass = cls;
--- a/devtools/shared/webconsole/test/common.js
+++ b/devtools/shared/webconsole/test/common.js
@@ -106,26 +106,26 @@ var _attachConsole = async function(
 
     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");
       return;
     }
-    const [workerResponse, workerClient] =
+    const [workerResponse, workerTargetFront] =
       await targetFront.attachWorker(workerTargetActor);
-    if (!workerClient || workerResponse.error) {
-      console.error("attachWorker failed. No worker client or " +
+    if (!workerTargetFront || workerResponse.error) {
+      console.error("attachWorker failed. No worker target front or " +
                     " error: " + workerResponse.error);
       return;
     }
-    await workerClient.attachThread({});
-    state.actor = workerClient.consoleActor;
-    state.dbgClient.attachConsole(workerClient.consoleActor, listeners)
+    await workerTargetFront.attachThread({});
+    state.actor = workerTargetFront.consoleActor;
+    state.dbgClient.attachConsole(workerTargetFront.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/style/test_interpolation-from-interpolatematrix-to-none.html
+++ b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html
@@ -32,12 +32,12 @@ test(function(t) {
                                     Math.sin(Math.PI / 8) + ',' +
                                    -Math.sin(Math.PI / 8) + ',' +
                                     Math.cos(Math.PI / 8) + ',' +
                                     '25, 0)';
   assert_matrix_equals(getComputedStyle(target).transform,
                        interpolated_matrix,
                        'the expected matrix from interpolatematrix(' +
                        'translateX(100px), rotate(90deg), 0.5) to none at 50%');
-}, 'Test interpolation from interpolatmatrix to none at 50%');
+}, 'Test interpolation from interpolatematrix to none at 50%');
 
 </script>
 </html>
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -253,16 +253,17 @@
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/StyleSheet.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "mozilla/dom/SVGDocument.h"
 #include "mozilla/dom/SVGSVGElement.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/TabGroup.h"
 #ifdef MOZ_XUL
+#include "mozilla/dom/XULBroadcastManager.h"
 #include "mozilla/dom/TreeBoxObject.h"
 #include "nsIXULWindow.h"
 #include "nsXULCommandDispatcher.h"
 #include "nsXULPopupManager.h"
 #include "nsIDocShellTreeOwner.h"
 #endif
 #include "nsIPresShellInlines.h"
 
@@ -1740,16 +1741,20 @@ nsDocument::~nsDocument()
     // Could be null here if Init() failed or if we have been unlinked.
     mCSSLoader->DropDocumentReference();
   }
 
   if (mStyleImageLoader) {
     mStyleImageLoader->DropDocumentReference();
   }
 
+  if (mXULBroadcastManager) {
+    mXULBroadcastManager->DropDocumentReference();
+  }
+
   delete mHeaderData;
 
   ClearAllBoxObjects();
 
   mPendingTitleChangeEvent.Revoke();
 
   mPlugins.Clear();
 }
@@ -5108,16 +5113,19 @@ nsDocument::EndUpdate()
 
   --mUpdateNestLevel;
 
   // This set of updates may have created XBL bindings.  Let the
   // binding manager know we're done.
   MaybeEndOutermostXBLUpdate();
 
   MaybeInitializeFinalizeFrameLoaders();
+  if (mXULBroadcastManager) {
+    mXULBroadcastManager->MaybeBroadcast();
+  }
 }
 
 void
 nsDocument::BeginLoad()
 {
   MOZ_ASSERT(!mDidCallBeginLoad);
   mDidCallBeginLoad = true;
 
@@ -10242,16 +10250,25 @@ nsIDocument::GetCommandDispatcher()
   }
   if (!mCommandDispatcher) {
     // Create our command dispatcher and hook it up.
     mCommandDispatcher = new nsXULCommandDispatcher(this);
   }
   return mCommandDispatcher;
 }
 
+void
+nsIDocument::InitializeXULBroadcastManager()
+{
+  if (mXULBroadcastManager) {
+    return;
+  }
+  mXULBroadcastManager = new XULBroadcastManager(this);
+}
+
 static JSObject*
 GetScopeObjectOfNode(nsINode* node)
 {
     MOZ_ASSERT(node, "Must not be called with null.");
 
     // Window root occasionally keeps alive a node of a document whose
     // window is already dead. If in this brief period someone calls
     // GetPopupNode and we return that node, we can end up creating a
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -143,16 +143,17 @@ class ImageLoader;
 class Rule;
 } // namespace css
 
 namespace dom {
 class Animation;
 class AnonymousContent;
 class Attr;
 class BoxObject;
+class XULBroadcastManager;
 class ClientInfo;
 class ClientState;
 class CDATASection;
 class Comment;
 struct CustomElementDefinition;
 class DocGroup;
 class DocumentL10n;
 class DocumentFragment;
@@ -3437,16 +3438,25 @@ public:
                                                        const mozilla::dom::BlockParsingOptions& aOptions,
                                                        mozilla::ErrorResult& aRv);
 
   already_AddRefed<nsIURI> GetMozDocumentURIIfNotForErrorPages();
 
   mozilla::dom::Promise* GetDocumentReadyForIdle(mozilla::ErrorResult& aRv);
 
   nsIDOMXULCommandDispatcher* GetCommandDispatcher();
+  bool HasXULBroadcastManager() const
+  {
+    return mXULBroadcastManager;
+  };
+  void InitializeXULBroadcastManager();
+  mozilla::dom::XULBroadcastManager* GetXULBroadcastManager() const
+  {
+    return mXULBroadcastManager;
+  }
   already_AddRefed<nsINode> GetPopupNode();
   void SetPopupNode(nsINode* aNode);
   nsINode* GetPopupRangeParent(ErrorResult& aRv);
   int32_t GetPopupRangeOffset(ErrorResult& aRv);
   already_AddRefed<nsINode> GetTooltipNode();
   void SetTooltipNode(nsINode* aNode) { /* do nothing */ }
 
   // ParentNode
@@ -4734,16 +4744,18 @@ protected:
   // document.close(), and document.write() when they are invoked by the parser.
   uint32_t mThrowOnDynamicMarkupInsertionCounter;
 
   // Count of unload/beforeunload/pagehide operations in progress.
   uint32_t mIgnoreOpensDuringUnloadCounter;
 
   nsCOMPtr<nsIDOMXULCommandDispatcher> mCommandDispatcher; // [OWNER] of the focus tracker
 
+  RefPtr<mozilla::dom::XULBroadcastManager> mXULBroadcastManager;
+
   // At the moment, trackers might be blocked by Tracking Protection or FastBlock.
   // In order to know the numbers of trackers detected and blocked, we add
   // these two values here and those are shared by TP and FB.
   uint32_t mNumTrackersFound;
   uint32_t mNumTrackersBlocked;
 
   mozilla::EnumSet<mozilla::Telemetry::LABELS_DOCUMENT_ANALYTICS_TRACKER_FASTBLOCKED>
     mTrackerBlockedReasons;
rename from dom/webidl/Flex.webidl
rename to dom/chrome-webidl/Flex.webidl
--- a/dom/webidl/Flex.webidl
+++ b/dom/chrome-webidl/Flex.webidl
@@ -4,50 +4,73 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 /**
  * These objects support visualization of flex containers by the
  * dev tools.
  */
 
+/**
+ * A flex container's main and cross axes are either horizontal or
+ * vertical, each with two possible directions.
+ */
+enum FlexPhysicalDirection {
+  "horizontal-lr",
+  "horizontal-rl",
+  "vertical-tb",
+  "vertical-bt",
+};
+
 [ChromeOnly]
 interface Flex
 {
-  sequence<FlexLine> getLines();
+  sequence<FlexLineValues> getLines();
+
+  /**
+   * The physical direction in which successive flex items are placed,
+   * within a flex line in this flex container.
+   */
+  readonly attribute FlexPhysicalDirection mainAxisDirection;
+
+  /**
+   * The physical direction in which successive flex lines are placed
+   * in this flex container (if it is or were multi-line).
+   */
+  readonly attribute FlexPhysicalDirection crossAxisDirection;
 };
 
 /**
  * Lines with items that have been shrunk are shrinking; with items
  * that have grown are growing, and all others are unchanged.
  */
 enum FlexLineGrowthState { "unchanged", "shrinking", "growing" };
 
 [ChromeOnly]
-interface FlexLine
+interface FlexLineValues
 {
   readonly attribute FlexLineGrowthState growthState;
   readonly attribute double crossStart;
   readonly attribute double crossSize;
 
   // firstBaselineOffset measures from flex-start edge.
   readonly attribute double firstBaselineOffset;
 
   // lastBaselineOffset measures from flex-end edge.
   readonly attribute double lastBaselineOffset;
 
   /**
-   * getItems() returns FlexItems only for the Elements in this Flex
-   * container -- ignoring struts and abs-pos Elements.
+   * getItems() returns FlexItemValues only for the Elements in
+   * this Flex container -- ignoring struts and abs-pos Elements.
    */
-  sequence<FlexItem> getItems();
+  sequence<FlexItemValues> getItems();
 };
 
 [ChromeOnly]
-interface FlexItem
+interface FlexItemValues
 {
   readonly attribute Node? node;
   readonly attribute double mainBaseSize;
   readonly attribute double mainDeltaSize;
   readonly attribute double mainMinSize;
   readonly attribute double mainMaxSize;
   readonly attribute double crossMinSize;
   readonly attribute double crossMaxSize;
--- a/dom/chrome-webidl/moz.build
+++ b/dom/chrome-webidl/moz.build
@@ -5,16 +5,19 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("**"):
     BUG_COMPONENT = ("Core", "DOM")
 
 with Files("ChannelWrapper.webidl"):
     BUG_COMPONENT = ("WebExtensions", "Request Handling")
 
+with Files("Flex.webidl"):
+    BUG_COMPONENT = ("Core", "CSS Parsing and Computation")
+
 with Files("HeapSnapshot.webidl"):
     BUG_COMPONENT = ("DevTools", "Memory")
 
 with Files("InspectorUtils.webidl"):
     BUG_COMPONENT = ("DevTools", "Inspector")
 
 with Files("MatchGlob.webidl"):
     BUG_COMPONENT = ("WebExtensions", "General")
@@ -28,16 +31,17 @@ with Files("WebExtension*.webidl"):
 PREPROCESSED_WEBIDL_FILES = [
     'ChromeUtils.webidl',
 ]
 
 WEBIDL_FILES = [
     'BrowsingContext.webidl',
     'ChannelWrapper.webidl',
     'DominatorTree.webidl',
+    'Flex.webidl',
     'HeapSnapshot.webidl',
     'InspectorUtils.webidl',
     'IteratorResult.webidl',
     'MatchGlob.webidl',
     'MatchPattern.webidl',
     'MessageManager.webidl',
     'MozDocumentObserver.webidl',
     'MozSharedMap.webidl',
--- a/dom/file/MemoryBlobImpl.h
+++ b/dom/file/MemoryBlobImpl.h
@@ -120,16 +120,17 @@ public:
                            uint32_t aLength,
                            nsIInputStream** _retval);
 
     NS_DECL_THREADSAFE_ISUPPORTS
 
     // These are mandatory.
     NS_FORWARD_NSIINPUTSTREAM(mStream->)
     NS_FORWARD_NSISEEKABLESTREAM(mSeekableStream->)
+    NS_FORWARD_NSITELLABLESTREAM(mSeekableStream->)
     NS_FORWARD_NSICLONEABLEINPUTSTREAM(mCloneableInputStream->)
 
     // This is optional. We use a conditional QI to keep it from being called
     // if the underlying stream doesn't support it.
     NS_FORWARD_NSIIPCSERIALIZABLEINPUTSTREAM(mSerializableInputStream->)
 
   private:
     ~DataOwnerAdapter() {}
--- a/dom/flex/Flex.cpp
+++ b/dom/flex/Flex.cpp
@@ -1,17 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "Flex.h"
 
-#include "FlexLine.h"
+#include "FlexLineValues.h"
 #include "mozilla/dom/FlexBinding.h"
 #include "nsFlexContainerFrame.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Flex, mParent, mLines)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(Flex)
@@ -32,28 +32,43 @@ Flex::Flex(Element* aParent,
   // going to keep it around.
   const ComputedFlexContainerInfo* containerInfo =
     aFrame->GetFlexContainerInfo();
   MOZ_ASSERT(containerInfo, "Should only be passed a frame with info.");
 
   mLines.SetLength(containerInfo->mLines.Length());
   uint32_t index = 0;
   for (auto&& l : containerInfo->mLines) {
-    FlexLine* line = new FlexLine(this, &l);
+    FlexLineValues* line = new FlexLineValues(this, &l);
     mLines.ElementAt(index) = line;
     index++;
   }
+
+  mMainAxisDirection = containerInfo->mMainAxisDirection;
+  mCrossAxisDirection = containerInfo->mCrossAxisDirection;
 }
 
 JSObject*
 Flex::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return Flex_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 void
-Flex::GetLines(nsTArray<RefPtr<FlexLine>>& aResult)
+Flex::GetLines(nsTArray<RefPtr<FlexLineValues>>& aResult)
 {
   aResult.AppendElements(mLines);
 }
 
+FlexPhysicalDirection
+Flex::MainAxisDirection() const
+{
+  return mMainAxisDirection;
+}
+
+FlexPhysicalDirection
+Flex::CrossAxisDirection() const
+{
+  return mCrossAxisDirection;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/flex/Flex.h
+++ b/dom/flex/Flex.h
@@ -3,25 +3,26 @@
 /* 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/. */
 
 #ifndef mozilla_dom_Flex_h
 #define mozilla_dom_Flex_h
 
 #include "mozilla/dom/Element.h"
+#include "mozilla/dom/FlexBinding.h"
 #include "nsISupports.h"
 #include "nsWrapperCache.h"
 
 class nsFlexContainerFrame;
 
 namespace mozilla {
 namespace dom {
 
-class FlexLine;
+class FlexLineValues;
 
 class Flex : public nsISupports
            , public nsWrapperCache
 {
 public:
   explicit Flex(Element* aParent, nsFlexContainerFrame* aFrame);
 
 protected:
@@ -32,19 +33,23 @@ public:
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Flex)
 
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
   Element* GetParentObject()
   {
     return mParent;
   }
 
-  void GetLines(nsTArray<RefPtr<FlexLine>>& aResult);