Merge mozilla-central to inbound. a=merge CLOSED TREE
authorshindli <shindli@mozilla.com>
Tue, 29 Jan 2019 23:54:31 +0200
changeset 513895 9f5f935c3f2739c5cba8e8c9407446b8dfff59b5
parent 513894 55e283588f80184c10de65f92fcf33af9e2f2288 (current diff)
parent 513844 4440fbf71c72e13cfcb6257bbae6024052ffd46d (diff)
child 513896 9652656423460a1eab36793103f83cbbd113d336
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound. a=merge CLOSED TREE
browser/components/urlbar/tests/browser/browser_bug1025195_switchToTabHavingURI_aOpenParams.js
browser/components/urlbar/tests/browser/browser_bug562649.js
browser/components/urlbar/tests/browser/browser_bug623155.js
browser/components/urlbar/tests/browser/browser_bug783614.js
browser/components/urlbar/tests/browser/file_bug562649.html
browser/components/urlbar/tests/browser/redirect_bug623155.sjs
browser/components/urlbar/tests/legacy/browser_bug1003461-switchtab-override.js
browser/components/urlbar/tests/legacy/browser_bug1024133-switchtab-override-keynav.js
browser/components/urlbar/tests/legacy/browser_bug1070778.js
browser/components/urlbar/tests/legacy/browser_bug1104165-switchtab-decodeuri.js
browser/components/urlbar/tests/legacy/browser_bug1225194-remotetab.js
browser/components/urlbar/tests/legacy/browser_bug304198.js
browser/components/urlbar/tests/legacy/browser_bug556061.js
caps/nsScriptSecurityManager.cpp
devtools/server/actors/inspector/event-parsers.js
dom/base/nsGlobalWindowInner.cpp
netwerk/protocol/res/ExtensionProtocolHandler.cpp
toolkit/xre/nsAppRunner.cpp
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -636,18 +636,17 @@ DocAccessible::OnPivotChanged(nsIAccessi
 ////////////////////////////////////////////////////////////////////////////////
 // nsIDocumentObserver
 
 NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
 NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
 
 void DocAccessible::AttributeWillChange(dom::Element* aElement,
                                         int32_t aNameSpaceID,
-                                        nsAtom* aAttribute, int32_t aModType,
-                                        const nsAttrValue* aNewValue) {
+                                        nsAtom* aAttribute, int32_t aModType) {
   Accessible* accessible = GetAccessible(aElement);
   if (!accessible) {
     if (aElement != mContent) return;
 
     accessible = this;
   }
 
   // Update dependent IDs cache. Take care of elements that are accessible
--- a/browser/base/content/test/webextensions/browser_extension_sideloading.js
+++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js
@@ -229,17 +229,17 @@ add_task(async function() {
   // Test telemetry events for addon1 (1 permission and 1 origin).
   info("Test telemetry events collected for addon1");
 
   const baseEventAddon1 = createBaseEventAddon(1);
   const collectedEventsAddon1 = getEventsForAddonId(amEvents, baseEventAddon1.value);
   const expectedEventsAddon1 = [
     {
       ...baseEventAddon1, method: "sideload_prompt",
-      extra: {...expectedExtra, num_perms: "1", num_origins: "1"},
+      extra: {...expectedExtra, num_strings: "2"},
     },
     {...baseEventAddon1, method: "uninstall"},
   ];
 
   let i = 0;
   for (let event of collectedEventsAddon1) {
     Assert.deepEqual(event, expectedEventsAddon1[i++],
                      "Got the expected telemetry event");
@@ -248,17 +248,17 @@ add_task(async function() {
   is(collectedEventsAddon1.length, expectedEventsAddon1.length,
      "Got the expected number of telemetry events for addon1");
 
   const baseEventAddon2 = createBaseEventAddon(2);
   const collectedEventsAddon2 = getEventsForAddonId(amEvents, baseEventAddon2.value);
   const expectedEventsAddon2 = [
     {
       ...baseEventAddon2, method: "sideload_prompt",
-      extra: {...expectedExtra, num_perms: "0", num_origins: "1"},
+      extra: {...expectedExtra, num_strings: "1"},
     },
     {...baseEventAddon2, method: "enable"},
     {...baseEventAddon2, method: "uninstall"},
   ];
 
   i = 0;
   for (let event of collectedEventsAddon2) {
     Assert.deepEqual(event, expectedEventsAddon2[i++],
--- a/browser/base/content/test/webextensions/browser_extension_update_background.js
+++ b/browser/base/content/test/webextensions/browser_extension_update_background.js
@@ -146,16 +146,17 @@ async function backgroundUpdateTest(url,
   await gCUITestUtils.openMainMenu();
 
   addons = PanelUI.addonNotificationContainer;
   is(addons.children.length, 1, "Have a menu entry for the update");
 
   // Click the menu item
   tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
   popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+
   addons.children[0].click();
 
   // Wait for about:addons to load
   tab = await tabPromise;
   is(tab.linkedBrowser.currentURI.spec, "about:addons");
 
   await promiseViewLoaded(tab, VIEW);
   win = tab.linkedBrowser.contentWindow;
@@ -199,19 +200,21 @@ async function backgroundUpdateTest(url,
   const object = "extension";
   const baseExtra = {
     addon_id: addonId,
     source: FAKE_INSTALL_TELEMETRY_SOURCE,
     step: "permissions_prompt",
     updated_from: "app",
   };
 
+  // Expect the telemetry events to have num_strings set to 1, as only the origin permissions is going
+  // to be listed in the permission prompt.
   Assert.deepEqual(updateEvents.filter(evt => evt.extra && evt.extra.step === "permissions_prompt"), [
-    {method, object, extra: {...baseExtra, num_perms: "1", num_origins: "1"}},
-    {method, object, extra: {...baseExtra, num_perms: "1", num_origins: "1"}},
+    {method, object, extra: {...baseExtra, num_strings: "1"}},
+    {method, object, extra: {...baseExtra, num_strings: "1"}},
   ], "Got the expected permission_prompts events");
 }
 
 function checkDefaultIcon(icon) {
   is(icon, "chrome://mozapps/skin/extensions/extensionGeneric.svg",
      "Popup has the default extension icon");
 }
 
--- a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
+++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
@@ -1,10 +1,17 @@
 const {AddonManagerPrivate} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 
+const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
+
+AddonTestUtils.initMochitest(this);
+
+hookExtensionsTelemetry();
+AddonTestUtils.hookAMTelemetryEvents();
+
 const ID_PERMS = "update_perms@tests.mozilla.org";
 const ID_ORIGINS = "update_origins@tests.mozilla.org";
 
 function getBadgeStatus() {
   let menuButton = document.getElementById("PanelUI-menu-button");
   return menuButton.getAttribute("badge-status");
 }
 
@@ -25,18 +32,16 @@ add_task(async function setup() {
 
   registerCleanupFunction(async function() {
     // Return to about:blank when we're done
     BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank");
     await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
   });
 });
 
-hookExtensionsTelemetry();
-
 // Helper function to test an upgrade that should not show a prompt
 async function testNoPrompt(origUrl, id) {
   await SpecialPowers.pushPrefEnv({set: [
     // Turn on background updates
     ["extensions.update.enabled", true],
 
     // Point updates to the local mochitest server
     ["extensions.update.background.url", `${BASE}/browser_webext_update.json`],
@@ -67,16 +72,30 @@ async function testNoPrompt(origUrl, id)
 
   ok(!sawPopup, "Should not have seen permissions notification");
 
   addon = await AddonManager.getAddonByID(id);
   is(addon.version, "2.0", "Update should have applied");
 
   await addon.uninstall();
   await SpecialPowers.popPrefEnv();
+
+  // Test that the expected telemetry events have been recorded (and that they do not
+  // include the permission_prompt event).
+  const amEvents = AddonTestUtils.getAMTelemetryEvents();
+  const updateEventsSteps = amEvents.filter(evt => {
+    return evt.method === "update" && evt.extra && evt.extra.addon_id == id;
+  }).map(evt => {
+    return evt.extra.step;
+  });
+
+  // Expect telemetry events related to a completed update with no permissions_prompt event.
+  Assert.deepEqual(updateEventsSteps, [
+    "started", "download_started", "download_completed", "completed",
+  ], "Got the steps from the collected telemetry events");
 }
 
 // Test that an update that adds new non-promptable permissions is just
 // applied without showing a notification dialog.
 add_task(() => testNoPrompt(`${BASE}/browser_webext_update_perms1.xpi`,
                             ID_PERMS));
 
 // Test that an update that narrows origin permissions is just applied without
--- a/browser/base/content/test/webextensions/head.js
+++ b/browser/base/content/test/webextensions/head.js
@@ -495,17 +495,19 @@ async function interactiveUpdateTest(aut
   ok(collectedUpdateEvents.every(evt => evt.extra.source === FAKE_INSTALL_SOURCE),
      "Every update telemetry event should have the expected source extra var");
 
   ok(collectedUpdateEvents.every(evt => evt.extra.updated_from === "user"),
      "Every update telemetry event should have the update_from extra var 'user'");
 
   let hasPermissionsExtras = collectedUpdateEvents.filter(evt => {
     return evt.extra.step === "permissions_prompt";
-  }).every(evt => !!evt.extra.num_perms && !!evt.extra.num_origins);
+  }).every(evt => {
+    return Number.isInteger(parseInt(evt.extra.num_strings, 10));
+  });
 
   ok(hasPermissionsExtras,
      "Every 'permissions_prompt' update telemetry event should have the permissions extra vars");
 
   let hasDownloadTimeExtras = collectedUpdateEvents.filter(evt => {
     return evt.extra.step === "download_completed";
   }).every(evt => {
     const download_time = parseInt(evt.extra.download_time, 10);
--- a/browser/components/migration/ChromeMigrationUtils.jsm
+++ b/browser/components/migration/ChromeMigrationUtils.jsm
@@ -4,16 +4,19 @@
 "use strict";
 
 var EXPORTED_SYMBOLS = ["ChromeMigrationUtils"];
 
 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
+const S100NS_PER_MS = 10;
+
 var ChromeMigrationUtils = {
   _extensionVersionDirectoryNames: {},
 
   // The cache for the locale strings.
   // For example, the data could be:
   // {
   //   "profile-id-1": {
   //     "extension-id-1": {
@@ -265,9 +268,34 @@ var ChromeMigrationUtils = {
     // For example, it could be "1.0_0", "1.0_1", "1.0_2", 1.1_0, 1.1_1, or 1.1_2.
     // The "1.0_1" strings mean that the "1.0_0" directory is existed and you install the version 1.0 again.
     // https://chromium.googlesource.com/chromium/src/+/0b58a813992b539a6b555ad7959adfad744b095a/chrome/common/extensions/extension_file_util_unittest.cc
     entries.sort((a, b) => Services.vc.compare(b, a));
 
     this._extensionVersionDirectoryNames[path] = entries;
     return entries;
   },
+
+  /**
+   * Convert Chrome time format to Date object
+   *
+   * @param   aTime
+   *          Chrome time
+   * @return  converted Date object
+   * @note    Google Chrome uses FILETIME / 10 as time.
+   *          FILETIME is based on same structure of Windows.
+   */
+  chromeTimeToDate(aTime) {
+    return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
+  },
+
+  /**
+   * Convert Date object to Chrome time format
+   *
+   * @param   aDate
+   *          Date object or integer equivalent
+   * @return  Chrome time
+   * @note    For details on Chrome time, see chromeTimeToDate.
+   */
+  dateToChromeTime(aDate) {
+    return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
+  },
 };
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -1,19 +1,16 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * vim: sw=2 ts=2 sts=2 et */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
-const S100NS_PER_MS = 10;
-
 const AUTH_TYPE = {
   SCHEME_HTML: 0,
   SCHEME_BASIC: 1,
   SCHEME_DIGEST: 2,
 };
 
 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 const {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
@@ -24,41 +21,16 @@ const {ChromeMigrationUtils} = ChromeUti
 const {MigrationUtils, MigratorPrototype} = ChromeUtils.import("resource:///modules/MigrationUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
                                "resource://gre/modules/PlacesUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "OSCrypto",
                                "resource://gre/modules/OSCrypto.jsm");
 
 /**
- * Convert Chrome time format to Date object
- *
- * @param   aTime
- *          Chrome time
- * @return  converted Date object
- * @note    Google Chrome uses FILETIME / 10 as time.
- *          FILETIME is based on same structure of Windows.
- */
-function chromeTimeToDate(aTime) {
-  return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
-}
-
-/**
- * Convert Date object to Chrome time format
- *
- * @param   aDate
- *          Date object or integer equivalent
- * @return  Chrome time
- * @note    For details on Chrome time, see chromeTimeToDate.
- */
-function dateToChromeTime(aDate) {
-  return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
-}
-
-/**
  * Converts an array of chrome bookmark objects into one our own places code
  * understands.
  *
  * @param   items
  *          bookmark items to be inserted on this parent
  * @param   errorAccumulator
  *          function that gets called with any errors thrown so we don't drop them on the floor.
  */
@@ -259,17 +231,18 @@ async function GetHistoryResource(aProfi
 
     migrate(aCallback) {
       (async function() {
         const MAX_AGE_IN_DAYS = Services.prefs.getIntPref("browser.migrate.chrome.history.maxAgeInDays");
         const LIMIT = Services.prefs.getIntPref("browser.migrate.chrome.history.limit");
 
         let query = "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
         if (MAX_AGE_IN_DAYS) {
-          let maxAge = dateToChromeTime(Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
+          let maxAge = ChromeMigrationUtils.dateToChromeTime(
+            Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
           query += " AND last_visit_time > " + maxAge;
         }
         if (LIMIT) {
           query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
         }
 
         let rows =
           await MigrationUtils.getRowsFromDBWithoutLocks(historyPath, "Chrome history", query);
@@ -281,17 +254,18 @@ async function GetHistoryResource(aProfi
             if (row.getResultByName("typed_count") > 0)
               transition = PlacesUtils.history.TRANSITIONS.TYPED;
 
             pageInfos.push({
               title: row.getResultByName("title"),
               url: new URL(row.getResultByName("url")),
               visits: [{
                 transition,
-                date: chromeTimeToDate(row.getResultByName("last_visit_time")),
+                date: ChromeMigrationUtils.chromeTimeToDate(
+                  row.getResultByName("last_visit_time")),
               }],
             });
           } catch (e) {
             Cu.reportError(e);
           }
         }
 
         if (pageInfos.length > 0) {
@@ -332,18 +306,18 @@ async function GetCookiesResource(aProfi
       for (let row of rows) {
         let host_key = row.getResultByName("host_key");
         if (host_key.match(/^\./)) {
           // 1st character of host_key may be ".", so we have to remove it
           host_key = host_key.substr(1);
         }
 
         try {
-          let expiresUtc =
-            chromeTimeToDate(row.getResultByName("expires_utc")) / 1000;
+          let expiresUtc = ChromeMigrationUtils.chromeTimeToDate(
+            row.getResultByName("expires_utc")) / 1000;
           Services.cookies.add(host_key,
                                row.getResultByName("path"),
                                row.getResultByName("name"),
                                row.getResultByName("value"),
                                row.getResultByName("secure"),
                                row.getResultByName("httponly"),
                                false,
                                parseInt(expiresUtc),
@@ -395,17 +369,18 @@ async function GetWindowsPasswordsResour
             password: crypto.
                       decryptData(crypto.arrayToString(row.getResultByName("password_value")),
                                                        null),
             hostname: origin_url.prePath,
             formSubmitURL: null,
             httpRealm: null,
             usernameElement: row.getResultByName("username_element"),
             passwordElement: row.getResultByName("password_element"),
-            timeCreated: chromeTimeToDate(row.getResultByName("date_created") + 0).getTime(),
+            timeCreated: ChromeMigrationUtils.chromeTimeToDate(
+              row.getResultByName("date_created") + 0).getTime(),
             timesUsed: row.getResultByName("times_used") + 0,
           };
 
           switch (row.getResultByName("scheme")) {
             case AUTH_TYPE.SCHEME_HTML:
               let action_url = NetUtil.newURI(row.getResultByName("action_url"));
               if (!kValidSchemes.has(action_url.scheme)) {
                 continue; // This continues the outer for loop.
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -1018,16 +1018,27 @@ var MigrationUtils = Object.freeze({
           let {parentGuid, guid, lastModified, type} = bm;
           bmData.push({parentGuid, guid, lastModified, type});
         }
       }
     }, ex => Cu.reportError(ex));
   },
 
   insertVisitsWrapper(pageInfos) {
+    let now = new Date();
+    // Ensure that none of the dates are in the future. If they are, rewrite
+    // them to be now. This means we don't loose history entries, but they will
+    // be valid for the history store.
+    for (let pageInfo of pageInfos) {
+      for (let visit of pageInfo.visits) {
+        if (visit.date && visit.date > now) {
+          visit.date = now;
+        }
+      }
+    }
     this._importQuantities.history += pageInfos.length;
     if (gKeepUndoData) {
       this._updateHistoryUndo(pageInfos);
     }
     return PlacesUtils.history.insertMany(pageInfos);
   },
 
   async insertLoginsWrapper(logins) {
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7fb19903b0879caa87505ae2089715fbd4b42b94
GIT binary patch
literal 118784
zc%1FsPi))P9S3kywDre-qWn`BC-uUn%Pcms9Xa*V&IJ-VQCzQzlsI;gtOtRXo+Y9Z
z>5x<$@1`UudT5JbhaQTaHeeWb*nk~&>8-#pV3!TpWf%q&Jq$&67*H%|2c#?Z9x0im
zMOp#6HjVK8#FowX9)Em%@9~keXy^L1f@%?ILpPd=C7tE=b3D&okR*=d*ykkYnxmgX
zo@JivPvU0y9}Tm@$%kxtGWl@y#W8*)Ir1>E6*uEgM;}D*55GUWJoNTZUU(q9D_n?t
z8hI}g<=^4He1!O4Q9E@sCSIK9+vioSO76TywzqVnS~iJdRO)4m7)`Tm=VXJjCN8ez
zbE|o2xp*mmL)v3ysZ<QimeSo+wVII{`W7+l3|SLn;<+ilJ!-FSl3J5!mRW9@N{!G6
zHCn!t=%gL>)g-VytAlTybazRu%I!l(V&XNr!3FOIJL_9&lLl$%R@q&{S3(Th`W`Fy
zR%NGDM)K6^dMYZOp5&Xm8o_9|6)=y4?73aYvmrC>B*|9}N5!d0p7?`mN|SUKQo$iv
z5nn)-Am2C?6=x^-Ej!$8)l_Lodv81mhkXHi3bCM0qLjA3+IjMggT0YA487@%d?+Zg
zTgV&PPPG^0(t)TrJ;B$!k?q!Tmk$O7-YMgaOB3a#{ZUa)@lGa9@DycB2YkW&`79Jm
zaRRlUPsYTlDgN%PrK~rIZ;<#F_IE<A<`xTi|6r2RwBwhHtNEq;iga~l`ATl(ru1_D
zX2vrByV=Fks#IJn6f!#lus6%;l8WBa+{{b)%el3}s&r<jn7p|!CQeTB?X%vRo87O;
zp!%AjRkg?3E4h`6FXdJ;ow4jztmtg9eR(`4&d+xnD`<iVO55kFO228Tw>^5Q(@A%y
zFR4%}E_G_}Y2vUPSsROqr%v(h6ZU>M?QQBUqvHF1+*faJPH2bQxji+N?>;*k6DKD4
z`?Kx_P3Lv2cOzz4c-jzQqlFEp-aPjZ?B-=GL(%B6s%v|Ut$>l+*-o`(DE(GewTeO5
z>ZDrMo0__=-zi($n{>&n=mwD%OQk|SSKL|ka;|Va?>5%e5jqK<=5IQ~`c{KpEzrSo
zt3|ZR?!{66Aopk5(<R-LTk>5#5ff7>{vNSovZJ4Uj`-F-$&yZnG`*BtTP-aY=?Yi!
z#Z|i_JOhg!%vOW=QXcIX&N$7|w^&0lt@2(SU017BvR9|b?NmG_E-divoZSY_hS`y2
zZflitleSD%v6PVHgs-JS3Q1|x(hYVBrU!FiBf4W&X|(7#D3liEwi=Cz)6@LDVY^;c
zeM@WTO4X!uqe}aG_j1fvlij&ex|<R>VdxthCJC5u4N@WMZMskE+ZGMdlP_(knrhZb
zZ%DVLGu|EbRwE-ho0TQ`d@L$npXNE|O28SdO3Tvi<?`;GDQ9=5#M_HQ&ICNQtFc&T
zRO*VVnL%m8A&usi@SPWS_S??uoHc`H<=|=8*X3!}0G>Fs6_$ph;;ZxhTfwcJw<X-v
z=<COXn0Vp@|EAz?jh%0Xg7-W0Bv@<FjGq0ev<!pIg`0|14>{2&Mvd%Mz@#O@4&8o(
z&(bZW5tc~<TLCevidJK%MtY$3W~y{zHjLJ$RW^uXdZu!BHc`yFeRR4>M5~6>U*9BJ
zz>(q1SxSWtTQg)$=Tt<rz$0>l){z*!y<xK9!p2@eFRX`WbOQ#Ls9V-1o5$*UK>x3A
zm&qObR2Y`!y?F98nWfZvQ|)GH>Q!|^B>@qe?6T1Aw@y}iZhl_&w6>wrt7tkP`V^Em
zc>9#scCI+s9L+u>z9WLAuq&^?K`X5lm#?kmeFuo-Ox&L0@>C=$E~LVaX10ZLmc0tj
z{qOP7ar!!x{3)0GSMtxv59l`l00000008g}m^mlpW{F}n*iSPyRp$r%Wvgxwv#vL)
z*|XW_=g*#dA$w+S?ztDvojEf$CmcU;n;0f7_FVQUL0C9o|5T%*H#h0J>kU%w<YvFW
zpBJ7v&}=H3W%i@0mN}gr;n@BEuejt#$=@e`N5268000000D#A6R5;FeZsh0o3C|=v
zxADh?1wQ1SU-*uY<HLWNaE`tI&+UKQ`vw32000000ADBE{{sL3000003_kY!Kkokl
z00000000IX-v0*x0000002q9D{~rJV00000VDRDne*gdg0001h!N>0ZBgx-z$-mMU
z000000002^HkgXUxvZFoN8@$N+Wf)ntg78s8ftZ>qBkvKT65gYhsQpcOTY0wo)%1b
z3X(fYbF)FZ1!=j7PgdTt@BjJaXB_<i00000004l266b{!-!d9z%z6L+k6iK}^aTI_
z000000KVlWA_<O<#)lF@Jkos%;0(Rhuf9;6;1gVeX3>A$_x)*3g!}n}U*q@x0RR91
z00006AMXDF00000000J`^ZWmQ<LCzf00000002C3zRM4BR}!7S{<q%HYcrZ!t6Poj
znWp|@wb4*!bfY%w-2Z>d(GLIs0000002mbG{4ke@Cb*f87C-*&1#X06fB)|<T=Jjv
z1poj500000o)kxfEZ_UT14dFpHqrZk0Xp~ppK-}A@IL?o00000000b@VS(o(5kW{K
zocsUJ+5P|LkGoa?00000008iRH6+A&djB8q-2Z>fB_Gfi000000001Zl8p&bo}2mb
z*avgzHzK0~`~Ck9Cq7wu%l`d;KKXZ!egFUf00000z!T^hq5t3ickchc;OGYc00000
z000c2?(hHe$$xY70{{R3000002FKV?G~xUI|M31l00000006+?>c0Qa?SI@g0RR91
z0001huM_V70RR9100006-+1yr+-URxm#iiKH1@;M7svRK<jBLsR@{s~9eohJKm7ji
z^3dBudEtTZu5cmpY2>{~lz)f&lDqv?w%Q(^h=~{H`SuA_tCBmfk?k$rsFqEl7?pb2
zB1Y3J+c|SVS;sD}<a4WeX}NeQe?!`1WvNsQ%$CwEqfu6?ayxa@smJ*~^~eTgO*~>f
zvf=gUrmEG9)X=wxVQ0vi7!%J;@$FH2eUsFhM6=9t%T#KFMyS#9okS<?sIMl0<yjqk
z>!iC&tQ`&=iHX<f1{b^=?5uC8O&X-3TV;0*UkNd2>wB!+Ta}$s8Oc+t>#3-CdXjJM
zY6QFMx{F6b_S`Py*^t@o@+*g<;?yKh{6RIPNxBQE;E=3{FCa^hZybt>vlIN59qzVj
zs<foNH=cyUzJNW2SWqWXO50!UJo(1K-pCt<-t<O36cpJl<c(~n+6!{&KvbNb;A`H<
zcI&vy2ZI9dl<~%;iSp9^s3@m+CzB?4in65xzF_`*7K)`ff!fa}W8%~le|OeW)*Hk(
zNPG+XJ0Vwdi-o*@FiC0J@yo^4{8D~Jy1KG_CAV@@dO3eH;~9Y6>|$wEDy|g@nVkXH
zo8@##MQ>?t=B51Q+*)B(I<r$u-rN@xCnx##S#QnF?$=~cea+CS+GFjN+{(q5ax0n6
zSavH`bhg;OJRTG0=evy+G{FR=?ekTo-?Y@*9zE6Rq`T9XR45gfI<@ySaafM5jm5-M
zr|8MR-Vdj}O}%AQeBY1z>g~-5?QlD{r>64VXGde=!~}nT*4?1#ypHv5#0(2h8zOA9
zu;J93=N^LHyo_Zi8eLX(ZI7`PFmgNFskRKI-^!|1F$i0oRLgo(Q`hx7WovtrE}0eG
zAkt!~RLJLwJF8yK6|U#q#=1H}C*jllO=no&YN!@Fqugo{t+IP@)IZ4mnf7!^_vDs*
zmrumRREobx?3nE6XP+a!wNJ97lOatn<<?e9%SF1vm3(p4?g-Doq6f3pAik7GJBBk(
zv-B<2P)w`5S4Y>?YL)EODfGf49upT9_;${2182kR$TGLJO1Vi}rm9#<NOHp0QXz$;
zv}x%EI|b8&Ij|AkF{?CMbQ}~)i*j3y#>DAq{@$=%ud2SKHFTwF(z#Kk{k?lR=Bvr>
zTq)g637jzWjSZ6oOt=QA5cM|Qr}b@%2I<L{HdIYD>!dfN+tL~Dj(V$+k(|xSl6*cE
z6|Yb8oO31Mj8>&(>GpDY_s*2FyHn!r#UW<`p4!z|tTQTgMb*rpwBe9Ob4&Qn3p@L5
z=XK7SL9=r3wCn5gG;07)9NG#?!%^|o`Tnio*3R1!Zff-PV?s<kae{wS@VCa!H$%bu
z9eNV1wP;4q{#06q!REqE#j1y#XcVJH_9|e~5@CmKzrknemeL5zq=Bt~m{moqu~Q>G
zP<t~~Ix!nYYtt$lL@_;6xjUOEX5BtI-6Wz_L+Y<@5-s4!aONzfLWivxvZiw?B3j@P
zxk2kljNaZb*>GWFFQ6CJ!!x=8gG;c#S<mLNx*pK~>)T~=$37K?WqB{2JWXaPwcb>_
zS(<uP-B3wD#3s8ewEL}-m7bfQmp!d*sPrnD4v0Pl<qh6G<+YtF4mL-#&xr4cU@7d%
zD{#<CYsKYjYkA)RA~_Scr?@;7iHZxUu%nr6p`2x}0=)^3vpaFT{|^8F00000F!;vl
zXEOc;m;A}TKkQo@e|P-aI5+l-vBKzoM}IkbaO7trBZ<3l{B1x0000;eZQ+>n^xyG5
qPyd|_%6jS%pZ+@={`8;ytRJ3*#Iuli781`w;#o*M3;E4I3;91NJA1eQ
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_history.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {ChromeMigrationUtils} =
+  ChromeUtils.import("resource:///modules/ChromeMigrationUtils.jsm");
+
+const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/";
+
+const PROFILE = {
+  id: "Default",
+  name: "Person 1",
+};
+
+/**
+ * TEST_URLS reflects the data stored in '${SOURCE_PROFILE_DIR}HistoryMaster'.
+ * The main object reflects the data in the 'urls' table. The visits property
+ * reflects the associated data in the 'visits' table.
+ */
+const TEST_URLS = [{
+  id: 1,
+  url: "http://example.com/",
+  title: "test",
+  visit_count: 1,
+  typed_count: 0,
+  last_visit_time: 13193151310368000,
+  hidden: 0,
+  visits: [{
+    id: 1,
+    url: 1,
+    visit_time: 13193151310368000,
+    from_visit: 0,
+    transition: 805306370,
+    segment_id: 0,
+    visit_duration: 10745006,
+    incremented_omnibox_typed_score: 0,
+  }],
+}, {
+  id: 2,
+  url: "http://invalid.com/",
+  title: "test2",
+  visit_count: 1,
+  typed_count: 0,
+  last_visit_time: 13193154948901000,
+  hidden: 0,
+  visits: [{
+    id: 2,
+    url: 2,
+    visit_time: 13193154948901000,
+    from_visit: 0,
+    transition: 805306376,
+    segment_id: 0,
+    visit_duration: 6568270,
+    incremented_omnibox_typed_score: 0,
+  }],
+}];
+
+async function setVisitTimes(time) {
+  let loginDataFile = do_get_file(`${SOURCE_PROFILE_DIR}History`);
+  let dbConn = await Sqlite.openConnection({ path: loginDataFile.path });
+
+  await dbConn.execute(`UPDATE urls SET last_visit_time = :last_visit_time`, {
+    last_visit_time: time,
+  });
+  await dbConn.execute(`UPDATE visits SET visit_time = :visit_time`, {
+    visit_time: time,
+  });
+
+  await dbConn.close();
+}
+
+function assertEntryMatches(entry, urlInfo, dateWasInFuture = false) {
+  info(`Checking url: ${urlInfo.url}`);
+  Assert.ok(entry, `Should have stored an entry`);
+
+  Assert.equal(entry.url, urlInfo.url, "Should have the correct URL");
+  Assert.equal(entry.title, urlInfo.title, "Should have the correct title");
+  Assert.equal(entry.visits.length, urlInfo.visits.length,
+    "Should have the correct number of visits");
+
+  for (let index in urlInfo.visits) {
+    Assert.equal(entry.visits[index].transition,
+      PlacesUtils.history.TRANSITIONS.LINK,
+      "Should have Link type transition");
+
+    if (dateWasInFuture) {
+      Assert.lessOrEqual(entry.visits[index].date.getTime(), new Date().getTime(),
+        "Should have moved the date to no later than the current date.");
+    } else {
+      Assert.equal(
+        entry.visits[index].date.getTime(),
+        ChromeMigrationUtils.chromeTimeToDate(urlInfo.visits[index].visit_time).getTime(),
+        "Should have the correct date");
+    }
+  }
+}
+
+function setupHistoryFile() {
+  removeHistoryFile();
+  let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryMaster`);
+  file.copyTo(file.parent, "History");
+}
+
+function removeHistoryFile() {
+  let file = do_get_file(`${SOURCE_PROFILE_DIR}History`, true);
+  try {
+    file.remove(false);
+  } catch (ex) {
+    // It is ok if this doesn't exist.
+    if (ex.result != Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+      throw ex;
+    }
+  }
+}
+
+add_task(async function setup() {
+  registerFakePath("ULibDir", do_get_file("Library/"));
+
+  registerCleanupFunction(async () => {
+    await PlacesUtils.history.clear();
+    removeHistoryFile();
+  });
+});
+
+add_task(async function test_import() {
+  setupHistoryFile();
+  await PlacesUtils.history.clear();
+
+  let migrator = await MigrationUtils.getMigrator("chrome");
+  Assert.ok(await migrator.isSourceAvailable(), "Sanity check the source exists");
+
+  await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY, PROFILE);
+
+  for (let urlInfo of TEST_URLS) {
+    let entry = await PlacesUtils.history.fetch(urlInfo.url, {includeVisits: true});
+    assertEntryMatches(entry, urlInfo);
+  }
+});
+
+add_task(async function test_import_future_date() {
+  setupHistoryFile();
+  await PlacesUtils.history.clear();
+  const futureDate = new Date().getTime() + 6000 * 60 * 24;
+  await setVisitTimes(ChromeMigrationUtils.dateToChromeTime(futureDate));
+
+  let migrator = await MigrationUtils.getMigrator("chrome");
+  Assert.ok(await migrator.isSourceAvailable(), "Sanity check the source exists");
+
+  await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY, PROFILE);
+
+  for (let urlInfo of TEST_URLS) {
+    let entry = await PlacesUtils.history.fetch(urlInfo.url, {includeVisits: true});
+    assertEntryMatches(entry, urlInfo, true);
+  }
+});
--- a/browser/components/migration/tests/unit/xpcshell.ini
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -6,16 +6,18 @@ support-files =
   Library/**
   AppData/**
 
 [test_360se_bookmarks.js]
 skip-if = os != "win"
 [test_Chrome_bookmarks.js]
 [test_Chrome_cookies.js]
 skip-if = os != "mac" # Relies on ULibDir
+[test_Chrome_history.js]
+skip-if = os != "mac" # Relies on ULibDir
 [test_Chrome_passwords.js]
 skip-if = os != "win"
 [test_ChromeMigrationUtils.js]
 [test_ChromeMigrationUtils_path.js]
 [test_Edge_db_migration.js]
 skip-if = os != "win"
 [test_fx_telemetry.js]
 [test_IE_bookmarks.js]
--- a/browser/components/urlbar/UrlbarController.jsm
+++ b/browser/components/urlbar/UrlbarController.jsm
@@ -309,17 +309,17 @@ class UrlbarController {
   _handleDeleteEntry() {
     if (!this._lastQueryContext) {
       Cu.reportError("Cannot delete - the latest query is not present");
       return false;
     }
 
     const selectedResult = this.input.view.selectedResult;
     if (!selectedResult ||
-        selectedResult.source != UrlbarUtils.MATCH_SOURCE.HISTORY) {
+        selectedResult.source != UrlbarUtils.RESULT_SOURCE.HISTORY) {
       return false;
     }
 
     let index = this._lastQueryContext.results.indexOf(selectedResult);
     if (!index) {
       Cu.reportError("Failed to find the selected result in the results");
       return false;
     }
--- a/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -91,21 +91,21 @@ class ProviderOpenTabs extends UrlbarPro
    * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
    */
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
 
   /**
    * Returns the sources returned by this provider.
-   * @returns {array} one or multiple types from UrlbarUtils.MATCH_SOURCE.*
+   * @returns {array} one or multiple types from UrlbarUtils.RESULT_SOURCE.*
    */
   get sources() {
     return [
-      UrlbarUtils.MATCH_SOURCE.TABS,
+      UrlbarUtils.RESULT_SOURCE.TABS,
     ];
   }
 
   /**
    * Registers a tab as open.
    * @param {string} url Address of the tab
    * @param {integer} userContextId Containers user context id
    */
@@ -158,17 +158,17 @@ class ProviderOpenTabs extends UrlbarPro
       SELECT url, userContextId
       FROM moz_openpages_temp
     `, {}, (row, cancel) => {
       if (!this.queries.has(queryContext)) {
         cancel();
         return;
       }
       addCallback(this, new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                                         UrlbarUtils.MATCH_SOURCE.TABS, {
+                                         UrlbarUtils.RESULT_SOURCE.TABS, {
         url: row.getResultByName("url"),
         userContextId: row.getResultByName("userContextId"),
       }));
     });
     // We are done.
     this.queries.delete(queryContext);
   }
 
--- a/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
+++ b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
@@ -55,26 +55,26 @@ class ProviderUnifiedComplete extends Ur
    * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
    */
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.IMMEDIATE;
   }
 
   /**
    * Returns the sources returned by this provider.
-   * @returns {array} one or multiple types from UrlbarUtils.MATCH_SOURCE.*
+   * @returns {array} one or multiple types from UrlbarUtils.RESULT_SOURCE.*
    */
   get sources() {
     return [
-      UrlbarUtils.MATCH_SOURCE.BOOKMARKS,
-      UrlbarUtils.MATCH_SOURCE.HISTORY,
-      UrlbarUtils.MATCH_SOURCE.SEARCH,
-      UrlbarUtils.MATCH_SOURCE.TABS,
-      UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL,
-      UrlbarUtils.MATCH_SOURCE.OTHER_NETWORK,
+      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+      UrlbarUtils.RESULT_SOURCE.HISTORY,
+      UrlbarUtils.RESULT_SOURCE.SEARCH,
+      UrlbarUtils.RESULT_SOURCE.TABS,
+      UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+      UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
     ];
   }
 
   /**
    * Starts querying.
    * @param {object} queryContext The query context object
    * @param {function} addCallback Callback invoked by the provider to add a new
    *        match.
@@ -220,116 +220,116 @@ function convertResultToMatches(context,
  */
 function makeUrlbarResult(tokens, info) {
   let action = PlacesUtils.parseActionUrl(info.url);
   if (action) {
     switch (action.type) {
       case "searchengine":
         return new UrlbarResult(
           UrlbarUtils.RESULT_TYPE.SEARCH,
-          UrlbarUtils.MATCH_SOURCE.SEARCH,
+          UrlbarUtils.RESULT_SOURCE.SEARCH,
           ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
             engine: [action.params.engineName, true],
             suggestion: [action.params.searchSuggestion, true],
             keyword: [action.params.alias, true],
             query: [action.params.searchQuery, true],
             icon: [info.icon, false],
           })
         );
       case "keyword":
         return new UrlbarResult(
           UrlbarUtils.RESULT_TYPE.KEYWORD,
-          UrlbarUtils.MATCH_SOURCE.BOOKMARKS,
+          UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
           ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
             url: [action.params.url, true],
             keyword: [info.firstToken, true],
             postData: [action.params.postData, false],
             icon: [info.icon, false],
           })
         );
       case "extension":
         return new UrlbarResult(
           UrlbarUtils.RESULT_TYPE.OMNIBOX,
-          UrlbarUtils.MATCH_SOURCE.OTHER_NETWORK,
+          UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
           ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
             title: [info.comment, true],
             content: [action.params.content, true],
             keyword: [action.params.keyword, true],
             icon: [info.icon, false],
           })
         );
       case "remotetab":
         return new UrlbarResult(
           UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
-          UrlbarUtils.MATCH_SOURCE.TABS,
+          UrlbarUtils.RESULT_SOURCE.TABS,
           ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
             url: [action.params.url, true],
             title: [info.comment, true],
             device: [action.params.deviceName, true],
             icon: [info.icon, false],
           })
         );
       case "switchtab":
         return new UrlbarResult(
           UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-          UrlbarUtils.MATCH_SOURCE.TABS,
+          UrlbarUtils.RESULT_SOURCE.TABS,
           ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
             url: [action.params.url, true],
             title: [info.comment, true],
             device: [action.params.deviceName, true],
             icon: [info.icon, false],
           })
         );
       case "visiturl":
         return new UrlbarResult(
           UrlbarUtils.RESULT_TYPE.URL,
-          UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL,
+          UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
           ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
             title: [info.comment, true],
             url: [action.params.url, true],
             icon: [info.icon, false],
           })
         );
       default:
         Cu.reportError(`Unexpected action type: ${action.type}`);
         return null;
     }
   }
 
   if (info.style.includes("priority-search")) {
     return new UrlbarResult(
       UrlbarUtils.RESULT_TYPE.SEARCH,
-      UrlbarUtils.MATCH_SOURCE.SEARCH,
+      UrlbarUtils.RESULT_SOURCE.SEARCH,
       ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
         engine: [info.comment, true],
         icon: [info.icon, false],
       })
     );
   }
 
   // This is a normal url/title tuple.
   let source;
   let tags = [];
   let comment = info.comment;
   let hasTags = info.style.includes("tag");
   if (info.style.includes("bookmark") || hasTags) {
-    source = UrlbarUtils.MATCH_SOURCE.BOOKMARKS;
+    source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
     if (hasTags) {
       // Split title and tags.
       [comment, tags] = info.comment.split(TITLE_TAGS_SEPARATOR);
       // Tags are separated by a comma and in a random order.
       // We should also just include tags that match the searchString.
       tags = tags.split(",").map(t => t.trim()).filter(tag => {
         return tokens.some(token => tag.includes(token.value));
       }).sort();
     }
   } else if (info.style.includes("preloaded-top-sites")) {
-    source = UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL;
+    source = UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL;
   } else {
-    source = UrlbarUtils.MATCH_SOURCE.HISTORY;
+    source = UrlbarUtils.RESULT_SOURCE.HISTORY;
   }
   return new UrlbarResult(
     UrlbarUtils.RESULT_TYPE.URL,
     source,
     ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
       url: [info.url, true],
       icon: [info.icon, false],
       title: [comment, true],
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -195,31 +195,31 @@ var UrlbarProvidersManager = new Provide
 class Query {
   /**
    * Initializes the query object.
    * @param {object} queryContext
    *        The query context
    * @param {object} controller
    *        The controller to be notified
    * @param {object} muxer
-   *        The muxer to sort matches
+   *        The muxer to sort results
    * @param {object} providers
    *        Map of all the providers by type and name
    */
   constructor(queryContext, controller, muxer, providers) {
     this.context = queryContext;
     this.context.results = [];
     this.muxer = muxer;
     this.controller = controller;
     this.providers = providers;
     this.started = false;
     this.canceled = false;
     this.complete = false;
-    // Array of acceptable MATCH_SOURCE values for this query. Providers not
-    // returning any of these will be skipped, as well as matches not part of
+    // Array of acceptable RESULT_SOURCE values for this query. Providers not
+    // returning any of these will be skipped, as well as results not part of
     // this subset (Note we still expect the provider to do its own internal
     // filtering, our additional filtering will be for sanity).
     this.acceptableSources = [];
   }
 
   /**
    * Starts querying.
    */
@@ -427,58 +427,58 @@ function getAcceptableMatchSources(conte
   // There can be only one restrict token about sources.
   let restrictToken = context.tokens.find(t => [ UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
                                                  UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
                                                  UrlbarTokenizer.TYPE.RESTRICT_TAG,
                                                  UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE,
                                                  UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
                                                ].includes(t.type));
   let restrictTokenType = restrictToken ? restrictToken.type : undefined;
-  for (let source of Object.values(UrlbarUtils.MATCH_SOURCE)) {
+  for (let source of Object.values(UrlbarUtils.RESULT_SOURCE)) {
     // Skip sources that the context doesn't care about.
     if (context.sources && !context.sources.includes(source)) {
       continue;
     }
     // Check prefs and restriction tokens.
     switch (source) {
-      case UrlbarUtils.MATCH_SOURCE.BOOKMARKS:
+      case UrlbarUtils.RESULT_SOURCE.BOOKMARKS:
         if (UrlbarPrefs.get("suggest.bookmark") &&
             (!restrictTokenType ||
              restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK ||
              restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_TAG)) {
           acceptedSources.push(source);
         }
         break;
-      case UrlbarUtils.MATCH_SOURCE.HISTORY:
+      case UrlbarUtils.RESULT_SOURCE.HISTORY:
         if (UrlbarPrefs.get("suggest.history") &&
             (!restrictTokenType ||
              restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_HISTORY)) {
           acceptedSources.push(source);
         }
         break;
-      case UrlbarUtils.MATCH_SOURCE.SEARCH:
+      case UrlbarUtils.RESULT_SOURCE.SEARCH:
         if (UrlbarPrefs.get("suggest.searches") &&
             (!restrictTokenType ||
              restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_SEARCH)) {
           acceptedSources.push(source);
         }
         break;
-      case UrlbarUtils.MATCH_SOURCE.TABS:
+      case UrlbarUtils.RESULT_SOURCE.TABS:
         if (UrlbarPrefs.get("suggest.openpage") &&
             (!restrictTokenType ||
              restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE)) {
           acceptedSources.push(source);
         }
         break;
-      case UrlbarUtils.MATCH_SOURCE.OTHER_NETWORK:
+      case UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK:
         if (!context.isPrivate && !restrictTokenType) {
           acceptedSources.push(source);
         }
         break;
-      case UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL:
+      case UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL:
       default:
         if (!restrictTokenType) {
           acceptedSources.push(source);
         }
         break;
     }
   }
   return acceptedSources;
--- a/browser/components/urlbar/UrlbarResult.jsm
+++ b/browser/components/urlbar/UrlbarResult.jsm
@@ -21,40 +21,40 @@ XPCOMUtils.defineLazyModuleGetters(this,
 
 /**
  * Class used to create a single result.
  */
 class UrlbarResult {
   /**
    * Creates a result.
    * @param {integer} resultType one of UrlbarUtils.RESULT_TYPE.* values
-   * @param {integer} matchSource one of UrlbarUtils.MATCH_SOURCE.* values
+   * @param {integer} resultSource one of UrlbarUtils.RESULT_SOURCE.* values
    * @param {object} payload data for this result. A payload should always
    *        contain a way to extract a final url to visit. The url getter
    *        should have a case for each of the types.
    * @param {object} [payloadHighlights] payload highlights, if any. Each
    *        property in the payload may have a corresponding property in this
    *        object. The value of each property should be an array of [index,
    *        length] tuples. Each tuple indicates a substring in the correspoding
    *        payload property.
    */
-  constructor(resultType, matchSource, payload, payloadHighlights = {}) {
+  constructor(resultType, resultSource, payload, payloadHighlights = {}) {
     // Type describes the payload and visualization that should be used for
     // this result.
     if (!Object.values(UrlbarUtils.RESULT_TYPE).includes(resultType)) {
       throw new Error("Invalid result type");
     }
     this.type = resultType;
 
-    // Source describes which data has been used to derive this match. In case
+    // Source describes which data has been used to derive this result. In case
     // multiple sources are involved, use the more privacy restricted.
-    if (!Object.values(UrlbarUtils.MATCH_SOURCE).includes(matchSource)) {
-      throw new Error("Invalid match source");
+    if (!Object.values(UrlbarUtils.RESULT_SOURCE).includes(resultSource)) {
+      throw new Error("Invalid result source");
     }
-    this.source = matchSource;
+    this.source = resultSource;
 
     // May be used to indicate an heuristic result. Heuristic results can bypass
     // source filters in the ProvidersManager, that otherwise may skip them.
     this.heuristic = false;
 
     // The payload contains result data. Some of the data is common across
     // multiple types, but most of it will vary.
     if (!payload || (typeof payload != "object")) {
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -83,21 +83,21 @@ var UrlbarUtils = {
     // A WebExtension Omnibox result.
     // Payload: { icon, keyword, title, content }
     OMNIBOX: 5,
     // A tab from another synced device.
     // Payload: { url, icon, device, title }
     REMOTE_TAB: 6,
   },
 
-  // This defines the source of matches returned by a provider. Each provider
-  // can return matches from more than one source. This is used by the
+  // This defines the source of results returned by a provider. Each provider
+  // can return results from more than one source. This is used by the
   // ProvidersManager to decide which providers must be queried and which
-  // matches can be returned.
-  MATCH_SOURCE: {
+  // results can be returned.
+  RESULT_SOURCE: {
     BOOKMARKS: 1,
     HISTORY: 2,
     SEARCH: 3,
     TABS: 4,
     OTHER_LOCAL: 5,
     OTHER_NETWORK: 6,
   },
 
@@ -412,17 +412,17 @@ class UrlbarProvider {
   /**
    * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
    * @abstract
    */
   get type() {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
-   * List of UrlbarUtils.MATCH_SOURCE, representing the data sources used by
+   * List of UrlbarUtils.RESULT_SOURCE, representing the data sources used by
    * the provider.
    * @abstract
    */
   get sources() {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
    * Starts querying.
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -255,19 +255,19 @@ class UrlbarView {
   }
 
   _addRow(resultIndex) {
     let result = this._queryContext.results[resultIndex];
     let item = this._createElement("div");
     item.className = "urlbarView-row";
     item.setAttribute("resultIndex", resultIndex);
 
-    if (result.source == UrlbarUtils.MATCH_SOURCE.TABS) {
+    if (result.source == UrlbarUtils.RESULT_SOURCE.TABS) {
       item.setAttribute("type", "tab");
-    } else if (result.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS) {
+    } else if (result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
       item.setAttribute("type", "bookmark");
     }
 
     let content = this._createElement("span");
     content.className = "urlbarView-row-inner";
     item.appendChild(content);
 
     let typeIcon = this._createElement("span");
--- a/browser/components/urlbar/tests/browser/browser.ini
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -5,26 +5,22 @@
 [DEFAULT]
 prefs=browser.urlbar.quantumbar=true
 tags=quantumbar
 support-files =
   dummy_page.html
   head.js
   head-common.js
 
-[browser_bug562649.js]
-support-files = file_bug562649.html
-[browser_bug623155.js]
-support-files =
-  redirect_bug623155.sjs
-[browser_bug783614.js]
-[browser_bug1025195_switchToTabHavingURI_aOpenParams.js]
 [browser_locationBarExternalLoad.js]
 [browser_moz_action_link.js]
 [browser_populateAfterPushState.js]
+[browser_redirect_error.js]
+support-files = redirect_error.sjs
+[browser_switchToTabHavingURI_aOpenParams.js]
 [browser_urlbar_blanking.js]
 support-files =
   file_blank_but_not_blank.html
 [browser_urlbar_content_opener.js]
 [browser_urlbar_locationchange_urlbar_edit_dos.js]
 support-files =
   file_urlbar_edit_dos.html
 [browser_urlbar_remoteness_switch.js]
@@ -35,16 +31,17 @@ support-files =
   searchSuggestionEngine2.xml
   searchSuggestionEngine.sjs
 [browser_urlbar_speculative_connect_not_with_client_cert.js]
 [browser_urlbar_whereToOpen.js]
 [browser_urlbarCopying.js]
 subsuite = clipboard
 support-files =
   authenticate.sjs
+[browser_urlbarCutting.js]
 [browser_urlbarEnter.js]
 [browser_urlbarFocusedCmdK.js]
 [browser_urlbarHashChangeProxyState.js]
 [browser_UrlbarInput_formatValue.js]
 [browser_UrlbarInput_hiddenFocus.js]
 [browser_UrlbarInput_overflow.js]
 [browser_UrlbarInput_tooltip.js]
 [browser_UrlbarInput_trimURLs.js]
@@ -56,12 +53,14 @@ support-files = empty.xul
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_URLBarSetURI.js]
 skip-if = (os == "linux" || os == "mac") && debug # bug 970052, bug 970053
 [browser_urlbarUpdateForDomainCompletion.js]
+[browser_userTypedValue.js]
+support-files = file_userTypedValue.html
 [browser_wyciwyg_urlbarCopying.js]
 subsuite = clipboard
 support-files =
   test_wyciwyg_copying.html
rename from browser/components/urlbar/tests/browser/browser_bug623155.js
rename to browser/components/urlbar/tests/browser/browser_redirect_error.js
--- a/browser/components/urlbar/tests/browser/browser_bug623155.js
+++ b/browser/components/urlbar/tests/browser/browser_redirect_error.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_PATH = getRootDirectory(gTestPath)
   .replace("chrome://mochitests/content", "http://example.com");
-const REDIRECT_FROM = `${TEST_PATH}redirect_bug623155.sjs`;
+const REDIRECT_FROM = `${TEST_PATH}redirect_error.sjs`;
 
 const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host.
 
 function isRedirectedURISpec(aURISpec) {
   return isRedirectedURI(Services.io.newURI(aURISpec));
 }
 
 function isRedirectedURI(aURI) {
rename from browser/components/urlbar/tests/browser/browser_bug1025195_switchToTabHavingURI_aOpenParams.js
rename to browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js
rename from browser/components/urlbar/tests/browser/browser_bug783614.js
rename to browser/components/urlbar/tests/browser/browser_urlbarCutting.js
rename from browser/components/urlbar/tests/browser/browser_bug562649.js
rename to browser/components/urlbar/tests/browser/browser_userTypedValue.js
--- a/browser/components/urlbar/tests/browser/browser_bug562649.js
+++ b/browser/components/urlbar/tests/browser/browser_userTypedValue.js
@@ -1,11 +1,11 @@
 function test() {
   const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com");
-  const URI = TEST_PATH + "file_bug562649.html";
+  const URI = TEST_PATH + "file_userTypedValue.html";
   window.browserDOMWindow.openURI(makeURI(URI),
                                   null,
                                   Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
                                   Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
                                   Services.scriptSecurityManager.getSystemPrincipal());
 
   is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI");
   is(gURLBar.value, URI, "location bar value matches test URI");
rename from browser/components/urlbar/tests/browser/file_bug562649.html
rename to browser/components/urlbar/tests/browser/file_userTypedValue.html
rename from browser/components/urlbar/tests/browser/redirect_bug623155.sjs
rename to browser/components/urlbar/tests/browser/redirect_error.sjs
--- a/browser/components/urlbar/tests/legacy/browser.ini
+++ b/browser/components/urlbar/tests/legacy/browser.ini
@@ -25,35 +25,33 @@ skip-if = (verify && !debug && (os == 'w
 [browser_autocomplete_cursor.js]
 skip-if = verify
 [browser_autocomplete_edit_completed.js]
 [browser_autocomplete_enter_race.js]
 [browser_autocomplete_no_title.js]
 [browser_autocomplete_readline_navigation.js]
 skip-if = os != "mac" # Mac only feature
 [browser_autocomplete_tag_star_visibility.js]
-[browser_bug1104165-switchtab-decodeuri.js]
-[browser_bug1003461-switchtab-override.js]
-skip-if = (verify && debug && (os == 'win'))
-[browser_bug1024133-switchtab-override-keynav.js]
-[browser_bug1070778.js]
-[browser_bug1225194-remotetab.js]
-[browser_bug304198.js]
-[browser_bug556061.js]
-subsuite = clipboard
 [browser_canonizeURL.js]
 [browser_dragdropURL.js]
+[browser_keyword_select_and_type.js]
 [browser_locationBarCommand.js]
 [browser_new_tab_urlbar_reset.js]
 [browser_pasteAndGo.js]
 subsuite = clipboard
-[../browser/browser_populateAfterPushState.js]
+[browser_remotetab.js]
 [browser_removeUnsafeProtocolsFromURLBarPaste.js]
 subsuite = clipboard
 [browser_search_favicon.js]
+[browser_switchtab_copy.js]
+subsuite = clipboard
+[browser_switchtab_decodeuri.js]
+[browser_switchtab_override_keynav.js]
+[browser_switchtab_override.js]
+skip-if = (verify && debug && (os == 'win'))
 [browser_tabMatchesInAwesomebar.js]
 support-files =
   ../browser/moz.png
 [browser_tabMatchesInAwesomebar_perwindowpb.js]
 skip-if = os == 'linux' # Bug 1104755
 [browser_urlbarAddonIframe.js]
 support-files =
   ../browser/Panel.jsm
@@ -103,36 +101,36 @@ support-files =
 [browser_urlbar_remove_match.js]
 [browser_urlbar_stop_pending.js]
 support-files =
   ../browser/slow-page.sjs
 [browser_urlbarStopSearchOnSelection.js]
 support-files =
   ../browser/searchSuggestionEngineSlow.xml
   ../browser/searchSuggestionEngine.sjs
+[browser_urlbarValueOnTabSwitch.js]
+
 
 # These are tests that are already running with QuantumBar, but we want to run them
 # against both the legacy urlbar and the new QuantumBar. The references in this
 # directory will run them against the old urlbar as per the pref above.
 
 [../browser/browser_URLBarSetURI.js]
 skip-if = (os == "linux" || os == "mac") && debug # bug 970052, bug 970053
-[../browser/browser_bug1025195_switchToTabHavingURI_aOpenParams.js]
-[../browser/browser_bug562649.js]
-support-files = ../browser/file_bug562649.html
-[../browser/browser_bug623155.js]
-support-files =
-  ../browser/redirect_bug623155.sjs
-[../browser/browser_bug783614.js]
 [../browser/browser_locationBarExternalLoad.js]
 [../browser/browser_moz_action_link.js]
+[../browser/browser_populateAfterPushState.js]
+[../browser/browser_redirect_error.js]
+support-files = ../browser/redirect_error.sjs
+[../browser/browser_switchToTabHavingURI_aOpenParams.js]
 [../browser/browser_urlbarCopying.js]
 subsuite = clipboard
 support-files =
   ../browser/authenticate.sjs
+[../browser/browser_urlbarCutting.js]
 [../browser/browser_urlbar_blanking.js]
 support-files =
   ../browser/file_blank_but_not_blank.html
 [../browser/browser_urlbar_content_opener.js]
 [../browser/browser_urlbar_locationchange_urlbar_edit_dos.js]
 support-files =
   ../browser/file_urlbar_edit_dos.html
 [../browser/browser_urlbarEnter.js]
@@ -149,12 +147,14 @@ support-files =
 [../browser/browser_urlbar_searchsettings.js]
 [../browser/browser_urlbar_speculative_connect.js]
 support-files =
   ../browser/searchSuggestionEngine2.xml
   ../browser/searchSuggestionEngine.sjs
 [../browser/browser_urlbar_speculative_connect_not_with_client_cert.js]
 [../browser/browser_urlbar_remoteness_switch.js]
 run-if = e10s
+[../browser/browser_userTypedValue.js]
+support-files = ../browser/file_userTypedValue.html
 [../browser/browser_wyciwyg_urlbarCopying.js]
 subsuite = clipboard
 support-files =
   ../browser/test_wyciwyg_copying.html
rename from browser/components/urlbar/tests/legacy/browser_bug1070778.js
rename to browser/components/urlbar/tests/legacy/browser_keyword_select_and_type.js
rename from browser/components/urlbar/tests/legacy/browser_bug1225194-remotetab.js
rename to browser/components/urlbar/tests/legacy/browser_remotetab.js
rename from browser/components/urlbar/tests/legacy/browser_bug556061.js
rename to browser/components/urlbar/tests/legacy/browser_switchtab_copy.js
rename from browser/components/urlbar/tests/legacy/browser_bug1104165-switchtab-decodeuri.js
rename to browser/components/urlbar/tests/legacy/browser_switchtab_decodeuri.js
rename from browser/components/urlbar/tests/legacy/browser_bug1003461-switchtab-override.js
rename to browser/components/urlbar/tests/legacy/browser_switchtab_override.js
rename from browser/components/urlbar/tests/legacy/browser_bug1024133-switchtab-override-keynav.js
rename to browser/components/urlbar/tests/legacy/browser_switchtab_override_keynav.js
--- a/browser/components/urlbar/tests/legacy/browser_urlbarDecode.js
+++ b/browser/components/urlbar/tests/legacy/browser_urlbarDecode.js
@@ -29,17 +29,17 @@ add_task(async function injectJSON() {
   gURLBar.blur();
 });
 
 add_task(function losslessDecode() {
   let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
   let url = "http://" + urlNoScheme;
   if (Services.prefs.getBoolPref("browser.urlbar.quantumbar", true)) {
     const result = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                                    UrlbarUtils.MATCH_SOURCE.TABS,
+                                    UrlbarUtils.RESULT_SOURCE.TABS,
                                     { url });
     gURLBar.setValueFromResult(result);
   } else {
     gURLBar.textValue = url;
   }
   // Since this is directly setting textValue, it is expected to be trimmed.
   Assert.equal(gURLBar.inputField.value, urlNoScheme,
                "The string displayed in the textbox should not be escaped");
rename from browser/components/urlbar/tests/legacy/browser_bug304198.js
rename to browser/components/urlbar/tests/legacy/browser_urlbarValueOnTabSwitch.js
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -6,17 +6,17 @@
  */
 
 "use strict";
 
 const {PromiseUtils} = ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 
 const TEST_URL = "http://example.com";
 const match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                               UrlbarUtils.MATCH_SOURCE.TABS,
+                               UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: TEST_URL });
 let controller;
 
 /**
  * Asserts that the query context has the expected values.
  *
  * @param {UrlbarQueryContext} context
  * @param {object} expectedValues The expected values for the UrlbarQueryContext.
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
@@ -5,17 +5,17 @@
  * These tests unit test the functionality of UrlbarController by stubbing out the
  * model and providing stubs to be called.
  */
 
 "use strict";
 
 const TEST_URL = "http://example.com";
 const MATCH = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                               UrlbarUtils.MATCH_SOURCE.TABS,
+                               UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: TEST_URL });
 const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
 const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
 
 let controller;
 let firstHistogram;
 let sixthHistogram;
 
@@ -29,17 +29,17 @@ class DelayedProvider extends UrlbarProv
   }
   get name() {
     return this._name;
   }
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
   get sources() {
-    return [UrlbarUtils.MATCH_SOURCE.TABS];
+    return [UrlbarUtils.RESULT_SOURCE.TABS];
   }
   async startQuery(context, add) {
     Assert.ok(context, "context is passed-in");
     Assert.equal(typeof add, "function", "add is a callback");
     this._context = context;
     this._add = add;
   }
   cancelQuery(context) {
@@ -152,17 +152,17 @@ add_task(async function test_n_autocompl
   Assert.equal(getHistogramReportsCount(first6Results), 0,
     "Should not have recorded any times (first 6 results)");
 
   // Now add 5 more results, so that the first 6 results is triggered.
   for (let i = 0; i < 5; i++) {
     resultsPromise = promiseControllerNotification(controller, "onQueryResults");
     provider.addResults([
       new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                       UrlbarUtils.MATCH_SOURCE.TABS,
+                       UrlbarUtils.RESULT_SOURCE.TABS,
                        { url: TEST_URL + "/i" }),
     ]);
     await resultsPromise;
   }
 
   Assert.ok(!TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
     "Should have stopped the first stopwatch");
   Assert.ok(!TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
@@ -174,17 +174,17 @@ add_task(async function test_n_autocompl
     "Should not have changed the histogram for the first result");
   Assert.equal(getHistogramReportsCount(updated6Results), 1,
     "Should have recorded one time for the first 6 results");
 
   // Add one more, to check neither are updated.
   resultsPromise = promiseControllerNotification(controller, "onQueryResults");
   provider.addResults([
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                     UrlbarUtils.MATCH_SOURCE.TABS,
+                     UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: TEST_URL + "/6" }),
   ]);
   await resultsPromise;
 
   let secondUpdateResults = firstHistogram.snapshot();
   let secondUpdate6Results = sixthHistogram.snapshot();
   Assert.deepEqual(secondUpdateResults, firstResults,
     "Should not have changed the histogram for the first result");
--- a/browser/components/urlbar/tests/unit/test_muxer.js
+++ b/browser/components/urlbar/tests/unit/test_muxer.js
@@ -19,23 +19,23 @@ add_task(async function test_muxer() {
                   name: "test",
                   sort: "no",
                 }),
                 /invalid muxer/,
                 "Should throw with invalid sort");
 
   let matches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                     UrlbarUtils.MATCH_SOURCE.TABS,
+                     UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/tab/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
-                     UrlbarUtils.MATCH_SOURCE.BOOKMARKS,
+                     UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
                      { url: "http://mozilla.org/bookmark/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
-                     UrlbarUtils.MATCH_SOURCE.HISTORY,
+                     UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/history/" }),
   ];
 
   let providerName = registerBasicTestProvider(matches);
   let context = createContext(undefined, {providers: [providerName]});
   let controller = new UrlbarController({
     browserWindow: {
       location: {
@@ -47,23 +47,23 @@ add_task(async function test_muxer() {
    * A test muxer.
    */
   class TestMuxer extends UrlbarMuxer {
     get name() {
       return "TestMuxer";
     }
     sort(queryContext) {
       queryContext.results.sort((a, b) => {
-        if (b.source == UrlbarUtils.MATCH_SOURCE.TABS) {
+        if (b.source == UrlbarUtils.RESULT_SOURCE.TABS) {
           return -1;
         }
-        if (b.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS) {
+        if (b.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
           return 1;
         }
-        return a.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS ? -1 : 1;
+        return a.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS ? -1 : 1;
       });
     }
   }
   let muxer = new TestMuxer();
 
   UrlbarProvidersManager.registerMuxer(muxer);
   context.muxer = "TestMuxer";
 
--- a/browser/components/urlbar/tests/unit/test_providersManager.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -25,17 +25,17 @@ add_task(async function test_providers()
                   name: "test",
                   startQuery: () => {},
                   cancelQuery: "no",
                 }),
                 /invalid provider/,
                 "Should throw with invalid cancelQuery");
 
   let match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                               UrlbarUtils.MATCH_SOURCE.TABS,
+                               UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: "http://mozilla.org/foo/" });
 
   let providerName = registerBasicTestProvider([match]);
   let context = createContext(undefined, {providers: [providerName]});
   let controller = new UrlbarController({
     browserWindow: {
       location: {
         href: AppConstants.BROWSER_CHROME_URL,
--- a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 add_task(async function test_filtering() {
   let match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                               UrlbarUtils.MATCH_SOURCE.TABS,
+                               UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: "http://mozilla.org/foo/" });
   let providerName = registerBasicTestProvider([match]);
   let context = createContext(undefined, {providers: [providerName]});
   let controller = new UrlbarController({
     browserWindow: {
       location: {
         href: AppConstants.BROWSER_CHROME_URL,
       },
@@ -25,17 +25,17 @@ add_task(async function test_filtering()
   ]);
   await controller.startQuery(context);
   await promise;
   Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
 
   let matches = [
     match,
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                     UrlbarUtils.MATCH_SOURCE.HISTORY,
+                     UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo/" }),
   ];
   providerName = registerBasicTestProvider(matches);
   context = createContext(undefined, {providers: [providerName]});
 
   info("Disable one of the sources, should get a single match");
   Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
   promise = Promise.all([
@@ -63,20 +63,20 @@ add_task(async function test_filter_java
   let controller = new UrlbarController({
     browserWindow: {
       location: {
         href: AppConstants.BROWSER_CHROME_URL,
       },
     },
   });
   let match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                               UrlbarUtils.MATCH_SOURCE.TABS,
+                               UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: "http://mozilla.org/foo/" });
   let jsMatch = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                                 UrlbarUtils.MATCH_SOURCE.HISTORY,
+                                 UrlbarUtils.RESULT_SOURCE.HISTORY,
                                  { url: "javascript:foo" });
   let providerName = registerBasicTestProvider([match, jsMatch]);
   let context = createContext(undefined, {providers: [providerName]});
 
   info("By default javascript should be filtered out");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
@@ -106,83 +106,83 @@ add_task(async function test_filter_sour
       location: {
         href: AppConstants.BROWSER_CHROME_URL,
       },
     },
   });
 
   let goodMatches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                     UrlbarUtils.MATCH_SOURCE.TABS,
+                     UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
-                     UrlbarUtils.MATCH_SOURCE.HISTORY,
+                     UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo/" }),
   ];
   /**
    * A test provider that should be invoked.
    */
   class TestProvider extends UrlbarProvider {
     get name() {
       return "GoodProvider";
     }
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     }
     get sources() {
       return [
-        UrlbarUtils.MATCH_SOURCE.TABS,
-        UrlbarUtils.MATCH_SOURCE.HISTORY,
+        UrlbarUtils.RESULT_SOURCE.TABS,
+        UrlbarUtils.RESULT_SOURCE.HISTORY,
       ];
     }
     async startQuery(context, add) {
       Assert.ok(true, "expected provider was invoked");
       for (const match of goodMatches) {
         add(this, match);
       }
     }
     cancelQuery(context) {}
   }
   UrlbarProvidersManager.registerProvider(new TestProvider());
 
   let badMatches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
-                     UrlbarUtils.MATCH_SOURCE.BOOKMARKS,
+                     UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
                      { url: "http://mozilla.org/foo/" }),
   ];
 
   /**
    * A test provider that should not be invoked.
    */
   class NoInvokeProvider extends UrlbarProvider {
     get name() {
       return "BadProvider";
     }
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     }
     get sources() {
-      return [UrlbarUtils.MATCH_SOURCE.BOOKMARKS];
+      return [UrlbarUtils.RESULT_SOURCE.BOOKMARKS];
     }
     async startQuery(context, add) {
       Assert.ok(false, "Provider should no be invoked");
       for (const match of badMatches) {
         add(this, match);
       }
     }
     cancelQuery(context) {}
   }
 
   UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
 
   let context = createContext(undefined, {
-    sources: [UrlbarUtils.MATCH_SOURCE.TABS],
+    sources: [UrlbarUtils.RESULT_SOURCE.TABS],
     providers: ["GoodProvider", "BadProvider"],
   });
 
   info("Only tabs should be returned");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Assert.deepEqual(context.results.length, 1, "Should find only one match");
-  Assert.deepEqual(context.results[0].source, UrlbarUtils.MATCH_SOURCE.TABS,
+  Assert.deepEqual(context.results[0].source, UrlbarUtils.RESULT_SOURCE.TABS,
                    "Should find only a tab match");
 });
--- a/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
@@ -3,17 +3,17 @@
 
 "use strict";
 
 add_task(async function test_maxResults() {
   const MATCHES_LENGTH = 20;
   let matches = [];
   for (let i = 0; i < MATCHES_LENGTH; i++) {
     matches.push(new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
-                                  UrlbarUtils.MATCH_SOURCE.TABS,
+                                  UrlbarUtils.RESULT_SOURCE.TABS,
                                   { url: `http://mozilla.org/foo/${i}` }));
   }
   let providerName = registerBasicTestProvider(matches);
   let context = createContext(undefined, {providers: [providerName]});
   let controller = new UrlbarController({
     browserWindow: {
       location: {
         href: AppConstants.BROWSER_CHROME_URL,
--- a/browser/docs/AddressBar.rst
+++ b/browser/docs/AddressBar.rst
@@ -67,17 +67,17 @@ It is augmented as it progresses through
     userContextId; // {integer} The user context ID (containers feature).
 
     // Optional properties.
     muxer; // {string} Name of a registered muxer. Muxers can be registered
            // through the UrlbarProvidersManager.
     providers; // {array} List of registered provider names. Providers can be
                // registered through the UrlbarProvidersManager.
     sources; // {array} If provided is the list of sources, as defined by
-             // MATCH_SOURCE.*, that can be returned by the model.
+             // RESULT_SOURCE.*, that can be returned by the model.
 
     // Properties added by the Model.
     autofillValue; // {string} the text value that should be autofilled in the
                    // input, if any.
     preselected; // {boolean} whether the first result should be preselected.
     results; // {array} list of UrlbarResult objects.
     tokens; // {array} tokens extracted from the searchString, each token is an
             // object in the form {type, value}.
@@ -168,17 +168,17 @@ class UrlbarProvider {
   /**
    * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
    * @abstract
    */
   get type() {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
-   * List of UrlbarUtils.MATCH_SOURCE, representing the data sources used by
+   * List of UrlbarUtils.RESULT_SOURCE, representing the data sources used by
    * the provider.
    * @abstract
    */
   get sources() {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
    * Starts querying.
@@ -366,17 +366,17 @@ properties, supported by all of the resu
 
 .. highlight:: JavaScript
 .. code::
 
   UrlbarResult {
     constructor(resultType, payload);
 
     type: {integer} One of UrlbarUtils.RESULT_TYPE.
-    source: {integer} One of UrlbarUtils.MATCH_SOURCE.
+    source: {integer} One of UrlbarUtils.RESULT_SOURCE.
     title: {string} A title that may be used as a label for this result.
     icon: {string} Url of an icon for this result.
     payload: {object} Object containing properties for the specific RESULT_TYPE.
   }
 
 The following RESULT_TYPEs are supported:
 
 .. highlight:: JavaScript
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -124,32 +124,34 @@ var ExtensionsUI = {
 
     let strings = this._buildStrings({
       addon,
       permissions: addon.userPermissions,
       type: "sideload",
     });
 
     AMTelemetry.recordManageEvent(addon, "sideload_prompt", {
-      num_perms: addon.userPermissions && addon.userPermissions.permissions ?
-        addon.userPermissions.permissions.length : 0,
-      num_origins: addon.userPermissions && addon.userPermissions.origins ?
-        addon.userPermissions.origins.length : 0,
+      num_strings: strings.msgs.length,
     });
 
     this.showAddonsManager(browser, strings, addon.iconURL, "sideload")
         .then(async answer => {
           if (answer) {
             await addon.enable();
           }
           this.emit("sideload-response");
         });
   },
 
   showUpdate(browser, info) {
+    AMTelemetry.recordInstallEvent(info.install, {
+      step: "permissions_prompt",
+      num_strings: info.strings.msgs.length,
+    });
+
     this.showAddonsManager(browser, info.strings, info.addon.iconURL, "update")
         .then(answer => {
           if (answer) {
             info.resolve();
           } else {
             info.reject();
           }
           // At the moment, this prompt will re-appear next time we do an update
@@ -197,16 +199,27 @@ var ExtensionsUI = {
       } else if (info.source == "AMO") {
         histkey = "installAmo";
       } else if (info.source == "local") {
         histkey = "installLocal";
       } else {
         histkey = "installWeb";
       }
 
+      if (info.type == "sideload") {
+        AMTelemetry.recordManageEvent(info.addon, "sideload_prompt", {
+          num_strings: strings.msgs.length,
+        });
+      } else {
+        AMTelemetry.recordInstallEvent(info.install, {
+          step: "permissions_prompt",
+          num_strings: strings.msgs.length,
+        });
+      }
+
       this.showPermissionsPrompt(browser, strings, icon, histkey)
           .then(answer => {
             if (answer) {
               info.resolve();
             } else {
               info.reject();
             }
           });
@@ -218,16 +231,18 @@ var ExtensionsUI = {
       // If we don't prompt for any new permissions, just apply it
       if (strings.msgs.length == 0) {
         info.resolve();
         return;
       }
 
       let update = {
         strings,
+        permissions: info.permissions,
+        install: info.install,
         addon: info.addon,
         resolve: info.resolve,
         reject: info.reject,
       };
 
       this.updates.add(update);
       this._updateNotifications();
     } else if (topic == "webextension-install-notify") {
--- a/browser/themes/shared/browser.inc.css
+++ b/browser/themes/shared/browser.inc.css
@@ -147,17 +147,17 @@
 :root:not([privatebrowsingmode=temporary]) .private-browsing-indicator {
   display: none;
 }
 
 /* End private browsing and accessibility indicators */
 
 /* Override theme colors since the picker uses extra colors that
    themes cannot set */
-#DateTimePickerPanel[active="true"] {
+#DateTimePickerPanel {
   --arrowpanel-background: var(--default-arrowpanel-background);
   --arrowpanel-color: var(--default-arrowpanel-color);
   --arrowpanel-border-color: var(--default-arrowpanel-border-color);
 }
 
 #DateTimePickerPanel[side="top"],
 #DateTimePickerPanel[side="bottom"] {
   margin-left: 0;
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -606,17 +606,16 @@ toolbarbutton[constrain-size="true"][cui
   list-style-image: url(chrome://browser/skin/tracking-protection.svg);
 }
 
 #appMenu-tp-button {
   -moz-box-flex: 1;
 }
 
 #appMenu-tp-category {
-  color: var(--panel-disabled-color);
   margin-inline-end: 0;
 }
 
 .addon-banner-item > .toolbarbutton-text,
 .panel-banner-item > .toolbarbutton-text {
   margin: 0;
   padding: 0 6px;
   text-align: start;
--- a/build/moz.configure/bindgen.configure
+++ b/build/moz.configure/bindgen.configure
@@ -55,17 +55,17 @@ def llvm_config_paths(host):
         ]
     llvm_config_progs.append('llvm-config')
 
     # Homebrew on macOS doesn't make clang available on PATH, so we have to
     # look for it in non-standard places.
     if host.kernel == 'Darwin':
         brew = find_program('brew')
         if brew:
-            brew_config = check_cmd_output([brew, 'config']).strip()
+            brew_config = check_cmd_output(brew, 'config').strip()
 
             for line in brew_config.splitlines():
                 if line.startswith('HOMEBREW_PREFIX'):
                     fields = line.split(None, 2)
                     prefix = fields[1] if len(fields) == 2 else ''
                     path = ['opt', 'llvm', 'bin', 'llvm-config']
                     llvm_config_progs.append(os.path.join(prefix, *path))
                     break
--- a/build/moz.configure/windows.configure
+++ b/build/moz.configure/windows.configure
@@ -275,28 +275,28 @@ def vc_path(c_compiler, toolchain_search
 
 option(env='DIA_SDK_PATH', nargs=1,
        help='Path to the Debug Interface Access SDK')
 
 
 @depends(vc_path, 'DIA_SDK_PATH')
 @checking('for the Debug Interface Access SDK', lambda x: x or 'not found')
 @imports('os')
-@imports(_from='os.path', _import='isdir')
 def dia_sdk_dir(vc_path, dia_sdk_path):
     if dia_sdk_path:
-        return os.path.normpath(dia_sdk_path[0])
+        path = os.path.normpath(dia_sdk_path[0])
 
-    if vc_path:
+    elif vc_path:
         # This would be easier if we had the installationPath that
         # get_vc_paths works with, since 'DIA SDK' is relative to that.
         path = os.path.normpath(os.path.join(
             vc_path, r'..\..\..\..\DIA SDK'))
-        if isdir(path):
-            return path
+
+    if os.path.exists(os.path.join(path, 'include', 'dia2.h')):
+        return path
 
 
 @depends(vc_path, valid_windows_sdk_dir, valid_ucrt_sdk_dir, dia_sdk_dir)
 @imports('os')
 def include_path(vc_path, windows_sdk_dir, ucrt_sdk_dir, dia_sdk_dir):
     if not vc_path:
         return
     atlmfc_dir = os.path.join(vc_path, 'atlmfc', 'include')
@@ -328,43 +328,72 @@ def include_path(vc_path, windows_sdk_di
     os.environ['INCLUDE'] = includes
     return includes
 
 
 set_config('INCLUDE', include_path)
 
 
 @template
-def lib_path_for(host_or_target):
-    @depends(host_or_target, dependable(host_or_target is host), vc_path,
-             valid_windows_sdk_dir, valid_ucrt_sdk_dir, dia_sdk_dir)
-    @imports('os')
-    def lib_path(target, is_host, vc_path, windows_sdk_dir, ucrt_sdk_dir, dia_sdk_dir):
-        if not vc_path:
+def dia_sdk_subdir(host_or_target, subdir):
+    @depends(dia_sdk_dir, host_or_target, dependable(subdir))
+    def dia_sdk_subdir(dia_sdk_dir, target, subdir):
+        if not dia_sdk_dir:
             return
-        sdk_target = {
-            'x86': 'x86',
-            'x86_64': 'x64',
-            'arm': 'arm',
-            'aarch64': 'arm64',
-        }.get(target.cpu)
-
+        # For some reason the DIA SDK still uses the old-style targets
+        # even in a newer MSVC.
         old_target = {
             'x86': '',
             'x86_64': 'amd64',
             'arm': 'arm',
             'aarch64': 'arm64'
         }.get(target.cpu)
         if old_target is None:
             return
         # As old_target can be '', and os.path.join will happily use the empty
         # string, leading to a string ending with a backslash, that Make will
         # interpret as a "string continues on next line" indicator, use variable
         # args.
         old_target = (old_target,) if old_target else ()
+        return os.path.join(dia_sdk_dir, subdir, *old_target)
+
+    return dia_sdk_subdir
+
+
+# XXX: remove after bug 1523201
+js_option(env='WIN_DIA_SDK_BIN_DIR', nargs=1, help='Path to the DIA DLLs')
+
+
+@depends('WIN_DIA_SDK_BIN_DIR', dia_sdk_subdir(host, 'bin'))
+@imports('os')
+def dia_sdk_bin_dir(from_env, guessed):
+    if from_env:
+        if not os.path.isdir(from_env[0]):
+            die('Invalid Windows DIA SDK directory: {}'.format(from_env))
+        return from_env[0]
+    return guessed
+
+
+set_config('WIN_DIA_SDK_BIN_DIR', dia_sdk_bin_dir)
+
+
+@template
+def lib_path_for(host_or_target):
+    @depends(host_or_target, dependable(host_or_target is host), vc_path,
+             valid_windows_sdk_dir, valid_ucrt_sdk_dir, dia_sdk_subdir(host_or_target, 'lib'))
+    @imports('os')
+    def lib_path(target, is_host, vc_path, windows_sdk_dir, ucrt_sdk_dir, dia_sdk_lib_dir):
+        if not vc_path:
+            return
+        sdk_target = {
+            'x86': 'x86',
+            'x86_64': 'x64',
+            'arm': 'arm',
+            'aarch64': 'arm64',
+        }.get(target.cpu)
 
         # MSVC2017 switched to use the same target naming as the sdk.
         atlmfc_dir = os.path.join(vc_path, 'atlmfc', 'lib', sdk_target)
         if not os.path.isdir(atlmfc_dir):
             die('Cannot find the ATL/MFC libraries in the Visual C++ directory '
                 '(%s). Please install them.' % vc_path)
 
         libs = []
@@ -372,20 +401,18 @@ def lib_path_for(host_or_target):
         if lib_env and not is_host:
             libs.extend(lib_env.split(os.pathsep))
         libs.extend((
             os.path.join(vc_path, 'lib', sdk_target),
             atlmfc_dir,
             os.path.join(windows_sdk_dir.lib, 'um', sdk_target),
             os.path.join(ucrt_sdk_dir.lib, 'ucrt', sdk_target),
         ))
-        if dia_sdk_dir:
-            # For some reason the DIA SDK still uses the old-style targets
-            # even in a newer MSVC.
-            libs.append(os.path.join(dia_sdk_dir, 'lib', *old_target))
+        if dia_sdk_lib_dir:
+            libs.append(dia_sdk_lib_dir)
         return libs
 
     return lib_path
 
 
 @depends(lib_path_for(target))
 @imports('os')
 def lib_path(libs):
--- a/build/win32/mozconfig.vs2017
+++ b/build/win32/mozconfig.vs2017
@@ -5,21 +5,18 @@ fi
 
 if [ -d "${VSPATH}" ]; then
     VSWINPATH="$(cd ${VSPATH} && pwd -W)"
 
     export WINDOWSSDKDIR="${VSWINPATH}/SDK"
     export WIN32_REDIST_DIR="${VSPATH}/VC/redist/x86/Microsoft.VC141.CRT"
     export WIN_UCRT_REDIST_DIR="${VSPATH}/SDK/Redist/ucrt/DLLs/x86"
     export WIN_DIA_SDK_BIN_DIR="${VSPATH}/DIA SDK/bin/amd64"
+    export DIA_SDK_PATH="${VSPATH}/DIA SDK"
+    export VC_PATH="${VSPATH}/VC"
 
     win_sdk_version="10.0.17134.0"
-    export PATH="${VSPATH}/VC/bin/Hostx86/x86:${VSPATH}/VC/bin/Hostx64/x86:${VSPATH}/VC/bin/Hostx64/x64:${VSPATH}/SDK/bin/${win_sdk_version}/x64:${WIN_DIA_SDK_BIN_DIR}:${PATH}"
-    export PATH="${VSPATH}/VC/redist/x86/Microsoft.VC141.CRT:${VSPATH}/SDK/Redist/ucrt/DLLs/x86:${PATH}"
-
-    export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/${win_sdk_version}/ucrt:${VSPATH}/SDK/Include/${win_sdk_version}/shared:${VSPATH}/SDK/Include/${win_sdk_version}/um:${VSPATH}/SDK/Include/${win_sdk_version}/winrt:${VSPATH}/DIA SDK/include"
-    export LIB="${VSPATH}/VC/lib/x86:${VSPATH}/VC/atlmfc/lib/x86:${VSPATH}/SDK/Lib/${win_sdk_version}/ucrt/x86:${VSPATH}/SDK/Lib/${win_sdk_version}/um/x86:${VSPATH}/DIA SDK/lib"
 
     export WIN64_LINK="${VSPATH}/VC/bin/Hostx64/x64/link.exe"
     export WIN64_LIB="${VSPATH}/VC/lib/x64:${VSPATH}/VC/atlmfc/lib/x64:${VSPATH}/SDK/Lib/${win_sdk_version}/ucrt/x64:${VSPATH}/SDK/Lib/${win_sdk_version}/um/x64:${VSPATH}/DIA SDK/lib/amd64"
 fi
 
 ac_add_options --target=i686-pc-mingw32
--- a/build/win64-aarch64/mozconfig.vs2017
+++ b/build/win64-aarch64/mozconfig.vs2017
@@ -4,34 +4,16 @@ if [ -z "${VSPATH}" ]; then
 fi
 
 if [ -d "${VSPATH}" ]; then
     VSWINPATH="$(cd ${VSPATH} && pwd -W)"
 
     export WINDOWSSDKDIR="${VSWINPATH}/SDK"
     export WIN32_REDIST_DIR=${VSPATH}/VC/redist/arm64/Microsoft.VC141.CRT
     export WIN_DIA_SDK_BIN_DIR="${VSPATH}/DIA SDK/bin/amd64"
+    export DIA_SDK_PATH="${VSPATH}/DIA SDK"
+    export VC_PATH="${VSPATH}/VC"
 
     win_sdk_version="10.0.17134.0"
 
-    # Need to run x86-64 host binaries.
-    export PATH="${VSPATH}/VC/bin/Hostx64/x64:${VSPATH}/SDK/bin/${win_sdk_version}/x64:${VSPATH}/VC/redist/x64/Microsoft.VC141.CRT:${VSPATH}/SDK/Redist/ucrt/DLLs/x64:${WIN_DIA_SDK_BIN_DIR}:${PATH}"
-
-    export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/${win_sdk_version}/ucrt:${VSPATH}/SDK/Include/${win_sdk_version}/shared:${VSPATH}/SDK/Include/${win_sdk_version}/um:${VSPATH}/SDK/Include/${win_sdk_version}/winrt:${VSPATH}/DIA SDK/include"
-    export LIB="${VSPATH}/VC/lib/arm64:${VSPATH}/VC/atlmfc/lib/arm64:${VSPATH}/SDK/Lib/${win_sdk_version}/ucrt/arm64:${VSPATH}/SDK/Lib/${win_sdk_version}/um/arm64:${VSPATH}/DIA SDK/lib/amd64"
-
-    # We need to declare host and target tools separately.
-    arm_bin="${VSPATH}/VC/bin/Hostx64/arm64"
-    export AS="${arm_bin}/armasm64.exe"
-
-    # We provided LIB, above, but we also need to provide HOST_LDFLAGS so host
-    # links are not completely confused.  LIBPATH wants its argument with
-    # Windows-style drives.
-    libs=""
-    libs="${libs} -LIBPATH:${VSWINPATH}/VC/lib/x64"
-    libs="${libs} -LIBPATH:${VSWINPATH}/VC/atlmfc/lib/x64"
-    libs="${libs} -LIBPATH:${VSWINPATH}/SDK/Lib/${win_sdk_version}/um/x64"
-    libs="${libs} -LIBPATH:${VSWINPATH}/SDK/Lib/${win_sdk_version}/ucrt/x64"
-    export HOST_LDFLAGS="${libs}"
-
     export WIN64_LINK="${VSPATH}/VC/bin/Hostx64/x64/link.exe"
     export WIN64_LIB="${VSPATH}/VC/lib/x64:${VSPATH}/VC/atlmfc/lib/x64:${VSPATH}/SDK/Lib/${win_sdk_version}/ucrt/x64:${VSPATH}/SDK/Lib/${win_sdk_version}/um/x64:${VSPATH}/DIA SDK/lib/amd64"
 fi
--- a/build/win64/mozconfig.vs2017
+++ b/build/win64/mozconfig.vs2017
@@ -5,15 +5,11 @@ fi
 
 if [ -d "${VSPATH}" ]; then
     VSWINPATH="$(cd ${VSPATH} && pwd -W)"
 
     export WINDOWSSDKDIR="${VSWINPATH}/SDK"
     export WIN32_REDIST_DIR=${VSPATH}/VC/redist/x64/Microsoft.VC141.CRT
     export WIN_UCRT_REDIST_DIR="${VSPATH}/SDK/Redist/ucrt/DLLs/x64"
     export WIN_DIA_SDK_BIN_DIR="${VSPATH}/DIA SDK/bin/amd64"
-
-    win_sdk_version="10.0.17134.0"
-    export PATH="${VSPATH}/VC/bin/Hostx64/x64:${VSPATH}/SDK/bin/${win_sdk_version}/x64:${VSPATH}/VC/redist/x64/Microsoft.VC141.CRT:${VSPATH}/SDK/Redist/ucrt/DLLs/x64:${WIN_DIA_SDK_BIN_DIR}:${PATH}"
-
-    export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/${win_sdk_version}/ucrt:${VSPATH}/SDK/Include/${win_sdk_version}/shared:${VSPATH}/SDK/Include/${win_sdk_version}/um:${VSPATH}/SDK/Include/${win_sdk_version}/winrt:${VSPATH}/DIA SDK/include"
-    export LIB="${VSPATH}/VC/lib/x64:${VSPATH}/VC/atlmfc/lib/x64:${VSPATH}/SDK/Lib/${win_sdk_version}/ucrt/x64:${VSPATH}/SDK/Lib/${win_sdk_version}/um/x64:${VSPATH}/DIA SDK/lib/amd64"
+    export DIA_SDK_PATH="${VSPATH}/DIA SDK"
+    export VC_PATH="${VSPATH}/VC"
 fi
--- a/caps/nsScriptSecurityManager.cpp
+++ b/caps/nsScriptSecurityManager.cpp
@@ -927,16 +927,29 @@ nsresult nsScriptSecurityManager::CheckL
   if (NS_FAILED(rv)) {
     // Deny access, since the origin principal is not system
     if (reportErrors) {
       ReportError(errorTag, aSourceURI, aTargetURI, aFromPrivateWindow);
     }
     return rv;
   }
 
+  // Used by ExtensionProtocolHandler to prevent loading extension resources
+  // in private contexts if the extension does not have permission.
+  if (aFromPrivateWindow) {
+    rv = DenyAccessIfURIHasFlags(
+        aTargetURI, nsIProtocolHandler::URI_DISALLOW_IN_PRIVATE_CONTEXT);
+    if (NS_FAILED(rv)) {
+      if (reportErrors) {
+        ReportError(errorTag, aSourceURI, aTargetURI, aFromPrivateWindow);
+      }
+      return rv;
+    }
+  }
+
   // Check for chrome target URI
   bool hasFlags = false;
   rv = NS_URIChainHasFlags(aTargetURI, nsIProtocolHandler::URI_IS_UI_RESOURCE,
                            &hasFlags);
   NS_ENSURE_SUCCESS(rv, rv);
   if (hasFlags) {
     if (aFlags & nsIScriptSecurityManager::ALLOW_CHROME) {
       // Allow a URI_IS_UI_RESOURCE source to link to a URI_IS_UI_RESOURCE
--- a/config/config.mk
+++ b/config/config.mk
@@ -196,23 +196,16 @@ COMPILE_CMFLAGS = $(MOZ_LTO_CFLAGS) $(OS
 COMPILE_CMMFLAGS = $(MOZ_LTO_CFLAGS) $(OS_COMPILE_CMMFLAGS) $(MOZBUILD_CMMFLAGS)
 ASFLAGS = $(COMPUTED_ASFLAGS)
 SFLAGS = $(COMPUTED_SFLAGS)
 
 HOST_CFLAGS = $(COMPUTED_HOST_CFLAGS) $(_DEPEND_CFLAGS)
 HOST_CXXFLAGS = $(COMPUTED_HOST_CXXFLAGS) $(_DEPEND_CFLAGS)
 HOST_C_LDFLAGS = $(COMPUTED_HOST_C_LDFLAGS)
 HOST_CXX_LDFLAGS = $(COMPUTED_HOST_CXX_LDFLAGS)
-# Win32 Cross-builds on win64 need to override LIB when invoking the linker,
-# which we do for rust through cargo-linker.bat, so we abuse it here.
-# Ideally, we'd empty LIB and pass -LIBPATH options to the linker somehow but
-# we don't have this in place for rust, so...
-ifdef WIN64_CARGO_LINKER
-HOST_LINKER = $(topobjdir)/build/win64/cargo-linker.bat
-endif
 
 ifdef MOZ_LTO
 ifeq (Darwin,$(OS_TARGET))
 # When linking on macOS, debug info is not linked along with the final binary,
 # and the dwarf data stays in object files until they are "linked" with the
 # dsymutil tool.
 # With LTO, object files are temporary, and are not kept around, which
 # means there's no object file for dsymutil to do its job. Consequently,
--- a/devtools/client/aboutdebugging-new/aboutdebugging.js
+++ b/devtools/client/aboutdebugging-new/aboutdebugging.js
@@ -50,16 +50,19 @@ const AboutDebugging = {
 
     this.onAdbAddonUpdated = this.onAdbAddonUpdated.bind(this);
     this.onNetworkLocationsUpdated = this.onNetworkLocationsUpdated.bind(this);
     this.onUSBRuntimesUpdated = this.onUSBRuntimesUpdated.bind(this);
 
     this.store = configureStore();
     this.actions = bindActionCreators(actions, this.store.dispatch);
 
+    const width = this.getRoundedViewportWidth();
+    this.actions.recordTelemetryEvent("open_adbg", { width });
+
     await l10n.init();
 
     this.actions.createThisFirefoxRuntime();
 
     render(
       Provider(
         {
           store: this.store,
@@ -99,20 +102,21 @@ const AboutDebugging = {
     this.actions.updateNetworkLocations(getNetworkLocations());
   },
 
   onUSBRuntimesUpdated() {
     this.actions.updateUSBRuntimes(getUSBRuntimes());
   },
 
   async destroy() {
-    const state = this.store.getState();
-
+    const width = this.getRoundedViewportWidth();
+    this.actions.recordTelemetryEvent("close_adbg", { width });
     l10n.destroy();
 
+    const state = this.store.getState();
     const currentRuntimeId = state.runtimes.selectedRuntimeId;
     if (currentRuntimeId) {
       await this.actions.unwatchRuntime(currentRuntimeId);
     }
 
     // Remove all client listeners.
     this.actions.removeRuntimeListeners();
 
@@ -121,16 +125,23 @@ const AboutDebugging = {
     adbAddon.off("update", this.onAdbAddonUpdated);
     setDebugTargetCollapsibilities(state.ui.debugTargetCollapsibilities);
     unmountComponentAtNode(this.mount);
   },
 
   get mount() {
     return document.getElementById("mount");
   },
+
+  /**
+   * Computed viewport width, rounded at 50px. Used for telemetry events.
+   */
+  getRoundedViewportWidth() {
+    return Math.ceil(window.outerWidth / 50) * 50;
+  },
 };
 
 window.addEventListener("DOMContentLoaded", () => {
   AboutDebugging.init();
 }, { once: true });
 
 window.addEventListener("unload", () => {
   AboutDebugging.destroy();
--- a/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
+++ b/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
@@ -35,18 +35,20 @@ const {
   REQUEST_WORKERS_START,
   REQUEST_WORKERS_SUCCESS,
   TEMPORARY_EXTENSION_INSTALL_FAILURE,
   TEMPORARY_EXTENSION_INSTALL_START,
   TEMPORARY_EXTENSION_INSTALL_SUCCESS,
   RUNTIMES,
 } = require("../constants");
 
+const Actions = require("./index");
+
 function inspectDebugTarget(type, id) {
-  return async (_, getState) => {
+  return async (dispatch, getState) => {
     const runtime = getCurrentRuntime(getState().runtimes);
     const { runtimeDetails, type: runtimeType } = runtime;
 
     switch (type) {
       case DEBUG_TARGETS.TAB: {
         // Open tab debugger in new window.
         if (runtimeType === RUNTIMES.NETWORK || runtimeType === RUNTIMES.USB) {
           // Pass the remote id from the client manager so that about:devtools-toolbox can
@@ -69,22 +71,26 @@ function inspectDebugTarget(type, id) {
       }
       case DEBUG_TARGETS.WORKER: {
         // Open worker toolbox in new window.
         const devtoolsClient = runtimeDetails.clientWrapper.client;
         const front = devtoolsClient.getActor(id);
         gDevToolsBrowser.openWorkerToolbox(front);
         break;
       }
-
       default: {
         console.error("Failed to inspect the debug target of " +
                       `type: ${ type } id: ${ id }`);
       }
     }
+
+    dispatch(Actions.recordTelemetryEvent("inspect", {
+      "target_type": type,
+      "runtime_type": runtimeType,
+    }));
   };
 }
 
 function installTemporaryExtension() {
   const message = l10n.getString("about-debugging-tmp-extension-install-message");
   return async (dispatch, getState) => {
     dispatch({ type: TEMPORARY_EXTENSION_INSTALL_START });
     const file = await openTemporaryExtension(window, message);
--- a/devtools/client/aboutdebugging-new/src/actions/index.js
+++ b/devtools/client/aboutdebugging-new/src/actions/index.js
@@ -1,11 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const debugTargets = require("./debug-targets");
 const runtimes = require("./runtimes");
+const telemetry = require("./telemetry");
 const ui = require("./ui");
 
-Object.assign(exports, ui, runtimes, debugTargets);
+Object.assign(exports, ui, runtimes, telemetry, debugTargets);
--- a/devtools/client/aboutdebugging-new/src/actions/moz.build
+++ b/devtools/client/aboutdebugging-new/src/actions/moz.build
@@ -1,10 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'debug-targets.js',
     'index.js',
     'runtimes.js',
+    'telemetry.js',
     'ui.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/actions/telemetry.js
@@ -0,0 +1,23 @@
+/* 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 {
+  TELEMETRY_RECORD,
+} = require("../constants");
+
+/**
+ * If a given event cannot be mapped to an existing action, use this action that will only
+ * be processed by the event recording middleware.
+ */
+function recordTelemetryEvent(method, details) {
+  return (dispatch, getState) => {
+    dispatch({ type: TELEMETRY_RECORD, method, details });
+  };
+}
+
+module.exports = {
+  recordTelemetryEvent,
+};
--- a/devtools/client/aboutdebugging-new/src/constants.js
+++ b/devtools/client/aboutdebugging-new/src/constants.js
@@ -32,16 +32,17 @@ const actionTypes = {
   REQUEST_TABS_SUCCESS: "REQUEST_TABS_SUCCESS",
   REQUEST_WORKERS_FAILURE: "REQUEST_WORKERS_FAILURE",
   REQUEST_WORKERS_START: "REQUEST_WORKERS_START",
   REQUEST_WORKERS_SUCCESS: "REQUEST_WORKERS_SUCCESS",
   SELECT_PAGE_FAILURE: "SELECT_PAGE_FAILURE",
   SELECT_PAGE_START: "SELECT_PAGE_START",
   SELECT_PAGE_SUCCESS: "SELECT_PAGE_SUCCESS",
   SELECTED_RUNTIME_ID_UPDATED: "SELECTED_RUNTIME_ID_UPDATED",
+  TELEMETRY_RECORD: "TELEMETRY_RECORD",
   TEMPORARY_EXTENSION_INSTALL_FAILURE: "TEMPORARY_EXTENSION_INSTALL_FAILURE",
   TEMPORARY_EXTENSION_INSTALL_START: "TEMPORARY_EXTENSION_INSTALL_START",
   TEMPORARY_EXTENSION_INSTALL_SUCCESS: "TEMPORARY_EXTENSION_INSTALL_SUCCESS",
   THIS_FIREFOX_RUNTIME_CREATED: "THIS_FIREFOX_RUNTIME_CREATED",
   UNWATCH_RUNTIME_FAILURE: "UNWATCH_RUNTIME_FAILURE",
   UNWATCH_RUNTIME_START: "UNWATCH_RUNTIME_START",
   UNWATCH_RUNTIME_SUCCESS: "UNWATCH_RUNTIME_SUCCESS",
   UPDATE_CONNECTION_PROMPT_SETTING_FAILURE: "UPDATE_CONNECTION_PROMPT_SETTING_FAILURE",
--- a/devtools/client/aboutdebugging-new/src/create-store.js
+++ b/devtools/client/aboutdebugging-new/src/create-store.js
@@ -11,16 +11,17 @@ const { thunk } = require("devtools/clie
 const { waitUntilService } = require("devtools/client/shared/redux/middleware/wait-service.js");
 
 const rootReducer = require("./reducers/index");
 const { DebugTargetsState } = require("./reducers/debug-targets-state");
 const { RuntimesState } = require("./reducers/runtimes-state");
 const { UiState } = require("./reducers/ui-state");
 const debugTargetListenerMiddleware = require("./middleware/debug-target-listener");
 const errorLoggingMiddleware = require("./middleware/error-logging");
+const eventRecordingMiddleware = require("./middleware/event-recording");
 const extensionComponentDataMiddleware = require("./middleware/extension-component-data");
 const tabComponentDataMiddleware = require("./middleware/tab-component-data");
 const workerComponentDataMiddleware = require("./middleware/worker-component-data");
 const { getDebugTargetCollapsibilities } = require("./modules/debug-target-collapsibilities");
 const { getNetworkLocations } = require("./modules/network-locations");
 
 const { PREFERENCES } = require("./constants");
 
@@ -29,16 +30,17 @@ function configureStore() {
     debugTargets: new DebugTargetsState(),
     runtimes: new RuntimesState(),
     ui: getUiState(),
   };
 
   const middleware = applyMiddleware(thunk,
                                      debugTargetListenerMiddleware,
                                      errorLoggingMiddleware,
+                                     eventRecordingMiddleware,
                                      extensionComponentDataMiddleware,
                                      tabComponentDataMiddleware,
                                      workerComponentDataMiddleware,
                                      waitUntilService);
 
   return createStore(rootReducer, initialState, middleware);
 }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/middleware/event-recording.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";
+
+const Telemetry = require("devtools/client/shared/telemetry");
+loader.lazyGetter(this, "telemetry", () => new Telemetry());
+// This is a unique id that should be submitted with all about:debugging events.
+loader.lazyGetter(this, "sessionId", () => parseInt(telemetry.msSinceProcessStart(), 10));
+
+const {
+  SELECT_PAGE_SUCCESS,
+  TELEMETRY_RECORD,
+} = require("../constants");
+
+function recordEvent(method, details) {
+  // Add the session id to the event details.
+  const eventDetails = Object.assign({}, details, { "session_id": sessionId });
+  telemetry.recordEvent(method, "aboutdebugging", null, eventDetails);
+}
+
+/**
+ * This middleware will record events to telemetry for some specific actions.
+ */
+function eventRecordingMiddleware() {
+  return next => action => {
+    switch (action.type) {
+      case SELECT_PAGE_SUCCESS:
+        recordEvent("select_page", { "page_type": action.page });
+        break;
+      case TELEMETRY_RECORD:
+        const { method, details } = action;
+        if (method) {
+          recordEvent(method, details);
+        } else {
+          console.error(`[RECORD EVENT FAILED] ${action.type}: no "method" property`);
+        }
+        break;
+    }
+
+    return next(action);
+  };
+}
+
+module.exports = eventRecordingMiddleware;
--- a/devtools/client/aboutdebugging-new/src/middleware/moz.build
+++ b/devtools/client/aboutdebugging-new/src/middleware/moz.build
@@ -1,11 +1,12 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'debug-target-listener.js',
     'error-logging.js',
+    'event-recording.js',
     'extension-component-data.js',
     'tab-component-data.js',
     'worker-component-data.js',
 )
--- a/devtools/client/aboutdebugging-new/src/modules/l10n.js
+++ b/devtools/client/aboutdebugging-new/src/modules/l10n.js
@@ -1,9 +1,8 @@
-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Services = require("Services");
 
--- a/devtools/client/aboutdebugging-new/test/browser/browser.ini
+++ b/devtools/client/aboutdebugging-new/test/browser/browser.ini
@@ -7,32 +7,39 @@ prefs =
   devtools.aboutdebugging.showSystemAddons=false
 support-files =
   head.js
   helper-adb.js
   helper-addons.js
   helper-collapsibilities.js
   helper-mocks.js
   helper-serviceworker.js
+  helper-telemetry.js
   mocks/*
   resources/bad-extension/*
   resources/packaged-extension/*
   resources/service-workers/*
   resources/test-adb-extension/*
   resources/test-temporary-extension/*
   test-tab-favicons.html
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/shared-redux-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
+[browser_aboutdebugging_addons_debug_console.js]
+tags = webextensions
+[browser_aboutdebugging_addons_debug_inspector.js]
+tags = webextensions
 [browser_aboutdebugging_addons_debug_nobg.js]
+tags = webextensions
 [browser_aboutdebugging_addons_manifest_url.js]
 skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
 [browser_aboutdebugging_addons_remote_runtime.js]
 [browser_aboutdebugging_addons_temporary_addon_buttons.js]
+skip-if = (os == 'win') # On windows the AddonManager locks the XPI file loaded as a temporary extension and we can not test the reload of the extension.
 [browser_aboutdebugging_addons_temporary_id_message.js]
 [browser_aboutdebugging_addons_temporary_install_error.js]
 [browser_aboutdebugging_addons_warnings.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_connection_prompt_setting.js]
 [browser_aboutdebugging_debug-target-pane_collapsibilities_interaction.js]
@@ -68,12 +75,15 @@ skip-if = (os == "win" && ccov) # Bug 15
 [browser_aboutdebugging_sidebar_usb_runtime_select.js]
 [browser_aboutdebugging_sidebar_usb_status.js]
 skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
 [browser_aboutdebugging_sidebar_usb_unknown_runtime.js]
 [browser_aboutdebugging_stop_adb.js]
 skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
 [browser_aboutdebugging_system_addons.js]
 [browser_aboutdebugging_tab_favicons.js]
+[browser_aboutdebugging_telemetry_basic.js]
+[browser_aboutdebugging_telemetry_inspect.js]
+[browser_aboutdebugging_telemetry_navigate.js]
 [browser_aboutdebugging_thisfirefox.js]
 [browser_aboutdebugging_thisfirefox_runtime_info.js]
 [browser_aboutdebugging_thisfirefox_worker_inspection.js]
 [browser_aboutdebugging_workers_remote_runtime.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_debug_console.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+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 {
+  BrowserToolboxProcess,
+} = ChromeUtils.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+// This is a migration from:
+// https://searchfox.org/mozilla-central/source/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
+
+/**
+ * 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() {
+  await enableExtensionDebugging();
+  const { document, tab } = await openAboutDebugging();
+
+  await installTemporaryExtensionFromXPI({
+    background: function() {
+      window.myWebExtensionAddonFunction = function() {
+        console.log("Background page function called",
+                    this.browser.runtime.getManifest());
+      };
+    },
+    id: ADDON_ID,
+    name: ADDON_NAME,
+  }, document);
+  const target = findDebugTargetByText(ADDON_NAME, document);
+
+  info("Setup the toolbox test function as environment variable");
+  const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + toolboxTestScript);
+  registerCleanupFunction(() => env.set("MOZ_TOOLBOX_TEST_SCRIPT", ""));
+
+  info("Click inspect to open the addon toolbox, wait for toolbox close event");
+  const onToolboxClose = BrowserToolboxProcess.once("close");
+  const inspectButton = target.querySelector(".js-debug-target-inspect-button");
+  inspectButton.click();
+  await onToolboxClose;
+
+  // The test script will not close the toolbox and will timeout if it fails, so reaching
+  // this point in the test is enough to assume the test was successful.
+  ok(true, "Addon toolbox closed");
+
+  await removeTemporaryExtension(ADDON_NAME, document);
+  await removeTab(tab);
+});
+
+// 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!
+function toolboxTestScript() {
+  /* eslint-disable no-undef */
+  function findMessages(hud, text, selector = ".message") {
+    const messages = hud.ui.outputNode.querySelectorAll(selector);
+    const elements = Array.prototype.filter.call(
+      messages,
+      (el) => el.textContent.includes(text)
+    );
+    return elements;
+  }
+
+  async function waitFor(condition) {
+    while (!condition()) {
+      await new Promise(done => toolbox.win.setTimeout(done, 1000));
+    }
+  }
+
+  toolbox.selectTool("webconsole")
+    .then(async console => {
+      const { hud } = console;
+      const { jsterm } = hud;
+      const onMessage = waitFor(() => {
+        return findMessages(hud, "Background page function called").length > 0;
+      });
+      await jsterm.execute("myWebExtensionAddonFunction()");
+      await onMessage;
+      await toolbox.destroy();
+    })
+    .catch(e => dump("Exception from browser toolbox process: " + e + "\n"));
+  /* eslint-enable no-undef */
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_debug_inspector.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from helper-addons.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// See bug 1018184 for resolving these issues.
+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 {
+  BrowserToolboxProcess,
+} = ChromeUtils.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+// This is a migration from:
+// https://searchfox.org/mozilla-central/source/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
+
+/**
+ * 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 testWebExtensionsToolboxWebConsole() {
+  await enableExtensionDebugging();
+  const { document, tab } = await openAboutDebugging();
+
+  await installTemporaryExtensionFromXPI({
+    background: function() {
+      document.body.innerText = "Background Page Body Test Content";
+    },
+    id: ADDON_ID,
+    name: ADDON_NAME,
+  }, document);
+  const target = findDebugTargetByText(ADDON_NAME, document);
+
+  info("Setup the toolbox test function as environment variable");
+  const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + toolboxTestScript);
+  registerCleanupFunction(() => env.set("MOZ_TOOLBOX_TEST_SCRIPT", ""));
+
+  info("Click inspect to open the addon toolbox, wait for toolbox close event");
+  const onToolboxClose = BrowserToolboxProcess.once("close");
+  const inspectButton = target.querySelector(".js-debug-target-inspect-button");
+  inspectButton.click();
+  await onToolboxClose;
+
+  // The test script will not close the toolbox and will timeout if it fails, so reaching
+  // this point in the test is enough to assume the test was successful.
+  ok(true, "Addon toolbox closed");
+
+  await removeTemporaryExtension(ADDON_NAME, document);
+  await removeTab(tab);
+});
+
+// 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!
+function toolboxTestScript() {
+  /* 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");
+      }
+
+      dump("Got a nodeActor\n");
+
+      if (!(nodeActor.inlineTextChild)) {
+        throw new Error("inlineTextChild not found");
+      }
+
+      dump("Got a nodeActor with an inline text child\n");
+
+      const expectedValue = "Background Page Body Test Content";
+      const actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+      if (String(actualValue).trim() !== String(expectedValue).trim()) {
+        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");
+    });
+  /* eslint-enable no-undef */
+}
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_debug_nobg.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_debug_nobg.js
@@ -23,28 +23,21 @@ const {
  *   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() {
   await enableExtensionDebugging();
   const { document, tab } = await openAboutDebugging();
 
-  const manifest = {
-    manifest_version: 2,
+  await installTemporaryExtensionFromXPI({
+    // Do not pass any `background` script.
+    id: ADDON_NOBG_ID,
     name: ADDON_NOBG_NAME,
-    version: "1.0",
-    applications: {
-      gecko: {
-        id: ADDON_NOBG_ID,
-      },
-    },
-  };
-  await installTemporaryExtensionFromManifest(manifest, document);
-
+  }, document);
   const target = findDebugTargetByText(ADDON_NOBG_NAME, document);
 
   info("Setup the toolbox test function as environment variable");
   const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
   env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + toolboxTestScript);
   env.set("MOZ_TOOLBOX_TEST_ADDON_NOBG_NAME", ADDON_NOBG_NAME);
   registerCleanupFunction(() => {
     env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_temporary_addon_buttons.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_temporary_addon_buttons.js
@@ -8,33 +8,26 @@ Services.scriptloader.loadSubScript(CHRO
 // Test that the reload button updates the addon list with the correct metadata.
 add_task(async function() {
   const { document, tab } = await openAboutDebugging();
 
   const ORIGINAL_EXTENSION_NAME = "Temporary web extension (original)";
   const UPDATED_EXTENSION_NAME = "Temporary web extension (updated)";
   const EXTENSION_ID = "test-devtools@mozilla.org";
 
-  const manifestBase = {
-    "manifest_version": 2,
-    "name": ORIGINAL_EXTENSION_NAME,
-    "version": "1.0",
-    "applications": {
-      "gecko": {
-        "id": EXTENSION_ID,
-      },
-    },
-  };
-  const tempExt = await installTemporaryExtensionFromManifest(manifestBase, document);
+  const addonFile = await installTemporaryExtensionFromXPI({
+    id: EXTENSION_ID,
+    name: ORIGINAL_EXTENSION_NAME,
+  }, document);
 
   const originalTarget = findDebugTargetByText(ORIGINAL_EXTENSION_NAME, document);
   ok(!!originalTarget, "The temporary extension isinstalled with the expected name");
 
   info("Update the name of the temporary extension in the manifest");
-  tempExt.writeManifest(Object.assign({}, manifestBase, {name: UPDATED_EXTENSION_NAME}));
+  updateTemporaryXPI({ id: EXTENSION_ID, name: UPDATED_EXTENSION_NAME }, addonFile);
 
   info("Click on the reload button for the temporary extension");
   const reloadButton =
     originalTarget.querySelector(".js-temporary-extension-reload-button");
   reloadButton.click();
 
   info("Wait until the debug target with the original extension name disappears");
   await waitUntil(() => !findDebugTargetByText(ORIGINAL_EXTENSION_NAME, document));
@@ -57,17 +50,17 @@ add_task(async function() {
 });
 
 add_task(async function() {
   const PACKAGED_EXTENSION_ID = "packaged-extension@tests";
   const PACKAGED_EXTENSION_NAME = "Packaged extension";
 
   const { document, tab } = await openAboutDebugging();
 
-  await installRegularAddon("resources/packaged-extension/packaged-extension.xpi");
+  await installRegularExtension("resources/packaged-extension/packaged-extension.xpi");
 
   info("Wait until extension appears in about:debugging");
   await waitUntil(() => findDebugTargetByText(PACKAGED_EXTENSION_NAME, document));
   const target = findDebugTargetByText(PACKAGED_EXTENSION_NAME, document);
 
   const reloadButton = target.querySelector(".js-temporary-extension-reload-button");
   ok(!reloadButton, "No reload button displayed for a regularly installed extension");
 
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_temporary_id_message.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_temporary_id_message.js
@@ -8,60 +8,44 @@ Services.scriptloader.loadSubScript(CHRO
 // Test that temporary extensions show a message about temporary ids, with a learn more
 // link.
 add_task(async function() {
   const EXTENSION_NAME = "Temporary web extension";
   const EXTENSION_ID = "test-devtools@mozilla.org";
 
   const { document, tab } = await openAboutDebugging();
 
-  const manifest = {
-    "manifest_version": 2,
-    "name": EXTENSION_NAME,
-    "version": "1.0",
-    "applications": {
-      "gecko": {
-        "id": EXTENSION_ID,
-      },
-    },
-  };
-
-  const tempExt = new TemporaryExtension(EXTENSION_ID);
-  tempExt.writeManifest(manifest);
-
-  info("Install a temporary extension");
-  await AddonManager.installTemporaryAddon(tempExt.sourceDir);
+  await installTemporaryExtensionFromXPI({
+    id: EXTENSION_ID,
+    name: EXTENSION_NAME,
+  }, document);
 
   info("Wait until a debug target item appears");
   await waitUntil(() => findDebugTargetByText(EXTENSION_NAME, document));
 
   const target = findDebugTargetByText(EXTENSION_NAME, document);
 
   const message = target.querySelector(".js-temporary-id-message");
   ok(!!message, "Temporary id message is displayed for temporary extensions");
 
   const link = target.querySelector(".js-temporary-id-link");
   ok(!!link, "Temporary id link is displayed for temporary extensions");
 
   await removeTemporaryExtension(EXTENSION_NAME, document);
-
-  info("Remove the temporary web extension");
-  tempExt.remove();
-
   await removeTab(tab);
 });
 
 // Test that the message and the link are not displayed for a regular extension.
 add_task(async function() {
   const PACKAGED_EXTENSION_ID = "packaged-extension@tests";
   const PACKAGED_EXTENSION_NAME = "Packaged extension";
 
   const { document, tab } = await openAboutDebugging();
 
-  await installRegularAddon("resources/packaged-extension/packaged-extension.xpi");
+  await installRegularExtension("resources/packaged-extension/packaged-extension.xpi");
 
   info("Wait until extension appears in about:debugging");
   await waitUntil(() => findDebugTargetByText(PACKAGED_EXTENSION_NAME, document));
   const target = findDebugTargetByText(PACKAGED_EXTENSION_NAME, document);
 
   const tmpIdMessage = target.querySelector(".js-temporary-id-message");
   ok(!tmpIdMessage, "No temporary id message is displayed for a regular extension");
 
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_warnings.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_warnings.js
@@ -8,35 +8,24 @@ Services.scriptloader.loadSubScript(CHRO
 
 // Test that extension warnings are displayed in about:debugging.
 add_task(async function() {
   const EXTENSION_NAME = "Temporary web extension";
   const EXTENSION_ID = "test-devtools@mozilla.org";
 
   const { document, tab } = await openAboutDebugging();
 
-  const manifest = {
-    "manifest_version": 2,
-    "name": EXTENSION_NAME,
-    "version": "1.0",
-    "applications": {
-      "gecko": {
-        "id": EXTENSION_ID,
-      },
+  await installTemporaryExtensionFromXPI({
+    id: EXTENSION_ID,
+    name: EXTENSION_NAME,
+    extraProperties: {
+      // This property is not expected in the manifest and should trigger a warning!
+      "wrongProperty": {},
     },
-    // This property is not expected in the manifest and should trigger a warning!
-    "wrongProperty": {},
-  };
-
-  const tempExt = new TemporaryExtension(EXTENSION_ID);
-  tempExt.writeManifest(manifest);
-  registerCleanupFunction(() => tempExt.remove());
-
-  info("Install a temporary extension");
-  await AddonManager.installTemporaryAddon(tempExt.sourceDir);
+  }, document);
 
   info("Wait until a debug target item appears");
   await waitUntil(() => findDebugTargetByText(EXTENSION_NAME, document));
   const target = findDebugTargetByText(EXTENSION_NAME, document);
 
   const warningMessage = target.querySelector(".js-message");
   ok(!!warningMessage, "A warning message is displayed for the installed addon");
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_basic.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-telemetry.js", this);
+
+/**
+ * Check that telemetry events are recorded when opening and closing about debugging.
+ */
+add_task(async function() {
+  setupTelemetryTest();
+
+  const { tab } = await openAboutDebugging();
+
+  const openEvents = readAboutDebuggingEvents().filter(e => e.method === "open_adbg");
+  is(openEvents.length, 1, "Exactly one open event was logged for about:debugging");
+  const sessionId = openEvents[0].extras.session_id;
+  ok(!isNaN(sessionId), "Open event has a valid session id");
+
+  await removeTab(tab);
+
+  const closeEvents = readAboutDebuggingEvents().filter(e => e.method === "close_adbg");
+  is(closeEvents.length, 1, "Exactly one close event was logged for about:debugging");
+  is(closeEvents[0].extras.session_id, sessionId,
+    "Close event has the same session id as the open event");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_inspect.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-telemetry.js", this);
+
+const TAB_URL = "data:text/html,<title>TEST_TAB</title>";
+
+/**
+ * Check that telemetry events are recorded when inspecting a target.
+ */
+add_task(async function() {
+  setupTelemetryTest();
+
+  const { document, tab, window } = await openAboutDebugging();
+
+  const sessionId = getOpenEventSessionId();
+  ok(!isNaN(sessionId), "Open event has a valid session id");
+
+  info("Open a new background tab TEST_TAB");
+  const backgroundTab1 = await addTab(TAB_URL, { background: true });
+
+  info("Wait for the tab to appear in the debug targets with the correct name");
+  await waitUntil(() => findDebugTargetByText("TEST_TAB", document));
+
+  const tabTarget = findDebugTargetByText("TEST_TAB", document);
+  const inspectButton = tabTarget.querySelector(".js-debug-target-inspect-button");
+  ok(inspectButton, "Inspect button for the tab is available");
+
+  info("Click on the inspect button for the test tab");
+  inspectButton.click();
+  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  const newTabUrl = gBrowser.selectedBrowser.currentURI.spec;
+  ok(newTabUrl.startsWith("about:devtools-toolbox"),
+    "about:devtools-toolbox opened in a new tab");
+
+  const evts = readAboutDebuggingEvents().filter(e => e.method === "inspect");
+  is(evts.length, 1, "Exactly one Inspect event found");
+  is(evts[0].extras.target_type, "TAB", "Inspect event has the expected target type");
+  is(evts[0].extras.runtime_type, "this-firefox",
+    "Inspect event has the expected runtime type");
+  is(evts[0].extras.session_id, sessionId, "Inspect event has the expected session");
+
+  info("Close the about:devtools-toolbox tab");
+  await removeTab(gBrowser.selectedTab);
+  await waitForRequestsToSettle(window.AboutDebugging.store);
+
+  info("Remove first background tab");
+  await removeTab(backgroundTab1);
+  await waitUntil(() => !findDebugTargetByText("TEST_TAB", document));
+  await waitForRequestsToSettle(window.AboutDebugging.store);
+
+  await removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_navigate.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-mocks.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-mocks.js", this);
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-telemetry.js", this);
+
+/**
+ * Check that telemetry events are recorded when navigating between different
+ * about:debugging pages.
+ */
+add_task(async function() {
+  // enable USB devices mocks
+  const mocks = new Mocks();
+
+  setupTelemetryTest();
+
+  const { tab, document } = await openAboutDebugging();
+
+  const sessionId = getOpenEventSessionId();
+  ok(!isNaN(sessionId), "Open event has a valid session id");
+
+  info("Navigate to 'Connect' page");
+  document.location.hash = "#/connect";
+  await waitUntil(() => document.querySelector(".js-connect-page"));
+  checkSelectPageEvent("connect", sessionId);
+
+  info("Navigate to 'USB device runtime' page");
+  await navigateToUSBRuntime(mocks, document);
+  checkSelectPageEvent("runtime", sessionId);
+
+  await removeTab(tab);
+});
+
+function checkSelectPageEvent(expectedType, expectedSessionId) {
+  const evts = readAboutDebuggingEvents().filter(e => e.method === "select_page");
+  is(evts.length, 1, "Exactly one select_page event recorded");
+  is(evts[0].extras.page_type, expectedType, "Select page event has the expected type");
+  is(evts[0].extras.session_id, expectedSessionId,
+    "Select page event has the expected session");
+}
+
+async function navigateToUSBRuntime(mocks, doc) {
+  mocks.createUSBRuntime("1337id", {
+    deviceName: "Fancy Phone",
+    name: "Lorem ipsum",
+  });
+  mocks.emitUSBUpdate();
+  await connectToRuntime("Fancy Phone", doc);
+  // navigate to it via URL
+  doc.location.hash = "#/runtime/1337id";
+  await waitUntil(() => doc.querySelector(".js-runtime-page"));
+}
--- a/devtools/client/aboutdebugging-new/test/browser/helper-addons.js
+++ b/devtools/client/aboutdebugging-new/test/browser/helper-addons.js
@@ -22,22 +22,46 @@ async function enableExtensionDebugging(
   // Disable security prompt
   await pushPref("devtools.debugger.prompt-connection", false);
   // Enable Browser toolbox test script execution via env variable
   await pushPref("devtools.browser-toolbox.allow-unsafe-script", true);
 }
 /* exported enableExtensionDebugging */
 
 /**
+ * Install an extension using the AddonManager so it does not show up as temporary.
+ */
+function installRegularExtension(pathOrFile) {
+  const isFile = typeof pathOrFile.isFile === "function" && pathOrFile.isFile();
+  const file = isFile ? pathOrFile : _getSupportsFile(pathOrFile).file;
+  return new Promise(async (resolve, reject) => {
+    const install = await AddonManager.getInstallForFile(file);
+    if (!install) {
+      throw new Error(`An install was not created for ${file.path}`);
+    }
+    install.addListener({
+      onDownloadFailed: reject,
+      onDownloadCancelled: reject,
+      onInstallFailed: reject,
+      onInstallCancelled: reject,
+      onInstallEnded: resolve,
+    });
+    install.install();
+  });
+}
+/* exported installRegularExtension */
+
+/**
  * Install a temporary extension at the provided path, with the provided name.
  * Will use a mock file picker to select the file.
  */
-async function installTemporaryExtension(path, name, document) {
+async function installTemporaryExtension(pathOrFile, name, document) {
+  info("Install temporary extension named " + name);
   // Mock the file picker to select a test addon
-  prepareMockFilePicker(path);
+  prepareMockFilePicker(pathOrFile);
 
   const onAddonInstalled = new Promise(done => {
     Management.on("startup", function listener(event, extension) {
       if (extension.name != name) {
         return;
       }
 
       Management.off("startup", listener);
@@ -48,35 +72,71 @@ async function installTemporaryExtension
   // Trigger the file picker by clicking on the button
   document.querySelector(".js-temporary-extension-install-button").click();
 
   info("Wait for addon to be installed");
   await onAddonInstalled;
 }
 /* exported installTemporaryExtension */
 
+function createTemporaryXPI(xpiData) {
+  const { ExtensionTestCommon } =
+    ChromeUtils.import("resource://testing-common/ExtensionTestCommon.jsm", {});
+
+  const { background, id, name, extraProperties } = xpiData;
+  info("Generate XPI file for " + id);
+
+  const manifest = Object.assign({}, {
+    applications: { gecko: { id }},
+    manifest_version: 2,
+    name,
+    version: "1.0",
+  }, extraProperties);
+
+  const xpiFile = ExtensionTestCommon.generateXPI({ background, manifest });
+  registerCleanupFunction(() => xpiFile.exists() && xpiFile.remove(false));
+  return xpiFile;
+}
+/* exported createTemporaryXPI */
+
 /**
- * Install a fake temporary extension just using the manifest information.
- * @return {TemporaryExtension} the temporary extension instance created
+ * Remove the existing temporary XPI file generated by ExtensionTestCommon and create a
+ * new one at the same location.
+ * @return {File} the temporary extension XPI file created
  */
-async function installTemporaryExtensionFromManifest(manifest, document) {
-  const addonId = manifest.applications.gecko.id;
-  const temporaryExtension = new TemporaryExtension(addonId);
-  temporaryExtension.writeManifest(manifest);
-  registerCleanupFunction(() => temporaryExtension.remove(false));
+function updateTemporaryXPI(xpiData, existingXPI) {
+  info("Delete and regenerate XPI for " + xpiData.id);
 
-  info("Install a temporary extension");
-  await AddonManager.installTemporaryAddon(temporaryExtension.sourceDir);
+  // Store the current name to check the xpi is correctly replaced.
+  const existingName = existingXPI.leafName;
+  info("Delete existing XPI named: " + existingName);
+  existingXPI.exists() && existingXPI.remove(false);
+
+  const xpiFile = createTemporaryXPI(xpiData);
+  // Check that the name of the new file is correct
+  if (xpiFile.leafName !== existingName) {
+    throw new Error("New XPI created with unexpected name: " + xpiFile.leafName);
+  }
+  return xpiFile;
+}
+/* exported updateTemporaryXPI */
 
-  info("Wait until the corresponding debug target item appears");
-  await waitUntil(() => findDebugTargetByText(manifest.name, document));
+/**
+ * Install a fake temporary extension by creating a temporary in-memory XPI file.
+ * @return {File} the temporary extension XPI file created
+ */
+async function installTemporaryExtensionFromXPI(xpiData, document) {
+  const xpiFile = createTemporaryXPI(xpiData);
+  await installTemporaryExtension(xpiFile, xpiData.name, document);
 
-  return temporaryExtension;
+  info("Wait until the addon debug target appears");
+  await waitUntil(() => findDebugTargetByText(xpiData.name, document));
+  return xpiFile;
 }
-/* exported installTemporaryExtensionFromManifest */
+/* exported installTemporaryExtensionFromXPI */
 
 async function removeTemporaryExtension(name, document) {
   info(`Remove the temporary extension with name: '${name}'`);
   const temporaryExtensionItem = findDebugTargetByText(name, document);
   temporaryExtensionItem.querySelector(".js-temporary-extension-remove-button").click();
 
   info("Wait until the debug target item disappears");
   await waitUntil(() => !findDebugTargetByText(name, document));
@@ -88,80 +148,18 @@ async function removeExtension(id, name,
   const extension = await AddonManager.getAddonByID(id);
   extension.uninstall();
 
   info("Wait until the addon disappears from about:debugging");
   await waitUntil(() => !findDebugTargetByText(name, document));
 }
 /* exported removeExtension */
 
-function prepareMockFilePicker(path) {
+function prepareMockFilePicker(pathOrFile) {
+  const isFile = typeof pathOrFile.isFile === "function" && pathOrFile.isFile();
+  const file = isFile ? pathOrFile : _getSupportsFile(pathOrFile).file;
+
   // Mock the file picker to select a test addon
   const MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(window);
-  MockFilePicker.setFiles([_getSupportsFile(path).file]);
+  MockFilePicker.setFiles([file]);
 }
 /* exported prepareMockFilePicker */
-
-/**
- * Creates a web extension from scratch in a temporary location.
- * The object must be removed when you're finished working with it.
- */
-class TemporaryExtension {
-  constructor(addonId) {
-    this.addonId = addonId;
-    this.tmpDir = FileUtils.getDir("TmpD", ["browser_addons_reload"]);
-    if (!this.tmpDir.exists()) {
-      this.tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-    }
-    this.sourceDir = this.tmpDir.clone();
-    this.sourceDir.append(this.addonId);
-    if (!this.sourceDir.exists()) {
-      this.sourceDir.create(Ci.nsIFile.DIRECTORY_TYPE,
-                           FileUtils.PERMS_DIRECTORY);
-    }
-  }
-
-  writeManifest(manifestData) {
-    const manifest = this.sourceDir.clone();
-    manifest.append("manifest.json");
-    if (manifest.exists()) {
-      manifest.remove(true);
-    }
-    const fos = Cc["@mozilla.org/network/file-output-stream;1"]
-                              .createInstance(Ci.nsIFileOutputStream);
-    fos.init(manifest,
-             FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
-             FileUtils.MODE_TRUNCATE,
-             FileUtils.PERMS_FILE, 0);
-
-    const manifestString = JSON.stringify(manifestData);
-    fos.write(manifestString, manifestString.length);
-    fos.close();
-  }
-
-  remove() {
-    return this.tmpDir.remove(true);
-  }
-}
-/* exported TemporaryExtension */
-
-/**
- * Install an add-on using the AddonManager so it does not show up as temporary.
- */
-function installRegularAddon(filePath) {
-  const file = _getSupportsFile(filePath).file;
-  return new Promise(async (resolve, reject) => {
-    const install = await AddonManager.getInstallForFile(file);
-    if (!install) {
-      throw new Error(`An install was not created for ${filePath}`);
-    }
-    install.addListener({
-      onDownloadFailed: reject,
-      onDownloadCancelled: reject,
-      onInstallFailed: reject,
-      onInstallCancelled: reject,
-      onInstallEnded: resolve,
-    });
-    install.install();
-  });
-}
-/* exported installRegularAddon */
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/helper-telemetry.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+/**
+ * Reset all telemetry events.
+ */
+function setupTelemetryTest() {
+  // Let's reset the counts.
+  Services.telemetry.clearEvents();
+
+  // Ensure no events have been logged
+  const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+  const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
+  ok(!snapshot.parent, "No events have been logged for the main process");
+}
+/* exported setupTelemetryTest */
+
+/**
+ * Retrieve the session id from an "open" event.
+ * Note that calling this will "clear" all the events.
+ */
+function getOpenEventSessionId() {
+  const openEvents = readAboutDebuggingEvents().filter(e => e.method === "open_adbg");
+  ok(!!openEvents[0], "Found an about:debugging open event");
+  return openEvents[0].extras.session_id;
+}
+/* exported getOpenEventSessionId */
+
+/**
+ * Read all the pending events that have "aboutdebugging" as their object property.
+ * WARNING: Calling this method also flushes/clears the events.
+ */
+function readAboutDebuggingEvents() {
+  const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+  // Retrieve and clear telemetry events.
+  const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
+  // about:debugging events are logged in the parent process
+  const parentEvents = snapshot.parent || [];
+
+  return parentEvents
+    .map(_toEventObject)
+    .filter(e => e.object === "aboutdebugging");
+}
+/* exported getLoggedEvents */
+
+/**
+ * The telemetry event data structure is simply an array. This helper remaps the array to
+ * an object with more user friendly properties.
+ */
+function _toEventObject(rawEvent) {
+  return {
+    // Category is typically devtools.main for us.
+    category: rawEvent[1],
+    // Method is the event's name (eg open, select_page etc...)
+    method: rawEvent[2],
+    // Object will usually be aboutdebugging for our tests
+    object: rawEvent[3],
+    // Value is usually empty for devtools events
+    value: rawEvent[4],
+    // Extras contain all the details of the event, including the session_id.
+    extras: rawEvent[5],
+  };
+}
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Group.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Group.js
@@ -154,16 +154,17 @@ export default class Group extends Compo
         onClick={this.toggleFrames}
         tabIndex={0}
         title={title}
       >
         {selectable && <FrameIndent />}
         <FrameLocation frame={frame} expanded={expanded} />
         {selectable && <span className="clipboard-only"> </span>}
         <Badge>{this.props.group.length}</Badge>
+        {selectable && <br className="clipboard-only"/>}
       </div>
     );
   }
 
   render() {
     const { expanded } = this.state;
     const { disableContextMenu } = this.props;
     return (
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -471,20 +471,25 @@ Toolbox.prototype = {
       this.isReady = true;
 
       const framesPromise = this._listFrames();
 
       Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings);
       Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
                                  this._applyServiceWorkersTestingSettings);
 
+      // Register listener for handling context menus in standard
+      // input elements: <input> and <textarea>.
+      // There is also support for custom input elements using
+      // .devtools-input class (e.g. CodeMirror instances).
       this.doc.addEventListener("contextmenu", (e) => {
         if (e.originalTarget.closest("input[type=text]") ||
             e.originalTarget.closest("input[type=search]") ||
             e.originalTarget.closest("input:not([type])") ||
+            e.originalTarget.closest(".devtools-input") ||
             e.originalTarget.closest("textarea")) {
           e.stopPropagation();
           e.preventDefault();
           this.openTextBoxContextMenu(e.screenX, e.screenY);
         }
       });
 
       this.shortcuts = new KeyShortcuts({
--- a/devtools/client/inspector/changes/ChangesContextMenu.js
+++ b/devtools/client/inspector/changes/ChangesContextMenu.js
@@ -81,16 +81,17 @@ class ChangesContextMenu {
   }
 
   /**
    * Copy the selected text to clipboard.
    */
   _onCopy() {
     const text = this.window.getSelection().toString();
     clipboardHelper.copyString(text);
+    this.view.onContextMenuCopy();
   }
 
   destroy() {
     this.inspector = null;
     this.panel = null;
     this.view = null;
     this.window = null;
   }
--- a/devtools/client/inspector/changes/ChangesView.js
+++ b/devtools/client/inspector/changes/ChangesView.js
@@ -9,46 +9,55 @@
 const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 loader.lazyRequireGetter(this, "ChangesContextMenu", "devtools/client/inspector/changes/ChangesContextMenu");
 
 const ChangesApp = createFactory(require("./components/ChangesApp"));
 
 const {
+  TELEMETRY_SCALAR_CONTEXTMENU,
+  TELEMETRY_SCALAR_CONTEXTMENU_COPY,
+  TELEMETRY_SCALAR_COPY,
+} = require("./constants");
+
+const {
   resetChanges,
   trackChange,
 } = require("./actions/changes");
 
 class ChangesView {
   constructor(inspector, window) {
     this.document = window.document;
     this.inspector = inspector;
     this.store = this.inspector.store;
+    this.telemetry = this.inspector.telemetry;
 
     this.onAddChange = this.onAddChange.bind(this);
     this.onClearChanges = this.onClearChanges.bind(this);
     this.onChangesFront = this.onChangesFront.bind(this);
     this.onContextMenu = this.onContextMenu.bind(this);
+    this.onCopy = this.onCopy.bind(this);
     this.destroy = this.destroy.bind(this);
 
     this.init();
   }
 
   get contextMenu() {
     if (!this._contextMenu) {
       this._contextMenu = new ChangesContextMenu(this);
     }
 
     return this._contextMenu;
   }
 
   init() {
     const changesApp = ChangesApp({
       onContextMenu: this.onContextMenu,
+      onCopy: this.onCopy,
     });
 
     // listen to the front for initialization, add listeners
     // when it is ready
     this._getChangesFront();
 
     // Expose the provider to let inspector.js use it in setupSidebar.
     this.provider = createElement(Provider, {
@@ -95,18 +104,39 @@ class ChangesView {
     // Turn data into a suitable change to send to the store.
     this.store.dispatch(trackChange(change));
   }
 
   onClearChanges() {
     this.store.dispatch(resetChanges());
   }
 
+  /**
+   * Event handler for the "contextmenu" event fired when the context menu is requested.
+   * @param {Event} e
+   */
   onContextMenu(e) {
     this.contextMenu.show(e);
+    this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU, 1);
+  }
+
+  /**
+   * Callback function ran after the "Copy" option from the context menu is used.
+   * This is not an event handler. The copy event cannot be prevented from this method.
+   */
+  onContextMenuCopy() {
+    this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU_COPY, 1);
+  }
+
+  /**
+   * Event handler for the "copy" event fired when content is copied to the clipboard.
+   * We don't change the default behavior. We only log the increment count of this action.
+   */
+  onCopy() {
+    this.telemetry.scalarAdd(TELEMETRY_SCALAR_COPY, 1);
   }
 
   /**
    * Destruction function called when the inspector is destroyed.
    */
   async destroy() {
     this.store.dispatch(resetChanges());
 
--- a/devtools/client/inspector/changes/components/ChangesApp.js
+++ b/devtools/client/inspector/changes/components/ChangesApp.js
@@ -16,16 +16,18 @@ const { getStr } = require("../utils/l10
 
 class ChangesApp extends PureComponent {
   static get propTypes() {
     return {
       // Nested CSS rule tree structure of CSS changes grouped by source (stylesheet)
       changesTree: PropTypes.object.isRequired,
       // Event handler for "contextmenu" event
       onContextMenu: PropTypes.func.isRequired,
+      // Event handler for "copy" event
+      onCopy: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
   }
 
   renderDeclarations(remove = [], add = []) {
@@ -144,16 +146,17 @@ class ChangesApp extends PureComponent {
 
   render() {
     const hasChanges = Object.keys(this.props.changesTree).length > 0;
     return dom.div(
       {
         className: "theme-sidebar inspector-tabpanel",
         id: "sidebar-panel-changes",
         onContextMenu: this.props.onContextMenu,
+        onCopy: this.props.onCopy,
       },
       !hasChanges && this.renderEmptyState(),
       hasChanges && this.renderDiff(this.props.changesTree)
     );
   }
 }
 
 /**
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/constants.js
@@ -0,0 +1,13 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+module.exports = {
+  TELEMETRY_SCALAR_CONTEXTMENU: "devtools.changesview.contextmenu",
+  TELEMETRY_SCALAR_CONTEXTMENU_COPY: "devtools.changesview.contextmenu_copy",
+  TELEMETRY_SCALAR_COPY: "devtools.changesview.copy",
+};
--- a/devtools/client/inspector/changes/moz.build
+++ b/devtools/client/inspector/changes/moz.build
@@ -10,11 +10,12 @@ DIRS += [
     'reducers',
     'selectors',
     'utils',
 ]
 
 DevToolsModules(
     'ChangesContextMenu.js',
     'ChangesView.js',
+    'constants.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/flexbox/flexbox.js
+++ b/devtools/client/inspector/flexbox/flexbox.js
@@ -13,17 +13,16 @@ const {
   updateFlexboxColor,
   updateFlexboxHighlighted,
 } = require("./actions/flexbox");
 
 loader.lazyRequireGetter(this, "parseURL", "devtools/client/shared/source-utils", true);
 loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
 
 const FLEXBOX_COLOR = "#9400FF";
-const TELEMETRY_ELEMENT_TYPE_DISPLAYED = "DEVTOOLS_FLEXINSPECTOR_ELEMENT_TYPE_DISPLAYED";
 
 class FlexboxInspector {
   constructor(inspector, window) {
     this.document = window.document;
     this.inspector = inspector;
     this.selection = inspector.selection;
     this.store = inspector.store;
     this.walker = inspector.walker;
@@ -422,38 +421,16 @@ class FlexboxInspector {
     if (!this.isPanelVisible()) {
       return;
     }
 
     this.update(null, null, reason === "treepanel");
   }
 
   /**
-   * Track usage of the tool via telemetry.
-   *
-   * @param  {Boolean} isContainerInfoShown
-   *         Whether the flex container accordion is displayed.
-   * @param  {Boolean} isItemInfoShown
-   *         Whether the flex item accordion is displayed.
-   */
-  sendTelemetryProbes(isContainerInfoShown, isItemInfoShown) {
-    const { telemetry } = this.inspector;
-
-    // Log the type of element being shown now (it can either be a container, or an item
-    // or both at the same time, but can never be none of these since this function is
-    // only ever called when something is being displayed).
-    let elementType = isContainerInfoShown ? "container" : "item";
-    if (isContainerInfoShown && isItemInfoShown) {
-      elementType = "both";
-    }
-
-    telemetry.getHistogramById(TELEMETRY_ELEMENT_TYPE_DISPLAYED).add(elementType);
-  }
-
-  /**
    * Updates the flexbox panel by dispatching the new flexbox data. This is called when
    * the layout view becomes visible or a new node is selected and needs to be update
    * with new flexbox data.
    *
    * @param  {Object|null} flexContainer
    *         An object consisting of the current flex container's flex items and
    *         properties.
    * @param  {Object|null} flexItemContainer
@@ -500,20 +477,16 @@ class FlexboxInspector {
 
       this.store.dispatch(updateFlexbox({
         color,
         flexContainer,
         flexItemContainer,
         highlighted,
         initiatedByMarkupViewSelection,
       }));
-
-      const isContainerInfoShown = !flexContainer.flexItemShown || !!flexItemContainer;
-      const isItemInfoShown = !!flexContainer.flexItemShown || !!flexItemContainer;
-      this.sendTelemetryProbes(isContainerInfoShown, isItemInfoShown);
     } catch (e) {
       // This call might fail if called asynchrously after the toolbox is finished
       // closing.
     }
 
     this._isUpdating = false;
   }
 }
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js
@@ -19,243 +19,227 @@ const TEST_DATA = [
     expected: [
       {
         type: "DOMContentLoaded",
         filename: URL_ROOT + TEST_LIB + ":1117",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  // Make sure that the DOM is not already loaded\n" +
-                 "  if (!jQuery.isReady) {\n" +
-                 "    // Remember that the DOM is ready\n" +
-                 "    jQuery.isReady = true;\n" +
-                 "\n" +
-                 "    // If there are functions bound, to execute\n" +
-                 "    if (jQuery.readyList) {\n" +
-                 "      // Execute all of them\n" +
-                 "      for (var i = 0; i < jQuery.readyList.length; i++)\n" +
-                 "        jQuery.readyList[i].apply(document);\n" +
-                 "\n" +
-                 "      // Reset the list of functions\n" +
-                 "      jQuery.readyList = null;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "}"
-      },
-      {
-        type: "load",
-        filename: URL_ROOT + TEST_LIB + ":1117",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function() {\n" +
-                 "  // Make sure that the DOM is not already loaded\n" +
-                 "  if (!jQuery.isReady) {\n" +
-                 "    // Remember that the DOM is ready\n" +
-                 "    jQuery.isReady = true;\n" +
-                 "\n" +
-                 "    // If there are functions bound, to execute\n" +
-                 "    if (jQuery.readyList) {\n" +
-                 "      // Execute all of them\n" +
-                 "      for (var i = 0; i < jQuery.readyList.length; i++)\n" +
-                 "        jQuery.readyList[i].apply(document);\n" +
-                 "\n" +
-                 "      // Reset the list of functions\n" +
-                 "      jQuery.readyList = null;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function() {
+            // Make sure that the DOM is not already loaded
+            if (!jQuery.isReady) {
+              // Remember that the DOM is ready
+              jQuery.isReady = true;
+
+              // If there are functions bound, to execute
+              if (jQuery.readyList) {
+                // Execute all of them
+                for (var i = 0; i < jQuery.readyList.length; i++)
+                  jQuery.readyList[i].apply(document);
+
+                // Reset the list of functions
+                jQuery.readyList = null;
+              }
+            }
+          }`
       },
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       },
       {
         type: "load",
         filename: URL_ROOT + TEST_LIB + ":894",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(event) {\n" +
-                 "  if (typeof jQuery == \"undefined\") return;\n" +
-                 "\n" +
-                 "  event = event || jQuery.event.fix(window.event);\n" +
-                 "\n" +
-                 "  // If no correct event was found, fail\n" +
-                 "  if (!event) return;\n" +
-                 "\n" +
-                 "  var returnValue = true;\n" +
-                 "\n" +
-                 "  var c = this.events[event.type];\n" +
-                 "\n" +
-                 "  for (var j in c) {\n" +
-                 "    if (c[j].apply(this, [event]) === false) {\n" +
-                 "      event.preventDefault();\n" +
-                 "      event.stopPropagation();\n" +
-                 "      returnValue = false;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  return returnValue;\n" +
-                 "}"
+        handler: `
+          function(event) {
+            if (typeof jQuery == "undefined") return;
+
+            event = event || jQuery.event.fix(window.event);
+
+            // If no correct event was found, fail
+            if (!event) return;
+
+            var returnValue = true;
+
+            var c = this.events[event.type];
+
+            for (var j in c) {
+              if (c[j].apply(this, [event]) === false) {
+                event.preventDefault();
+                event.stopPropagation();
+                returnValue = false;
+              }
+            }
+
+            return returnValue;
+          }`
       }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "click",
         filename: URL_ROOT + TEST_LIB + ":894",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(event) {\n" +
-                 "  if (typeof jQuery == \"undefined\") return;\n" +
-                 "\n" +
-                 "  event = event || jQuery.event.fix(window.event);\n" +
-                 "\n" +
-                 "  // If no correct event was found, fail\n" +
-                 "  if (!event) return;\n" +
-                 "\n" +
-                 "  var returnValue = true;\n" +
-                 "\n" +
-                 "  var c = this.events[event.type];\n" +
-                 "\n" +
-                 "  for (var j in c) {\n" +
-                 "    if (c[j].apply(this, [event]) === false) {\n" +
-                 "      event.preventDefault();\n" +
-                 "      event.stopPropagation();\n" +
-                 "      returnValue = false;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  return returnValue;\n" +
-                 "}"
+        handler: `
+          function(event) {
+            if (typeof jQuery == "undefined") return;
+
+            event = event || jQuery.event.fix(window.event);
+
+            // If no correct event was found, fail
+            if (!event) return;
+
+            var returnValue = true;
+
+            var c = this.events[event.type];
+
+            for (var j in c) {
+              if (c[j].apply(this, [event]) === false) {
+                event.preventDefault();
+                event.stopPropagation();
+                returnValue = false;
+              }
+            }
+
+            return returnValue;
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       },
       {
         type: "keydown",
         filename: URL_ROOT + TEST_LIB + ":894",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(event) {\n" +
-                 "  if (typeof jQuery == \"undefined\") return;\n" +
-                 "\n" +
-                 "  event = event || jQuery.event.fix(window.event);\n" +
-                 "\n" +
-                 "  // If no correct event was found, fail\n" +
-                 "  if (!event) return;\n" +
-                 "\n" +
-                 "  var returnValue = true;\n" +
-                 "\n" +
-                 "  var c = this.events[event.type];\n" +
-                 "\n" +
-                 "  for (var j in c) {\n" +
-                 "    if (c[j].apply(this, [event]) === false) {\n" +
-                 "      event.preventDefault();\n" +
-                 "      event.stopPropagation();\n" +
-                 "      returnValue = false;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  return returnValue;\n" +
-                 "}"
+        handler: `
+          function(event) {
+            if (typeof jQuery == "undefined") return;
+
+            event = event || jQuery.event.fix(window.event);
+
+            // If no correct event was found, fail
+            if (!event) return;
+
+            var returnValue = true;
+
+            var c = this.events[event.type];
+
+            for (var j in c) {
+              if (c[j].apply(this, [event]) === false) {
+                event.preventDefault();
+                event.stopPropagation();
+                returnValue = false;
+              }
+            }
+
+            return returnValue;
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js
@@ -14,257 +14,236 @@ loadHelperScript("helper_events_test_run
 
 /*eslint-disable */
 const TEST_DATA = [
   {
     selector: "html",
     expected: [
       {
         type: "load",
-        filename: URL_ROOT + TEST_LIB + ":1387",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function() {\n" +
-                 "  // Make sure that the DOM is not already loaded\n" +
-                 "  if (!jQuery.isReady) {\n" +
-                 "    // Remember that the DOM is ready\n" +
-                 "    jQuery.isReady = true;\n" +
-                 "\n" +
-                 "    // If there are functions bound, to execute\n" +
-                 "    if (jQuery.readyList) {\n" +
-                 "      // Execute all of them\n" +
-                 "      jQuery.each(jQuery.readyList, function() {\n" +
-                 "        this.apply(document);\n" +
-                 "      });\n" +
-                 "\n" +
-                 "      // Reset the list of functions\n" +
-                 "      jQuery.readyList = null;\n" +
-                 "    }\n" +
-                 "    // Remove event lisenter to avoid memory leak\n" +
-                 "    if (jQuery.browser.mozilla || jQuery.browser.opera)\n" +
-                 "      document.removeEventListener(\"DOMContentLoaded\", jQuery.ready, false);\n" +
-                 "  }\n" +
-                 "}"
-      },
-      {
-        type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       },
       {
         type: "load",
         filename: URL_ROOT + TEST_LIB + ":1224",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(event) {\n" +
-                 "  if (typeof jQuery == \"undefined\") return false;\n" +
-                 "\n" +
-                 "  // Empty object is for triggered events with no data\n" +
-                 "  event = jQuery.event.fix(event || window.event || {});\n" +
-                 "\n" +
-                 "  // returned undefined or false\n" +
-                 "  var returnValue;\n" +
-                 "\n" +
-                 "  var c = this.events[event.type];\n" +
-                 "\n" +
-                 "  var args = [].slice.call(arguments, 1);\n" +
-                 "  args.unshift(event);\n" +
-                 "\n" +
-                 "  for (var j in c) {\n" +
-                 "    // Pass in a reference to the handler function itself\n" +
-                 "    // So that we can later remove it\n" +
-                 "    args[0].handler = c[j];\n" +
-                 "    args[0].data = c[j].data;\n" +
-                 "\n" +
-                 "    if (c[j].apply(this, args) === false) {\n" +
-                 "      event.preventDefault();\n" +
-                 "      event.stopPropagation();\n" +
-                 "      returnValue = false;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  // Clean up added properties in IE to prevent memory leak\n" +
-                 "  if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
-                 "\n" +
-                 "  return returnValue;\n" +
-                 "}"
+        handler: `
+          function(event) {
+            if (typeof jQuery == "undefined") return false;
+
+            // Empty object is for triggered events with no data
+            event = jQuery.event.fix(event || window.event || {});
+
+            // returned undefined or false
+            var returnValue;
+
+            var c = this.events[event.type];
+
+            var args = [].slice.call(arguments, 1);
+            args.unshift(event);
+
+            for (var j in c) {
+              // Pass in a reference to the handler function itself
+              // So that we can later remove it
+              args[0].handler = c[j];
+              args[0].data = c[j].data;
+
+              if (c[j].apply(this, args) === false) {
+                event.preventDefault();
+                event.stopPropagation();
+                returnValue = false;
+              }
+            }
+
+            // Clean up added properties in IE to prevent memory leak
+            if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;
+
+            return returnValue;
+          }`
       }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "click",
         filename: URL_ROOT + TEST_LIB + ":1224",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(event) {\n" +
-                 "  if (typeof jQuery == \"undefined\") return false;\n" +
-                 "\n" +
-                 "  // Empty object is for triggered events with no data\n" +
-                 "  event = jQuery.event.fix(event || window.event || {});\n" +
-                 "\n" +
-                 "  // returned undefined or false\n" +
-                 "  var returnValue;\n" +
-                 "\n" +
-                 "  var c = this.events[event.type];\n" +
-                 "\n" +
-                 "  var args = [].slice.call(arguments, 1);\n" +
-                 "  args.unshift(event);\n" +
-                 "\n" +
-                 "  for (var j in c) {\n" +
-                 "    // Pass in a reference to the handler function itself\n" +
-                 "    // So that we can later remove it\n" +
-                 "    args[0].handler = c[j];\n" +
-                 "    args[0].data = c[j].data;\n" +
-                 "\n" +
-                 "    if (c[j].apply(this, args) === false) {\n" +
-                 "      event.preventDefault();\n" +
-                 "      event.stopPropagation();\n" +
-                 "      returnValue = false;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  // Clean up added properties in IE to prevent memory leak\n" +
-                 "  if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
-                 "\n" +
-                 "  return returnValue;\n" +
-                 "}"
+        handler: `
+          function(event) {
+            if (typeof jQuery == "undefined") return false;
+
+            // Empty object is for triggered events with no data
+            event = jQuery.event.fix(event || window.event || {});
+
+            // returned undefined or false
+            var returnValue;
+
+            var c = this.events[event.type];
+
+            var args = [].slice.call(arguments, 1);
+            args.unshift(event);
+
+            for (var j in c) {
+              // Pass in a reference to the handler function itself
+              // So that we can later remove it
+              args[0].handler = c[j];
+              args[0].data = c[j].data;
+
+              if (c[j].apply(this, args) === false) {
+                event.preventDefault();
+                event.stopPropagation();
+                returnValue = false;
+              }
+            }
+
+            // Clean up added properties in IE to prevent memory leak
+            if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;
+
+            return returnValue;
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       },
       {
         type: "keydown",
         filename: URL_ROOT + TEST_LIB + ":1224",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(event) {\n" +
-                 "  if (typeof jQuery == \"undefined\") return false;\n" +
-                 "\n" +
-                 "  // Empty object is for triggered events with no data\n" +
-                 "  event = jQuery.event.fix(event || window.event || {});\n" +
-                 "\n" +
-                 "  // returned undefined or false\n" +
-                 "  var returnValue;\n" +
-                 "\n" +
-                 "  var c = this.events[event.type];\n" +
-                 "\n" +
-                 "  var args = [].slice.call(arguments, 1);\n" +
-                 "  args.unshift(event);\n" +
-                 "\n" +
-                 "  for (var j in c) {\n" +
-                 "    // Pass in a reference to the handler function itself\n" +
-                 "    // So that we can later remove it\n" +
-                 "    args[0].handler = c[j];\n" +
-                 "    args[0].data = c[j].data;\n" +
-                 "\n" +
-                 "    if (c[j].apply(this, args) === false) {\n" +
-                 "      event.preventDefault();\n" +
-                 "      event.stopPropagation();\n" +
-                 "      returnValue = false;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  // Clean up added properties in IE to prevent memory leak\n" +
-                 "  if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
-                 "\n" +
-                 "  return returnValue;\n" +
-                 "}"
+        handler: `
+          function(event) {
+            if (typeof jQuery == "undefined") return false;
+
+            // Empty object is for triggered events with no data
+            event = jQuery.event.fix(event || window.event || {});
+
+            // returned undefined or false
+            var returnValue;
+
+            var c = this.events[event.type];
+
+            var args = [].slice.call(arguments, 1);
+            args.unshift(event);
+
+            for (var j in c) {
+              // Pass in a reference to the handler function itself
+              // So that we can later remove it
+              args[0].handler = c[j];
+              args[0].data = c[j].data;
+
+              if (c[j].apply(this, args) === false) {
+                event.preventDefault();
+                event.stopPropagation();
+                returnValue = false;
+              }
+            }
+
+            // Clean up added properties in IE to prevent memory leak
+            if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;
+
+            return returnValue;
+          }`
       }
     ]
   }
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js
@@ -19,155 +19,163 @@ const TEST_DATA = [
     expected: [
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+          
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+          
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+          
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       }
     ]
   },
 
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       }
     ]
   },
 
   {
     selector: "#livediv",
     expected: [
       {
         type: "dragend",
         filename: TEST_URL + ":31",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragEnd() {\n" +
-                 "  alert(4);\n" +
-                 "}"
+        handler: `
+          function liveDivDragEnd() {
+            alert(4);
+          }`
       },
       {
         type: "dragleave",
         filename: TEST_URL + ":30",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragLeave() {\n" +
-                 "  alert(3);\n" +
-                 "}"
+        handler: `
+          function liveDivDragLeave() {
+            alert(3);
+          }`
       },
       {
         type: "dragover",
         filename: TEST_URL + ":33",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragOver() {\n" +
-                 "  alert(6);\n" +
-                 "}"
+        handler: `
+          function liveDivDragOver() {
+            alert(6);
+          }`
       },
       {
         type: "drop",
         filename: TEST_URL + ":32",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDrop() {\n" +
-                 "  alert(5);\n" +
-                 "}"
+        handler: `
+          function liveDivDrop() {
+            alert(5);
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js
@@ -19,172 +19,138 @@ const TEST_DATA = [
     expected: [
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       },
-      {
-        type: "load",
-        filename: URL_ROOT + TEST_LIB,
-        attributes: [
-          "Bubbling",
-          "DOM0"
-        ],
-        handler: "function(event) {\n" +
-                 "  if (typeof jQuery == \"undefined\") return false;\n" +
-                 "\n" +
-                 "  // Empty object is for triggered events with no data\n" +
-                 "  event = jQuery.event.fix(event || window.event || {});\n" +
-                 "\n" +
-                 "  // returned undefined or false\n" +
-                 "  var returnValue;\n" +
-                 "\n" +
-                 "  var c = this.events[event.type];\n" +
-                 "\n" +
-                 "  var args = [].slice.call(arguments, 1);\n" +
-                 "  args.unshift(event);\n" +
-                 "\n" +
-                 "  for (var j in c) {\n" +
-                 "    // Pass in a reference to the handler function itself\n" +
-                 "    // So that we can later remove it\n" +
-                 "    args[0].handler = c[j];\n" +
-                 "    args[0].data = c[j].data;\n" +
-                 "\n" +
-                 "    if (c[j].apply(this, args) === false) {\n" +
-                 "      event.preventDefault();\n" +
-                 "      event.stopPropagation();\n" +
-                 "      returnValue = false;\n" +
-                 "    }\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  // Clean up added properties in IE to prevent memory leak\n" +
-                 "  if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" +
-                 "\n" +
-                 "  return returnValue;\n" +
-                 "}"
-      }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "click",
         filename: URL_ROOT + TEST_LIB + ":24",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  var val;\n" +
-                 "  if (typeof jQuery == \"undefined\" || jQuery.event.triggered) return val;\n" +
-                 "  val = jQuery.event.handle.apply(element, arguments);\n" +
-                 "  return val;\n" +
-                 "}"
+        handler: `
+          function() {
+            var val;
+            if (typeof jQuery == "undefined" || jQuery.event.triggered) return val;
+            val = jQuery.event.handle.apply(element, arguments);
+            return val;
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       },
       {
         type: "keydown",
         filename: URL_ROOT + TEST_LIB + ":24",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  var val;\n" +
-                 "  if (typeof jQuery == \"undefined\" || jQuery.event.triggered) return val;\n" +
-                 "  val = jQuery.event.handle.apply(element, arguments);\n" +
-                 "  return val;\n" +
-                 "}"
+        handler: `
+          function() {
+            var val;
+            if (typeof jQuery == "undefined" || jQuery.event.triggered) return val;
+            val = jQuery.event.handle.apply(element, arguments);
+            return val;
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js
@@ -13,257 +13,156 @@ const TEST_URL = URL_ROOT + "doc_markup_
 loadHelperScript("helper_events_test_runner.js");
 
 /*eslint-disable */
 const TEST_DATA = [
   {
     selector: "html",
     expected: [
       {
-        type: "dblclick",
-        filename: URL_ROOT + TEST_LIB + ":19",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function c(G) {\n" +
-                 "  var D = RegExp(\"(^|\\\\.)\" + G.type + \"(\\\\.|$)\"),\n" +
-                 "    F = true,\n" +
-                 "    E = [];\n" +
-                 "  n.each(n.data(this, \"events\").live || [], function(H, I) {\n" +
-                 "    if (D.test(I.type)) {\n" +
-                 "      var J = n(G.target).closest(I.data)[0];\n" +
-                 "      if (J) {\n" +
-                 "        E.push({\n" +
-                 "          elem: J,\n" +
-                 "          fn: I\n" +
-                 "        })\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "  });\n" +
-                 "  n.each(E, function() {\n" +
-                 "    if (!G.isImmediatePropagationStopped() && " + "this.fn.call(this.elem, G, this.fn.data) === false) {\n" +
-                 "      F = false\n" +
-                 "    }\n" +
-                 "  });\n" +
-                 "  return F\n" +
-                 "}"
-      },
-      {
         type: "DOMContentLoaded",
         filename: URL_ROOT + TEST_LIB + ":19",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  document.removeEventListener(\"DOMContentLoaded\", arguments.callee, false);\n" +
-                 "  n.ready()\n" +
-                 "}"
-      },
-      {
-        type: "dragstart",
-        filename: URL_ROOT + TEST_LIB + ":19",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function c(G) {\n" +
-                 "  var D = RegExp(\"(^|\\\\.)\" + G.type + \"(\\\\.|$)\"),\n" +
-                 "    F = true,\n" +
-                 "    E = [];\n" +
-                 "  n.each(n.data(this, \"events\").live || [], function(H, I) {\n" +
-                 "    if (D.test(I.type)) {\n" +
-                 "      var J = n(G.target).closest(I.data)[0];\n" +
-                 "      if (J) {\n" +
-                 "        E.push({\n" +
-                 "          elem: J,\n" +
-                 "          fn: I\n" +
-                 "        })\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "  });\n" +
-                 "  n.each(E, function() {\n" +
-                 "    if (!G.isImmediatePropagationStopped() && " + "this.fn.call(this.elem, G, this.fn.data) === false) {\n" +
-                 "      F = false\n" +
-                 "    }\n" +
-                 "  });\n" +
-                 "  return F\n" +
-                 "}"
-      },
-      {
-        type: "live",
-        filename: URL_ROOT + TEST_LIB + ":19",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function() {\n" +
-                 "  return E.apply(this, arguments)\n" +
-                 "}"
-      },
-      {
-        type: "live",
-        filename: URL_ROOT + TEST_LIB + ":19",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function() {\n" +
-                 "  return E.apply(this, arguments)\n" +
-                 "}"
-      },
-      {
-        type: "load",
-        filename: URL_ROOT + TEST_LIB + ":19",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function() {\n" +
-                 "  if (!n.isReady) {\n" +
-                 "    n.isReady = true;\n" +
-                 "    if (n.readyList) {\n" +
-                 "      n.each(n.readyList, function() {\n" +
-                 "        this.call(document, n)\n" +
-                 "      });\n" +
-                 "      n.readyList = null\n" +
-                 "    }\n" +
-                 "    n(document).triggerHandler(\"ready\")\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function() {
+            document.removeEventListener("DOMContentLoaded", arguments.callee, false);
+            n.ready()
+          }`
       },
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
-      },
-      {
-        type: "unload",
-        filename: URL_ROOT + TEST_LIB + ":19",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function(H) {\n" +
-                 "  n(this).unbind(H, D);\n" +
-                 "  return (E || G).apply(this, arguments)\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       }
     ]
   },
   {
     selector: "#livediv",
     expected: [
       {
         type: "dblclick",
         filename: TEST_URL + ":28",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function() {\n" +
-                 "  return E.apply(this, arguments)\n" +
-                 "}"
+        handler: `
+          function() {
+            return E.apply(this, arguments)
+          }`
       },
       {
         type: "dragstart",
         filename: TEST_URL + ":29",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function() {\n" +
-                 "  return E.apply(this, arguments)\n" +
-                 "}"
+        handler: `
+          function() {
+            return E.apply(this, arguments)
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js
@@ -13,210 +13,200 @@ const TEST_URL = URL_ROOT + "doc_markup_
 loadHelperScript("helper_events_test_runner.js");
 
 /*eslint-disable */
 const TEST_DATA = [
   {
     selector: "html",
     expected: [
       {
-        type: "dblclick",
-        filename: URL_ROOT + TEST_LIB + ":31",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function() {\n" +
-                 "  return a.apply(d || this, arguments)\n" +
-                 "}"
-      },
-      {
         type: "DOMContentLoaded",
         filename: URL_ROOT + TEST_LIB + ":32",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  s.removeEventListener(\"DOMContentLoaded\", M, false);\n" +
-                 "  c.ready()\n" +
-                 "}"
-      },
-      {
-        type: "dragstart",
-        filename: URL_ROOT + TEST_LIB + ":31",
-        attributes: [
-          "jQuery"
-        ],
-        handler: "function() {\n" +
-                 "  return a.apply(d || this, arguments)\n" +
-                 "}"
+        handler: `
+          function() {
+            s.removeEventListener(\"DOMContentLoaded\", M, false);
+            c.ready()
+          }`
       },
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       },
       {
         type: "load",
         filename: URL_ROOT + TEST_LIB + ":26",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  if (!c.isReady) {\n" +
-                 "    if (!s.body) return setTimeout(c.ready, 13);\n" +
-                 "    c.isReady = true;\n" +
-                 "    if (Q) {\n" +
-                 "      for (var a, b = 0; a = Q[b++];) a.call(s, c);\n" +
-                 "      Q = null\n" +
-                 "    }\n" +
-                 "    c.fn.triggerHandler && c(s).triggerHandler(\"ready\")\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function() {
+            if (!c.isReady) {
+              if (!s.body) return setTimeout(c.ready, 13);
+              c.isReady = true;
+              if (Q) {
+                for (var a, b = 0; a = Q[b++];) a.call(s, c);
+                Q = null
+              }
+              c.fn.triggerHandler && c(s).triggerHandler("ready")
+            }
+          }`
       }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       }
     ]
   },
   {
     selector: "#livediv",
     expected: [
       {
         type: "dblclick",
         filename: TEST_URL + ":28",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function() {\n" +
-                 "  return a.apply(d || this, arguments)\n" +
-                 "}"
+        handler: `
+          function() {
+            return a.apply(d || this, arguments)
+          }`
       },
       {
         type: "dblclick",
         filename: URL_ROOT + TEST_LIB + ":17",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function() {\n" +
-                 "  return a.apply(d || this, arguments)\n" +
-                 "}"
+        handler: `
+          function() {
+            return a.apply(d || this, arguments)
+          }`
       },
       {
         type: "dragstart",
         filename: TEST_URL + ":29",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function() {\n" +
-                 "  return a.apply(d || this, arguments)\n" +
-                 "}"
+        handler: `
+          function() {
+            return a.apply(d || this, arguments)
+          }`
       },
       {
         type: "dragstart",
         filename: URL_ROOT + TEST_LIB + ":17",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function() {\n" +
-                 "  return a.apply(d || this, arguments)\n" +
-                 "}"
+        handler: `
+          function() {
+            return a.apply(d || this, arguments)
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js
@@ -21,356 +21,370 @@ const TEST_DATA = [
     expected: [
       {
         type: "DOMContentLoaded",
         filename: URL_ROOT + TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  c.removeEventListener(\"DOMContentLoaded\", z, !1), e.ready()\n" +
-                 "}"
+        handler: `
+          function() {
+            c.removeEventListener("DOMContentLoaded", z, !1), e.ready()
+          }`
       },
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       },
       {
         type: "load",
         filename: URL_ROOT + TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(a) {\n" +
-                 "  if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {\n" +
-                 "    if (!c.body) return setTimeout(e.ready, 1);\n" +
-                 "    e.isReady = !0;\n" +
-                 "    if (a !== !0 && --e.readyWait > 0) return;\n" +
-                 "    y.resolveWith(c, [e]), e.fn.trigger && e(c).trigger(\"ready\").unbind(\"ready\")\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function(a) {
+            if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {
+              if (!c.body) return setTimeout(e.ready, 1);
+              e.isReady = !0;
+              if (a !== !0 && --e.readyWait > 0) return;
+              y.resolveWith(c, [e]), e.fn.trigger && e(c).trigger("ready").unbind("ready")
+            }
+          }`
       }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       }
     ]
   },
   {
     selector: "#livediv",
     expected: [
       {
         type: "dblclick",
         filename: TEST_URL + ":28",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDblClick() {\n" +
-                 "  alert(1);\n" +
-                 "}"
+        handler: `
+          function liveDivDblClick() {
+            alert(1);
+          }`
       },
       {
         type: "dblclick",
         filename: URL_ROOT + TEST_LIB + ":16",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function M(a) {\n" +
-                 "  var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
-                 "    q = [],\n" +
-                 "    r = f._data(this, \"events\");\n" +
-                 "  if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
-                 "    a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
-                 "    var s = r.live.slice(0);\n" +
-                 "    for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
-                 "    e = f(a.target).closest(q, a.currentTarget);\n" +
-                 "    for (j = 0, k = e.length; j < k; j++) {\n" +
-                 "      m = e[j];\n" +
-                 "      for (i = 0; i < s.length; i++) {\n" +
-                 "        g = s[i];\n" +
-                 "        if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
-                 "          h = m.elem, d = null;\n" +
-                 "          if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
-                 "          (!d || d !== h) && p.push({\n" +
-                 "            elem: h,\n" +
-                 "            handleObj: g,\n" +
-                 "            level: m.level\n" +
-                 "          })\n" +
-                 "        }\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    for (j = 0, k = p.length; j < k; j++) {\n" +
-                 "      e = p[j];\n" +
-                 "      if (c && e.level > c) break;\n" +
-                 "      a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
-                 "      if (o === !1 || a.isPropagationStopped()) {\n" +
-                 "        c = e.level, o === !1 && (b = !1);\n" +
-                 "        if (a.isImmediatePropagationStopped()) break\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    return b\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function M(a) {
+            var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],
+              q = [],
+              r = f._data(this, "events");
+            if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) {
+              a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this;
+              var s = r.live.slice(0);
+              for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1);
+              e = f(a.target).closest(q, a.currentTarget);
+              for (j = 0, k = e.length; j < k; j++) {
+                m = e[j];
+                for (i = 0; i < s.length; i++) {
+                  g = s[i];
+                  if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {
+                    h = m.elem, d = null;
+                    if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);
+                    (!d || d !== h) && p.push({
+                      elem: h,
+                      handleObj: g,
+                      level: m.level
+                    })
+                  }
+                }
+              }
+              for (j = 0, k = p.length; j < k; j++) {
+                e = p[j];
+                if (c && e.level > c) break;
+                a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);
+                if (o === !1 || a.isPropagationStopped()) {
+                  c = e.level, o === !1 && (b = !1);
+                  if (a.isImmediatePropagationStopped()) break
+                }
+              }
+              return b
+            }
+          }`
       },
       {
         type: "dragend",
         filename: TEST_URL + ":31",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragEnd() {\n" +
-                 "  alert(4);\n" +
-                 "}"
+        handler: `
+          function liveDivDragEnd() {
+            alert(4);
+          }`
       },
       {
         type: "dragend",
         filename: URL_ROOT + TEST_LIB + ":16",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function M(a) {\n" +
-                 "  var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
-                 "    q = [],\n" +
-                 "    r = f._data(this, \"events\");\n" +
-                 "  if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
-                 "    a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
-                 "    var s = r.live.slice(0);\n" +
-                 "    for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
-                 "    e = f(a.target).closest(q, a.currentTarget);\n" +
-                 "    for (j = 0, k = e.length; j < k; j++) {\n" +
-                 "      m = e[j];\n" +
-                 "      for (i = 0; i < s.length; i++) {\n" +
-                 "        g = s[i];\n" +
-                 "        if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
-                 "          h = m.elem, d = null;\n" +
-                 "          if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
-                 "          (!d || d !== h) && p.push({\n" +
-                 "            elem: h,\n" +
-                 "            handleObj: g,\n" +
-                 "            level: m.level\n" +
-                 "          })\n" +
-                 "        }\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    for (j = 0, k = p.length; j < k; j++) {\n" +
-                 "      e = p[j];\n" +
-                 "      if (c && e.level > c) break;\n" +
-                 "      a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
-                 "      if (o === !1 || a.isPropagationStopped()) {\n" +
-                 "        c = e.level, o === !1 && (b = !1);\n" +
-                 "        if (a.isImmediatePropagationStopped()) break\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    return b\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function M(a) {
+            var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],
+              q = [],
+              r = f._data(this, "events");
+            if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) {
+              a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this;
+              var s = r.live.slice(0);
+              for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1);
+              e = f(a.target).closest(q, a.currentTarget);
+              for (j = 0, k = e.length; j < k; j++) {
+                m = e[j];
+                for (i = 0; i < s.length; i++) {
+                  g = s[i];
+                  if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {
+                    h = m.elem, d = null;
+                    if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);
+                    (!d || d !== h) && p.push({
+                      elem: h,
+                      handleObj: g,
+                      level: m.level
+                    })
+                  }
+                }
+              }
+              for (j = 0, k = p.length; j < k; j++) {
+                e = p[j];
+                if (c && e.level > c) break;
+                a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);
+                if (o === !1 || a.isPropagationStopped()) {
+                  c = e.level, o === !1 && (b = !1);
+                  if (a.isImmediatePropagationStopped()) break
+                }
+              }
+              return b
+            }
+          }`
       },
       {
         type: "dragleave",
         filename: TEST_URL + ":30",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragLeave() {\n" +
-                 "  alert(3);\n" +
-                 "}"
+        handler: `
+          function liveDivDragLeave() {
+            alert(3);
+          }`
       },
       {
         type: "dragleave",
         filename: URL_ROOT + TEST_LIB + ":16",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function M(a) {\n" +
-                 "  var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
-                 "    q = [],\n" +
-                 "    r = f._data(this, \"events\");\n" +
-                 "  if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
-                 "    a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
-                 "    var s = r.live.slice(0);\n" +
-                 "    for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
-                 "    e = f(a.target).closest(q, a.currentTarget);\n" +
-                 "    for (j = 0, k = e.length; j < k; j++) {\n" +
-                 "      m = e[j];\n" +
-                 "      for (i = 0; i < s.length; i++) {\n" +
-                 "        g = s[i];\n" +
-                 "        if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
-                 "          h = m.elem, d = null;\n" +
-                 "          if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
-                 "          (!d || d !== h) && p.push({\n" +
-                 "            elem: h,\n" +
-                 "            handleObj: g,\n" +
-                 "            level: m.level\n" +
-                 "          })\n" +
-                 "        }\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    for (j = 0, k = p.length; j < k; j++) {\n" +
-                 "      e = p[j];\n" +
-                 "      if (c && e.level > c) break;\n" +
-                 "      a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
-                 "      if (o === !1 || a.isPropagationStopped()) {\n" +
-                 "        c = e.level, o === !1 && (b = !1);\n" +
-                 "        if (a.isImmediatePropagationStopped()) break\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    return b\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function M(a) {
+            var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],
+              q = [],
+              r = f._data(this, "events");
+            if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) {
+              a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this;
+              var s = r.live.slice(0);
+              for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1);
+              e = f(a.target).closest(q, a.currentTarget);
+              for (j = 0, k = e.length; j < k; j++) {
+                m = e[j];
+                for (i = 0; i < s.length; i++) {
+                  g = s[i];
+                  if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {
+                    h = m.elem, d = null;
+                    if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);
+                    (!d || d !== h) && p.push({
+                      elem: h,
+                      handleObj: g,
+                      level: m.level
+                    })
+                  }
+                }
+              }
+              for (j = 0, k = p.length; j < k; j++) {
+                e = p[j];
+                if (c && e.level > c) break;
+                a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);
+                if (o === !1 || a.isPropagationStopped()) {
+                  c = e.level, o === !1 && (b = !1);
+                  if (a.isImmediatePropagationStopped()) break
+                }
+              }
+              return b
+            }
+          }`
       },
       {
         type: "dragstart",
         filename: TEST_URL + ":29",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragStart() {\n" +
-                 "  alert(2);\n" +
-                 "}"
+        handler: `
+          function liveDivDragStart() {
+            alert(2);
+          }`
       },
       {
         type: "dragstart",
         filename: URL_ROOT + TEST_LIB + ":16",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function M(a) {\n" +
-                 "  var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" +
-                 "    q = [],\n" +
-                 "    r = f._data(this, \"events\");\n" +
-                 "  if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" +
-                 "    a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" +
-                 "    var s = r.live.slice(0);\n" +
-                 "    for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" +
-                 "    e = f(a.target).closest(q, a.currentTarget);\n" +
-                 "    for (j = 0, k = e.length; j < k; j++) {\n" +
-                 "      m = e[j];\n" +
-                 "      for (i = 0; i < s.length; i++) {\n" +
-                 "        g = s[i];\n" +
-                 "        if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" +
-                 "          h = m.elem, d = null;\n" +
-                 "          if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" +
-                 "          (!d || d !== h) && p.push({\n" +
-                 "            elem: h,\n" +
-                 "            handleObj: g,\n" +
-                 "            level: m.level\n" +
-                 "          })\n" +
-                 "        }\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    for (j = 0, k = p.length; j < k; j++) {\n" +
-                 "      e = p[j];\n" +
-                 "      if (c && e.level > c) break;\n" +
-                 "      a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" +
-                 "      if (o === !1 || a.isPropagationStopped()) {\n" +
-                 "        c = e.level, o === !1 && (b = !1);\n" +
-                 "        if (a.isImmediatePropagationStopped()) break\n" +
-                 "      }\n" +
-                 "    }\n" +
-                 "    return b\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function M(a) {
+            var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],
+              q = [],
+              r = f._data(this, "events");
+            if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === "click")) {
+              a.namespace && (n = new RegExp("(^|\\\\.)" + a.namespace.split(".").join("\\\\.(?:.*\\\\.)?") + "(\\\\.|$)")), a.liveFired = this;
+              var s = r.live.slice(0);
+              for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, "") === a.type ? q.push(g.selector) : s.splice(i--, 1);
+              e = f(a.target).closest(q, a.currentTarget);
+              for (j = 0, k = e.length; j < k; j++) {
+                m = e[j];
+                for (i = 0; i < s.length; i++) {
+                  g = s[i];
+                  if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {
+                    h = m.elem, d = null;
+                    if (g.preType === "mouseenter" || g.preType === "mouseleave") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);
+                    (!d || d !== h) && p.push({
+                      elem: h,
+                      handleObj: g,
+                      level: m.level
+                    })
+                  }
+                }
+              }
+              for (j = 0, k = p.length; j < k; j++) {
+                e = p[j];
+                if (c && e.level > c) break;
+                a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);
+                if (o === !1 || a.isPropagationStopped()) {
+                  c = e.level, o === !1 && (b = !1);
+                  if (a.isImmediatePropagationStopped()) break
+                }
+              }
+              return b
+            }
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js
@@ -21,202 +21,214 @@ const TEST_DATA = [
     expected: [
       {
         type: "DOMContentLoaded",
         filename: URL_ROOT + TEST_LIB + ":2",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {\n" +
-                 "  c.removeEventListener(\"DOMContentLoaded\", C, !1), e.ready()\n" +
-                 "}"
+        handler: `
+          function() {
+            c.removeEventListener("DOMContentLoaded", C, !1), e.ready()
+          }`
       },
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       },
       {
         type: "load",
         filename: URL_ROOT + TEST_LIB + ":2",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function(a) {\n" +
-                 "  if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {\n" +
-                 "    if (!c.body) return setTimeout(e.ready, 1);\n" +
-                 "    e.isReady = !0;\n" +
-                 "    if (a !== !0 && --e.readyWait > 0) return;\n" +
-                 "    B.fireWith(c, [e]), e.fn.trigger && e(c).trigger(\"ready\").unbind(\"ready\")\n" +
-                 "  }\n" +
-                 "}"
+        handler: `
+          function(a) {
+            if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {
+              if (!c.body) return setTimeout(e.ready, 1);
+              e.isReady = !0;
+              if (a !== !0 && --e.readyWait > 0) return;
+              B.fireWith(c, [e]), e.fn.trigger && e(c).trigger("ready").unbind("ready")
+            }
+          }`
       }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       }
     ]
   },
   {
     selector: "#livediv",
     expected: [
       {
         type: "dblclick",
         filename: TEST_URL + ":28",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDblClick() {\n" +
-                 "  alert(1);\n" +
-                 "}"
+        handler: `
+          function liveDivDblClick() {
+            alert(1);
+          }`
       },
       {
         type: "dragend",
         filename: TEST_URL + ":31",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragEnd() {\n" +
-                 "  alert(4);\n" +
-                 "}"
+        handler: `
+          function liveDivDragEnd() {
+            alert(4);
+          }`
       },
       {
         type: "dragleave",
         filename: TEST_URL + ":30",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragLeave() {\n" +
-                 "  alert(3);\n" +
-                 "}"
+        handler: `
+          function liveDivDragLeave() {
+            alert(3);
+          }`
       },
       {
         type: "dragover",
         filename: TEST_URL + ":33",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragOver() {\n" +
-                 "  alert(6);\n" +
-                 "}"
+        handler: `
+          function liveDivDragOver() {
+            alert(6);
+          }`
       },
       {
         type: "dragstart",
         filename: TEST_URL + ":29",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragStart() {\n" +
-                 "  alert(2);\n" +
-                 "}"
+        handler: `
+          function liveDivDragStart() {
+            alert(2);
+          }`
       },
       {
         type: "drop",
         filename: TEST_URL + ":32",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDrop() {\n" +
-                 "  alert(5);\n" +
-                 "}"
+        handler: `
+          function liveDivDrop() {
+            alert(5);
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js
@@ -21,153 +21,161 @@ const TEST_DATA = [
     expected: [
       {
         type: "load",
         filename: TEST_URL + ":27",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "() => {\n" +
-                 "  var handler1 = function liveDivDblClick() {\n" +
-                 "    alert(1);\n" +
-                 "  };\n" +
-                 "  var handler2 = function liveDivDragStart() {\n" +
-                 "    alert(2);\n" +
-                 "  };\n" +
-                 "  var handler3 = function liveDivDragLeave() {\n" +
-                 "    alert(3);\n" +
-                 "  };\n" +
-                 "  var handler4 = function liveDivDragEnd() {\n" +
-                 "    alert(4);\n" +
-                 "  };\n" +
-                 "  var handler5 = function liveDivDrop() {\n" +
-                 "    alert(5);\n" +
-                 "  };\n" +
-                 "  var handler6 = function liveDivDragOver() {\n" +
-                 "    alert(6);\n" +
-                 "  };\n" +
-                 "  var handler7 = function divClick1() {\n" +
-                 "    alert(7);\n" +
-                 "  };\n" +
-                 "  var handler8 = function divClick2() {\n" +
-                 "    alert(8);\n" +
-                 "  };\n" +
-                 "  var handler9 = function divKeyDown() {\n" +
-                 "    alert(9);\n" +
-                 "  };\n" +
-                 "  var handler10 = function divDragOut() {\n" +
-                 "    alert(10);\n" +
-                 "  };\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").live) {\n" +
-                 "    $(\"#livediv\").live(\"dblclick\", handler1);\n" +
-                 "    $(\"#livediv\").live(\"dragstart\", handler2);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").delegate) {\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" +
-                 "    $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  if ($(\"#livediv\").on) {\n" +
-                 "    $(document).on(\"drop\", \"#livediv\", handler5);\n" +
-                 "    $(document).on(\"dragover\", \"#livediv\", handler6);\n" +
-                 "    $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" +
-                 "  }\n" +
-                 "\n" +
-                 "  var div = $(\"div\")[0];\n" +
-                 "  $(div).click(handler7);\n" +
-                 "  $(div).click(handler8);\n" +
-                 "  $(div).keydown(handler9);\n" +
-                 "}"
+        handler: `
+          () => {
+            var handler1 = function liveDivDblClick() {
+              alert(1);
+            };
+            var handler2 = function liveDivDragStart() {
+              alert(2);
+            };
+            var handler3 = function liveDivDragLeave() {
+              alert(3);
+            };
+            var handler4 = function liveDivDragEnd() {
+              alert(4);
+            };
+            var handler5 = function liveDivDrop() {
+              alert(5);
+            };
+            var handler6 = function liveDivDragOver() {
+              alert(6);
+            };
+            var handler7 = function divClick1() {
+              alert(7);
+            };
+            var handler8 = function divClick2() {
+              alert(8);
+            };
+            var handler9 = function divKeyDown() {
+              alert(9);
+            };
+            var handler10 = function divDragOut() {
+              alert(10);
+            };
+
+            if ($("#livediv").live) {
+              $("#livediv").live("dblclick", handler1);
+              $("#livediv").live("dragstart", handler2);
+            }
+
+            if ($("#livediv").delegate) {
+              $(document).delegate("#livediv", "dragleave", handler3);
+              $(document).delegate("#livediv", "dragend", handler4);
+            }
+
+            if ($("#livediv").on) {
+              $(document).on("drop", "#livediv", handler5);
+              $(document).on("dragover", "#livediv", handler6);
+              $(document).on("dragout", "#livediv:xxxxx", handler10);
+            }
+
+            var div = $("div")[0];
+            $(div).click(handler7);
+            $(div).click(handler8);
+            $(div).keydown(handler9);
+          }`
       }
     ]
   },
   {
     selector: "#testdiv",
     expected: [
       {
         type: "click",
         filename: TEST_URL + ":34",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick1() {\n" +
-                 "  alert(7);\n" +
-                 "}"
+        handler: `
+          function divClick1() {
+            alert(7);
+          }`
       },
       {
         type: "click",
         filename: TEST_URL + ":35",
         attributes: [
           "jQuery"
         ],
-        handler: "function divClick2() {\n" +
-                 "  alert(8);\n" +
-                 "}"
+        handler: `
+          function divClick2() {
+            alert(8);
+          }`
       },
       {
         type: "keydown",
         filename: TEST_URL + ":36",
         attributes: [
           "jQuery"
         ],
-        handler: "function divKeyDown() {\n" +
-                 "  alert(9);\n" +
-                 "}"
+        handler: `
+          function divKeyDown() {
+            alert(9);
+          }`
       }
     ]
   },
   {
     selector: "#livediv",
     expected: [
       {
         type: "dragend",
         filename: TEST_URL + ":31",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragEnd() {\n" +
-                 "  alert(4);\n" +
-                 "}"
+        handler: `
+          function liveDivDragEnd() {
+            alert(4);
+          }`
       },
       {
         type: "dragleave",
         filename: TEST_URL + ":30",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragLeave() {\n" +
-                 "  alert(3);\n" +
-                 "}"
+        handler: `
+          function liveDivDragLeave() {
+            alert(3);
+          }`
       },
       {
         type: "dragover",
         filename: TEST_URL + ":33",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDragOver() {\n" +
-                 "  alert(6);\n" +
-                 "}"
+        handler: `
+          function liveDivDragOver() {
+            alert(6);
+          }`
       },
       {
         type: "drop",
         filename: TEST_URL + ":32",
         attributes: [
           "jQuery",
           "Live"
         ],
-        handler: "function liveDivDrop() {\n" +
-                 "  alert(5);\n" +
-                 "}"
+        handler: `
+          function liveDivDrop() {
+            alert(5);
+          }`
       }
     ]
   },
 ];
 /* eslint-enable */
 
 add_task(async function() {
   await runEventPopupTests(TEST_URL, TEST_DATA);
--- a/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1.js
@@ -22,110 +22,110 @@ const TEST_DATA = [
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":17530",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function emptyFunction() {}"
+        handler: `function emptyFunction() {}`
       },
       {
         type: "onClick",
         filename: TEST_URL + ":21",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#external",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":17530",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function emptyFunction() {}"
+        handler: `function emptyFunction() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalinline",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":17530",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function emptyFunction() {}"
+        handler: `function emptyFunction() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       },
       {
         type: "onMouseUp",
         filename: TEST_URL + ":21",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalcapturing",
     expected: [
       {
         type: "onClickCapture",
         filename: TEST_EXTERNAL_LISTENERS + ":8",
         attributes: [
           "Capturing",
           "React"
         ],
-        handler:
-`function externalCapturingFunction() {
-  alert("externalCapturingFunction");
-}`
+        handler: `
+          function externalCapturingFunction() {
+            alert("externalCapturingFunction");
+          }`
       }
     ]
   }
 ];
 /* eslint-enable */
 
 add_task(async function() {
   info("Switch to 2 pane inspector to avoid sidebar width issues with opening events");
--- a/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1_jsx.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_react_development_15.4.1_jsx.js
@@ -23,110 +23,110 @@ const TEST_DATA = [
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":17530",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function emptyFunction() {}"
+        handler: `function emptyFunction() {}`
       },
       {
         type: "onClick",
         filename: TEST_LIB_BABEL + ":10",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function inlineFunction() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function inlineFunction() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externaljsx",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":17530",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function emptyFunction() {}"
+        handler: `function emptyFunction() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalinlinejsx",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":17530",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function emptyFunction() {}"
+        handler: `function emptyFunction() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       },
       {
         type: "onMouseUp",
         filename: TEST_LIB_BABEL + ":10",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function inlineFunction() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function inlineFunction() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalcapturingjsx",
     expected: [
       {
         type: "onClickCapture",
         filename: TEST_EXTERNAL_LISTENERS + ":8",
         attributes: [
           "Capturing",
           "React"
         ],
-        handler:
-`function externalCapturingFunction() {
-  alert("externalCapturingFunction");
-}`
+        handler: `
+          function externalCapturingFunction() {
+            alert("externalCapturingFunction");
+          }`
       }
     ]
   }
 ];
 /* eslint-enable */
 
 add_task(async function() {
   info("Switch to 2 pane inspector to avoid sidebar width issues with opening events");
--- a/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1.js
@@ -22,110 +22,110 @@ const TEST_DATA = [
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_URL + ":21",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#external",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalinline",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       },
       {
         type: "onMouseUp",
         filename: TEST_URL + ":21",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalcapturing",
     expected: [
       {
         type: "onClickCapture",
         filename: TEST_EXTERNAL_LISTENERS + ":8",
         attributes: [
           "Capturing",
           "React"
         ],
-        handler:
-`function externalCapturingFunction() {
-  alert("externalCapturingFunction");
-}`
+        handler: `
+          function externalCapturingFunction() {
+            alert("externalCapturingFunction");
+          }`
       }
     ]
   }
 ];
 /* eslint-enable */
 
 add_task(async function() {
   info("Switch to 2 pane inspector to avoid sidebar width issues with opening events");
--- a/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1_jsx.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_15.3.1_jsx.js
@@ -23,110 +23,110 @@ const TEST_DATA = [
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_LIB_BABEL + ":10",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externaljsx",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalinlinejsx",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":16",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       },
       {
         type: "onMouseUp",
         filename: TEST_LIB_BABEL + ":10",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalcapturingjsx",
     expected: [
       {
         type: "onClickCapture",
         filename: TEST_EXTERNAL_LISTENERS + ":8",
         attributes: [
           "Capturing",
           "React"
         ],
-        handler:
-`function externalCapturingFunction() {
-  alert("externalCapturingFunction");
-}`
+        handler: `
+          function externalCapturingFunction() {
+            alert("externalCapturingFunction");
+          }`
       }
     ]
   }
 ];
 /* eslint-enable */
 
 add_task(async function() {
   info("Switch to 2 pane inspector to avoid sidebar width issues with opening events");
--- a/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0.js
@@ -22,110 +22,110 @@ const TEST_DATA = [
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":93",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_URL + ":21",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`inlineFunction() {
-  alert("inlineFunction");
-}`
+        handler: `
+          inlineFunction() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#external",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":93",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalinline",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":93",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       },
       {
         type: "onMouseUp",
         filename: TEST_URL + ":21",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`inlineFunction() {
-  alert("inlineFunction");
-}`
+        handler: `
+          inlineFunction() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalcapturing",
     expected: [
       {
         type: "onClickCapture",
         filename: TEST_EXTERNAL_LISTENERS + ":8",
         attributes: [
           "Capturing",
           "React"
         ],
-        handler:
-`function externalCapturingFunction() {
-  alert("externalCapturingFunction");
-}`
+        handler: `
+          function externalCapturingFunction() {
+            alert("externalCapturingFunction");
+          }`
       }
     ]
   }
 ];
 /* eslint-enable */
 
 add_task(async function() {
   info("Switch to 2 pane inspector to avoid sidebar width issues with opening events");
--- a/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0_jsx.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_react_production_16.2.0_jsx.js
@@ -23,110 +23,110 @@ const TEST_DATA = [
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":93",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_LIB_BABEL + ":26",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function inlineFunction() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function inlineFunction() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externaljsx",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":93",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalinlinejsx",
     expected: [
       {
         type: "click",
         filename: TEST_LIB + ":93",
         attributes: [
           "Bubbling",
           "DOM2"
         ],
-        handler: "function() {}"
+        handler: `function() {}`
       },
       {
         type: "onClick",
         filename: TEST_EXTERNAL_LISTENERS + ":4",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function externalFunction() {
-  alert("externalFunction");
-}`
+        handler: `
+          function externalFunction() {
+            alert("externalFunction");
+          }`
       },
       {
         type: "onMouseUp",
         filename: TEST_LIB_BABEL + ":26",
         attributes: [
           "Bubbling",
           "React"
         ],
-        handler:
-`function inlineFunction() {
-  alert("inlineFunction");
-}`
+        handler: `
+          function inlineFunction() {
+            alert("inlineFunction");
+          }`
       }
     ]
   },
   {
     selector: "#externalcapturingjsx",
     expected: [
       {
         type: "onClickCapture",
         filename: TEST_EXTERNAL_LISTENERS + ":8",
         attributes: [
           "Capturing",
           "React"
         ],
-        handler:
-`function externalCapturingFunction() {
-  alert("externalCapturingFunction");
-}`
+        handler: `
+          function externalCapturingFunction() {
+            alert("externalCapturingFunction");
+          }`
       }
     ]
   }
 ];
 /* eslint-enable */
 
 add_task(async function() {
   info("Switch to 2 pane inspector to avoid sidebar width issues with opening events");
--- a/devtools/client/inspector/markup/test/helper_events_test_runner.js
+++ b/devtools/client/inspector/markup/test/helper_events_test_runner.js
@@ -1,16 +1,18 @@
 /* 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/. */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 /* import-globals-from head.js */
 /* import-globals-from helper_diff.js */
 "use strict";
 
+const beautify = require("devtools/shared/jsbeautify/beautify");
+
 loadHelperScript("helper_diff.js");
 
 /**
  * Generator function that runs checkEventsForNode() for each object in the
  * TEST_DATA array.
  */
 async function runEventPopupTests(url, tests) {
   const {inspector, testActor} = await openInspectorForURL(url);
@@ -124,17 +126,20 @@ async function checkEventsForNode(test, 
     // Avoid clicking the header's center (could hit the debugger button)
     EventUtils.synthesizeMouse(header, 2, 2, {}, type.ownerGlobal);
     await tooltip.once("event-tooltip-ready");
 
     is(header.classList.contains("content-expanded"), true,
         "We are in expanded state and icon changed");
 
     const editor = tooltip.eventTooltip._eventEditors.get(contentBox).editor;
-    testDiff(editor.getText(), expected[i].handler,
+    const tidiedHandler = beautify.js(expected[i].handler, {
+      "indent_size": 2,
+    });
+    testDiff(editor.getText(), tidiedHandler,
        "handler matches for " + cssSelector, ok);
   }
 
   tooltip.hide();
 }
 
 /**
  * Create diff of two strings.
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -53,18 +53,22 @@ pref("devtools.inspector.showUserAgentSh
 // Enable the CSS shapes highlighter
 pref("devtools.inspector.shapesHighlighter.enabled", true);
 // Enable the font highlight-on-hover feature
 pref("devtools.inspector.fonthighlighter.enabled", true);
 // Enable tracking of style changes and the Changes panel in the Inspector
 pref("devtools.inspector.changes.enabled", true);
 // Enable the new Rules View
 pref("devtools.inspector.new-rulesview.enabled", false);
-// Hide the 'scrollable' markup-badges for now
+// Enable the 'scrollable' markup-badges in nightly only for now
+#if defined(NIGHTLY_BUILD)
+pref("devtools.inspector.scrollable-badges.enabled", true);
+#else
 pref("devtools.inspector.scrollable-badges.enabled", false);
+#endif
 
 // Flexbox preferences
 pref("devtools.inspector.flexboxHighlighter.enabled", true);
 pref("devtools.flexboxinspector.enabled", true);
 
 #if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION)
 pref("devtools.inspector.flexboxHighlighter.combine", true);
 #else
--- a/devtools/client/shared/components/SmartTrace.css
+++ b/devtools/client/shared/components/SmartTrace.css
@@ -66,28 +66,34 @@
 
 .group-description {
   display: flex;
   align-items: center;
   color: var(--console-output-color);
 }
 
 .frames .frames-group .frames-list {
-  display: block;
+  grid-column: 1 / -1;
+  margin-block-start: 2px;
+  /*
+   * We want to display each frame name on its own row, without having new lines in the
+   * clipboard when copying it. This does the trick.
+   */
+  display: grid;
+  grid-template-columns: 1fr;
 }
 
 .frames .frames-group .frames-list .frame {
   padding-inline-start: 16px;
-  display: block;
   text-overflow: ellipsis;
 }
 
-.frames .frames-group .frames-list {
-  grid-column: 1 / -1;
-  margin-block-start: 2px;
+.frames-group .frames-list .title {
+  grid-column: -1 / 1;
+  padding-inline-start: 16px;
 }
 
 .frames .frames-group .frames-list .frame:first-of-type {
   border-top: 1px solid var(--theme-splitter-color);
   padding-block-start: 4px;
 }
 
 .frames .frames-group .frames-list .frame:last-of-type {
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -280,17 +280,16 @@ checkbox:-moz-focusring {
 /* Tab and button of toolbox does not need to display outline */
 
 .devtools-button:-moz-focusring,
 .devtools-tab:-moz-focusring {
   outline:none;
 }
 
 /* Toolbar buttons */
-.devtools-menulist,
 .devtools-toolbarbutton,
 .devtools-button {
   -moz-appearance: none;
   background: transparent;
   border: none;
   border-radius: 2px;
   color: var(--theme-body-color);
   transition: background-color 0.05s ease-in-out;
--- a/devtools/client/themes/fonts.css
+++ b/devtools/client/themes/fonts.css
@@ -260,24 +260,19 @@
   box-shadow: none;
 }
 
 /* Do not show dotted line focus outline */
 .font-value-input:-moz-focusring {
   outline: none;
 }
 
-/* Add space between input text from number stepper */
-.font-value-input[type=number]::-moz-number-spin-box {
-  margin-left: 3px;
-}
-
-/* Make native number steppers darker to fit the dark theme */
-.theme-dark .font-value-input[type=number]::-moz-number-spin-box {
-  filter: invert(25%);
+/* Make native number steppers disappear by treating it as text field*/
+.font-value-input[type=number] {
+  -moz-appearance: textfield;
 }
 
 /* Do not show number stepper for line height and font-size */
 .font-value-input[name=line-height],
 .font-value-input[name=font-size] {
   -moz-appearance: textfield;
   padding-right: 5px;
   border-right: none;
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -1551,17 +1551,17 @@ class JSTerm extends Component {
   render() {
     if (this.props.hud.isBrowserConsole &&
         !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
       return null;
     }
 
     if (this.props.codeMirrorEnabled) {
       return dom.div({
-        className: "jsterm-input-container devtools-monospace",
+        className: "jsterm-input-container devtools-input devtools-monospace",
         key: "jsterm-container",
         style: {direction: "ltr"},
         "aria-live": "off",
         onContextMenu: this.onContextMenu,
         ref: node => {
           this.node = node;
         },
       });
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -221,16 +221,17 @@ skip-if = verify
 [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_dollar_underscore.js]
 [browser_jsterm_completion_dollar_zero.js]
 [browser_jsterm_completion.js]
 [browser_jsterm_content_defined_helpers.js]
+[browser_jsterm_context_menu_labels.js]
 [browser_jsterm_copy_command.js]
 [browser_jsterm_ctrl_a_select_all.js]
 [browser_jsterm_ctrl_key_nav.js]
 skip-if = os != 'mac' # The tested ctrl+key shortcuts are OSX only
 [browser_jsterm_document_no_xray.js]
 [browser_jsterm_error_docs.js]
 [browser_jsterm_error_outside_valid_range.js]
 [browser_jsterm_focus_reload.js]
@@ -289,16 +290,18 @@ skip-if = e10s # SharedWorkers console e
 [browser_webconsole_context_menu_export_console_output_clipboard.js]
 subsuite = clipboard
 [browser_webconsole_context_menu_copy_entire_message.js]
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_webconsole_context_menu_copy_link_location.js]
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) || (os == 'linux') # bug 1328915, disable linux32 debug devtools for timeouts, bug 1473120
+[browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js]
+subsuite = clipboard
 [browser_webconsole_context_menu_copy_object.js]
 subsuite = clipboard
 [browser_webconsole_context_menu_object_in_sidebar.js]
 [browser_webconsole_context_menu_open_url.js]
 [browser_webconsole_context_menu_store_as_global.js]
 [browser_webconsole_cors_errors.js]
 [browser_webconsole_csp_ignore_reflected_xss_message.js]
 [browser_webconsole_csp_violation.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_context_menu_labels.js
@@ -0,0 +1,36 @@
+/* -*- 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/ */
+
+// Tests that context menu for CodeMirror is properly localized.
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf8,<p>test page</p>`;
+
+add_task(async function() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const { jsterm } = hud;
+
+  const target = await TargetFactory.forTab(gBrowser.selectedTab);
+  const toolbox = gDevTools.getToolbox(target);
+
+  // Open context menu and wait until it's visible
+  const element = jsterm.node.querySelector(".CodeMirror-wrap");
+  const menuPopup = await openTextBoxContextMenu(toolbox, element);
+
+  // Check label of the 'undo' menu item.
+  const undoMenuItem = menuPopup.querySelector("#editmenu-undo");
+  await waitUntil(() => !!undoMenuItem.getAttribute("label"));
+
+  is(undoMenuItem.getAttribute("label"), "Undo",
+    "Undo is visible and localized");
+});
+
+async function openTextBoxContextMenu(toolbox, element) {
+  const onConsoleMenuOpened = toolbox.once("menu-open");
+  synthesizeContextMenuEvent(element);
+  await onConsoleMenuOpened;
+  return toolbox.doc.getElementById("toolbox-menu");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js
@@ -0,0 +1,104 @@
+/* -*- 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";
+
+const httpServer = createTestHTTPServer();
+httpServer.registerPathHandler(`/`, function(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "OK");
+  response.write(`
+    <meta charset=utf8>
+    <h1>Test "copy message" context menu entry on message with framework stacktrace</h1>
+    <script type="text/javascript" src="react.js"></script>
+    <script type="text/javascript" src="test.js"></script>`);
+});
+
+httpServer.registerPathHandler("/test.js", function(_, response) {
+  response.setHeader("Content-Type", "application/javascript");
+  response.write(`
+    window.myFunc = () => wrapper();
+    const wrapper = () => console.trace("wrapperTrace");
+  `);
+});
+
+httpServer.registerPathHandler("/react.js", function(_, response) {
+  response.setHeader("Content-Type", "application/javascript");
+  response.write(`
+    window.render = function() {
+      const renderFinal = () => window.myFunc();
+      renderFinal();
+    };
+  `);
+});
+
+const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`;
+
+// Test the Copy menu item of the webconsole copies the expected clipboard text for
+// a message with a "framework" stacktrace (i.e. with grouped frames).
+
+add_task(async function() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  info("Call the log function defined in the test page");
+  const onMessage = waitForMessage(hud, "wrapperTrace");
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+    content.wrappedJSObject.render();
+  });
+  const message = await onMessage;
+  const messageEl = message.node;
+  await waitFor(() => messageEl.querySelector(".frames"));
+
+  let clipboardText = await copyMessageContent(hud, messageEl);
+  ok(true, "Clipboard text was found and saved");
+
+  const newLineString = "\n";
+  info("Check copied text for the console.trace message");
+  let lines = clipboardText.split(newLineString);
+  is(lines.length, 5, "Correct number of lines in the copied text");
+  is(lines[lines.length - 1], "", "The last line is an empty new line");
+  is(lines[0], `console.trace() wrapperTrace test.js:3:27`,
+    "Message first line has the expected text");
+  is(lines[1], `    wrapper ${TEST_URI}test.js:3`,
+    "Stacktrace first line has the expected text");
+  is(lines[2], `    myFunc ${TEST_URI}test.js:2`,
+    "Stacktrace second line has the expected text");
+  is(lines[3], `    React 2`, "Stacktrace third line has the expected text");
+
+  info("Expand the React group");
+  const getFrames = () => messageEl.querySelectorAll(".frame");
+  const frames = getFrames().length;
+  messageEl.querySelector(".frames .group").click();
+  // Let's wait until all React frames are displayed.
+  await waitFor(() => getFrames().length > frames);
+
+  clipboardText = await copyMessageContent(hud, messageEl);
+  ok(true, "Clipboard text was found and saved");
+
+  info("Check copied text for the console.trace message with expanded React frames");
+  lines = clipboardText.split(newLineString);
+  is(lines.length, 7, "Correct number of lines in the copied text");
+  is(lines[lines.length - 1], "", "The last line is an empty new line");
+  is(lines[0], `console.trace() wrapperTrace test.js:3:27`,
+    "Message first line has the expected text");
+  is(lines[1], `    wrapper ${TEST_URI}test.js:3`,
+    "Stacktrace first line has the expected text");
+  is(lines[2], `    myFunc ${TEST_URI}test.js:2`,
+    "Stacktrace second line has the expected text");
+  is(lines[3], `    React 2`, "Stacktrace third line has the expected text");
+  is(lines[4], `        renderFinal`, "Stacktrace fourth line has the expected text");
+  is(lines[5], `        render`, "Stacktrace fifth line has the expected text");
+});
+
+/**
+ * Simple helper method to open the context menu on a given message, and click on the copy
+ * menu item.
+ */
+async function copyMessageContent(hud, messageEl) {
+  const menuPopup = await openContextMenu(hud, messageEl);
+  const copyMenuItem = menuPopup.querySelector("#console-menu-copy");
+  ok(copyMenuItem, "copy menu item is enabled");
+
+  return waitForClipboardPromise(() => copyMenuItem.click(), data => data);
+}
rename from devtools/server/actors/inspector/event-parsers.js
rename to devtools/server/actors/inspector/event-collector.js
--- a/devtools/server/actors/inspector/event-parsers.js
+++ b/devtools/server/actors/inspector/event-collector.js
@@ -1,623 +1,896 @@
 /* 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/. */
 
-// This file contains event parsers that are then used by developer tools in
+// This file contains event collectors that are then used by developer tools in
 // order to find information about events affecting an HTML element.
 
 "use strict";
 
 const { Cu } = require("chrome");
 const Services = require("Services");
+const makeDebugger = require("devtools/server/actors/utils/make-debugger");
 
 // eslint-disable-next-line
 const JQUERY_LIVE_REGEX = /return typeof \w+.*.event\.triggered[\s\S]*\.event\.(dispatch|handle).*arguments/;
 
-var parsers = [
-  {
-    id: "jQuery events",
-    hasListeners: function(node) {
-      const global = node.ownerGlobal.wrappedJSObject;
-      const hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery;
+const REACT_EVENT_NAMES = [
+  "onAbort",
+  "onAnimationEnd",
+  "onAnimationIteration",
+  "onAnimationStart",
+  "onAuxClick",
+  "onBeforeInput",
+  "onBlur",
+  "onCanPlay",
+  "onCanPlayThrough",
+  "onCancel",
+  "onChange",
+  "onClick",
+  "onClose",
+  "onCompositionEnd",
+  "onCompositionStart",
+  "onCompositionUpdate",
+  "onContextMenu",
+  "onCopy",
+  "onCut",
+  "onDoubleClick",
+  "onDrag",
+  "onDragEnd",
+  "onDragEnter",
+  "onDragExit",
+  "onDragLeave",
+  "onDragOver",
+  "onDragStart",
+  "onDrop",
+  "onDurationChange",
+  "onEmptied",
+  "onEncrypted",
+  "onEnded",
+  "onError",
+  "onFocus",
+  "onGotPointerCapture",
+  "onInput",
+  "onInvalid",
+  "onKeyDown",
+  "onKeyPress",
+  "onKeyUp",
+  "onLoad",
+  "onLoadStart",
+  "onLoadedData",
+  "onLoadedMetadata",
+  "onLostPointerCapture",
+  "onMouseDown",
+  "onMouseEnter",
+  "onMouseLeave",
+  "onMouseMove",
+  "onMouseOut",
+  "onMouseOver",
+  "onMouseUp",
+  "onPaste",
+  "onPause",
+  "onPlay",
+  "onPlaying",
+  "onPointerCancel",
+  "onPointerDown",
+  "onPointerEnter",
+  "onPointerLeave",
+  "onPointerMove",
+  "onPointerOut",
+  "onPointerOver",
+  "onPointerUp",
+  "onProgress",
+  "onRateChange",
+  "onReset",
+  "onScroll",
+  "onSeeked",
+  "onSeeking",
+  "onSelect",
+  "onStalled",
+  "onSubmit",
+  "onSuspend",
+  "onTimeUpdate",
+  "onToggle",
+  "onTouchCancel",
+  "onTouchEnd",
+  "onTouchMove",
+  "onTouchStart",
+  "onTransitionEnd",
+  "onVolumeChange",
+  "onWaiting",
+  "onWheel",
+  "onAbortCapture",
+  "onAnimationEndCapture",
+  "onAnimationIterationCapture",
+  "onAnimationStartCapture",
+  "onAuxClickCapture",
+  "onBeforeInputCapture",
+  "onBlurCapture",
+  "onCanPlayCapture",
+  "onCanPlayThroughCapture",
+  "onCancelCapture",
+  "onChangeCapture",
+  "onClickCapture",
+  "onCloseCapture",
+  "onCompositionEndCapture",
+  "onCompositionStartCapture",
+  "onCompositionUpdateCapture",
+  "onContextMenuCapture",
+  "onCopyCapture",
+  "onCutCapture",
+  "onDoubleClickCapture",
+  "onDragCapture",
+  "onDragEndCapture",
+  "onDragEnterCapture",
+  "onDragExitCapture",
+  "onDragLeaveCapture",
+  "onDragOverCapture",
+  "onDragStartCapture",
+  "onDropCapture",
+  "onDurationChangeCapture",
+  "onEmptiedCapture",
+  "onEncryptedCapture",
+  "onEndedCapture",
+  "onErrorCapture",
+  "onFocusCapture",
+  "onGotPointerCaptureCapture",
+  "onInputCapture",
+  "onInvalidCapture",
+  "onKeyDownCapture",
+  "onKeyPressCapture",
+  "onKeyUpCapture",
+  "onLoadCapture",
+  "onLoadStartCapture",
+  "onLoadedDataCapture",
+  "onLoadedMetadataCapture",
+  "onLostPointerCaptureCapture",
+  "onMouseDownCapture",
+  "onMouseEnterCapture",
+  "onMouseLeaveCapture",
+  "onMouseMoveCapture",
+  "onMouseOutCapture",
+  "onMouseOverCapture",
+  "onMouseUpCapture",
+  "onPasteCapture",
+  "onPauseCapture",
+  "onPlayCapture",
+  "onPlayingCapture",
+  "onPointerCancelCapture",
+  "onPointerDownCapture",
+  "onPointerEnterCapture",
+  "onPointerLeaveCapture",
+  "onPointerMoveCapture",
+  "onPointerOutCapture",
+  "onPointerOverCapture",
+  "onPointerUpCapture",
+  "onProgressCapture",
+  "onRateChangeCapture",
+  "onResetCapture",
+  "onScrollCapture",
+  "onSeekedCapture",
+  "onSeekingCapture",
+  "onSelectCapture",
+  "onStalledCapture",
+  "onSubmitCapture",
+  "onSuspendCapture",
+  "onTimeUpdateCapture",
+  "onToggleCapture",
+  "onTouchCancelCapture",
+  "onTouchEndCapture",
+  "onTouchMoveCapture",
+  "onTouchStartCapture",
+  "onTransitionEndCapture",
+  "onVolumeChangeCapture",
+  "onWaitingCapture",
+  "onWheelCapture",
+];
 
-      if (!hasJQuery) {
-        return false;
+/**
+ * The base class that all the enent collectors should be based upon.
+ */
+class MainEventCollector {
+  /**
+   * Check if a node has any event listeners attached. Please do not override
+   * this method... your getListeners() implementation needs to have the
+   * following signature:
+   * `getListeners(node, {checkOnly} = {})`
+   *
+   * @param  {DOMNode} node
+   *         The not for which we want to check for event listeners.
+   * @return {Boolean}
+   *         true if the node has event listeners, false otherwise.
+   */
+  hasListeners(node) {
+    return this.getListeners(node, {
+      checkOnly: true,
+    });
+  }
+
+  /**
+   * Get all listeners for a node. This method must be overridden.
+   *
+   * @param  {DOMNode} node
+   *         The not for which we want to get event listeners.
+   * @param  {Object} options
+   *         An object for passing in options.
+   * @param  {Boolean} [options.checkOnly = false]
+   *         Don't get any listeners but return true when the first event is
+   *         found.
+   * @return {Array}
+   *         An array of event handlers.
+   */
+  getListeners(node, {checkOnly}) {
+    throw new Error("You have to implement the method getListeners()!");
+  }
+
+  /**
+   * Get unfiltered DOM Event listeners for a node.
+   * NOTE: These listeners may contain invalid events and events based
+   *       on C++ rather than JavaScript.
+   *
+   * @param  {DOMNode} node
+   *         The node for which we want to get unfiltered event listeners.
+   * @return {Array}
+   *         An array of unfiltered event listeners or an empty array
+   */
+  getDOMListeners(node) {
+    if (typeof node.nodeName !== "undefined" && node.nodeName.toLowerCase() === "html") {
+      const winListeners =
+        Services.els.getListenerInfoFor(node.ownerGlobal) || [];
+      const docElementListeners =
+        Services.els.getListenerInfoFor(node) || [];
+      const docListeners =
+        Services.els.getListenerInfoFor(node.parentNode) || [];
+
+      return [...winListeners, ...docElementListeners, ...docListeners];
+    }
+    return Services.els.getListenerInfoFor(node) || [];
+  }
+
+  getJQuery(node) {
+    const global = this.unwrap(node.ownerGlobal);
+    const hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery;
+
+    if (hasJQuery) {
+      return global.jQuery;
+    }
+    return null;
+  }
+
+  unwrap(obj) {
+    return Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
+  }
+}
+
+/**
+ * Get or detect DOM events. These may include DOM events created by libraries
+ * that enable their custom events to work. At this point we are unable to
+ * effectively filter them as they may be proxied or wrapped. Although we know
+ * there is an event, we may not know the true contents until it goes
+ * through `processHandlerForEvent()`.
+ */
+class DOMEventCollector extends MainEventCollector {
+  getListeners(node, {checkOnly} = {}) {
+    const handlers = [];
+    const listeners = this.getDOMListeners(node);
+
+    for (const listener of listeners) {
+      // Ignore listeners without a type, e.g.
+      // node.addEventListener("", function() {})
+      if (!listener.type) {
+        continue;
       }
 
-      const jQuery = global.jQuery;
-      const handlers = [];
+      // Get the listener object, either a Function or an Object.
+      const obj = listener.listenerObject;
+
+      // Ignore listeners without any listener, e.g.
+      // node.addEventListener("mouseover", null);
+      if (!obj) {
+        continue;
+      }
+
+      let handler = null;
 
-      // jQuery 1.2+
-      const data = jQuery._data || jQuery.data;
-      if (data) {
-        const eventsObj = data(node, "events");
-        for (const type in eventsObj) {
-          const events = eventsObj[type];
-          for (const key in events) {
-            const event = events[key];
+      // An object without a valid handleEvent is not a valid listener.
+      if (typeof obj === "object") {
+        const unwrapped = this.unwrap(obj);
+        if (typeof unwrapped.handleEvent === "function") {
+          handler = Cu.unwaiveXrays(unwrapped.handleEvent);
+        }
+      } else if (typeof obj === "function") {
+        // Ignore DOM events used to trigger jQuery events as they are only
+        // useful to the developers of the jQuery library.
+        if (JQUERY_LIVE_REGEX.test(obj.toString())) {
+          continue;
+        }
+        // Otherwise, the other valid listener type is function.
+        handler = obj;
+      }
 
-            if (node.wrappedJSObject == global.document && event.selector) {
-              continue;
-            }
+      // Ignore listeners that have no handler.
+      if (!handler) {
+        continue;
+      }
 
-            if (typeof event === "object" || typeof event === "function") {
-              return true;
-            }
-          }
-        }
+      // If this is checking if a node has any listeners then we have found one
+      // so return now.
+      if (checkOnly) {
+        return true;
       }
 
+      const eventInfo = {
+        capturing: listener.capturing,
+        type: listener.type,
+        handler: handler,
+      };
+
+      handlers.push(eventInfo);
+    }
+
+    // If this is checking if a node has any listeners then none were found so
+    // return false.
+    if (checkOnly) {
+      return false;
+    }
+
+    return handlers;
+  }
+}
+
+/**
+ * Get or detect jQuery events.
+ */
+class JQueryEventCollector extends MainEventCollector {
+  getListeners(node, {checkOnly} = {}) {
+    const jQuery = this.getJQuery(node);
+    const handlers = [];
+
+    if (!jQuery) {
+      if (checkOnly) {
+        return false;
+      }
+      return handlers;
+    }
+
+    let eventsObj = null;
+
+    const data = jQuery._data || jQuery.data;
+    if (data) {
+      // jQuery 1.2+
+      eventsObj = data(node, "events");
+    } else {
       // JQuery 1.0 & 1.1
       const entry = jQuery(node)[0];
-      if (!entry) {
+
+      if (!entry || !entry.events) {
+        if (checkOnly) {
+          return false;
+        }
         return handlers;
       }
 
-      for (const type in entry.events) {
-        const events = entry.events[type];
-        for (const key in events) {
-          const event = events[key];
+      eventsObj = entry.events;
+    }
 
-          if (node.wrappedJSObject == global.document && event.selector) {
+    if (eventsObj) {
+      for (const [type, events] of Object.entries(eventsObj)) {
+        for (const [, event] of Object.entries(events)) {
+          // Skip events that are part of jQueries internals.
+          if (node.nodeType == node.DOCUMENT_NODE && event.selector) {
             continue;
           }
 
-          if (typeof events[key] === "function") {
-            return true;
-          }
-        }
-      }
-
-      return false;
-    },
-    getListeners: function(node) {
-      const global = node.ownerGlobal.wrappedJSObject;
-      const hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery;
-
-      if (!hasJQuery) {
-        return undefined;
-      }
-
-      const jQuery = global.jQuery;
-      const handlers = [];
-
-      // jQuery 1.2+
-      const data = jQuery._data || jQuery.data;
-      if (data) {
-        const eventsObj = data(node, "events");
-        for (const type in eventsObj) {
-          const events = eventsObj[type];
-          for (const key in events) {
-            const event = events[key];
-
-            if (node.wrappedJSObject == global.document && event.selector) {
-              continue;
+          if (typeof event === "function" || typeof event === "object") {
+            if (checkOnly) {
+              return true;
             }
 
-            if (typeof event === "object" || typeof event === "function") {
-              const eventInfo = {
-                type: type,
-                handler: event.handler || event,
-                tags: "jQuery",
-                hide: {
-                  capturing: true,
-                  dom0: true,
-                },
-              };
-
-              handlers.push(eventInfo);
-            }
-          }
-        }
-      }
-
-      // JQuery 1.0 & 1.1
-      const entry = jQuery(node)[0];
-
-      if (!entry) {
-        return handlers;
-      }
-
-      for (const type in entry.events) {
-        const events = entry.events[type];
-        for (const key in events) {
-          const event = events[key];
-
-          if (node.wrappedJSObject == global.document && event.selector) {
-            continue;
-          }
-
-          if (typeof events[key] === "function") {
             const eventInfo = {
               type: type,
-              handler: events[key],
+              handler: event.handler || event,
               tags: "jQuery",
               hide: {
                 capturing: true,
                 dom0: true,
               },
             };
 
             handlers.push(eventInfo);
           }
         }
       }
+    }
 
+    if (checkOnly) {
+      return false;
+    }
+    return handlers;
+  }
+}
+
+/**
+ * Get or detect jQuery live events.
+ */
+class JQueryLiveEventCollector extends MainEventCollector {
+  getListeners(node, {checkOnly} = {}) {
+    const jQuery = this.getJQuery(node);
+    const handlers = [];
+
+    if (!jQuery) {
+      if (checkOnly) {
+        return false;
+      }
       return handlers;
-    },
-  },
-  {
-    id: "jQuery live events",
-    hasListeners: function(node) {
-      return jQueryLiveGetListeners(node, true);
-    },
-    getListeners: function(node) {
-      return jQueryLiveGetListeners(node, false);
-    },
-    normalizeListener: function(handlerDO) {
-      function isFunctionInProxy(funcDO) {
-        // If the anonymous function is inside the |proxy| function and the
-        // function only has guessed atom, the guessed atom should starts with
-        // "proxy/".
-        const displayName = funcDO.displayName;
-        if (displayName && displayName.startsWith("proxy/")) {
-          return true;
+    }
+
+    const data = jQuery._data || jQuery.data;
+
+    if (data) {
+      // Live events are added to the document and bubble up to all elements.
+      // Any element matching the specified selector will trigger the live
+      // event.
+      const win = this.unwrap(node.ownerGlobal);
+      const events = data(win.document, "events");
+
+      if (events) {
+        for (const [, eventHolder] of Object.entries(events)) {
+          for (const [idx, event] of Object.entries(eventHolder)) {
+            if (typeof idx !== "string" || isNaN(parseInt(idx, 10))) {
+              continue;
+            }
+
+            let selector = event.selector;
+
+            if (!selector && event.data) {
+              selector = event.data.selector || event.data || event.selector;
+            }
+
+            if (!selector || !node.ownerDocument) {
+              continue;
+            }
+
+            let matches;
+            try {
+              matches = node.matches && node.matches(selector);
+            } catch (e) {
+              // Invalid selector, do nothing.
+            }
+
+            if (!matches) {
+              continue;
+            }
+
+            if (typeof event === "function" || typeof event === "object") {
+              if (checkOnly) {
+                return true;
+              }
+              const eventInfo = {
+                type: event.origType || event.type.substr(selector.length + 1),
+                handler: event.handler || event,
+                tags: "jQuery,Live",
+                hide: {
+                  dom0: true,
+                  capturing: true,
+                },
+              };
+
+              if (!eventInfo.type && event.data && event.data.live) {
+                eventInfo.type = event.data.live;
+              }
+
+              handlers.push(eventInfo);
+            }
+          }
         }
+      }
+    }
 
-        // If the anonymous function is inside the |proxy| function and the
-        // function gets name at compile time by SetFunctionName, its guessed
-        // atom doesn't contain "proxy/".  In that case, check if the caller is
-        // "proxy" function, as a fallback.
-        const calleeDO = funcDO.environment.callee;
-        if (!calleeDO) {
-          return false;
-        }
-        const calleeName = calleeDO.displayName;
-        return calleeName == "proxy";
+    if (checkOnly) {
+      return false;
+    }
+    return handlers;
+  }
+
+  normalizeListener(handlerDO) {
+    function isFunctionInProxy(funcDO) {
+      // If the anonymous function is inside the |proxy| function and the
+      // function only has guessed atom, the guessed atom should starts with
+      // "proxy/".
+      const displayName = funcDO.displayName;
+      if (displayName && displayName.startsWith("proxy/")) {
+        return true;
       }
 
-      function getFirstFunctionVariable(funcDO) {
-        // The handler function inside the |proxy| function should point the
-        // unwrapped function via environment variable.
-        const names = funcDO.environment.names();
-        for (const varName of names) {
-          const varDO = handlerDO.environment.getVariable(varName);
-          if (!varDO) {
-            continue;
-          }
-          if (varDO.class == "Function") {
-            return varDO;
-          }
+      // If the anonymous function is inside the |proxy| function and the
+      // function gets name at compile time by SetFunctionName, its guessed
+      // atom doesn't contain "proxy/".  In that case, check if the caller is
+      // "proxy" function, as a fallback.
+      const calleeDO = funcDO.environment.callee;
+      if (!calleeDO) {
+        return false;
+      }
+      const calleeName = calleeDO.displayName;
+      return calleeName == "proxy";
+    }
+
+    function getFirstFunctionVariable(funcDO) {
+      // The handler function inside the |proxy| function should point the
+      // unwrapped function via environment variable.
+      const names = funcDO.environment.names();
+      for (const varName of names) {
+        const varDO = handlerDO.environment.getVariable(varName);
+        if (!varDO) {
+          continue;
         }
-        return null;
+        if (varDO.class == "Function") {
+          return varDO;
+        }
       }
+      return null;
+    }
 
-      if (!isFunctionInProxy(handlerDO)) {
+    if (!isFunctionInProxy(handlerDO)) {
+      return handlerDO;
+    }
+
+    const MAX_NESTED_HANDLER_COUNT = 2;
+    for (let i = 0; i < MAX_NESTED_HANDLER_COUNT; i++) {
+      const funcDO = getFirstFunctionVariable(handlerDO);
+      if (!funcDO) {
         return handlerDO;
       }
 
-      const MAX_NESTED_HANDLER_COUNT = 2;
-      for (let i = 0; i < MAX_NESTED_HANDLER_COUNT; i++) {
-        const funcDO = getFirstFunctionVariable(handlerDO);
-        if (!funcDO) {
-          return handlerDO;
-        }
+      handlerDO = funcDO;
+      if (isFunctionInProxy(handlerDO)) {
+        continue;
+      }
+      break;
+    }
 
-        handlerDO = funcDO;
-        if (isFunctionInProxy(handlerDO)) {
-          continue;
-        }
-        break;
-      }
+    return handlerDO;
+  }
+}
+
+/**
+ * Get or detect React events.
+ */
+class ReactEventCollector extends MainEventCollector {
+  getListeners(node, {checkOnly} = {}) {
+    const handlers = [];
+    const props = this.getProps(node);
 
-      return handlerDO;
-    },
-  },
-  {
-    id: "DOM events",
-    hasListeners: function(node) {
-      let listeners;
+    if (props) {
+      for (const [name, prop] of Object.entries(props)) {
+        if (REACT_EVENT_NAMES.includes(name)) {
+          const listener = prop.__reactBoundMethod || prop;
+
+          if (typeof listener !== "function") {
+            continue;
+          }
+
+          if (checkOnly) {
+            return true;
+          }
 
-      if (node.nodeName.toLowerCase() === "html") {
-        const winListeners =
-          Services.els.getListenerInfoFor(node.ownerGlobal) || [];
-        const docElementListeners =
-          Services.els.getListenerInfoFor(node) || [];
-        const docListeners =
-          Services.els.getListenerInfoFor(node.parentNode) || [];
+          const handler = {
+            type: name,
+            handler: listener,
+            tags: "React",
+            hide: {
+              dom0: true,
+            },
+            override: {
+              capturing: name.endsWith("Capture"),
+            },
+          };
 
-        listeners = [...winListeners, ...docElementListeners, ...docListeners];
-      } else {
-        listeners = Services.els.getListenerInfoFor(node) || [];
-      }
-
-      for (const listener of listeners) {
-        if (isValidDOMListener(listener)) {
-          return true;
+          handlers.push(handler);
         }
       }
-
-      return false;
-    },
-    getListeners: function(node) {
-      const handlers = [];
-      const listeners = Services.els.getListenerInfoFor(node);
-
-      // The Node actor's getEventListenerInfo knows that when an html tag has
-      // been passed we need the window object so we don't need to account for
-      // event hoisting here as we did in hasListeners.
-
-      for (const listener of listeners) {
-        if (!isValidDOMListener(listener)) {
-          continue;
-        }
-
-        // Get the listener object, either a Function or an Object.
-        let obj = listener.listenerObject;
-
-        // Unwrap the listener in order to see content objects.
-        if (Cu.isXrayWrapper(obj)) {
-          obj = listener.listenerObject.wrappedJSObject;
-        }
-
-        let handler = null;
+    }
 
-        // An object without a valid handleEvent is not a valid listener.
-        if (typeof obj === "object") {
-          const unwrapped = Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
-          if (typeof unwrapped.handleEvent === "function") {
-            handler = Cu.unwaiveXrays(unwrapped.handleEvent);
-          }
-        } else if (typeof obj === "function") {
-          // Ignore DOM events used to trigger jQuery events as they are only
-          // useful to the developers of the jQuery library.
-          if (JQUERY_LIVE_REGEX.test(obj.toString())) {
-            continue;
-          }
-          // Otherwise, the other valid listener type is function.
-          handler = obj;
-        } else {
-          continue;
-        }
-
-        const eventInfo = {
-          capturing: listener.capturing,
-          type: listener.type,
-          handler: handler,
-        };
-
-        handlers.push(eventInfo);
-      }
+    if (checkOnly) {
+      return false;
+    }
 
-      return handlers;
-    },
-  },
-  {
-    id: "React events",
-    hasListeners: function(node) {
-      return reactGetListeners(node, true);
-    },
-
-    getListeners: function(node) {
-      return reactGetListeners(node, false);
-    },
-
-    normalizeListener: function(handlerDO, listener) {
-      let functionText = "";
-
-      if (handlerDO.boundTargetFunction) {
-        handlerDO = handlerDO.boundTargetFunction;
-      }
-
-      const introScript = handlerDO.script.source.introductionScript;
-      const script = handlerDO.script;
+    return handlers;
+  }
 
-      // If this is a Babel transpiled function we have no access to the
-      // source location so we need to hide the filename and debugger
-      // icon.
-      if (introScript && introScript.displayName.endsWith("/transform.run")) {
-        listener.hide.debugger = true;
-        listener.hide.filename = true;
-
-        if (!handlerDO.isArrowFunction) {
-          functionText += "function (";
-        } else {
-          functionText += "(";
-        }
-
-        functionText += handlerDO.parameterNames.join(", ");
+  getProps(node) {
+    node = this.unwrap(node);
 
-        functionText += ") {\n";
-
-        const scriptSource = script.source.text;
-        functionText +=
-          scriptSource.substr(script.sourceStart, script.sourceLength);
-
-        listener.override.handler = functionText;
-      }
-
-      return handlerDO;
-    },
-  },
-];
-
-function reactGetListeners(node, boolOnEventFound) {
-  function getProps() {
     for (const key of Object.keys(node)) {
       if (key.startsWith("__reactInternalInstance$")) {
         const value = node[key];
         if (value.memoizedProps) {
           return value.memoizedProps; // React 16
         }
         return value._currentElement.props; // React 15
       }
     }
     return null;
   }
 
-  node = node.wrappedJSObject || node;
+  normalizeListener(handlerDO, listener) {
+    let functionText = "";
 
-  const handlers = [];
-  const props = getProps();
+    if (handlerDO.boundTargetFunction) {
+      handlerDO = handlerDO.boundTargetFunction;
+    }
 
-  if (props) {
-    for (const name in props) {
-      if (name.startsWith("on")) {
-        const prop = props[name];
-        const listener = prop.__reactBoundMethod || prop;
+    const introScript = handlerDO.script.source.introductionScript;
+    const script = handlerDO.script;
 
-        if (typeof listener !== "function") {
-          continue;
-        }
-
-        if (boolOnEventFound) {
-          return true;
-        }
+    // If this is a Babel transpiled function we have no access to the
+    // source location so we need to hide the filename and debugger
+    // icon.
+    if (introScript && introScript.displayName.endsWith("/transform.run")) {
+      listener.hide.debugger = true;
+      listener.hide.filename = true;
 
-        const handler = {
-          type: name,
-          handler: listener,
-          tags: "React",
-          hide: {
-            dom0: true,
-          },
-          override: {
-            capturing: name.endsWith("Capture"),
-          },
-        };
+      if (!handlerDO.isArrowFunction) {
+        functionText += "function (";
+      } else {
+        functionText += "(";
+      }
+
+      functionText += handlerDO.parameterNames.join(", ");
+
+      functionText += ") {\n";
 
-        handlers.push(handler);
-      }
-    }
-  }
+      const scriptSource = script.source.text;
+      functionText +=
+        scriptSource.substr(script.sourceStart, script.sourceLength);
 
-  if (boolOnEventFound) {
-    return false;
+      listener.override.handler = functionText;
+    }
+
+    return handlerDO;
   }
-
-  return handlers;
 }
 
-function jQueryLiveGetListeners(node, boolOnEventFound) {
-  const global = node.ownerGlobal.wrappedJSObject;
-  const hasJQuery = global.jQuery && global.jQuery.fn && global.jQuery.fn.jquery;
+/**
+ * The exposed class responsible for gathering events.
+ */
+class EventCollector {
+  constructor() {
+    // The event collector array. Please preserve the order otherwise there will
+    // be multiple failing tests.
+    this.eventCollectors = [
+      new ReactEventCollector(),
+      new JQueryLiveEventCollector(),
+      new JQueryEventCollector(),
+      new DOMEventCollector(),
+    ];
 
-  if (!hasJQuery) {
-    return undefined;
+    // Add a method to create a simple debugger.
+    this.makeDebuggerForContent = makeDebugger.bind(null, {
+      findDebuggees: dbg => [],
+      shouldAddNewGlobalAsDebuggee: global => true,
+    });
   }
 
-  const jQuery = global.jQuery;
-  const handlers = [];
-  const data = jQuery._data || jQuery.data;
-
-  if (data) {
-    // Live events are added to the document and bubble up to all elements.
-    // Any element matching the specified selector will trigger the live
-    // event.
-    const events = data(global.document, "events");
-
-    for (const type in events) {
-      const eventHolder = events[type];
-
-      for (const idx in eventHolder) {
-        if (typeof idx !== "string" || isNaN(parseInt(idx, 10))) {
-          continue;
-        }
-
-        const event = eventHolder[idx];
-        let selector = event.selector;
-
-        if (!selector && event.data) {
-          selector = event.data.selector || event.data || event.selector;
-        }
-
-        if (!selector || !node.ownerDocument) {
-          continue;
-        }
+  /**
+   * Destructor (must be called manually).
+   */
+  destroy() {
+    this.eventCollectors = null;
+    this.makeDebuggerForContent = null;
+  }
 
-        let matches;
-        try {
-          matches = node.matches && node.matches(selector);
-        } catch (e) {
-          // Invalid selector, do nothing.
-        }
-
-        if (boolOnEventFound && matches) {
-          return true;
-        }
-
-        if (!matches) {
-          continue;
-        }
-
-        if (!boolOnEventFound &&
-            (typeof event === "object" || typeof event === "function")) {
-          const eventInfo = {
-            type: event.origType || event.type.substr(selector.length + 1),
-            handler: event.handler || event,
-            tags: "jQuery,Live",
-            hide: {
-              dom0: true,
-              capturing: true,
-            },
-          };
-
-          if (!eventInfo.type && event.data && event.data.live) {
-            eventInfo.type = event.data.live;
-          }
-
-          handlers.push(eventInfo);
-        }
+  /**
+   * Iterate through all event collectors returning on the first found event.
+   *
+   * @param  {DOMNode} node
+   *         The node to be checked for events.
+   * @return {Boolean}
+   *         True if the node has event listeners, false otherwise.
+   */
+  hasEventListeners(node) {
+    for (const collector of this.eventCollectors) {
+      if (collector.hasListeners(node)) {
+        return true;
       }
     }
-  }
-
-  if (boolOnEventFound) {
-    return false;
-  }
-  return handlers;
-}
-
-function isValidDOMListener(listener) {
-  // Ignore listeners without a type, e.g.
-  // node.addEventListener("", function() {})
-  if (!listener.type) {
     return false;
   }
 
-  // Get the listener object, either a Function or an Object.
-  let obj = listener.listenerObject;
-
-  // Ignore listeners without any listener, e.g.
-  // node.addEventListener("mouseover", null);
-  if (!obj) {
-    return false;
-  }
+  /**
+   *
+   * @param  {DOMNode} node
+   *         The node for which events are to be gathered.
+   * @return {Array}
+   *         An array containing objects in the following format:
+   *         {
+   *           type: type,        // e.g. "click"
+   *           handler: handler,  // The function called when event is triggered.
+   *           tags: "jQuery",    // Comma seperated list of tags displayed
+   *                              // inside event bubble.
+   *           hide: {            // Flags for hiding certain properties.
+   *             capturing: true,
+   *             dom0: true,
+   *           }
+   *         }
+   */
+  getEventListeners(node) {
+    const listenerArray = [];
+    const dbg = this.makeDebuggerForContent();
+    const global = Cu.getGlobalForObject(node);
+    const globalDO = dbg.addDebuggee(global);
 
-  // Unwrap the listener in order to see content objects.
-  if (Cu.isXrayWrapper(obj)) {
-    obj = listener.listenerObject.wrappedJSObject;
-  }
+    for (const collector of this.eventCollectors) {
+      const listeners = collector.getListeners(node);
+
+      if (!listeners) {
+        continue;
+      }
 
-  // An object without a valid handleEvent is not a valid listener.
-  if (typeof obj === "object") {
-    const unwrapped = Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
-    if (typeof unwrapped.handleEvent === "function") {
-      return Cu.unwaiveXrays(unwrapped.handleEvent);
+      for (const listener of listeners) {
+        if (collector.normalizeListener) {
+          listener.normalizeListener = collector.normalizeListener;
+        }
+        this.processHandlerForEvent(listenerArray, listener, globalDO);
+      }
     }
-    return false;
-  } else if (typeof obj === "function") {
-    if (JQUERY_LIVE_REGEX.test(obj.toString())) {
-      return false;
-    }
-    return obj;
-  }
-  return false;
-}
+
+    dbg.removeDebuggee(globalDO);
 
-this.EventParsers = function EventParsers() {
-  if (this._eventParsers.size === 0) {
-    for (const parserObj of parsers) {
-      this.registerEventParser(parserObj);
-    }
-  }
-};
+    listenerArray.sort((a, b) => {
+      return a.type.localeCompare(b.type);
+    });
 
-exports.EventParsers = EventParsers;
-
-EventParsers.prototype = {
-  // NOTE: This is shared amongst all instances.
-  _eventParsers: new Map(),
-
-  get parsers() {
-    return this._eventParsers;
-  },
+    return listenerArray;
+  }
 
   /**
-   * Register a new event parser to be used in the processing of event info.
-   *
-   * @param {Object} parserObj
-   *        Each parser must contain the following properties:
-   *        - parser, which must take the following form:
-   *   {
-   *     id {String}: "jQuery events",          // Unique id.
-   *     getListeners: function(node) { },      // Function that takes a node
-   *                                            // and returns an array of
-   *                                            // eventInfo objects (see
-   *                                            // below).
+   * Process an event listener.
    *
-   *     hasListeners: function(node) { },      // Optional function that takes
-   *                                            // a node and returns a boolean
-   *                                            // indicating whether a node has
-   *                                            // listeners attached.
-   *
-   *     normalizeListener:
-   *        function(fnDO, listener) { },       // Optional function that takes
-   *                                            // a Debugger.Object instance
-   *                                            // and an optional listener
-   *                                            // object. Both objects can be
-   *                                            // manipulated to display the
-   *                                            // correct information in the
-   *                                            // event bubble.
-   *   }
+   * @param  {Array} listenerArray
+   *         listenerArray contains all event objects that we have gathered
+   *         so far.
+   * @param  {Object} eventInfo
+   *         See event-parsers.js.registerEventParser() for a description of the
+   *         eventInfo object.
    *
-   * An eventInfo object should take the following form:
-   *   {
-   *     type {String}:      "click",
-   *     handler {Function}: event handler,
-   *     tags {String}:      "jQuery,Live", // These tags will be displayed as
-   *                                        // attributes in the events popup.
-   *     hide: {               // Hide or show fields:
-   *       debugger: false,    // Debugger icon
-   *       type: false,        // Event type e.g. click
-   *       filename: false,    // Filename
-   *       capturing: false,   // Capturing
-   *       dom0: false         // DOM 0
-   *     },
-   *
-   *     override: {                        // The following can be overridden:
-   *       handler: someEventHandler,
-   *       type: "click",
-   *       origin: "http://www.mozilla.com",
-   *       searchString: 'onclick="doSomething()"',
-   *       dom0: true,
-   *       capturing: true
-   *     }
-   *   }
+   * @return {Array}
+   *         An array of objects where a typical object looks like this:
+   *           {
+   *             type: "click",
+   *             handler: function() { doSomething() },
+   *             origin: "http://www.mozilla.com",
+   *             tags: tags,
+   *             DOM0: true,
+   *             capturing: true,
+   *             hide: {
+   *               DOM0: true
+   *             },
+   *             native: false
+   *           }
    */
-  registerEventParser: function(parserObj) {
-    const parserId = parserObj.id;
+  processHandlerForEvent(listenerArray, listener, globalDO) {
+    const { capturing, handler } = listener;
+    let listenerDO = globalDO.makeDebuggeeValue(handler);
+
+    const { normalizeListener } = listener;
+
+    if (normalizeListener) {
+      listenerDO = normalizeListener(listenerDO, listener);
+    }
 
-    if (!parserId) {
-      throw new Error("Cannot register new event parser with id " + parserId);
+    const hide = listener.hide || {};
+    const override = listener.override || {};
+    const tags = listener.tags || "";
+    const type = listener.type || "";
+    let dom0 = false;
+    let functionSource = handler.toString();
+    let line = 0;
+    let native = false;
+    let url = "";
+
+    // If the listener is an object with a 'handleEvent' method, use that.
+    if (listenerDO.class === "Object" || /^XUL\w*Element$/.test(listenerDO.class)) {
+      let desc;
+
+      while (!desc && listenerDO) {
+        desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
+        listenerDO = listenerDO.proto;
+      }
+
+      if (desc && desc.value) {
+        listenerDO = desc.value;
+      }
     }
-    if (this._eventParsers.has(parserId)) {
-      throw new Error("Duplicate event parser id " + parserId);
+
+    // If the listener is bound to a different context then we need to switch
+    // to the bound function.
+    if (listenerDO.isBoundFunction) {
+      listenerDO = listenerDO.boundTargetFunction;
     }
 
-    this._eventParsers.set(parserId, {
-      getListeners: parserObj.getListeners,
-      hasListeners: parserObj.hasListeners,
-      normalizeListener: parserObj.normalizeListener,
-    });
-  },
+    const { isArrowFunction, name, script, parameterNames } = listenerDO;
+
+    if (script) {
+      const scriptSource = script.source.text;
+
+      // Scripts are provided via script tags. If it wasn't provided by a
+      // script tag it must be a DOM0 event.
+      if (script.source.element) {
+        dom0 = script.source.element.class !== "HTMLScriptElement";
+      } else {
+        dom0 = false;
+      }
+
+      line = script.startLine;
+      url = script.url;
+
+      // Checking for the string "[native code]" is the only way at this point
+      // to check for native code. Even if this provides a false positive then
+      // grabbing the source code a second time is harmless.
+      if (functionSource === "[object Object]" ||
+          functionSource === "[object XULElement]" ||
+          functionSource.includes("[native code]")) {
+        functionSource =
+          scriptSource.substr(script.sourceStart, script.sourceLength);
+
+        // At this point the script looks like this:
+        // () { ... }
+        // We prefix this with "function" if it is not a fat arrow function.
+        if (!isArrowFunction) {
+          functionSource = "function " + functionSource;
+        }
+      }
+    } else {
+      // If the listener is a native one (provided by C++ code) then we have no
+      // access to the script. We use the native flag to prevent showing the
+      // debugger button because the script is not available.
+      native = true;
+    }
 
-  /**
-   * Removes parser that matches a given parserId.
-   *
-   * @param {String} parserId
-   *        id of the event parser to unregister.
-   */
-  unregisterEventParser: function(parserId) {
-    this._eventParsers.delete(parserId);
-  },
+    // Arrow function text always contains the parameters. Function
+    // parameters are often missing e.g. if Array.sort is used as a handler.
+    // If they are missing we provide the parameters ourselves.
+    if (parameterNames && parameterNames.length > 0) {
+      const prefix = "function " + name + "()";
+      const paramString = parameterNames.join(", ");
+
+      if (functionSource.startsWith(prefix)) {
+        functionSource = functionSource.substr(prefix.length);
+
+        functionSource = `function ${name} (${paramString})${functionSource}`;
+      }
+    }
+
+    // If the listener is native code we display the filename "[native code]."
+    // This is the official string and should *not* be translated.
+    let origin;
+    if (native) {
+      origin = "[native code]";
+    } else {
+      origin = url + ((dom0 || line === 0) ? "" : ":" + line);
+    }
 
-  /**
-   * Tidy up parsers.
-   */
-  destroy: function() {
-    for (const [id] of this._eventParsers) {
-      this.unregisterEventParser(id, true);
+    const eventObj = {
+      type: override.type || type,
+      handler: override.handler || functionSource.trim(),
+      origin: override.origin || origin,
+      tags: override.tags || tags,
+      DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
+      capturing: typeof override.capturing !== "undefined" ?
+                        override.capturing : capturing,
+      hide: typeof override.hide !== "undefined" ? override.hide : hide,
+      native,
+    };
+
+    // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
+    // generated dynamically from e.g. an onclick="" attribute so the script
+    // doesn't actually exist.
+    if (native || dom0) {
+      eventObj.hide.debugger = true;
     }
-  },
-};
+
+    listenerArray.push(eventObj);
+  }
+}
+
+exports.EventCollector = EventCollector;
--- a/devtools/server/actors/inspector/moz.build
+++ b/devtools/server/actors/inspector/moz.build
@@ -3,17 +3,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
   'css-logic.js',
   'custom-element-watcher.js',
   'document-walker.js',
-  'event-parsers.js',
+  'event-collector.js',
   'inspector.js',
   'node.js',
   'utils.js',
   'walker.js',
 )
 
 with Files('**'):
     BUG_COMPONENT = ('DevTools', 'Inspector')
--- a/devtools/server/actors/inspector/node.js
+++ b/devtools/server/actors/inspector/node.js
@@ -24,17 +24,17 @@ loader.lazyRequireGetter(this, "isShadow
 loader.lazyRequireGetter(this, "isShadowRoot", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "getShadowRootMode", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
 
 loader.lazyRequireGetter(this, "InspectorActorUtils", "devtools/server/actors/inspector/utils");
 loader.lazyRequireGetter(this, "LongStringActor", "devtools/server/actors/string", true);
 loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
 loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "EventParsers", "devtools/server/actors/inspector/event-parsers", true);
+loader.lazyRequireGetter(this, "EventCollector", "devtools/server/actors/inspector/event-collector", true);
 loader.lazyRequireGetter(this, "DocumentWalker", "devtools/server/actors/inspector/document-walker", true);
 loader.lazyRequireGetter(this, "scrollbarTreeWalkerFilter", "devtools/server/actors/inspector/utils", true);
 
 const SUBGRID_ENABLED =
   Services.prefs.getBoolPref("layout.css.grid-template-subgrid-value.enabled");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-within"];
 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
@@ -43,17 +43,17 @@ const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20
 /**
  * Server side of the node actor.
  */
 const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
   initialize: function(walker, node) {
     protocol.Actor.prototype.initialize.call(this, null);
     this.walker = walker;
     this.rawNode = node;
-    this._eventParsers = new EventParsers().parsers;
+    this._eventCollector = new EventCollector();
 
     // Store the original display type and scrollable state and whether or not the node is
     // displayed to track changes when reflows occur.
     this.currentDisplayType = this.displayType;
     this.wasDisplayed = this.isDisplayed;
     this.wasScrollable = this.isScrollable;
   },
 
@@ -87,16 +87,18 @@ const NodeActor = protocol.ActorClassWit
 
     if (this.slotchangeListener) {
       if (!InspectorActorUtils.isNodeDead(this)) {
         this.rawNode.removeEventListener("slotchange", this.slotchangeListener);
       }
       this.slotchangeListener = null;
     }
 
+    this._eventCollector.destroy();
+    this._eventCollector = null;
     this.rawNode = null;
     this.walker = null;
   },
 
   // Returns the JSON representation of this object over the wire.
   form: function() {
     const parentNode = this.walker.parentNode(this);
     const inlineTextChild = this.walker.inlineTextChild(this);
@@ -282,28 +284,17 @@ const NodeActor = protocol.ActorClassWit
   },
 
   /**
    * Are there event listeners that are listening on this node? This method
    * uses all parsers registered via event-parsers.js.registerEventParser() to
    * check if there are any event listeners.
    */
   get _hasEventListeners() {
-    const parsers = this._eventParsers;
-    for (const [, {hasListeners}] of parsers) {
-      try {
-        if (hasListeners && hasListeners(this.rawNode)) {
-          return true;
-        }
-      } catch (e) {
-        // An object attached to the node looked like a listener but wasn't...
-        // do nothing.
-      }
-    }
-    return false;
+    return this._eventCollector.hasEventListeners(this.rawNode);
   },
 
   writeAttrs: function() {
     if (!this.rawNode.attributes) {
       return undefined;
     }
 
     return [...this.rawNode.attributes].map(attr => {
@@ -327,46 +318,17 @@ const NodeActor = protocol.ActorClassWit
 
   /**
    * Gets event listeners and adds their information to the events array.
    *
    * @param  {Node} node
    *         Node for which we are to get listeners.
    */
   getEventListeners: function(node) {
-    const parsers = this._eventParsers;
-    const dbg = this.parent().targetActor.makeDebugger();
-    const listenerArray = [];
-
-    for (const [, {getListeners, normalizeListener}] of parsers) {
-      try {
-        const listeners = getListeners(node);
-
-        if (!listeners) {
-          continue;
-        }
-
-        for (const listener of listeners) {
-          if (normalizeListener) {
-            listener.normalizeListener = normalizeListener;
-          }
-
-          this.processHandlerForEvent(node, listenerArray, dbg, listener);
-        }
-      } catch (e) {
-        // An object attached to the node looked like a listener but wasn't...
-        // do nothing.
-      }
-    }
-
-    listenerArray.sort((a, b) => {
-      return a.type.localeCompare(b.type);
-    });
-
-    return listenerArray;
+    return this._eventCollector.getEventListeners(node);
   },
 
   /**
    * Retrieve the script location of the custom element definition for this node, when
    * relevant. To be linked to a custom element definition
    */
   getCustomElementLocation: function() {
     // Get a reference to the custom element definition function.
@@ -390,175 +352,16 @@ const NodeActor = protocol.ActorClassWit
 
     return {
       url: customElementDO.script.url,
       line: customElementDO.script.startLine,
     };
   },
 
   /**
-   * Process a handler
-   *
-   * @param  {Node} node
-   *         The node for which we want information.
-   * @param  {Array} listenerArray
-   *         listenerArray contains all event objects that we have gathered
-   *         so far.
-   * @param  {Debugger} dbg
-   *         JSDebugger instance.
-   * @param  {Object} eventInfo
-   *         See event-parsers.js.registerEventParser() for a description of the
-   *         eventInfo object.
-   *
-   * @return {Array}
-   *         An array of objects where a typical object looks like this:
-   *           {
-   *             type: "click",
-   *             handler: function() { doSomething() },
-   *             origin: "http://www.mozilla.com",
-   *             searchString: 'onclick="doSomething()"',
-   *             tags: tags,
-   *             DOM0: true,
-   *             capturing: true,
-   *             hide: {
-   *               DOM0: true
-   *             },
-   *             native: false
-   *           }
-   */
-  processHandlerForEvent: function(node, listenerArray, dbg, listener) {
-    const { handler } = listener;
-    const global = Cu.getGlobalForObject(handler);
-    const globalDO = dbg.addDebuggee(global);
-    let listenerDO = globalDO.makeDebuggeeValue(handler);
-
-    const { normalizeListener } = listener;
-
-    if (normalizeListener) {
-      listenerDO = normalizeListener(listenerDO, listener);
-    }
-
-    const { capturing } = listener;
-    let dom0 = false;
-    let functionSource = handler.toString();
-    const hide = listener.hide || {};
-    let line = 0;
-    let native = false;
-    const override = listener.override || {};
-    const tags = listener.tags || "";
-    const type = listener.type || "";
-    let url = "";
-
-    // If the listener is an object with a 'handleEvent' method, use that.
-    if (listenerDO.class === "Object" || /^XUL\w*Element$/.test(listenerDO.class)) {
-      let desc;
-
-      while (!desc && listenerDO) {
-        desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
-        listenerDO = listenerDO.proto;
-      }
-
-      if (desc && desc.value) {
-        listenerDO = desc.value;
-      }
-    }
-
-    // If the listener is bound to a different context then we need to switch
-    // to the bound function.
-    if (listenerDO.isBoundFunction) {
-      listenerDO = listenerDO.boundTargetFunction;
-    }
-
-    const { isArrowFunction, name, script, parameterNames } = listenerDO;
-
-    if (script) {
-      const scriptSource = script.source.text;
-
-      // Scripts are provided via script tags. If it wasn't provided by a
-      // script tag it must be a DOM0 event.
-      if (script.source.element) {
-        dom0 = script.source.element.class !== "HTMLScriptElement";
-      } else {
-        dom0 = false;
-      }
-
-      line = script.startLine;
-      url = script.url;
-
-      // Checking for the string "[native code]" is the only way at this point
-      // to check for native code. Even if this provides a false positive then
-      // grabbing the source code a second time is harmless.
-      if (functionSource === "[object Object]" ||
-          functionSource === "[object XULElement]" ||
-          functionSource.includes("[native code]")) {
-        functionSource =
-          scriptSource.substr(script.sourceStart, script.sourceLength);
-
-        // At this point the script looks like this:
-        // () { ... }
-        // We prefix this with "function" if it is not a fat arrow function.
-        if (!isArrowFunction) {
-          functionSource = "function " + functionSource;
-        }
-      }
-    } else {
-      // If the listener is a native one (provided by C++ code) then we have no
-      // access to the script. We use the native flag to prevent showing the
-      // debugger button because the script is not available.
-      native = true;
-    }
-
-    // Fat arrow function text always contains the parameters. Function
-    // parameters are often missing e.g. if Array.sort is used as a handler.
-    // If they are missing we provide the parameters ourselves.
-    if (parameterNames && parameterNames.length > 0) {
-      const prefix = "function " + name + "()";
-      const paramString = parameterNames.join(", ");
-
-      if (functionSource.startsWith(prefix)) {
-        functionSource = functionSource.substr(prefix.length);
-
-        functionSource = `function ${name} (${paramString})${functionSource}`;
-      }
-    }
-
-    // If the listener is native code we display the filename "[native code]."
-    // This is the official string and should *not* be translated.
-    let origin;
-    if (native) {
-      origin = "[native code]";
-    } else {
-      origin = url + ((dom0 || line === 0) ? "" : ":" + line);
-    }
-
-    const eventObj = {
-      type: override.type || type,
-      handler: override.handler || functionSource.trim(),
-      origin: override.origin || origin,
-      tags: override.tags || tags,
-      DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
-      capturing: typeof override.capturing !== "undefined" ?
-                 override.capturing : capturing,
-      hide: typeof override.hide !== "undefined" ? override.hide : hide,
-      native,
-    };
-
-    // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
-    // generated dynamically from e.g. an onclick="" attribute so the script
-    // doesn't actually exist.
-    if (native || dom0) {
-      eventObj.hide.debugger = true;
-    }
-
-    listenerArray.push(eventObj);
-
-    dbg.removeDebuggee(globalDO);
-  },
-
-  /**
    * Returns a LongStringActor with the node's value.
    */
   getNodeValue: function() {
     return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
   },
 
   /**
    * Set the node's value to a given string.
@@ -627,28 +430,17 @@ const NodeActor = protocol.ActorClassWit
       };
     });
   },
 
   /**
    * Get all event listeners that are listening on this node.
    */
   getEventListenerInfo: function() {
-    const node = this.rawNode;
-
-    if (this.rawNode.nodeName.toLowerCase() === "html") {
-      const winListeners = this.getEventListeners(node.ownerGlobal) || [];
-      const docElementListeners = this.getEventListeners(node) || [];
-      const docListeners = this.getEventListeners(node.parentNode) || [];
-
-      return [...winListeners, ...docElementListeners, ...docListeners].sort((a, b) => {
-        return a.type.localeCompare(b.type);
-      });
-    }
-    return this.getEventListeners(node);
+    return this.getEventListeners(this.rawNode);
   },
 
   /**
    * Modify a node's attributes.  Passed an array of modifications
    * similar in format to "attributes" mutations.
    * {
    *   attributeName: <string>
    *   attributeNamespace: <optional string>
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -2340,53 +2340,40 @@ nsresult Element::SetAttr(int32_t aNames
   // above.
 
   NS_ENSURE_ARG_POINTER(aName);
   NS_ASSERTION(aNamespaceID != kNameSpaceID_Unknown,
                "Don't call SetAttr with unknown namespace");
 
   uint8_t modType;
   bool hasListeners;
-  // We don't want to spend time preparsing class attributes if the value is not
-  // changing, so just init our nsAttrValueOrString with aValue for the
-  // OnlyNotifySameValueSet call.
   nsAttrValueOrString value(aValue);
   nsAttrValue oldValue;
   bool oldValueSet;
 
   if (OnlyNotifySameValueSet(aNamespaceID, aName, aPrefix, value, aNotify,
                              oldValue, &modType, &hasListeners, &oldValueSet)) {
     return OnAttrSetButNotChanged(aNamespaceID, aName, value, aNotify);
   }
 
-  nsAttrValue attrValue;
-  nsAttrValue* preparsedAttrValue;
-  if (aNamespaceID == kNameSpaceID_None && aName == nsGkAtoms::_class) {
-    attrValue.ParseAtomArray(aValue);
-    value.ResetToAttrValue(attrValue);
-    preparsedAttrValue = &attrValue;
-  } else {
-    preparsedAttrValue = nullptr;
-  }
-
   if (aNotify) {
-    nsNodeUtils::AttributeWillChange(this, aNamespaceID, aName, modType,
-                                     preparsedAttrValue);
+    nsNodeUtils::AttributeWillChange(this, aNamespaceID, aName, modType);
   }
 
   // Hold a script blocker while calling ParseAttribute since that can call
   // out to id-observers
   Document* document = GetComposedDoc();
   mozAutoDocUpdate updateBatch(document, aNotify);
 
   nsresult rv = BeforeSetAttr(aNamespaceID, aName, &value, aNotify);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (!preparsedAttrValue && !ParseAttribute(aNamespaceID, aName, aValue,
-                                             aSubjectPrincipal, attrValue)) {
+  nsAttrValue attrValue;
+  if (!ParseAttribute(aNamespaceID, aName, aValue, aSubjectPrincipal,
+                      attrValue)) {
     attrValue.SetTo(aValue);
   }
 
   PreIdMaybeChange(aNamespaceID, aName, &value);
 
   return SetAttrAndNotify(aNamespaceID, aName, aPrefix,
                           oldValueSet ? &oldValue : nullptr, attrValue,
                           aSubjectPrincipal, modType, hasListeners, aNotify,
@@ -2409,18 +2396,17 @@ nsresult Element::SetParsedAttr(int32_t 
   bool oldValueSet;
 
   if (OnlyNotifySameValueSet(aNamespaceID, aName, aPrefix, value, aNotify,
                              oldValue, &modType, &hasListeners, &oldValueSet)) {
     return OnAttrSetButNotChanged(aNamespaceID, aName, value, aNotify);
   }
 
   if (aNotify) {
-    nsNodeUtils::AttributeWillChange(this, aNamespaceID, aName, modType,
-                                     &aParsedValue);
+    nsNodeUtils::AttributeWillChange(this, aNamespaceID, aName, modType);
   }
 
   nsresult rv = BeforeSetAttr(aNamespaceID, aName, &value, aNotify);
   NS_ENSURE_SUCCESS(rv, rv);
 
   PreIdMaybeChange(aNamespaceID, aName, &value);
 
   Document* document = GetComposedDoc();
@@ -2577,19 +2563,21 @@ bool Element::ParseAttribute(int32_t aNa
                              nsIPrincipal* aMaybeScriptedPrincipal,
                              nsAttrValue& aResult) {
   if (aAttribute == nsGkAtoms::lang) {
     aResult.ParseAtom(aValue);
     return true;
   }
 
   if (aNamespaceID == kNameSpaceID_None) {
-    MOZ_ASSERT(aAttribute != nsGkAtoms::_class,
-               "The class attribute should be preparsed and therefore should "
-               "never be passed to Element::ParseAttribute");
+    if (aAttribute == nsGkAtoms::_class) {
+      aResult.ParseAtomArray(aValue);
+      return true;
+    }
+
     if (aAttribute == nsGkAtoms::id) {
       // Store id as an atom.  id="" means that the element has no id,
       // not that it has an emptystring as the id.
       if (aValue.IsEmpty()) {
         return false;
       }
       aResult.ParseAtom(aValue);
       return true;
@@ -2713,17 +2701,17 @@ nsresult Element::UnsetAttr(int32_t aNam
     return NS_OK;
   }
 
   Document* document = GetComposedDoc();
   mozAutoDocUpdate updateBatch(document, aNotify);
 
   if (aNotify) {
     nsNodeUtils::AttributeWillChange(this, aNameSpaceID, aName,
-                                     MutationEvent_Binding::REMOVAL, nullptr);
+                                     MutationEvent_Binding::REMOVAL);
   }
 
   nsresult rv = BeforeSetAttr(aNameSpaceID, aName, nullptr, aNotify);
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool hasMutationListeners =
       aNotify && nsContentUtils::HasMutationListeners(
                      this, NS_EVENT_BITS_MUTATION_ATTRMODIFIED, this);
--- a/dom/base/nsDOMMutationObserver.cpp
+++ b/dom/base/nsDOMMutationObserver.cpp
@@ -134,18 +134,17 @@ void nsMutationReceiver::NativeAnonymous
     m->mAddedNodes = new nsSimpleContentList(parent);
     m->mAddedNodes->AppendElement(aContent);
   }
 }
 
 void nsMutationReceiver::AttributeWillChange(mozilla::dom::Element* aElement,
                                              int32_t aNameSpaceID,
                                              nsAtom* aAttribute,
-                                             int32_t aModType,
-                                             const nsAttrValue* aNewValue) {
+                                             int32_t aModType) {
   if (nsAutoMutationBatch::IsBatching() ||
       !ObservesAttr(RegisterTarget(), aElement, aNameSpaceID, aAttribute)) {
     return;
   }
 
   nsDOMMutationRecord* m = Observer()->CurrentRecord(nsGkAtoms::attributes);
 
   NS_ASSERTION(!m->mTarget || m->mTarget == aElement, "Wrong target!");
--- a/dom/base/nsDOMMutationObserver.h
+++ b/dom/base/nsDOMMutationObserver.h
@@ -354,18 +354,17 @@ class nsMutationReceiver : public nsMuta
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
   NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
 
   virtual void AttributeSetToCurrentValue(mozilla::dom::Element* aElement,
                                           int32_t aNameSpaceID,
                                           nsAtom* aAttribute) override {
     // We can reuse AttributeWillChange implementation.
     AttributeWillChange(aElement, aNameSpaceID, aAttribute,
-                        mozilla::dom::MutationEvent_Binding::MODIFICATION,
-                        nullptr);
+                        mozilla::dom::MutationEvent_Binding::MODIFICATION);
   }
 
  protected:
   nsMutationReceiver(nsINode* aTarget, nsDOMMutationObserver* aObserver);
 
   nsMutationReceiver(nsINode* aRegisterTarget, nsMutationReceiverBase* aParent)
       : nsMutationReceiverBase(aRegisterTarget, aParent) {
     NS_ASSERTION(!static_cast<nsMutationReceiver*>(aParent)->GetParent(),
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -809,22 +809,26 @@ class PromiseDocumentFlushedResolver fin
  public:
   PromiseDocumentFlushedResolver(Promise* aPromise,
                                  PromiseDocumentFlushedCallback& aCallback)
       : mPromise(aPromise), mCallback(&aCallback) {}
 
   virtual ~PromiseDocumentFlushedResolver() = default;
 
   void Call() {
+    nsMutationGuard guard;
     ErrorResult error;
     JS::Rooted<JS::Value> returnVal(RootingCx());
     mCallback->Call(&returnVal, error);
 
     if (error.Failed()) {
       mPromise->MaybeReject(error);
+    } else if (guard.Mutated(0)) {
+      // Something within the callback mutated the DOM.
+      mPromise->MaybeReject(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
     } else {
       mPromise->MaybeResolve(returnVal);
     }
   }
 
   void Cancel() { mPromise->MaybeReject(NS_ERROR_ABORT); }
 
   RefPtr<Promise> mPromise;
--- a/dom/base/nsIMutationObserver.h
+++ b/dom/base/nsIMutationObserver.h
@@ -159,18 +159,17 @@ class nsIMutationObserver : public nsISu
    * @note Callers of this method might not hold a strong reference to the
    *       observer.  The observer is responsible for making sure it stays
    *       alive for the duration of the call as needed.  The observer may
    *       assume that this call will happen when there are script blockers on
    *       the stack.
    */
   virtual void AttributeWillChange(mozilla::dom::Element* aElement,
                                    int32_t aNameSpaceID, nsAtom* aAttribute,
-                                   int32_t aModType,
-                                   const nsAttrValue* aNewValue) = 0;
+                                   int32_t aModType) = 0;
 
   /**
    * Notification that an attribute of an element has changed.
    *
    * @param aElement     The element whose attribute changed
    * @param aNameSpaceID The namespace id of the changed attribute
    * @param aAttribute   The name of the changed attribute
    * @param aModType     Whether or not the attribute was added, changed, or
@@ -303,18 +302,17 @@ NS_DEFINE_STATIC_IID_ACCESSOR(nsIMutatio
 
 #define NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED \
   virtual void CharacterDataChanged(                     \
       nsIContent* aContent, const CharacterDataChangeInfo& aInfo) override;
 
 #define NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTEWILLCHANGE                      \
   virtual void AttributeWillChange(mozilla::dom::Element* aElement,          \
                                    int32_t aNameSpaceID, nsAtom* aAttribute, \
-                                   int32_t aModType,                         \
-                                   const nsAttrValue* aNewValue) override;
+                                   int32_t aModType) override;
 
 #define NS_DECL_NSIMUTATIONOBSERVER_NATIVEANONYMOUSCHILDLISTCHANGE  \
   virtual void NativeAnonymousChildListChange(nsIContent* aContent, \
                                               bool aIsRemove) override;
 
 #define NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED                      \
   virtual void AttributeChanged(mozilla::dom::Element* aElement,          \
                                 int32_t aNameSpaceID, nsAtom* aAttribute, \
@@ -352,19 +350,19 @@ NS_DEFINE_STATIC_IID_ACCESSOR(nsIMutatio
 #define NS_IMPL_NSIMUTATIONOBSERVER_CORE_STUB(_class) \
   void _class::NodeWillBeDestroyed(const nsINode* aNode) {}
 
 #define NS_IMPL_NSIMUTATIONOBSERVER_CONTENT(_class)                          \
   void _class::CharacterDataWillChange(                                      \
       nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {}         \
   void _class::CharacterDataChanged(nsIContent* aContent,                    \
                                     const CharacterDataChangeInfo& aInfo) {} \
-  void _class::AttributeWillChange(                                          \
-      mozilla::dom::Element* aElement, int32_t aNameSpaceID,                 \
-      nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aNewValue) {} \
+  void _class::AttributeWillChange(mozilla::dom::Element* aElement,          \
+                                   int32_t aNameSpaceID, nsAtom* aAttribute, \
+                                   int32_t aModType) {}                      \
   void _class::NativeAnonymousChildListChange(nsIContent* aContent,          \
                                               bool aIsRemove) {}             \
   void _class::AttributeChanged(                                             \
       mozilla::dom::Element* aElement, int32_t aNameSpaceID,                 \
       nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aOldValue) {} \
   void _class::ContentAppended(nsIContent* aFirstNewContent) {}              \
   void _class::ContentInserted(nsIContent* aChild) {}                        \
   void _class::ContentRemoved(nsIContent* aChild,                            \
--- a/dom/base/nsNodeUtils.cpp
+++ b/dom/base/nsNodeUtils.cpp
@@ -136,23 +136,21 @@ void nsNodeUtils::CharacterDataWillChang
 void nsNodeUtils::CharacterDataChanged(nsIContent* aContent,
                                        const CharacterDataChangeInfo& aInfo) {
   Document* doc = aContent->OwnerDoc();
   IMPL_MUTATION_NOTIFICATION(CharacterDataChanged, aContent, (aContent, aInfo),
                              IsRemoveNotification::No);
 }
 
 void nsNodeUtils::AttributeWillChange(Element* aElement, int32_t aNameSpaceID,
-                                      nsAtom* aAttribute, int32_t aModType,
-                                      const nsAttrValue* aNewValue) {
+                                      nsAtom* aAttribute, int32_t aModType) {
   Document* doc = aElement->OwnerDoc();
-  IMPL_MUTATION_NOTIFICATION(
-      AttributeWillChange, aElement,
-      (aElement, aNameSpaceID, aAttribute, aModType, aNewValue),
-      IsRemoveNotification::No);
+  IMPL_MUTATION_NOTIFICATION(AttributeWillChange, aElement,
+                             (aElement, aNameSpaceID, aAttribute, aModType),
+                             IsRemoveNotification::No);
 }
 
 void nsNodeUtils::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
                                    nsAtom* aAttribute, int32_t aModType,
                                    const nsAttrValue* aOldValue) {
   Document* doc = aElement->OwnerDoc();
   IMPL_MUTATION_NOTIFICATION(
       AttributeChanged, aElement,
--- a/dom/base/nsNodeUtils.h
+++ b/dom/base/nsNodeUtils.h
@@ -52,18 +52,17 @@ class nsNodeUtils {
    * @param aAttribute    Local-name of changing attribute
    * @param aModType      Type of change (add/change/removal)
    * @param aNewValue     The parsed new value, but only if BeforeSetAttr
    *                      preparsed it!!!
    * @see nsIMutationObserver::AttributeWillChange
    */
   static void AttributeWillChange(mozilla::dom::Element* aElement,
                                   int32_t aNameSpaceID, nsAtom* aAttribute,
-                                  int32_t aModType,
-                                  const nsAttrValue* aNewValue);
+                                  int32_t aModType);
 
   /**
    * Send AttributeChanged notifications to nsIMutationObservers.
    * @param aElement      Element whose data changed
    * @param aNameSpaceID  Namespace of changed attribute
    * @param aAttribute    Local-name of changed attribute
    * @param aModType      Type of change (add/change/removal)
    * @param aOldValue     If the old value was StoresOwnData() (or absent),
--- a/dom/base/nsStyledElement.cpp
+++ b/dom/base/nsStyledElement.cpp
@@ -92,17 +92,17 @@ void nsStyledElement::InlineStyleDeclara
       modification = HasAttr(kNameSpaceID_None, nsGkAtoms::style);
     }
   }
 
   aData.mModType =
       modification ? static_cast<uint8_t>(MutationEvent_Binding::MODIFICATION)
                    : static_cast<uint8_t>(MutationEvent_Binding::ADDITION);
   nsNodeUtils::AttributeWillChange(this, kNameSpaceID_None, nsGkAtoms::style,