Merge mozilla-inbound to mozilla-central. a=merge
authorDaniel Varga <dvarga@mozilla.com>
Sat, 02 Feb 2019 11:44:15 +0200
changeset 456555 d8cebb3b46cfd216ab60e58588e585f28750a5f3
parent 456532 cb5ed33ae30da0deceb984932ec69a1a02b0e88b (current diff)
parent 456554 037220dfa6cf167aaf3fe3c49e560c77cef80dae (diff)
child 456556 03c5c501ca198591712dd507f764b4fb3e9ac714
push id77334
push userdvarga@mozilla.com
push dateSat, 02 Feb 2019 09:48:53 +0000
treeherderautoland@03c5c501ca19 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone67.0a1
first release with
nightly linux32
d8cebb3b46cf / 67.0a1 / 20190202094451 / files
nightly linux64
d8cebb3b46cf / 67.0a1 / 20190202094451 / files
nightly mac
d8cebb3b46cf / 67.0a1 / 20190202094451 / files
nightly win32
d8cebb3b46cf / 67.0a1 / 20190202094451 / files
nightly win64
d8cebb3b46cf / 67.0a1 / 20190202094451 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-inbound to mozilla-central. a=merge
toolkit/components/passwordmgr/moz.build
toolkit/components/passwordmgr/test/mochitest.ini
toolkit/components/passwordmgr/test/mochitest/mochitest.ini
toolkit/components/passwordmgr/test/mochitest/test_xhr.html
toolkit/components/passwordmgr/test/mochitest/test_xml_load.html
toolkit/components/passwordmgr/test/prompt_common.js
toolkit/components/passwordmgr/test/pwmgr_common.js
toolkit/components/passwordmgr/test/test_xhr.html
toolkit/components/prompts/test/chromeScript.js
toolkit/components/prompts/test/prompt_common.js
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -724,17 +724,17 @@ html|input.urlbar-input {
   display: none;
 }
 
 #PopupAutoComplete[firstresultstyle="insecureWarning"] {
   min-width: 200px;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
-  -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem-insecure-field");
+  -moz-binding: none;
   height: auto;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-site-icon {
   margin-inline-start: 0;
   display: initial;
 }
 
--- a/browser/base/content/test/performance/browser_urlbar_keyed_search.js
+++ b/browser/base/content/test/performance/browser_urlbar_keyed_search.js
@@ -32,30 +32,30 @@ if (AppConstants.DEBUG ||
       "openPopup@chrome://global/content/bindings/autocomplete.xml",
       "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
     ],
   });
 }
 EXPECTED_REFLOWS_FIRST_OPEN.push(
   {
     stack: [
-      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
-      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
-      "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_handleOverflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "handleOverUnderflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "_reuseAcItem@chrome://global/content/elements/autocomplete-richlistitem.js",
       "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
       "_invalidate@chrome://global/content/bindings/autocomplete.xml",
       "invalidate@chrome://global/content/bindings/autocomplete.xml",
     ],
     maxCount: 60, // This number should only ever go down - never up.
   },
 
   {
     stack: [
-      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
-      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
+      "_handleOverflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "handleOverUnderflow@chrome://global/content/elements/autocomplete-richlistitem.js",
       "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
       "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
       "openPopup@chrome://global/content/bindings/autocomplete.xml",
       "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
     ],
     maxCount: 6, // This number should only ever go down - never up.
   },
 
@@ -86,18 +86,18 @@ if (AppConstants.RELEASE_OR_BETA) {
       "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
       "_onOverflow@chrome://global/content/bindings/autocomplete.xml",
       "onoverflow@chrome://browser/content/browser.xul",
     ],
     maxCount: 6,
   },
   {
     stack: [
-      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
-      "_adjustAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_handleOverflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "_adjustAcItem@chrome://global/content/elements/autocomplete-richlistitem.js",
       "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
       "_invalidate@chrome://global/content/bindings/autocomplete.xml",
       "invalidate@chrome://global/content/bindings/autocomplete.xml",
     ],
     maxCount: 12,
   });
 }
 
--- a/browser/base/content/test/performance/browser_urlbar_search.js
+++ b/browser/base/content/test/performance/browser_urlbar_search.js
@@ -33,30 +33,30 @@ if (AppConstants.DEBUG ||
       "openPopup@chrome://global/content/bindings/autocomplete.xml",
       "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
     ],
   });
 }
 EXPECTED_REFLOWS_FIRST_OPEN.push(
   {
     stack: [
-      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
-      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
-      "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_handleOverflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "handleOverUnderflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "_reuseAcItem@chrome://global/content/elements/autocomplete-richlistitem.js",
       "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
       "_invalidate@chrome://global/content/bindings/autocomplete.xml",
       "invalidate@chrome://global/content/bindings/autocomplete.xml",
     ],
     maxCount: 36, // This number should only ever go down - never up.
   },
 
   {
     stack: [
-      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
-      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
+      "_handleOverflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "handleOverUnderflow@chrome://global/content/elements/autocomplete-richlistitem.js",
       "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
       "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
       "openPopup@chrome://global/content/bindings/autocomplete.xml",
       "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
     ],
     maxCount: 6, // This number should only ever go down - never up.
   },
 
@@ -70,19 +70,19 @@ EXPECTED_REFLOWS_FIRST_OPEN.push(
     ],
   }
 );
 
 /* These reflows happen everytime the awesomebar panel opens. */
 const EXPECTED_REFLOWS_SECOND_OPEN = [
   {
     stack: [
-      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
-      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
-      "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_handleOverflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "handleOverUnderflow@chrome://global/content/elements/autocomplete-richlistitem.js",
+      "_reuseAcItem@chrome://global/content/elements/autocomplete-richlistitem.js",
       "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
       "_invalidate@chrome://global/content/bindings/autocomplete.xml",
       "invalidate@chrome://global/content/bindings/autocomplete.xml",
     ],
     maxCount: 24, // This number should only ever go down - never up.
   },
 
   // Bug 1359989
--- a/browser/extensions/formautofill/test/mochitest/mochitest.ini
+++ b/browser/extensions/formautofill/test/mochitest/mochitest.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 support-files =
   ../../../../../toolkit/components/satchel/test/satchel_common.js
   ../../../../../toolkit/components/satchel/test/parent_utils.js
   formautofill_common.js
   formautofill_parent_utils.js
 
 [test_address_level_1_submission.html]
+[test_autofill_and_ordinal_forms.html]
 [test_autofocus_form.html]
 skip-if = verify
 [test_basic_autocomplete_form.html]
 skip-if = verify
 [test_form_changes.html]
 [test_formautofill_preview_highlight.html]
 skip-if = verify
 [test_multi_locale_CA_address_form.html]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test autofill submit</title>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="text/javascript" src="formautofill_common.js"></script>
+  <script type="text/javascript" src="satchel_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+  "given-name": "John",
+  "additional-name": "R",
+  "family-name": "Smith",
+  "organization": "Sesame Street",
+  "street-address": "123 Sesame Street.",
+  "tel": "+13453453456",
+  "country": "US",
+  "address-level1": "NY",
+}];
+
+initPopupListener();
+
+add_task(async function setupStorage() {
+  await addAddress(MOCK_STORAGE[0]);
+
+  await updateFormHistory([
+    {op: "add", fieldname: "username", value: "petya"},
+    {op: "add", fieldname: "current-password", value: "abrh#25_,K"},
+  ]);
+});
+
+add_task(async function check_switch_autofill_form_popup() {
+  await setInput("#tel", "");
+  synthesizeKey("KEY_ArrowDown");
+  await expectPopup();
+  checkMenuEntries(
+    [
+      `{"primary":"+13453453456","secondary":"123 Sesame Street."}`,
+      `{"primary":"","secondary":"","categories":["name","organization","address","tel"],"focusedCategory":"tel"}`,
+    ],
+    false
+  );
+
+  await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)");
+});
+
+add_task(async function check_switch_oridnal_form_popup() {
+  // We need an intentional wait here before switching form.
+  await sleep();
+  await setInput("#username", "");
+  synthesizeKey("KEY_ArrowDown");
+  await expectPopup();
+  checkMenuEntries(["petya"], false);
+
+  await testMenuEntry(0, "el instanceof MozElements.MozAutocompleteRichlistitem");
+});
+
+add_task(async function check_switch_autofill_form_popup_back() {
+  // We need an intentional wait here before switching form.
+  await sleep();
+  await setInput("#tel", "");
+  synthesizeKey("KEY_ArrowDown");
+  await expectPopup();
+  checkMenuEntries(
+    [
+      `{"primary":"+13453453456","secondary":"123 Sesame Street."}`,
+      `{"primary":"","secondary":"","categories":["name","organization","address","tel"],"focusedCategory":"tel"}`,
+    ],
+    false
+  );
+
+  await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)");
+});
+
+</script>
+
+<div>
+
+  <h2>Address form</h2>
+  <form class="alignedLabels">
+    <label>given-name: <input autocomplete="given-name" autofocus></label>
+    <label>additional-name: <input id="additional-name" autocomplete="additional-name"></label>
+    <label>family-name: <input autocomplete="family-name"></label>
+    <label>organization: <input autocomplete="organization"></label>
+    <label>street-address: <input autocomplete="street-address"></label>
+    <label>address-level1: <input autocomplete="address-level1"></label>
+    <label>postal-code: <input autocomplete="postal-code"></label>
+    <label>country: <input autocomplete="country"></label>
+    <label>country-name: <input autocomplete="country-name"></label>
+    <label>tel: <input id="tel" autocomplete="tel"></label>
+    <p>
+      <input type="submit" value="Submit">
+      <button type="reset">Reset</button>
+    </p>
+  </form>
+
+  <h2>Ordinal form</h2>
+  <form class="alignedLabels">
+    <label>username: <input id="username" autocomplete="username"></label>
+    <p><input type="submit" value="Username"></p>
+  </form>
+
+</div>
+</body>
+</html>
--- a/devtools/client/sourceeditor/editor.js
+++ b/devtools/client/sourceeditor/editor.js
@@ -1249,16 +1249,21 @@ Editor.prototype = {
    * Sets up autocompletion for the editor. Lazily imports the required
    * dependencies because they vary by editor mode.
    *
    * Autocompletion is special, because we don't want to automatically use
    * it just because it is preffed on (it still needs to be requested by the
    * editor), but we do want to always disable it if it is preffed off.
    */
   setupAutoCompletion: function() {
+    if (!this.config.autocomplete && !this.initializeAutoCompletion) {
+      // Do nothing since there is no autocomplete config and no autocompletion have
+      // been initialized.
+      return;
+    }
     // The autocomplete module will overwrite this.initializeAutoCompletion
     // with a mode specific autocompletion handler.
     if (!this.initializeAutoCompletion) {
       this.extend(require("./autocomplete"));
     }
 
     if (this.config.autocomplete && Services.prefs.getBoolPref(AUTOCOMPLETE)) {
       this.initializeAutoCompletion(this.config.autocompleteOpts);
--- a/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js
@@ -6,21 +6,30 @@
 // Test that autocomplete can be disabled.
 
 const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
 
 // Pref which decides if CSS autocompletion is enabled in Style Editor or not.
 const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
 
 add_task(async function() {
-  Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
   const { ui } = await openStyleEditorForURL(TESTCASE_URI);
   const editor = await ui.editors[0].getSourceEditor();
+  editor.sourceEditor.setOption("autocomplete", false);
 
   is(editor.sourceEditor.getOption("autocomplete"), false,
      "Autocompletion option does not exist");
   ok(!editor.sourceEditor.getAutocompletionPopup(),
      "Autocompletion popup does not exist");
 });
 
+add_task(async function() {
+  Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
+  const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+  const editor = await ui.editors[0].getSourceEditor();
+
+  is(editor.sourceEditor.getOption("autocomplete"), false,
+     "Autocompletion option does not exist");
+});
+
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
 });
--- a/dom/media/BufferReader.h
+++ b/dom/media/BufferReader.h
@@ -143,16 +143,17 @@ class MOZ_RAII BufferReader {
               ("%s: failure", __func__));
       return mozilla::Err(NS_ERROR_FAILURE);
     }
     return mozilla::BigEndian::readInt64(ptr);
   }
 
   const uint8_t* Read(size_t aCount) {
     if (aCount > mRemaining) {
+      mPtr += mRemaining;
       mRemaining = 0;
       return nullptr;
     }
     mRemaining -= aCount;
 
     const uint8_t* result = mPtr;
     mPtr += aCount;
 
--- a/dom/plugins/test/mochitest/mochitest.ini
+++ b/dom/plugins/test/mochitest/mochitest.ini
@@ -22,17 +22,16 @@ support-files =
   mixed_case_mime.sjs
   neverending.sjs
   npruntime_identifiers_subpage.html
   plugin-stream-referer.sjs
   plugin_window.html
   pluginstream.js
   post.sjs
   plugin-utils.js
-  !/toolkit/components/passwordmgr/test/authenticate.sjs
 
 [test_bug1028200-1.html]
 skip-if = !crashreporter
 [test_bug1028200-2.html]
 skip-if = !crashreporter
 [test_bug1028200-3.html]
 skip-if = !crashreporter
 [test_bug1028200-4.html]
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -2106,19 +2106,21 @@ void gfxPlatform::TransformPixel(const C
 void gfxPlatform::GetPlatformCMSOutputProfile(void*& mem, size_t& size) {
   mem = nullptr;
   size = 0;
 }
 
 void gfxPlatform::GetCMSOutputProfileData(void*& mem, size_t& size) {
   nsAutoCString fname;
   Preferences::GetCString("gfx.color_management.display_profile", fname);
+  mem = nullptr;
   if (!fname.IsEmpty()) {
     qcms_data_from_path(fname.get(), &mem, &size);
-  } else {
+  }
+  if (mem == nullptr) {
     gfxPlatform::GetPlatform()->GetPlatformCMSOutputProfile(mem, size);
   }
 }
 
 void gfxPlatform::CreateCMSOutputProfile() {
   if (!gCMSOutputProfile) {
     /* Determine if we're using the internal override to force sRGB as
        an output profile for reftests. See Bug 452125.
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/inlining/getelem-getter-bailout.js
@@ -0,0 +1,48 @@
+// Test bailouts in inlined jsop_getelem accesses.
+
+// Defined outside of the test functions to ensure they're recognised as
+// constants in Ion.
+var atom = "prop";
+var symbol = Symbol();
+
+function testAtom() {
+    var holder = {
+        get [atom]() {
+            bailout();
+            return 1;
+        }
+    };
+
+    function f() {
+        for (var i = 0; i < 2000; ++i) {
+            var x = holder[atom];
+            assertEq(x, 1);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testAtom();
+
+function testSymbol() {
+    var holder = {
+        get [symbol]() {
+            bailout();
+            return 1;
+        }
+    };
+
+    function f() {
+        for (var i = 0; i < 2000; ++i) {
+            var x = holder[symbol];
+            assertEq(x, 1);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testSymbol();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/inlining/getelem-getter-frameiter.js
@@ -0,0 +1,48 @@
+// Test bailouts in inlined jsop_getelem accesses.
+
+// Defined outside of the test functions to ensure they're recognised as
+// constants in Ion.
+var atom = "prop";
+var symbol = Symbol();
+
+function testAtom() {
+    var holder = {
+        get [atom]() {
+            new Error().stack;
+            return 1;
+        }
+    };
+
+    function f() {
+        for (var i = 0; i < 2000; ++i) {
+            var x = holder[atom];
+            assertEq(x, 1);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testAtom();
+
+function testSymbol() {
+    var holder = {
+        get [symbol]() {
+            new Error().stack;
+            return 1;
+        }
+    };
+
+    function f() {
+        for (var i = 0; i < 2000; ++i) {
+            var x = holder[symbol];
+            assertEq(x, 1);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testSymbol();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/inlining/getelem-getter-id-mismatch.js
@@ -0,0 +1,113 @@
+// Ensure BaselineInspector properly handles mixed atom/symbols when determining
+// whether or not a jsop_getelem access to a getter can be inlined.
+
+// Defined outside of the test functions to ensure they're recognised as
+// constants in Ion.
+var atom1 = "prop1";
+var atom2 = "prop2";
+var sym1 = Symbol();
+var sym2 = Symbol();
+
+function testAtomAtom() {
+    var holder = {
+        get [atom1]() { return 1; },
+        get [atom2]() { return 2; },
+    };
+
+    function get(name) {
+        // Single access point called with different atoms.
+        return holder[name];
+    }
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = get(atom1);
+            var y = get(atom2);
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testAtomAtom();
+
+function testAtomSymbol() {
+    var holder = {
+        get [atom1]() { return 1; },
+        get [sym2]() { return 2; },
+    };
+
+    function get(name) {
+        // Single access point called with atom and symbol.
+        return holder[name];
+    }
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = get(atom1);
+            var y = get(sym2);
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testAtomSymbol();
+
+function testSymbolAtom() {
+    var holder = {
+        get [sym1]() { return 1; },
+        get [atom2]() { return 2; },
+    };
+
+    function get(name) {
+        // Single access point called with symbol and atom.
+        return holder[name];
+    }
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = get(sym1);
+            var y = get(atom2);
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testSymbolAtom();
+
+function testSymbolSymbol() {
+    var holder = {
+        get [sym1]() { return 1; },
+        get [sym2]() { return 2; },
+    };
+
+    function get(name) {
+        // Single access point called with different symbols.
+        return holder[name];
+    }
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = get(sym1);
+            var y = get(sym2);
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testSymbolSymbol();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/inlining/getelem-getter-megamorphic.js
@@ -0,0 +1,79 @@
+// Test for inlined getters for jsop_getelem accesses, where the getter is a
+// property on the prototype and a megamorphic IC is attached.
+
+function makeObjects(name) {
+    class Base {
+        constructor(v) {
+            this._prop = v;
+        }
+        get [name]() {
+            return this._prop;
+        }
+    }
+
+    // When we hit |TYPE_FLAG_OBJECT_COUNT_LIMIT|, the objects are marked as
+    // |TYPE_FLAG_ANYOBJECT|. That means less than |TYPE_FLAG_OBJECT_COUNT_LIMIT|
+    // objects need to be created to have no unknown objects in the type set.
+    const TYPE_FLAG_OBJECT_COUNT_LIMIT = 7;
+
+    // |ICState::ICState::MaxOptimizedStubs| defines the maximum number of
+    // optimized stubs, so as soon as we hit the maximum number, the megamorphic
+    // state is entered.
+    const ICState_MaxOptimizedStubs = 6;
+
+    // Create enough classes to enter megamorphic state, but not too much to
+    // have |TYPE_FLAG_ANYOBJECT| in the TypeSet.
+    const OBJECT_COUNT = Math.min(ICState_MaxOptimizedStubs, TYPE_FLAG_OBJECT_COUNT_LIMIT);
+
+    var objects = [];
+    for (var i = 0; i < OBJECT_COUNT; ++i) {
+        objects.push(new class extends Base {}(1));
+    }
+
+    return objects;
+}
+
+// Defined outside of the test functions to ensure they're recognised as
+// constants in Ion.
+var atom = "prop";
+var symbol = Symbol();
+
+function testAtom() {
+    var objects = makeObjects(atom);
+
+    function f() {
+        var actual = 0;
+        var expected = 0;
+        for (var i = 0; i < 1000; i++) {
+            var obj = objects[i % objects.length];
+            actual += obj[atom];
+            expected += obj._prop;
+        }
+        assertEq(actual, expected);
+    }
+
+    for (var i = 0; i < 2; ++i) {
+        f();
+    }
+}
+testAtom();
+
+function testSymbol() {
+    var objects = makeObjects(symbol);
+
+    function f() {
+        var actual = 0;
+        var expected = 0;
+        for (var i = 0; i < 1000; i++) {
+            var obj = objects[i % objects.length];
+            actual += obj[symbol];
+            expected += obj._prop;
+        }
+        assertEq(actual, expected);
+    }
+
+    for (var i = 0; i < 2; ++i) {
+        f();
+    }
+}
+testSymbol();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/inlining/getelem-getter-noninlined-call.js
@@ -0,0 +1,52 @@
+// With-Statements are not supported in Ion, therefore functions containing
+// them can't be inlined in Ion. However it's still possible to inline the
+// property access to the getter into a simple guard-shape instruction.
+
+// Defined outside of the test functions to ensure they're recognised as
+// constants in Ion.
+var atom = "prop";
+var symbol = Symbol();
+
+function testAtom() {
+    var holder = {
+        get [atom]() {
+            with ({}) {
+                return 1;
+            }
+        }
+    };
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = holder[atom];
+            assertEq(x, 1);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testAtom();
+
+function testSymbol() {
+    var holder = {
+        get [symbol]() {
+            with ({}) {
+                return 1;
+            }
+        }
+    };
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = holder[symbol];
+            assertEq(x, 1);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testSymbol();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/inlining/getelem-getter-own.js
@@ -0,0 +1,51 @@
+// Test for inlined getters for jsop_getelem accesses, where the getter is an
+// own property.
+
+// Defined outside of the test functions to ensure they're recognised as
+// constants in Ion.
+var atom1 = "prop1";
+var atom2 = "prop2";
+var sym1 = Symbol();
+var sym2 = Symbol();
+
+function testAtom() {
+    var holder = {
+        get [atom1]() { return 1; },
+        get [atom2]() { return 2; },
+    };
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = holder[atom1];
+            var y = holder[atom2];
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testAtom();
+
+function testSymbol() {
+    var holder = {
+        get [sym1]() { return 1; },
+        get [sym2]() { return 2; },
+    };
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = holder[sym1];
+            var y = holder[sym2];
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testSymbol();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/inlining/getelem-getter-proto.js
@@ -0,0 +1,55 @@
+// Test for inlined getters for jsop_getelem accesses, where the getter is a
+// property on the prototype.
+
+// Defined outside of the test functions to ensure they're recognised as
+// constants in Ion.
+var atom1 = "prop1";
+var atom2 = "prop2";
+var sym1 = Symbol();
+var sym2 = Symbol();
+
+function testAtom() {
+    var proto = {
+        get [atom1]() { return 1; },
+        get [atom2]() { return 2; },
+    };
+
+    var holder = Object.create(proto);
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = holder[atom1];
+            var y = holder[atom2];
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testAtom();
+
+function testSymbol() {
+    var proto = {
+        get [sym1]() { return 1; },
+        get [sym2]() { return 2; },
+    };
+
+    var holder = Object.create(proto);
+
+    function f() {
+        for (var i = 0; i < 1000; ++i) {
+            var x = holder[sym1];
+            var y = holder[sym2];
+            assertEq(x, 1);
+            assertEq(y, 2);
+        }
+    }
+
+    for (var i = 0; i < 2; i++) {
+        f();
+    }
+}
+testSymbol();
--- a/js/src/jit/BaselineBailouts.cpp
+++ b/js/src/jit/BaselineBailouts.cpp
@@ -8,16 +8,17 @@
 
 #include "jsutil.h"
 
 #include "jit/arm/Simulator-arm.h"
 #include "jit/BaselineFrame.h"
 #include "jit/BaselineIC.h"
 #include "jit/BaselineJIT.h"
 #include "jit/CompileInfo.h"
+#include "jit/Ion.h"
 #include "jit/JitSpewer.h"
 #include "jit/mips32/Simulator-mips32.h"
 #include "jit/mips64/Simulator-mips64.h"
 #include "jit/Recover.h"
 #include "jit/RematerializedFrame.h"
 #include "js/Utility.h"
 #include "vm/ArgumentsObject.h"
 #include "vm/Debugger.h"
@@ -431,29 +432,32 @@ struct BaselineStackBuilder {
   void setCheckGlobalDeclarationConflicts() {
     header_->checkGlobalDeclarationConflicts = true;
   }
 };
 
 #ifdef DEBUG
 static inline bool IsInlinableFallback(ICFallbackStub* icEntry) {
   return icEntry->isCall_Fallback() || icEntry->isGetProp_Fallback() ||
-         icEntry->isSetProp_Fallback();
+         icEntry->isSetProp_Fallback() || icEntry->isGetElem_Fallback();
 }
 #endif
 
 static inline void* GetStubReturnAddress(JSContext* cx, jsbytecode* pc) {
   JitRealm* jitRealm = cx->realm()->jitRealm();
 
   if (IsGetPropPC(pc)) {
     return jitRealm->bailoutReturnAddr(BailoutReturnStub::GetProp);
   }
   if (IsSetPropPC(pc)) {
     return jitRealm->bailoutReturnAddr(BailoutReturnStub::SetProp);
   }
+  if (IsGetElemPC(pc)) {
+    return jitRealm->bailoutReturnAddr(BailoutReturnStub::GetElem);
+  }
 
   // This should be a call op of some kind, now.
   MOZ_ASSERT(IsCallPC(pc) && !IsSpreadCallPC(pc));
   if (IsConstructorCallPC(pc)) {
     return jitRealm->bailoutReturnAddr(BailoutReturnStub::New);
   }
   return jitRealm->bailoutReturnAddr(BailoutReturnStub::Call);
 }
@@ -896,25 +900,25 @@ static bool InitFromBailout(JSContext* c
   MOZ_ASSERT_IF(IsSpreadCallPC(pc), !iter.moreFrames());
 
   // Fixup inlined JSOP_FUNCALL, JSOP_FUNAPPLY, and accessors on the caller
   // side. On the caller side this must represent like the function wasn't
   // inlined.
   uint32_t pushedSlots = 0;
   AutoValueVector savedCallerArgs(cx);
   bool needToSaveArgs =
-      op == JSOP_FUNAPPLY || IsGetPropPC(pc) || IsSetPropPC(pc);
+      op == JSOP_FUNAPPLY || IsIonInlinableGetterOrSetterPC(pc);
   if (iter.moreFrames() && (op == JSOP_FUNCALL || needToSaveArgs)) {
     uint32_t inlined_args = 0;
     if (op == JSOP_FUNCALL) {
       inlined_args = 2 + GET_ARGC(pc) - 1;
     } else if (op == JSOP_FUNAPPLY) {
       inlined_args = 2 + blFrame->numActualArgs();
     } else {
-      MOZ_ASSERT(IsGetPropPC(pc) || IsSetPropPC(pc));
+      MOZ_ASSERT(IsIonInlinableGetterOrSetterPC(pc));
       inlined_args = 2 + IsSetPropPC(pc);
     }
 
     MOZ_ASSERT(exprStackSlots >= inlined_args);
     pushedSlots = exprStackSlots - inlined_args;
 
     JitSpew(JitSpew_BaselineBailouts,
             "      pushing %u expression stack slots before fixup",
@@ -1080,26 +1084,28 @@ static bool InitFromBailout(JSContext* c
 
   if (reachablePC) {
     if (op != JSOP_FUNAPPLY || !iter.moreFrames() || resumeAfter) {
       if (op == JSOP_FUNCALL) {
         // For fun.call(this, ...); the reconstructStackDepth will
         // include the this. When inlining that is not included.
         // So the exprStackSlots will be one less.
         MOZ_ASSERT(expectedDepth - exprStackSlots <= 1);
-      } else if (iter.moreFrames() && (IsGetPropPC(pc) || IsSetPropPC(pc))) {
+      } else if (iter.moreFrames() && IsIonInlinableGetterOrSetterPC(pc)) {
         // Accessors coming out of ion are inlined via a complete
         // lie perpetrated by the compiler internally. Ion just rearranges
         // the stack, and pretends that it looked like a call all along.
         // This means that the depth is actually one *more* than expected
         // by the interpreter, as there is now a JSFunction, |this| and [arg],
-        // rather than the expected |this| and [arg]
+        // rather than the expected |this| and [arg].
+        // If the inlined accessor is a getelem operation, the numbers do match,
+        // but that's just because getelem expects one more item on the stack.
         // Note that none of that was pushed, but it's still reflected
         // in exprStackSlots.
-        MOZ_ASSERT(exprStackSlots - expectedDepth == 1);
+        MOZ_ASSERT(exprStackSlots - expectedDepth == (IsGetElemPC(pc) ? 0 : 1));
       } else {
         // For fun.apply({}, arguments) the reconstructStackDepth will
         // have stackdepth 4, but it could be that we inlined the
         // funapply. In that case exprStackSlots, will have the real
         // arguments in the slots and not be 4.
         MOZ_ASSERT(exprStackSlots == expectedDepth);
       }
     }
--- a/js/src/jit/BaselineIC.cpp
+++ b/js/src/jit/BaselineIC.cpp
@@ -494,21 +494,22 @@ void ICStubIterator::unlink(JSContext* c
     case Call_AnyScripted:
     case Call_Native:
     case Call_ClassHook:
     case Call_ScriptedApplyArray:
     case Call_ScriptedApplyArguments:
     case Call_ScriptedFunCall:
     case Call_ConstStringSplit:
     case WarmUpCounter_Fallback:
-    // These two fallback stubs don't actually make non-tail calls,
+    // These three fallback stubs don't actually make non-tail calls,
     // but the fallback code for the bailout path needs to pop the stub frame
     // pushed during the bailout.
     case GetProp_Fallback:
     case SetProp_Fallback:
+    case GetElem_Fallback:
       return true;
     default:
       return false;
   }
 }
 
 bool ICStub::makesGCCalls() const {
   switch (kind()) {
@@ -2190,32 +2191,65 @@ bool ICGetElem_Fallback::Compiler::gener
     masm.pushValue(R1);
     masm.pushValue(Address(masm.getStackPointer(), sizeof(Value) * 2));
 
     // Push arguments.
     masm.pushValue(R0);  // Receiver
     masm.pushValue(R1);  // Index
     masm.pushValue(Address(masm.getStackPointer(), sizeof(Value) * 5));  // Obj
     masm.push(ICStubReg);
-    pushStubPayload(masm, R0.scratchReg());
-
-    return tailCallVM(DoGetElemSuperFallbackInfo, masm);
-  }
-
-  // Ensure stack is fully synced for the expression decompiler.
-  masm.pushValue(R0);
-  masm.pushValue(R1);
-
-  // Push arguments.
-  masm.pushValue(R1);
-  masm.pushValue(R0);
-  masm.push(ICStubReg);
-  pushStubPayload(masm, R0.scratchReg());
-
-  return tailCallVM(DoGetElemFallbackInfo, masm);
+    masm.pushBaselineFramePtr(BaselineFrameReg, R0.scratchReg());
+
+    if (!tailCallVM(DoGetElemSuperFallbackInfo, masm)) {
+      return false;
+    }
+  } else {
+    // Ensure stack is fully synced for the expression decompiler.
+    masm.pushValue(R0);
+    masm.pushValue(R1);
+
+    // Push arguments.
+    masm.pushValue(R1);
+    masm.pushValue(R0);
+    masm.push(ICStubReg);
+    masm.pushBaselineFramePtr(BaselineFrameReg, R0.scratchReg());
+
+    if (!tailCallVM(DoGetElemFallbackInfo, masm)) {
+      return false;
+    }
+  }
+
+  // This is the resume point used when bailout rewrites call stack to undo
+  // Ion inlined frames. The return address pushed onto reconstructed stack
+  // will point here.
+  assumeStubFrame();
+  bailoutReturnOffset_.bind(masm.currentOffset());
+
+  leaveStubFrame(masm, true);
+
+  // When we get here, ICStubReg contains the ICGetElem_Fallback stub,
+  // which we can't use to enter the TypeMonitor IC, because it's a
+  // MonitoredFallbackStub instead of a MonitoredStub. So, we cheat. Note that
+  // we must have a non-null fallbackMonitorStub here because InitFromBailout
+  // delazifies.
+  masm.loadPtr(Address(ICStubReg,
+                       ICMonitoredFallbackStub::offsetOfFallbackMonitorStub()),
+               ICStubReg);
+  EmitEnterTypeMonitorIC(masm,
+                         ICTypeMonitor_Fallback::offsetOfFirstMonitorStub());
+
+  return true;
+}
+
+void ICGetElem_Fallback::Compiler::postGenerateStubCode(MacroAssembler& masm,
+                                                        Handle<JitCode*> code) {
+  BailoutReturnStub kind = hasReceiver_ ? BailoutReturnStub::GetElemSuper
+                                        : BailoutReturnStub::GetElem;
+  void* address = code->raw() + bailoutReturnOffset_.offset();
+  cx->realm()->jitRealm()->initBailoutReturnAddr(address, getKey(), kind);
 }
 
 static void SetUpdateStubData(ICCacheIR_Updated* stub,
                               const PropertyTypeCheckInfo* info) {
   if (info->isSet()) {
     stub->updateStubGroup() = info->group();
     stub->updateStubId() = info->id();
   }
--- a/js/src/jit/BaselineIC.h
+++ b/js/src/jit/BaselineIC.h
@@ -1689,18 +1689,21 @@ class ICGetElem_Fallback : public ICMoni
 
  public:
   void noteNegativeIndex() { extra_ |= EXTRA_NEGATIVE_INDEX; }
   bool hasNegativeIndex() const { return extra_ & EXTRA_NEGATIVE_INDEX; }
 
   // Compiler for this stub kind.
   class Compiler : public ICStubCompiler {
    protected:
+    CodeOffset bailoutReturnOffset_;
     bool hasReceiver_;
     MOZ_MUST_USE bool generateStubCode(MacroAssembler& masm) override;
+    void postGenerateStubCode(MacroAssembler& masm,
+                              Handle<JitCode*> code) override;
 
     virtual int32_t getKey() const override {
       return static_cast<int32_t>(kind) |
              (static_cast<int32_t>(hasReceiver_) << 16);
     }
 
    public:
     explicit Compiler(JSContext* cx, bool hasReceiver = false)
--- a/js/src/jit/BaselineInspector.cpp
+++ b/js/src/jit/BaselineInspector.cpp
@@ -1061,56 +1061,103 @@ static bool AddCacheIRGlobalGetter(
     return false;
   } else {
     MOZ_ASSERT(*commonGetter == getter);
   }
 
   return true;
 }
 
+static bool GuardSpecificAtomOrSymbol(CacheIRReader& reader, ICStub* stub,
+                                      const CacheIRStubInfo* stubInfo,
+                                      ValOperandId keyId, jsid id) {
+  // Try to match an id guard emitted by IRGenerator::emitIdGuard.
+  if (JSID_IS_ATOM(id)) {
+    if (!reader.matchOp(CacheOp::GuardIsString, keyId)) {
+      return false;
+    }
+    if (!reader.matchOp(CacheOp::GuardSpecificAtom, keyId)) {
+      return false;
+    }
+    JSString* str =
+        stubInfo->getStubField<JSString*>(stub, reader.stubOffset()).get();
+    if (AtomToId(&str->asAtom()) != id) {
+      return false;
+    }
+  } else {
+    MOZ_ASSERT(JSID_IS_SYMBOL(id));
+    if (!reader.matchOp(CacheOp::GuardIsSymbol, keyId)) {
+      return false;
+    }
+    if (!reader.matchOp(CacheOp::GuardSpecificSymbol, keyId)) {
+      return false;
+    }
+    Symbol* sym =
+        stubInfo->getStubField<Symbol*>(stub, reader.stubOffset()).get();
+    if (SYMBOL_TO_JSID(sym) != id) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
 static bool AddCacheIRGetPropFunction(
-    ICCacheIR_Monitored* stub, bool innerized, JSObject** holder,
+    ICCacheIR_Monitored* stub, jsid id, bool innerized, JSObject** holder,
     Shape** holderShape, JSFunction** commonGetter, Shape** globalShape,
     bool* isOwnProperty, BaselineInspector::ReceiverVector& receivers,
     BaselineInspector::ObjectGroupVector& convertUnboxedGroups,
     JSScript* script) {
   // We match either an own getter:
   //
   //   GuardIsObject objId
+  //   [..Id Guard..]
   //   [..WindowProxy innerization..]
   //   <GuardReceiver objId>
   //   Call(Scripted|Native)GetterResult objId
   //
   // Or a getter on the prototype:
   //
   //   GuardIsObject objId
+  //   [..Id Guard..]
   //   [..WindowProxy innerization..]
   //   <GuardReceiver objId>
   //   LoadObject holderId
   //   GuardShape holderId
   //   Call(Scripted|Native)GetterResult objId
   //
   // If |innerized| is true, we replaced a WindowProxy with the Window
   // object and we're only interested in Baseline getter stubs that performed
   // the same optimization. This means we expect the following ops for the
   // [..WindowProxy innerization..] above:
   //
   //   GuardClass objId WindowProxy
   //   objId = LoadWrapperTarget objId
   //   GuardSpecificObject objId, <global>
+  //
+  // If we test for a specific jsid, [..Id Guard..] is implemented through:
+  //   GuardIs(String|Symbol) keyId
+  //   GuardSpecific(Atom|Symbol) keyId, <atom|symbol>
 
   CacheIRReader reader(stub->stubInfo());
 
   ObjOperandId objId = ObjOperandId(0);
   if (!reader.matchOp(CacheOp::GuardIsObject, objId)) {
     return AddCacheIRGlobalGetter(stub, innerized, holder, holderShape,
                                   commonGetter, globalShape, isOwnProperty,
                                   receivers, convertUnboxedGroups, script);
   }
 
+  if (!JSID_IS_EMPTY(id)) {
+    ValOperandId keyId = ValOperandId(1);
+    if (!GuardSpecificAtomOrSymbol(reader, stub, stub->stubInfo(), keyId, id)) {
+      return false;
+    }
+  }
+
   if (innerized) {
     if (!reader.matchOp(CacheOp::GuardClass, objId) ||
         reader.guardClassKind() != GuardClassKind::WindowProxy) {
       return false;
     }
 
     if (!reader.matchOp(CacheOp::LoadWrapperTarget, objId)) {
       return false;
@@ -1206,33 +1253,40 @@ static bool AddCacheIRGetPropFunction(
   *holder = obj;
   *holderShape = objShape;
   *commonGetter = getter;
   *isOwnProperty = false;
   return true;
 }
 
 bool BaselineInspector::commonGetPropFunction(
-    jsbytecode* pc, bool innerized, JSObject** holder, Shape** holderShape,
-    JSFunction** commonGetter, Shape** globalShape, bool* isOwnProperty,
-    ReceiverVector& receivers, ObjectGroupVector& convertUnboxedGroups) {
+    jsbytecode* pc, jsid id, bool innerized, JSObject** holder,
+    Shape** holderShape, JSFunction** commonGetter, Shape** globalShape,
+    bool* isOwnProperty, ReceiverVector& receivers,
+    ObjectGroupVector& convertUnboxedGroups) {
   if (!hasICScript()) {
     return false;
   }
 
+  MOZ_ASSERT(IsGetPropPC(pc) || IsGetElemPC(pc) || JSOp(*pc) == JSOP_GETGNAME);
   MOZ_ASSERT(receivers.empty());
   MOZ_ASSERT(convertUnboxedGroups.empty());
 
+  // Only GetElem operations need to guard against a specific property id.
+  if (!IsGetElemPC(pc)) {
+    id = JSID_EMPTY;
+  }
+
   *globalShape = nullptr;
   *commonGetter = nullptr;
   const ICEntry& entry = icEntryFromPC(pc);
 
   for (ICStub* stub = entry.firstStub(); stub; stub = stub->next()) {
     if (stub->isCacheIR_Monitored()) {
-      if (!AddCacheIRGetPropFunction(stub->toCacheIR_Monitored(), innerized,
+      if (!AddCacheIRGetPropFunction(stub->toCacheIR_Monitored(), id, innerized,
                                      holder, holderShape, commonGetter,
                                      globalShape, isOwnProperty, receivers,
                                      convertUnboxedGroups, script)) {
         return false;
       }
     } else if (stub->isFallback()) {
       // If we have an unoptimizable access, don't try to optimize.
       if (stub->toFallbackStub()->state().hasFailures()) {
@@ -1249,65 +1303,87 @@ bool BaselineInspector::commonGetPropFun
 
   MOZ_ASSERT(*isOwnProperty == !*holder);
   MOZ_ASSERT(*isOwnProperty ==
              (receivers.empty() && convertUnboxedGroups.empty()));
   return true;
 }
 
 static JSFunction* GetMegamorphicGetterSetterFunction(
-    ICStub* stub, const CacheIRStubInfo* stubInfo, bool isGetter) {
+    ICStub* stub, const CacheIRStubInfo* stubInfo, jsid id, bool isGetter) {
   // We match:
   //
   //   GuardIsObject objId
+  //   [..Id Guard..]
   //   GuardHasGetterSetter objId propShape
   //
   // propShape has the getter/setter we're interested in.
+  //
+  // If we test for a specific jsid, [..Id Guard..] is implemented through:
+  //   GuardIs(String|Symbol) keyId
+  //   GuardSpecific(Atom|Symbol) keyId, <atom|symbol>
 
   CacheIRReader reader(stubInfo);
 
   ObjOperandId objId = ObjOperandId(0);
   if (!reader.matchOp(CacheOp::GuardIsObject, objId)) {
     return nullptr;
   }
 
+  if (!JSID_IS_EMPTY(id)) {
+    ValOperandId keyId = ValOperandId(1);
+    if (!GuardSpecificAtomOrSymbol(reader, stub, stubInfo, keyId, id)) {
+      return nullptr;
+    }
+  }
+
   if (!reader.matchOp(CacheOp::GuardHasGetterSetter, objId)) {
     return nullptr;
   }
   Shape* propShape = stubInfo->getStubField<Shape*>(stub, reader.stubOffset());
 
   JSObject* obj =
       isGetter ? propShape->getterObject() : propShape->setterObject();
   return &obj->as<JSFunction>();
 }
 
 bool BaselineInspector::megamorphicGetterSetterFunction(
-    jsbytecode* pc, bool isGetter, JSFunction** getterOrSetter) {
+    jsbytecode* pc, jsid id, bool isGetter, JSFunction** getterOrSetter) {
   if (!hasICScript()) {
     return false;
   }
 
+  MOZ_ASSERT(IsGetPropPC(pc) || IsGetElemPC(pc) || IsSetPropPC(pc) ||
+             JSOp(*pc) == JSOP_GETGNAME || JSOp(*pc) == JSOP_INITGLEXICAL ||
+             JSOp(*pc) == JSOP_INITPROP || JSOp(*pc) == JSOP_INITLOCKEDPROP ||
+             JSOp(*pc) == JSOP_INITHIDDENPROP);
+
+  // Only GetElem operations need to guard against a specific property id.
+  if (!IsGetElemPC(pc)) {
+    id = JSID_EMPTY;
+  }
+
   *getterOrSetter = nullptr;
   const ICEntry& entry = icEntryFromPC(pc);
 
   for (ICStub* stub = entry.firstStub(); stub; stub = stub->next()) {
     if (stub->isCacheIR_Monitored()) {
       MOZ_ASSERT(isGetter);
       JSFunction* getter = GetMegamorphicGetterSetterFunction(
-          stub, stub->toCacheIR_Monitored()->stubInfo(), isGetter);
+          stub, stub->toCacheIR_Monitored()->stubInfo(), id, isGetter);
       if (!getter || (*getterOrSetter && *getterOrSetter != getter)) {
         return false;
       }
       *getterOrSetter = getter;
       continue;
     }
     if (stub->isCacheIR_Updated()) {
       MOZ_ASSERT(!isGetter);
       JSFunction* setter = GetMegamorphicGetterSetterFunction(
-          stub, stub->toCacheIR_Updated()->stubInfo(), isGetter);
+          stub, stub->toCacheIR_Updated()->stubInfo(), id, isGetter);
       if (!setter || (*getterOrSetter && *getterOrSetter != setter)) {
         return false;
       }
       *getterOrSetter = setter;
       continue;
     }
     if (stub->isFallback()) {
       if (stub->toFallbackStub()->state().hasFailures()) {
@@ -1434,16 +1510,19 @@ static bool AddCacheIRSetPropFunction(
 bool BaselineInspector::commonSetPropFunction(
     jsbytecode* pc, JSObject** holder, Shape** holderShape,
     JSFunction** commonSetter, bool* isOwnProperty, ReceiverVector& receivers,
     ObjectGroupVector& convertUnboxedGroups) {
   if (!hasICScript()) {
     return false;
   }
 
+  MOZ_ASSERT(IsSetPropPC(pc) || JSOp(*pc) == JSOP_INITGLEXICAL ||
+             JSOp(*pc) == JSOP_INITPROP || JSOp(*pc) == JSOP_INITLOCKEDPROP ||
+             JSOp(*pc) == JSOP_INITHIDDENPROP);
   MOZ_ASSERT(receivers.empty());
   MOZ_ASSERT(convertUnboxedGroups.empty());
 
   *commonSetter = nullptr;
   const ICEntry& entry = icEntryFromPC(pc);
 
   for (ICStub* stub = entry.firstStub(); stub; stub = stub->next()) {
     if (stub->isCacheIR_Updated()) {
--- a/js/src/jit/BaselineInspector.h
+++ b/js/src/jit/BaselineInspector.h
@@ -116,22 +116,23 @@ class BaselineInspector {
   LexicalEnvironmentObject* templateNamedLambdaObject();
   CallObject* templateCallObject();
 
   // If |innerized| is true, we're doing a GETPROP on a WindowProxy and
   // IonBuilder unwrapped/innerized it to do the lookup on the Window (the
   // global object) instead. In this case we should only look for Baseline
   // stubs that performed the same optimization.
   MOZ_MUST_USE bool commonGetPropFunction(
-      jsbytecode* pc, bool innerized, JSObject** holder, Shape** holderShape,
-      JSFunction** commonGetter, Shape** globalShape, bool* isOwnProperty,
-      ReceiverVector& receivers, ObjectGroupVector& convertUnboxedGroups);
+      jsbytecode* pc, jsid id, bool innerized, JSObject** holder,
+      Shape** holderShape, JSFunction** commonGetter, Shape** globalShape,
+      bool* isOwnProperty, ReceiverVector& receivers,
+      ObjectGroupVector& convertUnboxedGroups);
 
   MOZ_MUST_USE bool megamorphicGetterSetterFunction(
-      jsbytecode* pc, bool isGetter, JSFunction** getterOrSetter);
+      jsbytecode* pc, jsid id, bool isGetter, JSFunction** getterOrSetter);
 
   MOZ_MUST_USE bool commonSetPropFunction(
       jsbytecode* pc, JSObject** holder, Shape** holderShape,
       JSFunction** commonSetter, bool* isOwnProperty, ReceiverVector& receivers,
       ObjectGroupVector& convertUnboxedGroups);
 
   MOZ_MUST_USE bool instanceOfData(jsbytecode* pc, Shape** shape,
                                    uint32_t* slot, JSObject** prototypeObject);
--- a/js/src/jit/Ion.h
+++ b/js/src/jit/Ion.h
@@ -164,22 +164,27 @@ static inline bool IsIonEnabled(JSContex
 #if defined(JS_CODEGEN_NONE) || defined(JS_CODEGEN_ARM64)
   return false;
 #else
   return cx->options().ion() && cx->options().baseline() &&
          cx->runtime()->jitSupportsFloatingPoint;
 #endif
 }
 
+inline bool IsIonInlinableGetterOrSetterPC(jsbytecode* pc) {
+  // GETPROP, CALLPROP, LENGTH, GETELEM, and JSOP_CALLELEM. (Inlined Getters)
+  // SETPROP, SETNAME, SETGNAME (Inlined Setters)
+  return IsGetPropPC(pc) || IsGetElemPC(pc) || IsSetPropPC(pc);
+}
+
 inline bool IsIonInlinablePC(jsbytecode* pc) {
   // CALL, FUNCALL, FUNAPPLY, EVAL, NEW (Normal Callsites)
-  // GETPROP, CALLPROP, and LENGTH. (Inlined Getters)
-  // SETPROP, SETNAME, SETGNAME (Inlined Setters)
-  return (IsCallPC(pc) && !IsSpreadCallPC(pc)) || IsGetPropPC(pc) ||
-         IsSetPropPC(pc);
+  // or an inlinable getter or setter.
+  return (IsCallPC(pc) && !IsSpreadCallPC(pc)) ||
+         IsIonInlinableGetterOrSetterPC(pc);
 }
 
 inline bool TooManyActualArguments(unsigned nargs) {
   return nargs > JitOptions.maxStackArgs;
 }
 
 inline bool TooManyFormalArguments(unsigned nargs) {
   return nargs >= SNAPSHOT_MAX_NARGS || TooManyActualArguments(nargs);
--- a/js/src/jit/IonBuilder.cpp
+++ b/js/src/jit/IonBuilder.cpp
@@ -7991,17 +7991,18 @@ AbortReasonOr<Ok> IonBuilder::jsop_getgn
     MOZ_TRY(getStaticName(&emitted, obj, name));
     if (emitted) {
       return Ok();
     }
 
     if (!forceInlineCaches() && obj->is<GlobalObject>()) {
       TemporaryTypeSet* types = bytecodeTypes(pc);
       MDefinition* globalObj = constant(ObjectValue(*obj));
-      MOZ_TRY(getPropTryCommonGetter(&emitted, globalObj, name, types));
+      MOZ_TRY(
+          getPropTryCommonGetter(&emitted, globalObj, NameToId(name), types));
       if (emitted) {
         return Ok();
       }
     }
   }
 
   return jsop_getname(name);
 }
@@ -8581,16 +8582,23 @@ AbortReasonOr<Ok> IonBuilder::getElemTry
 
   trackOptimizationAttempt(TrackedStrategy::GetProp_NotDefined);
   MOZ_TRY(getPropTryNotDefined(emitted, obj, id, types));
   if (*emitted) {
     index->setImplicitlyUsedUnchecked();
     return Ok();
   }
 
+  trackOptimizationAttempt(TrackedStrategy::GetProp_CommonGetter);
+  MOZ_TRY(getPropTryCommonGetter(emitted, obj, id, types));
+  if (*emitted) {
+    index->setImplicitlyUsedUnchecked();
+    return Ok();
+  }
+
   return Ok();
 }
 
 AbortReasonOr<Ok> IonBuilder::getElemTryDense(bool* emitted, MDefinition* obj,
                                               MDefinition* index) {
   MOZ_ASSERT(*emitted == false);
 
   if (!ElementAccessIsDenseNative(constraints(), obj, index)) {
@@ -10129,20 +10137,20 @@ AbortReasonOr<Ok> IonBuilder::jsop_getel
 
   MOZ_TRY(resumeAfter(ins));
 
   TemporaryTypeSet* types = bytecodeTypes(pc);
   return pushTypeBarrier(ins, types, BarrierKind::TypeSet);
 }
 
 NativeObject* IonBuilder::commonPrototypeWithGetterSetter(
-    TemporaryTypeSet* types, PropertyName* name, bool isGetter,
-    JSFunction* getterOrSetter, bool* guardGlobal) {
+    TemporaryTypeSet* types, jsid id, bool isGetter, JSFunction* getterOrSetter,
+    bool* guardGlobal) {
   // If there's a single object on the proto chain of all objects in |types|
-  // that contains a property |name| with |getterOrSetter| getter or setter
+  // that contains a property |id| with |getterOrSetter| getter or setter
   // function, return that object.
 
   // No sense looking if we don't know what's going on.
   if (!types || types->unknownObject()) {
     return nullptr;
   }
   *guardGlobal = false;
 
@@ -10158,17 +10166,17 @@ NativeObject* IonBuilder::commonPrototyp
         return nullptr;
       }
 
       const Class* clasp = key->clasp();
       if (!ClassHasEffectlessLookup(clasp)) {
         return nullptr;
       }
       JSObject* singleton = key->isSingleton() ? key->singleton() : nullptr;
-      if (ObjectHasExtraOwnProperty(realm, key, NameToId(name))) {
+      if (ObjectHasExtraOwnProperty(realm, key, id)) {
         if (!singleton || !singleton->is<GlobalObject>()) {
           return nullptr;
         }
         *guardGlobal = true;
       }
 
       // Look for a getter/setter on the class itself which may need
       // to be called.
@@ -10190,17 +10198,17 @@ NativeObject* IonBuilder::commonPrototyp
       // we're looking for a getter or setter on the proto chain and
       // these objects are singletons.
       if (singleton) {
         if (!singleton->is<NativeObject>()) {
           return nullptr;
         }
 
         NativeObject* singletonNative = &singleton->as<NativeObject>();
-        if (Shape* propShape = singletonNative->lookupPure(name)) {
+        if (Shape* propShape = singletonNative->lookupPure(id)) {
           // We found a property. Check if it's the getter or setter
           // we're looking for.
           Value getterSetterVal = ObjectValue(*getterOrSetter);
           if (isGetter) {
             if (propShape->getterOrUndefined() != getterSetterVal) {
               return nullptr;
             }
           } else {
@@ -10216,17 +10224,17 @@ NativeObject* IonBuilder::commonPrototyp
           }
           break;
         }
       }
 
       // Test for isOwnProperty() without freezing. If we end up
       // optimizing, freezePropertiesForCommonPropFunc will freeze the
       // property type sets later on.
-      HeapTypeSetKey property = key->property(NameToId(name));
+      HeapTypeSetKey property = key->property(id);
       if (TypeSet* types = property.maybeTypes()) {
         if (!types->empty() || types->nonDataProperty()) {
           return nullptr;
         }
       }
       if (singleton) {
         if (CanHaveEmptyPropertyTypesForOwnProperty(singleton)) {
           MOZ_ASSERT(singleton->is<GlobalObject>());
@@ -10249,18 +10257,18 @@ NativeObject* IonBuilder::commonPrototyp
       key = TypeSet::ObjectKey::get(proto);
     }
   }
 
   return foundProto;
 }
 
 AbortReasonOr<Ok> IonBuilder::freezePropertiesForCommonPrototype(
-    TemporaryTypeSet* types, PropertyName* name, JSObject* foundProto,
-    bool allowEmptyTypesforGlobal /* = false*/) {
+    TemporaryTypeSet* types, jsid id, JSObject* foundProto,
+    bool allowEmptyTypesforGlobal) {
   for (unsigned i = 0; i < types->getObjectCount(); i++) {
     // If we found a Singleton object's own-property, there's nothing to
     // freeze.
     if (types->getSingleton(i) == foundProto) {
       continue;
     }
 
     TypeSet::ObjectKey* key = types->getObject(i);
@@ -10268,72 +10276,71 @@ AbortReasonOr<Ok> IonBuilder::freezeProp
       continue;
     }
 
     while (true) {
       if (!alloc().ensureBallast()) {
         return abort(AbortReason::Alloc);
       }
 
-      HeapTypeSetKey property = key->property(NameToId(name));
+      HeapTypeSetKey property = key->property(id);
       MOZ_ALWAYS_TRUE(
           !property.isOwnProperty(constraints(), allowEmptyTypesforGlobal));
 
       // Don't mark the proto. It will be held down by the shape
       // guard. This allows us to use properties found on prototypes
       // with properties unknown to TI.
       if (key->proto() == TaggedProto(foundProto)) {
         break;
       }
       key = TypeSet::ObjectKey::get(key->proto().toObjectOrNull());
     }
   }
   return Ok();
 }
 
 AbortReasonOr<bool> IonBuilder::testCommonGetterSetter(
-    TemporaryTypeSet* types, PropertyName* name, bool isGetter,
-    JSFunction* getterOrSetter, MDefinition** guard,
-    Shape* globalShape /* = nullptr*/,
+    TemporaryTypeSet* types, jsid id, bool isGetter, JSFunction* getterOrSetter,
+    MDefinition** guard, Shape* globalShape /* = nullptr*/,
     MDefinition** globalGuard /* = nullptr */) {
   MOZ_ASSERT(getterOrSetter);
   MOZ_ASSERT_IF(globalShape, globalGuard);
   bool guardGlobal;
 
   // Check if all objects being accessed will lookup the name through
   // foundProto.
   NativeObject* foundProto = commonPrototypeWithGetterSetter(
-      types, name, isGetter, getterOrSetter, &guardGlobal);
+      types, id, isGetter, getterOrSetter, &guardGlobal);
   if (!foundProto || (guardGlobal && !globalShape)) {
     trackOptimizationOutcome(TrackedOutcome::MultiProtoPaths);
     return false;
   }
 
   // We can optimize the getter/setter, so freeze all involved properties to
   // ensure there isn't a lower shadowing getter or setter installed in the
   // future.
   MOZ_TRY(
-      freezePropertiesForCommonPrototype(types, name, foundProto, guardGlobal));
+      freezePropertiesForCommonPrototype(types, id, foundProto, guardGlobal));
 
   // Add a shape guard on the prototype we found the property on. The rest of
   // the prototype chain is guarded by TI freezes, except when name is a global
   // name. In this case, we also have to guard on the globals shape to be able
   // to optimize, because the way global property sets are handled means
   // freezing doesn't work for what we want here. Note that a shape guard is
   // good enough here, even in the proxy case, because we have ensured there
   // are no lookup hooks for this property.
   if (guardGlobal) {
     JSObject* obj = &script()->global();
     MDefinition* globalObj = constant(ObjectValue(*obj));
     *globalGuard = addShapeGuard(globalObj, globalShape, Bailout_ShapeGuard);
   }
 
   // If the getter/setter is not configurable we don't have to guard on the
   // proto's shape.
-  Shape* propShape = foundProto->lookupPure(name);
+  Shape* propShape = foundProto->lookupPure(id);
   MOZ_ASSERT_IF(isGetter, propShape->getterObject() == getterOrSetter);
   MOZ_ASSERT_IF(!isGetter, propShape->setterObject() == getterOrSetter);
   if (propShape && !propShape->configurable()) {
     return true;
   }
 
   MInstruction* wrapper = constant(ObjectValue(*foundProto));
   *guard =
@@ -10713,17 +10720,17 @@ AbortReasonOr<Ok> IonBuilder::jsop_getpr
     trackOptimizationAttempt(TrackedStrategy::GetProp_Unboxed);
     MOZ_TRY(getPropTryUnboxed(&emitted, obj, name, barrier, types));
     if (emitted) {
       return Ok();
     }
 
     // Try to inline a common property getter, or make a call.
     trackOptimizationAttempt(TrackedStrategy::GetProp_CommonGetter);
-    MOZ_TRY(getPropTryCommonGetter(&emitted, obj, name, types));
+    MOZ_TRY(getPropTryCommonGetter(&emitted, obj, NameToId(name), types));
     if (emitted) {
       return Ok();
     }
 
     // Try to emit a monomorphic/polymorphic access based on baseline caches.
     trackOptimizationAttempt(TrackedStrategy::GetProp_InlineAccess);
     MOZ_TRY(getPropTryInlineAccess(&emitted, obj, name, barrier, types));
     if (emitted) {
@@ -11323,18 +11330,17 @@ MDefinition* IonBuilder::addShapeGuardsF
 
   MDefinition* holderDef = constant(ObjectValue(*holder));
   addShapeGuard(holderDef, holderShape, Bailout_ShapeGuard);
 
   return addGuardReceiverPolymorphic(obj, receivers);
 }
 
 AbortReasonOr<Ok> IonBuilder::getPropTryCommonGetter(bool* emitted,
-                                                     MDefinition* obj,
-                                                     PropertyName* name,
+                                                     MDefinition* obj, jsid id,
                                                      TemporaryTypeSet* types,
                                                      bool innerized) {
   MOZ_ASSERT(*emitted == false);
 
   TemporaryTypeSet* objTypes = obj->resultTypeSet();
 
   JSFunction* commonGetter = nullptr;
   MDefinition* guard = nullptr;
@@ -11343,43 +11349,43 @@ AbortReasonOr<Ok> IonBuilder::getPropTry
   {
     Shape* lastProperty = nullptr;
     Shape* globalShape = nullptr;
     JSObject* foundProto = nullptr;
     bool isOwnProperty = false;
     BaselineInspector::ReceiverVector receivers(alloc());
     BaselineInspector::ObjectGroupVector convertUnboxedGroups(alloc());
     if (inspector->commonGetPropFunction(
-            pc, innerized, &foundProto, &lastProperty, &commonGetter,
+            pc, id, innerized, &foundProto, &lastProperty, &commonGetter,
             &globalShape, &isOwnProperty, receivers, convertUnboxedGroups)) {
       bool canUseTIForGetter = false;
       if (!isOwnProperty) {
         // If it's not an own property, try to use TI to avoid shape guards.
         // For own properties we use the path below.
         MOZ_TRY_VAR(canUseTIForGetter,
-                    testCommonGetterSetter(objTypes, name,
+                    testCommonGetterSetter(objTypes, id,
                                            /* isGetter = */ true, commonGetter,
                                            &guard, globalShape, &globalGuard));
       }
       if (!canUseTIForGetter) {
         // If it's an own property or type information is bad, we can still
         // optimize the getter if we shape guard.
         obj = addShapeGuardsForGetterSetter(obj, foundProto, lastProperty,
                                             receivers, convertUnboxedGroups,
                                             isOwnProperty);
         if (!obj) {
           return abort(AbortReason::Alloc);
         }
       }
     } else if (inspector->megamorphicGetterSetterFunction(
-                   pc, /* isGetter = */ true, &commonGetter)) {
+                   pc, id, /* isGetter = */ true, &commonGetter)) {
       // Try to use TI to guard on this getter.
       bool canUseTIForGetter = false;
       MOZ_TRY_VAR(canUseTIForGetter,
-                  testCommonGetterSetter(objTypes, name, /* isGetter = */ true,
+                  testCommonGetterSetter(objTypes, id, /* isGetter = */ true,
                                          commonGetter, &guard));
       if (!canUseTIForGetter) {
         return Ok();
       }
     } else {
       // The Baseline IC didn't have any information we can use.
       return Ok();
     }
@@ -11905,17 +11911,17 @@ AbortReasonOr<Ok> IonBuilder::getPropTry
 
     trackOptimizationAttempt(TrackedStrategy::GetProp_StaticName);
     MOZ_TRY(getStaticName(emitted, &script()->global(), name));
     if (*emitted) {
       return Ok();
     }
 
     trackOptimizationAttempt(TrackedStrategy::GetProp_CommonGetter);
-    MOZ_TRY(getPropTryCommonGetter(emitted, inner, name, types,
+    MOZ_TRY(getPropTryCommonGetter(emitted, inner, NameToId(name), types,
                                    /* innerized = */ true));
     if (*emitted) {
       return Ok();
     }
   }
 
   // Passing the inner object to GetProperty IC is safe, see the
   // needsOuterizedThisObject check in IsCacheableGetPropCallNative.
@@ -12020,38 +12026,39 @@ AbortReasonOr<Ok> IonBuilder::setPropTry
     BaselineInspector::ObjectGroupVector convertUnboxedGroups(alloc());
     if (inspector->commonSetPropFunction(pc, &foundProto, &lastProperty,
                                          &commonSetter, &isOwnProperty,
                                          receivers, convertUnboxedGroups)) {
       bool canUseTIForSetter = false;
       if (!isOwnProperty) {
         // If it's not an own property, try to use TI to avoid shape guards.
         // For own properties we use the path below.
-        MOZ_TRY_VAR(
-            canUseTIForSetter,
-            testCommonGetterSetter(objTypes, name, /* isGetter = */ false,
-                                   commonSetter, &guard));
+        MOZ_TRY_VAR(canUseTIForSetter,
+                    testCommonGetterSetter(objTypes, NameToId(name),
+                                           /* isGetter = */ false, commonSetter,
+                                           &guard));
       }
       if (!canUseTIForSetter) {
         // If it's an own property or type information is bad, we can still
         // optimize the setter if we shape guard.
         obj = addShapeGuardsForGetterSetter(obj, foundProto, lastProperty,
                                             receivers, convertUnboxedGroups,
                                             isOwnProperty);
         if (!obj) {
           return abort(AbortReason::Alloc);
         }
       }
     } else if (inspector->megamorphicGetterSetterFunction(
-                   pc, /* isGetter = */ false, &commonSetter)) {
+                   pc, NameToId(name), /* isGetter = */ false, &commonSetter)) {
       // Try to use TI to guard on this setter.
       bool canUseTIForSetter = false;
-      MOZ_TRY_VAR(canUseTIForSetter,
-                  testCommonGetterSetter(objTypes, name, /* isGetter = */ false,
-                                         commonSetter, &guard));
+      MOZ_TRY_VAR(
+          canUseTIForSetter,
+          testCommonGetterSetter(objTypes, NameToId(name),
+                                 /* isGetter = */ false, commonSetter, &guard));
       if (!canUseTIForSetter) {
         return Ok();
       }
     } else {
       // The Baseline IC didn't have any information we can use.
       return Ok();
     }
   }
--- a/js/src/jit/IonBuilder.h
+++ b/js/src/jit/IonBuilder.h
@@ -271,18 +271,17 @@ class IonBuilder : public MIRGenerator,
   AbortReasonOr<Ok> getPropTryModuleNamespace(bool* emitted, MDefinition* obj,
                                               PropertyName* name,
                                               BarrierKind barrier,
                                               TemporaryTypeSet* types);
   AbortReasonOr<Ok> getPropTryUnboxed(bool* emitted, MDefinition* obj,
                                       PropertyName* name, BarrierKind barrier,
                                       TemporaryTypeSet* types);
   AbortReasonOr<Ok> getPropTryCommonGetter(bool* emitted, MDefinition* obj,
-                                           PropertyName* name,
-                                           TemporaryTypeSet* types,
+                                           jsid id, TemporaryTypeSet* types,
                                            bool innerized = false);
   AbortReasonOr<Ok> getPropTryInlineAccess(bool* emitted, MDefinition* obj,
                                            PropertyName* name,
                                            BarrierKind barrier,
                                            TemporaryTypeSet* types);
   AbortReasonOr<Ok> getPropTryInlineProtoAccess(bool* emitted, MDefinition* obj,
                                                 PropertyName* name,
                                                 TemporaryTypeSet* types);
@@ -859,29 +858,28 @@ class IonBuilder : public MIRGenerator,
 
   MDefinition* patchInlinedReturn(CallInfo& callInfo, MBasicBlock* exit,
                                   MBasicBlock* bottom);
   MDefinition* patchInlinedReturns(CallInfo& callInfo, MIRGraphReturns& returns,
                                    MBasicBlock* bottom);
   MDefinition* specializeInlinedReturn(MDefinition* rdef, MBasicBlock* exit);
 
   NativeObject* commonPrototypeWithGetterSetter(TemporaryTypeSet* types,
-                                                PropertyName* name,
-                                                bool isGetter,
+                                                jsid id, bool isGetter,
                                                 JSFunction* getterOrSetter,
                                                 bool* guardGlobal);
   AbortReasonOr<Ok> freezePropertiesForCommonPrototype(
-      TemporaryTypeSet* types, PropertyName* name, JSObject* foundProto,
-      bool allowEmptyTypesForGlobal = false);
+      TemporaryTypeSet* types, jsid id, JSObject* foundProto,
+      bool allowEmptyTypesForGlobal);
   /*
    * Callers must pass a non-null globalGuard if they pass a non-null
    * globalShape.
    */
   AbortReasonOr<bool> testCommonGetterSetter(
-      TemporaryTypeSet* types, PropertyName* name, bool isGetter,
+      TemporaryTypeSet* types, jsid id, bool isGetter,
       JSFunction* getterOrSetter, MDefinition** guard,
       Shape* globalShape = nullptr, MDefinition** globalGuard = nullptr);
   AbortReasonOr<bool> testShouldDOMCall(TypeSet* inTypes, JSFunction* func,
                                         JSJitInfo::OpType opType);
 
   MDefinition* addShapeGuardsForGetterSetter(
       MDefinition* obj, JSObject* holder, Shape* holderShape,
       const BaselineInspector::ReceiverVector& receivers,
--- a/js/src/jit/JitFrames.cpp
+++ b/js/src/jit/JitFrames.cpp
@@ -2028,17 +2028,17 @@ void InlineFrameIterator::findNextFrame(
 
     // Recover the number of actual arguments from the script.
     if (JSOp(*pc_) != JSOP_FUNAPPLY) {
       numActualArgs_ = GET_ARGC(pc_);
     }
     if (JSOp(*pc_) == JSOP_FUNCALL) {
       MOZ_ASSERT(GET_ARGC(pc_) > 0);
       numActualArgs_ = GET_ARGC(pc_) - 1;
-    } else if (IsGetPropPC(pc_)) {
+    } else if (IsGetPropPC(pc_) || IsGetElemPC(pc_)) {
       numActualArgs_ = 0;
     } else if (IsSetPropPC(pc_)) {
       numActualArgs_ = 1;
     }
 
     if (numActualArgs_ == 0xbadbad) {
       MOZ_CRASH(
           "Couldn't deduce the number of arguments of an ionmonkey frame");
@@ -2202,17 +2202,17 @@ MachineState MachineState::FromBailout(R
 
 bool InlineFrameIterator::isConstructing() const {
   // Skip the current frame and look at the caller's.
   if (more()) {
     InlineFrameIterator parent(TlsContext.get(), this);
     ++parent;
 
     // Inlined Getters and Setters are never constructing.
-    if (IsGetPropPC(parent.pc()) || IsSetPropPC(parent.pc())) {
+    if (IsIonInlinableGetterOrSetterPC(parent.pc())) {
       return false;
     }
 
     // In the case of a JS frame, look up the pc from the snapshot.
     MOZ_ASSERT(IsCallPC(parent.pc()) && !IsSpreadCallPC(parent.pc()));
 
     return IsConstructorCallPC(parent.pc());
   }
--- a/js/src/jit/JitRealm.h
+++ b/js/src/jit/JitRealm.h
@@ -440,16 +440,18 @@ class JitZone {
   }
   void purgeIonCacheIRStubInfo() { ionCacheIRStubInfoSet_.clearAndCompact(); }
 };
 
 enum class BailoutReturnStub {
   GetProp,
   GetPropSuper,
   SetProp,
+  GetElem,
+  GetElemSuper,
   Call,
   New,
   Count
 };
 
 class JitRealm {
   friend class JitActivation;
 
--- a/js/src/jit/Recover.cpp
+++ b/js/src/jit/Recover.cpp
@@ -8,16 +8,17 @@
 
 #include "jsapi.h"
 #include "jsmath.h"
 
 #include "builtin/RegExp.h"
 #include "builtin/String.h"
 #include "builtin/TypedObject.h"
 #include "gc/Heap.h"
+#include "jit/Ion.h"
 #include "jit/JitSpewer.h"
 #include "jit/JSJitFrameIter.h"
 #include "jit/MIR.h"
 #include "jit/MIRGraph.h"
 #include "jit/VMFunctions.h"
 #include "vm/Interpreter.h"
 #include "vm/Iteration.h"
 #include "vm/JSContext.h"
@@ -82,18 +83,18 @@ bool MResumePoint::writeRecoverData(Comp
     }
 
     if (reachablePC) {
       if (JSOp(*bailPC) == JSOP_FUNCALL) {
         // For fun.call(this, ...); the reconstructStackDepth will
         // include the this. When inlining that is not included.  So the
         // exprStackSlots will be one less.
         MOZ_ASSERT(stackDepth - exprStack <= 1);
-      } else if (JSOp(*bailPC) != JSOP_FUNAPPLY && !IsGetPropPC(bailPC) &&
-                 !IsSetPropPC(bailPC)) {
+      } else if (JSOp(*bailPC) != JSOP_FUNAPPLY &&
+                 !IsIonInlinableGetterOrSetterPC(bailPC)) {
         // For fun.apply({}, arguments) the reconstructStackDepth will
         // have stackdepth 4, but it could be that we inlined the
         // funapply. In that case exprStackSlots, will have the real
         // arguments in the slots and not be 4.
 
         // With accessors, we have different stack depths depending on
         // whether or not we inlined the accessor, as the inlined stack
         // contains a callee function that should never have been there
--- a/media/webrtc/trunk/webrtc/modules/video_capture/video_capture_impl.cc
+++ b/media/webrtc/trunk/webrtc/modules/video_capture/video_capture_impl.cc
@@ -147,31 +147,32 @@ int32_t VideoCaptureImpl::IncomingFrame(
   // Not encoded, convert to I420.
   if (frameInfo.videoType != VideoType::kMJPEG &&
       CalcBufferSize(frameInfo.videoType, width, abs(height)) !=
           videoFrameLength) {
     RTC_LOG(LS_ERROR) << "Wrong incoming frame length.";
     return -1;
   }
 
-  int stride_y = width;
-  int stride_uv = (width + 1) / 2;
   int target_width = width;
   int target_height = height;
 
   // SetApplyRotation doesn't take any lock. Make a local copy here.
   bool apply_rotation = apply_rotation_;
 
   if (apply_rotation &&
       (_rotateFrame == kVideoRotation_90 ||
        _rotateFrame == kVideoRotation_270)) {
     target_width = abs(height);
     target_height = width;
   }
 
+  int stride_y = target_width;
+  int stride_uv = (target_width + 1) / 2;
+
   // Setting absolute height (in case it was negative).
   // In Windows, the image starts bottom left, instead of top left.
   // Setting a negative source height, inverts the image (within LibYuv).
 
   // TODO(nisse): Use a pool?
   rtc::scoped_refptr<I420Buffer> buffer = I420Buffer::Create(
       target_width, abs(target_height), stride_y, stride_uv, stride_uv);
 
--- a/testing/geckodriver/doc/Capabilities.md
+++ b/testing/geckodriver/doc/Capabilities.md
@@ -23,23 +23,28 @@ started and run. It may contain any of t
    <th>Type
    <th>Description
   </tr>
  </thead>
 
  <tr id=capability-binary>
   <td><code>binary</code>
   <td align="center">string
-  <td>Absolute path of the Firefox binary,
-   e.g. <code>/usr/bin/firefox</code>
-   or <code>/Applications/Firefox.app/Contents/MacOS/firefox</code>,
+  <td><p>
+   Absolute path of the Firefox binary
    to select which custom browser binary to use.
    If left undefined geckodriver will attempt
    to deduce the default location of Firefox
    on the current system.
+
+   <p>
+   On macOS the path must be absolute to the browser binary,
+   e.g. <code>/Applications/Firefox.app/Contents/MacOS/firefox</code>.
+   Specifying an application bundle such as <code>/Applications/Firefox.app</code>
+   will <em>not</em> work.
  </tr>
 
  <tr id=capability-args>
   <td><code>args</code>
   <td align="center">array&nbsp;of&nbsp;strings
   <td><p>Command line arguments to pass to the Firefox binary.
    These must include the leading dash (<code>-</code>) where required,
    e.g. <code>["-devtools"]</code>.
--- a/testing/mozbase/mozdevice/mozdevice/adb.py
+++ b/testing/mozbase/mozdevice/mozdevice/adb.py
@@ -720,18 +720,30 @@ class ADBDevice(ADBCommand):
             cleared = self.shell_bool('logcat -P ""', timeout=timeout)
         except ADBError:
             cleared = False
         if not cleared:
             self._logger.info("Unable to turn off logcat chatty")
 
         self._selinux = None
         self.enforcing = 'Permissive'
-        self.version = int(self.shell_output("getprop ro.build.version.sdk",
-                                             timeout=timeout))
+
+        self.version = 0
+        while self.version < 1 and (time.time() - start_time) <= float(timeout):
+            try:
+                version = self.shell_output("getprop ro.build.version.sdk",
+                                            timeout=timeout)
+                self.version = int(version)
+            except ValueError:
+                self._logger.info("unexpected ro.build.version.sdk: '%s'" % version)
+                time.sleep(2)
+        if self.version < 1:
+            # note slightly different meaning to the ADBTimeoutError here (and above):
+            # failed to get valid (numeric) version string in all attempts in allowed time
+            raise ADBTimeoutError("ADBDevice: unable to determine ro.build.version.sdk.")
 
         # Do we have pidof?
         if self.version >= version_codes.N:
             self._have_pidof = self.shell_bool("type pidof", timeout=timeout)
         else:
             # unexpected pidof behavior observed on Android 6 in bug 1514363
             self._have_pidof = False
         self._logger.info("Native pidof support: {}".format(self._have_pidof))
--- a/testing/mozharness/mozharness/mozilla/testing/android.py
+++ b/testing/mozharness/mozharness/mozilla/testing/android.py
@@ -319,18 +319,17 @@ class AndroidMixin(object):
         """
            Install the specified apk.
         """
         import mozdevice
         try:
             self.device.install_app(apk)
         except (mozdevice.ADBError, mozdevice.ADBTimeoutError):
             self.info('Failed to install %s on %s' %
-                      (self.installer_path, self.device_name),
-                      exc_info=1)
+                      (self.installer_path, self.device_name))
             self.fatal('INFRA-ERROR: Failed to install %s' %
                        self.installer_path,
                        EXIT_STATUS_DICT[TBPL_RETRY])
 
     def is_boot_completed(self):
         import mozdevice
         try:
             out = self.device.get_prop('sys.boot_completed', timeout=30)
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -412,27 +412,27 @@ class MarionetteCoverageProtocolPart(Cov
     def setup(self):
         self.marionette = self.parent.marionette
 
         if not self.parent.ccov:
             self.is_enabled = False
             return
 
         script = """
-            const {PerTestCoverageUtils} = ChromeUtils.import("resource://reftest/PerTestCoverageUtils.jsm");
+            const {PerTestCoverageUtils} = ChromeUtils.import("chrome://marionette/content/PerTestCoverageUtils.jsm");
             return PerTestCoverageUtils.enabled;
             """
         with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
             self.is_enabled = self.marionette.execute_script(script)
 
     def reset(self):
         script = """
             var callback = arguments[arguments.length - 1];
 
-            const {PerTestCoverageUtils} = ChromeUtils.import("resource://reftest/PerTestCoverageUtils.jsm");
+            const {PerTestCoverageUtils} = ChromeUtils.import("chrome://marionette/content/PerTestCoverageUtils.jsm");
             PerTestCoverageUtils.beforeTest().then(callback, callback);
             """
         with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
             try:
                 error = self.marionette.execute_async_script(script)
                 if error is not None:
                     raise Exception('Failure while resetting counters: %s' % json.dumps(error))
             except (errors.MarionetteException, IOError):
@@ -442,17 +442,17 @@ class MarionetteCoverageProtocolPart(Cov
     def dump(self):
         if len(self.marionette.window_handles):
             handle = self.marionette.window_handles[0]
             self.marionette.switch_to_window(handle)
 
         script = """
             var callback = arguments[arguments.length - 1];
 
-            const {PerTestCoverageUtils} = ChromeUtils.import("resource://reftest/PerTestCoverageUtils.jsm");
+            const {PerTestCoverageUtils} = ChromeUtils.import("chrome://marionette/content/PerTestCoverageUtils.jsm");
             PerTestCoverageUtils.afterTest().then(callback, callback);
             """
         with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
             try:
                 error = self.marionette.execute_async_script(script)
                 if error is not None:
                     raise Exception('Failure while dumping counters: %s' % json.dumps(error))
             except (errors.MarionetteException, IOError):
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js
@@ -145,22 +145,22 @@ add_task(async function test_popup_url()
                `Popup highlight background color should be set to ${POPUP_SELECTED_COLOR}`);
 
   Assert.equal(resultCS.color,
                `rgb(${hexToRGB(POPUP_SELECTED_TEXT_COLOR).join(", ")})`,
                `Popup highlight color should be set to ${POPUP_SELECTED_TEXT_COLOR}`);
 
   results[1].removeAttribute("selected");
 
-  let urlText = document.getAnonymousElementByAttribute(results[1], "anonid", "url-text");
+  let urlText = results[1]._urlText;
   Assert.equal(window.getComputedStyle(urlText).color,
                `rgb(${hexToRGB(POPUP_URL_COLOR_DARK).join(", ")})`,
                `Urlbar popup url color should be set to ${POPUP_URL_COLOR_DARK}`);
 
-  let actionText = document.getAnonymousElementByAttribute(results[1], "anonid", "action-text");
+  let actionText = results[1]._actionText;
   Assert.equal(window.getComputedStyle(actionText).color,
                `rgb(${hexToRGB(POPUP_ACTION_COLOR_DARK).join(", ")})`,
                `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_DARK}`);
 
   let root = document.documentElement;
   Assert.equal(root.getAttribute("lwt-popup-brighttext"),
                "",
                "brighttext should not be set!");
@@ -198,29 +198,29 @@ add_task(async function test_popup_url()
 
   await extension.startup();
 
   popupCS = window.getComputedStyle(popup);
   Assert.equal(popupCS.color,
                `rgb(${hexToRGB(POPUP_TEXT_COLOR_BRIGHT).join(", ")})`,
                `Popup color should be set to ${POPUP_TEXT_COLOR_BRIGHT}`);
 
-  urlText = document.getAnonymousElementByAttribute(results[1], "anonid", "url-text");
+  urlText = results[1]._urlText;
   Assert.equal(window.getComputedStyle(urlText).color,
                `rgb(${hexToRGB(POPUP_URL_COLOR_BRIGHT).join(", ")})`,
                `Urlbar popup url color should be set to ${POPUP_URL_COLOR_BRIGHT}`);
 
-  actionText = document.getAnonymousElementByAttribute(results[1], "anonid", "action-text");
+  actionText = results[1]._actionText;
   Assert.equal(window.getComputedStyle(actionText).color,
                `rgb(${hexToRGB(POPUP_ACTION_COLOR_BRIGHT).join(", ")})`,
                `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_BRIGHT}`);
 
   // Since brighttext is enabled, the seperator color should be
   // POPUP_TEXT_COLOR_BRIGHT with added alpha.
-  let separator = document.getAnonymousElementByAttribute(results[1], "anonid", "separator");
+  let separator = results[1]._separator;
   Assert.equal(window.getComputedStyle(separator).color,
                `rgba(${hexToRGB(POPUP_TEXT_COLOR_BRIGHT).join(", ")}, 0.5)`,
                `Urlbar popup separator color should be set to ${POPUP_TEXT_COLOR_BRIGHT} with alpha`);
 
   Assert.equal(root.getAttribute("lwt-popup-brighttext"),
                "true",
                "brighttext should be set to true!");
   Assert.equal(root.getAttribute("lwt-popup-darktext"),
@@ -240,13 +240,13 @@ add_task(async function test_popup_url()
 
   // Calculate what GrayText should be. May differ between platforms.
   let span = document.createXULElement("span");
   span.style.color = "GrayText";
   document.documentElement.appendChild(span);
   let GRAY_TEXT = window.getComputedStyle(span).color;
   span.remove();
 
-  separator = document.getAnonymousElementByAttribute(results[1], "anonid", "separator");
+  separator = results[1]._separator;
   Assert.equal(window.getComputedStyle(separator).color,
                GRAY_TEXT,
                `Urlbar popup separator color should be set to ${GRAY_TEXT}`);
 });
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
@@ -10,17 +10,17 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 <script>
 "use strict";
 
 // This file defines content scripts.
 /* eslint-env mozilla/frame-script */
 
-let baseUrl = "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/authenticate.sjs";
+let baseUrl = "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/authenticate.sjs";
 function testXHR(url) {
   return new Promise((resolve, reject) => {
     let xhr = new XMLHttpRequest();
     xhr.open("GET", url);
     xhr.onload = resolve;
     xhr.onabort = reject;
     xhr.onerror = reject;
     xhr.send();
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -2,17 +2,17 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 if CONFIG['MOZ_BUILD_APP'] == 'browser':
     DEFINES['MOZ_BUILD_APP_IS_BROWSER'] = True
 
-MOCHITEST_MANIFESTS += ['test/mochitest.ini', 'test/mochitest/mochitest.ini']
+MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 TESTING_JS_MODULES += [
     # Make this file available from the "resource:" URI of the test environment.
     'test/browser/form_basic.html',
     'test/LoginTestUtils.jsm',
 ]
--- a/toolkit/components/passwordmgr/test/.eslintrc.js
+++ b/toolkit/components/passwordmgr/test/.eslintrc.js
@@ -1,13 +1,20 @@
 "use strict";
 
 module.exports = {
   "extends": [
     "plugin:mozilla/mochitest-test",
-    "plugin:mozilla/chrome-test"
   ],
+  "globals": {
+    "promptDone": true,
+    "startTest": true,
+    // Make no-undef happy with our runInParent mixed environments since you
+    // can't indicate a single function is a new env.
+    "assert": true,
+    "addMessageListener": true,
+    "sendAsyncMessage": true,
+
+  },
   "rules": {
     "brace-style": ["error", "1tbs", {"allowSingleLine": false}],
-    "no-undef": "off",
-    "no-unused-vars": "off",
   },
 };
--- a/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
+++ b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-/*
+/**
  * Shared functions generally available for testing login components.
  */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "LoginTestUtils",
 ];
--- a/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
@@ -26,17 +26,16 @@ add_task(async function test_empty_passw
                               doc.getElementById("form-basic-username").value = "username";
                               doc.getElementById("form-basic-password").value = "p";
                               doc.getElementById("form-basic").submit();
                             });
     await promiseShown;
 
     let notificationElement = PopupNotifications.panel.childNodes[0];
     let passwordTextbox = notificationElement.querySelector("#password-notification-password");
-    let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
 
     // Synthesize input to empty the field
     passwordTextbox.focus();
     await EventUtils.synthesizeKey("KEY_ArrowRight");
     await EventUtils.synthesizeKey("KEY_Backspace");
 
     let mainActionButton = notificationElement.button;
     Assert.ok(mainActionButton.disabled, "Main action button is disabled");
--- a/toolkit/components/passwordmgr/test/browser/browser_private_window.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_private_window.js
@@ -128,16 +128,17 @@ add_task(async function test_normal_new_
   await BrowserTestUtils.withNewTab({
     gBrowser: normalWin.gBrowser,
     url: form2Url,
   }, async function(browser) {
     let fieldValues = await submitForm(browser, "formsubmit.sjs", {
       "#pass": "notifyp1",
       "#newpass": "notifyp2",
     });
+    is(fieldValues.password, "notifyp1", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change", PopupNotifications, browser);
     ok(notif, "got notification popup");
     if (notif) {
       notif.remove();
     }
   });
 });
 
@@ -167,16 +168,17 @@ add_task(async function test_normal_with
   await BrowserTestUtils.withNewTab({
     gBrowser: normalWin.gBrowser,
     url: form2Url,
   }, async function(browser) {
     let fieldValues = await submitForm(browser, "formsubmit.sjs", {
       "#pass": "notifyp1",
       "#newpass": "notifyp2",
     });
+    is(fieldValues.password, "notifyp1", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change", PopupNotifications, browser);
     ok(notif, "got notification popup");
     if (notif) {
       notif.remove();
     }
     Services.logins.removeLogin(login);
   });
 });
--- a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
@@ -4,17 +4,17 @@
   <meta charset="utf-8">
   <title>Subtest for Login Manager notifications - Popup Windows</title>
 </head>
 <body>
 <h2>Subtest 11 (popup windows)</h2>
 <script>
 
 // Ignore the '?' and split on |
-[username, password, features, autoClose] = window.location.search.substring(1).split("|");
+let [username, password, features, autoClose] = window.location.search.substring(1).split("|");
 
 var url = "subtst_notifications_11_popup.html?" + username + "|" + password;
 var popupWin = window.open(url, "subtst_11", features);
 
 // Popup window will call this function on form submission.
 function formSubmitted() {
   if (autoClose) {
     popupWin.close();
--- a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
@@ -10,17 +10,17 @@
   <input id="user" name="user">
   <input id="pass" name="pass" type="password">
   <button type='submit'>Submit</button>
 </form>
 
 <script>
 function submitForm() {
   // Get the password from the query string (exclude '?').
-  [username, password] = window.location.search.substring(1).split("|");
+  let [username, password] = window.location.search.substring(1).split("|");
   userField.value = username;
   passField.value = password;
   form.submit();
   window.opener.formSubmitted();
 }
 
 window.onload = submitForm;
 var form      = document.getElementById("form");
deleted file mode 100644
--- a/toolkit/components/passwordmgr/test/mochitest.ini
+++ /dev/null
@@ -1,16 +0,0 @@
-[DEFAULT]
-prefs =
-  signon.rememberSignons=true
-  signon.autofillForms.http=true
-  security.insecure_field_warning.contextual.enabled=false
-  network.auth.non-web-content-triggered-resources-http-auth-allow=true
-skip-if = e10s
-support-files =
-  authenticate.sjs
-  blank.html
-  formsubmit.sjs
-  prompt_common.js
-  pwmgr_common.js
-
-[test_xhr.html]
-skip-if = toolkit == 'android' # Tests desktop prompts
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -2,26 +2,27 @@
 prefs =
   signon.rememberSignons=true
   signon.autofillForms.http=true
   security.insecure_field_warning.contextual.enabled=false
   network.auth.non-web-content-triggered-resources-http-auth-allow=true
 
 support-files =
   ../../../prompts/test/chromeScript.js
-  ../../../prompts/test/prompt_common.js
+  !/toolkit/components/prompts/test/prompt_common.js
   ../../../satchel/test/parent_utils.js
-  ../../../satchel/test/satchel_common.js
+  !/toolkit/components/satchel/test/satchel_common.js
   ../blank.html
   ../browser/form_autofocus_js.html
   ../browser/form_basic.html
   ../browser/formless_basic.html
   ../browser/form_cross_origin_secure_action.html
-  ../pwmgr_common.js
   auth2/authenticate.sjs
+  pwmgr_common.js
+  pwmgr_common_parent.js
   ../authenticate.sjs
 skip-if = toolkit == 'android' && !isFennec # Don't run on GeckoView
 
 [test_autocomplete_https_upgrade.html]
 skip-if = toolkit == 'android' # autocomplete
 [test_autocomplete_sandboxed.html]
 scheme = https
 skip-if = toolkit == 'android' # autocomplete
@@ -88,12 +89,14 @@ skip-if = os == "linux" || toolkit == 'a
 skip-if = e10s || toolkit == 'android' # Tests desktop prompts. e10s: bug 1217876
 [test_prompt_promptAuth.html]
 skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
 [test_prompt_promptAuth_proxy.html]
 skip-if = e10s || os == "linux" || toolkit == 'android' # Tests desktop prompts
 [test_recipe_login_fields.html]
 [test_username_focus.html]
 skip-if = toolkit == 'android' # android:autocomplete.
+[test_xhr.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
 [test_xhr_2.html]
 [test_xml_load.html]
 skip-if = toolkit == 'android' # Tests desktop prompts
 
rename from toolkit/components/passwordmgr/test/pwmgr_common.js
rename to toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js
--- a/toolkit/components/passwordmgr/test/pwmgr_common.js
+++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js
@@ -1,34 +1,40 @@
+/**
+ * Helpers for password manager mochitest-plain tests.
+ */
+
+// Copied from LoginTestUtils.masterPassword.masterPassword to use from the content process.
+const MASTER_PASSWORD = "omgsecret!";
 const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
 
 /**
  * Returns the element with the specified |name| attribute.
  */
 function $_(formNum, name) {
   var form = document.getElementById("form" + formNum);
   if (!form) {
-    logWarning("$_ couldn't find requested form " + formNum);
+    ok(false, "$_ couldn't find requested form " + formNum);
     return null;
   }
 
   var element = form.children.namedItem(name);
   if (!element) {
-    logWarning("$_ couldn't find requested element " + name);
+    ok(false, "$_ couldn't find requested element " + name);
     return null;
   }
 
   // Note that namedItem is a bit stupid, and will prefer an
   // |id| attribute over a |name| attribute when looking for
   // the element. Login Mananger happens to use .namedItem
   // anyway, but let's rigorously check it here anyway so
   // that we don't end up with tests that mistakenly pass.
 
   if (element.getAttribute("name") != name) {
-    logWarning("$_ got confused.");
+    ok(false, "$_ got confused.");
     return null;
   }
 
   return element;
 }
 
 /**
  * Check a form for expected values. If an argument is null, a field's
@@ -100,63 +106,16 @@ function checkUnmodifiedForm(formNum) {
       continue;
     }
 
     is(ele.value, ele.defaultValue, "Test to default value of field " +
        ele.name + " in form " + formNum);
   }
 }
 
-/**
- * Init with a common login
- * If selfFilling is true or non-undefined, fires an event at the page so that
- * the test can start checking filled-in values. Tests that check observer
- * notifications might be confused by this.
- */
-function commonInit(selfFilling) {
-  // eslint-disable-next-line mozilla/use-services
-  var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"].
-              getService(SpecialPowers.Ci.nsILoginManager);
-  ok(pwmgr != null, "Access LoginManager");
-
-  // Check that initial state has no logins
-  var logins = pwmgr.getAllLogins();
-  is(logins.length, 0, "Not expecting logins to be present");
-  var disabledHosts = pwmgr.getAllDisabledHosts();
-  if (disabledHosts.length) {
-    ok(false, "Warning: wasn't expecting disabled hosts to be present.");
-    for (var host of disabledHosts) {
-      pwmgr.setLoginSavingEnabled(host, true);
-    }
-  }
-
-  // Add a login that's used in multiple tests
-  var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"].
-              createInstance(SpecialPowers.Ci.nsILoginInfo);
-  login.init("http://mochi.test:8888", "http://mochi.test:8888", null,
-             "testuser", "testpass", "uname", "pword");
-  pwmgr.addLogin(login);
-
-  // Last sanity check
-  logins = pwmgr.getAllLogins();
-  is(logins.length, 1, "Checking for successful init login");
-  disabledHosts = pwmgr.getAllDisabledHosts();
-  is(disabledHosts.length, 0, "Checking for no disabled hosts");
-
-  if (selfFilling) {
-    return;
-  }
-
-  if (this.sendAsyncMessage) {
-    sendAsyncMessage("registerRunTests");
-  } else {
-    registerRunTests();
-  }
-}
-
 function registerRunTests() {
   return new Promise(resolve => {
     // We provide a general mechanism for our tests to know when they can
     // safely run: we add a final form that we know will be filled in, wait
     // for the login manager to tell us that it's filled in and then continue
     // with the rest of the tests.
     window.addEventListener("DOMContentLoaded", (event) => {
       var form = document.createElement("form");
@@ -184,80 +143,39 @@ function registerRunTests() {
       });
       SpecialPowers.addObserver(observer, "passwordmgr-processed-form");
 
       document.body.appendChild(form);
     });
   });
 }
 
-const masterPassword = "omgsecret!";
-
 function enableMasterPassword() {
   setMasterPassword(true);
 }
 
 function disableMasterPassword() {
   setMasterPassword(false);
 }
 
 function setMasterPassword(enable) {
-  chromeScript.sendSyncMessage("setMasterPassword", { enable });
+  PWMGR_COMMON_PARENT.sendSyncMessage("setMasterPassword", { enable });
 }
 
 function isLoggedIn() {
-  return chromeScript.sendSyncMessage("isLoggedIn")[0][0];
+  return PWMGR_COMMON_PARENT.sendSyncMessage("isLoggedIn")[0][0];
 }
 
 function logoutMasterPassword() {
   runInParent(function parent_logoutMasterPassword() {
-    const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
     var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
     sdr.logoutAndTeardown();
   });
 }
 
-function dumpLogins(pwmgr) {
-  var logins = pwmgr.getAllLogins();
-  ok(true, "----- dumpLogins: have " + logins.length + " logins. -----");
-  for (var i = 0; i < logins.length; i++) {
-    dumpLogin("login #" + i + " --- ", logins[i]);
-  }
-}
-
-function dumpLogin(label, login) {
-  var loginText = "";
-  loginText += "host: ";
-  loginText += login.hostname;
-  loginText += " / formURL: ";
-  loginText += login.formSubmitURL;
-  loginText += " / realm: ";
-  loginText += login.httpRealm;
-  loginText += " / user: ";
-  loginText += login.username;
-  loginText += " / pass: ";
-  loginText += login.password;
-  loginText += " / ufield: ";
-  loginText += login.usernameField;
-  loginText += " / pfield: ";
-  loginText += login.passwordField;
-  ok(true, label + loginText);
-}
-
-function getRecipeParent() {
-  // eslint-disable-next-line no-shadow
-  var { LoginManagerParent } = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
-  if (!LoginManagerParent.recipeParentPromise) {
-    return null;
-  }
-  return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
-    return SpecialPowers.wrap(recipeParent);
-  });
-}
-
 /**
  * Resolves when a specified number of forms have been processed.
  */
 function promiseFormsProcessed(expectedCount = 1) {
   var processedCount = 0;
   return new Promise((resolve, reject) => {
     function onProcessedForm(subject, topic, data) {
       processedCount++;
@@ -268,57 +186,57 @@ function promiseFormsProcessed(expectedC
     }
     SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form");
   });
 }
 
 function loadRecipes(recipes) {
   info("Loading recipes");
   return new Promise(resolve => {
-    chromeScript.addMessageListener("loadedRecipes", function loaded() {
-      chromeScript.removeMessageListener("loadedRecipes", loaded);
+    PWMGR_COMMON_PARENT.addMessageListener("loadedRecipes", function loaded() {
+      PWMGR_COMMON_PARENT.removeMessageListener("loadedRecipes", loaded);
       resolve(recipes);
     });
-    chromeScript.sendAsyncMessage("loadRecipes", recipes);
+    PWMGR_COMMON_PARENT.sendAsyncMessage("loadRecipes", recipes);
   });
 }
 
 function resetRecipes() {
   info("Resetting recipes");
   return new Promise(resolve => {
-    chromeScript.addMessageListener("recipesReset", function reset() {
-      chromeScript.removeMessageListener("recipesReset", reset);
+    PWMGR_COMMON_PARENT.addMessageListener("recipesReset", function reset() {
+      PWMGR_COMMON_PARENT.removeMessageListener("recipesReset", reset);
       resolve();
     });
-    chromeScript.sendAsyncMessage("resetRecipes");
+    PWMGR_COMMON_PARENT.sendAsyncMessage("resetRecipes");
   });
 }
 
 function promiseStorageChanged(expectedChangeTypes) {
   return new Promise((resolve, reject) => {
     function onStorageChanged({ topic, data }) {
       let changeType = expectedChangeTypes.shift();
       is(data, changeType, "Check expected passwordmgr-storage-changed type");
       if (expectedChangeTypes.length === 0) {
-        chromeScript.removeMessageListener("storageChanged", onStorageChanged);
+        PWMGR_COMMON_PARENT.removeMessageListener("storageChanged", onStorageChanged);
         resolve();
       }
     }
-    chromeScript.addMessageListener("storageChanged", onStorageChanged);
+    PWMGR_COMMON_PARENT.addMessageListener("storageChanged", onStorageChanged);
   });
 }
 
 function promisePromptShown(expectedTopic) {
   return new Promise((resolve, reject) => {
     function onPromptShown({ topic, data }) {
       is(topic, expectedTopic, "Check expected prompt topic");
-      chromeScript.removeMessageListener("promptShown", onPromptShown);
+      PWMGR_COMMON_PARENT.removeMessageListener("promptShown", onPromptShown);
       resolve();
     }
-    chromeScript.addMessageListener("promptShown", onPromptShown);
+    PWMGR_COMMON_PARENT.addMessageListener("promptShown", onPromptShown);
   });
 }
 
 /**
  * Run a function synchronously in the parent process and destroy it in the test cleanup function.
  * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
  *                                           or the URL to a JS file.
  * @return {Object} - the return value of loadChromeScript providing message-related methods.
@@ -334,182 +252,83 @@ function runInParent(aFunctionOrURL) {
 
 /**
  * Run commonInit synchronously in the parent then run the test function after the runTests event.
  *
  * @param {Function} aFunction The test function to run
  */
 function runChecksAfterCommonInit(aFunction = null) {
   SimpleTest.waitForExplicitFinish();
-  let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
   if (aFunction) {
     window.addEventListener("runTests", aFunction);
-    pwmgrCommonScript.addMessageListener("registerRunTests", () => registerRunTests());
+    PWMGR_COMMON_PARENT.addMessageListener("registerRunTests", () => registerRunTests());
   }
-  pwmgrCommonScript.sendSyncMessage("setupParent");
-  return pwmgrCommonScript;
+  PWMGR_COMMON_PARENT.sendSyncMessage("setupParent");
+  return PWMGR_COMMON_PARENT;
 }
 
-// Code to run when loaded as a chrome script in tests via loadChromeScript
-if (this.addMessageListener) {
-  var SpecialPowers = { Cc, Ci, Cr, Cu };
-  var ok, is;
-  // Ignore ok/is in commonInit since they aren't defined in a chrome script.
-  ok = is = () => {};
+// Begin code that runs immediately for all tests that include this file.
 
-  ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-  var {LoginHelper} = ChromeUtils.import("resource://gre/modules/LoginHelper.jsm");
-  var {LoginManagerParent} = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
-  var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const PWMGR_COMMON_PARENT = runInParent(SimpleTest.getTestFileURL("pwmgr_common_parent.js"));
 
-  function onStorageChanged(subject, topic, data) {
-    sendAsyncMessage("storageChanged", {
-      topic,
-      data,
-    });
-  }
-  Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed");
-
-  function onPrompt(subject, topic, data) {
-    sendAsyncMessage("promptShown", {
-      topic,
-      data,
-    });
-  }
-  Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change");
-  Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save");
-
-  addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
-    commonInit(selfFilling);
-    sendAsyncMessage("doneSetup");
-  });
+SimpleTest.registerCleanupFunction(() => {
+  SpecialPowers.popPrefEnv();
+  runInParent(function cleanupParent() {
+    // eslint-disable-next-line no-shadow
+    const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+    // eslint-disable-next-line no-shadow
+    const {LoginManagerParent} = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
 
-  addMessageListener("loadRecipes", function(recipes) {
-    (async function() {
-      var recipeParent = await LoginManagerParent.recipeParentPromise;
-      await recipeParent.load(recipes);
-      sendAsyncMessage("loadedRecipes", recipes);
-    })();
-  });
+    // Remove all logins and disabled hosts
+    Services.logins.removeAllLogins();
 
-  addMessageListener("resetRecipes", function() {
-    (async function() {
-      let recipeParent = await LoginManagerParent.recipeParentPromise;
-      await recipeParent.reset();
-      sendAsyncMessage("recipesReset");
-    })();
-  });
-
-  addMessageListener("proxyLoginManager", msg => {
-    // Recreate nsILoginInfo objects from vanilla JS objects.
-    let recreatedArgs = msg.args.map((arg, index) => {
-      if (msg.loginInfoIndices.includes(index)) {
-        return LoginHelper.vanillaObjectToLogin(arg);
-      }
+    let disabledHosts = Services.logins.getAllDisabledHosts();
+    disabledHosts.forEach(host => Services.logins.setLoginSavingEnabled(host, true));
 
-      return arg;
-    });
-
-    let rv = Services.logins[msg.methodName](...recreatedArgs);
-    if (rv instanceof Ci.nsILoginInfo) {
-      rv = LoginHelper.loginToVanillaObject(rv);
-    } else if (Array.isArray(rv) && rv.length > 0 && rv[0] instanceof Ci.nsILoginInfo) {
-      rv = rv.map(login => LoginHelper.loginToVanillaObject(login));
-    }
-    return rv;
-  });
+    let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].
+                  getService(Ci.nsIHttpAuthManager);
+    authMgr.clearAll();
 
-  addMessageListener("isLoggedIn", () => {
-    // This can't use the LoginManager proxy below since it's not a method.
-    return Services.logins.isLoggedIn;
-  });
-
-  addMessageListener("setMasterPassword", ({ enable }) => {
-    let oldPW, newPW;
-    if (enable) {
-      oldPW = "";
-      newPW = masterPassword;
-    } else {
-      oldPW = masterPassword;
-      newPW = "";
+    if (LoginManagerParent._recipeManager) {
+      LoginManagerParent._recipeManager.reset();
     }
-    // Set master password. Note that this logs in the user if no password was
-    // set before. But after logging out the next invocation of pwmgr can
-    // trigger a MP prompt.
 
-    var pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB);
-    var token = pk11db.getInternalKeyToken();
-    dump("MP change from " + oldPW + " to " + newPW + "\n");
-    token.changePassword(oldPW, newPW);
-    token.logoutSimple();
-  });
-
-  Services.mm.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) {
-    sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
+    // Cleanup PopupNotifications (if on a relevant platform)
+    let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+    if (chromeWin && chromeWin.PopupNotifications) {
+      let notes = chromeWin.PopupNotifications._currentNotifications;
+      if (notes.length > 0) {
+        dump("Removing " + notes.length + " popup notifications.\n");
+      }
+      for (let note of notes) {
+        note.remove();
+      }
+    }
   });
-} else {
-  // Code to only run in the mochitest pages (not in the chrome script).
-  SimpleTest.registerCleanupFunction(() => {
-    SpecialPowers.popPrefEnv();
-    runInParent(function cleanupParent() {
-      // eslint-disable-next-line no-shadow
-      const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-      // eslint-disable-next-line no-shadow
-      const {LoginManagerParent} = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
-
-      // Remove all logins and disabled hosts
-      Services.logins.removeAllLogins();
-
-      let disabledHosts = Services.logins.getAllDisabledHosts();
-      disabledHosts.forEach(host => Services.logins.setLoginSavingEnabled(host, true));
-
-      let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].
-                    getService(Ci.nsIHttpAuthManager);
-      authMgr.clearAll();
-
-      if (LoginManagerParent._recipeManager) {
-        LoginManagerParent._recipeManager.reset();
-      }
+});
 
-      // Cleanup PopupNotifications (if on a relevant platform)
-      let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-      if (chromeWin && chromeWin.PopupNotifications) {
-        let notes = chromeWin.PopupNotifications._currentNotifications;
-        if (notes.length > 0) {
-          dump("Removing " + notes.length + " popup notifications.\n");
+let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {});
+/**
+ * Proxy for Services.logins (nsILoginManager).
+ * Only supports arguments which support structured clone plus {nsILoginInfo}
+ * Assumes properties are methods.
+ */
+this.LoginManager = new Proxy({}, {
+  get(target, prop, receiver) {
+    return (...args) => {
+      let loginInfoIndices = [];
+      let cloneableArgs = args.map((val, index) => {
+        if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) {
+          loginInfoIndices.push(index);
+          return LoginHelper.loginToVanillaObject(val);
         }
-        for (let note of notes) {
-          note.remove();
-        }
-      }
-    });
-  });
-
 
-  // eslint-disable-next-line no-shadow
-  let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {});
-  /**
-   * Proxy for Services.logins (nsILoginManager).
-   * Only supports arguments which support structured clone plus {nsILoginInfo}
-   * Assumes properties are methods.
-   */
-  this.LoginManager = new Proxy({}, {
-    get(target, prop, receiver) {
-      return (...args) => {
-        let loginInfoIndices = [];
-        let cloneableArgs = args.map((val, index) => {
-          if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) {
-            loginInfoIndices.push(index);
-            return LoginHelper.loginToVanillaObject(val);
-          }
+        return val;
+      });
 
-          return val;
-        });
-
-        return chromeScript.sendSyncMessage("proxyLoginManager", {
-          args: cloneableArgs,
-          loginInfoIndices,
-          methodName: prop,
-        })[0][0];
-      };
-    },
-  });
-}
+      return PWMGR_COMMON_PARENT.sendSyncMessage("proxyLoginManager", {
+        args: cloneableArgs,
+        loginInfoIndices,
+        methodName: prop,
+      })[0][0];
+    };
+  },
+});
copy from toolkit/components/passwordmgr/test/pwmgr_common.js
copy to toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js
--- a/toolkit/components/passwordmgr/test/pwmgr_common.js
+++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js
@@ -1,228 +1,70 @@
-const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
-
 /**
- * Returns the element with the specified |name| attribute.
+ * Loaded as a frame script to do privileged things in mochitest-plain tests.
+ * See pwmgr_common.js for the content process companion.
  */
-function $_(formNum, name) {
-  var form = document.getElementById("form" + formNum);
-  if (!form) {
-    logWarning("$_ couldn't find requested form " + formNum);
-    return null;
-  }
-
-  var element = form.children.namedItem(name);
-  if (!element) {
-    logWarning("$_ couldn't find requested element " + name);
-    return null;
-  }
 
-  // Note that namedItem is a bit stupid, and will prefer an
-  // |id| attribute over a |name| attribute when looking for
-  // the element. Login Mananger happens to use .namedItem
-  // anyway, but let's rigorously check it here anyway so
-  // that we don't end up with tests that mistakenly pass.
-
-  if (element.getAttribute("name") != name) {
-    logWarning("$_ got confused.");
-    return null;
-  }
-
-  return element;
-}
-
-/**
- * Check a form for expected values. If an argument is null, a field's
- * expected value will be the default value.
- *
- * <form id="form#">
- * checkForm(#, "foo");
- */
-function checkForm(formNum, val1, val2, val3) {
-  var e, form = document.getElementById("form" + formNum);
-  ok(form, "Locating form " + formNum);
-
-  var numToCheck = arguments.length - 1;
+"use strict";
 
-  if (!numToCheck--) {
-    return;
-  }
-  e = form.elements[0];
-  if (val1 == null) {
-    is(e.value, e.defaultValue, "Test default value of field " + e.name +
-       " in form " + formNum);
-  } else {
-    is(e.value, val1, "Test value of field " + e.name +
-       " in form " + formNum);
-  }
-
-
-  if (!numToCheck--) {
-    return;
-  }
-  e = form.elements[1];
-  if (val2 == null) {
-    is(e.value, e.defaultValue, "Test default value of field " + e.name +
-       " in form " + formNum);
-  } else {
-    is(e.value, val2, "Test value of field " + e.name +
-       " in form " + formNum);
-  }
-
+// assert is available to chrome scripts loaded via SpecialPowers.loadChromeScript.
+/* global assert */
+/* eslint-env mozilla/frame-script */
 
-  if (!numToCheck--) {
-    return;
-  }
-  e = form.elements[2];
-  if (val3 == null) {
-    is(e.value, e.defaultValue, "Test default value of field " + e.name +
-       " in form " + formNum);
-  } else {
-    is(e.value, val3, "Test value of field " + e.name +
-       " in form " + formNum);
-  }
-}
-
-/**
- * Check a form for unmodified values from when page was loaded.
- *
- * <form id="form#">
- * checkUnmodifiedForm(#);
- */
-function checkUnmodifiedForm(formNum) {
-  var form = document.getElementById("form" + formNum);
-  ok(form, "Locating form " + formNum);
-
-  for (var i = 0; i < form.elements.length; i++) {
-    var ele = form.elements[i];
-
-    // No point in checking form submit/reset buttons.
-    if (ele.type == "submit" || ele.type == "reset") {
-      continue;
-    }
-
-    is(ele.value, ele.defaultValue, "Test to default value of field " +
-       ele.name + " in form " + formNum);
-  }
-}
+var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+var {LoginHelper} = ChromeUtils.import("resource://gre/modules/LoginHelper.jsm");
+var {LoginManagerParent} = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
+const {LoginTestUtils} = ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 /**
  * Init with a common login
  * If selfFilling is true or non-undefined, fires an event at the page so that
  * the test can start checking filled-in values. Tests that check observer
  * notifications might be confused by this.
  */
 function commonInit(selfFilling) {
-  // eslint-disable-next-line mozilla/use-services
-  var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"].
-              getService(SpecialPowers.Ci.nsILoginManager);
-  ok(pwmgr != null, "Access LoginManager");
+  var pwmgr = Services.logins;
+  assert.ok(pwmgr != null, "Access LoginManager");
 
   // Check that initial state has no logins
   var logins = pwmgr.getAllLogins();
-  is(logins.length, 0, "Not expecting logins to be present");
+  assert.equal(logins.length, 0, "Not expecting logins to be present");
   var disabledHosts = pwmgr.getAllDisabledHosts();
   if (disabledHosts.length) {
-    ok(false, "Warning: wasn't expecting disabled hosts to be present.");
+    assert.ok(false, "Warning: wasn't expecting disabled hosts to be present.");
     for (var host of disabledHosts) {
       pwmgr.setLoginSavingEnabled(host, true);
     }
   }
 
   // Add a login that's used in multiple tests
-  var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"].
-              createInstance(SpecialPowers.Ci.nsILoginInfo);
+  var login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+              createInstance(Ci.nsILoginInfo);
   login.init("http://mochi.test:8888", "http://mochi.test:8888", null,
              "testuser", "testpass", "uname", "pword");
   pwmgr.addLogin(login);
 
   // Last sanity check
   logins = pwmgr.getAllLogins();
-  is(logins.length, 1, "Checking for successful init login");
+  assert.equal(logins.length, 1, "Checking for successful init login");
   disabledHosts = pwmgr.getAllDisabledHosts();
-  is(disabledHosts.length, 0, "Checking for no disabled hosts");
+  assert.equal(disabledHosts.length, 0, "Checking for no disabled hosts");
 
   if (selfFilling) {
     return;
   }
 
-  if (this.sendAsyncMessage) {
-    sendAsyncMessage("registerRunTests");
-  } else {
-    registerRunTests();
-  }
+  // Notify the content side that initialization is done and tests can start.
+  sendAsyncMessage("registerRunTests");
 }
 
-function registerRunTests() {
-  return new Promise(resolve => {
-    // We provide a general mechanism for our tests to know when they can
-    // safely run: we add a final form that we know will be filled in, wait
-    // for the login manager to tell us that it's filled in and then continue
-    // with the rest of the tests.
-    window.addEventListener("DOMContentLoaded", (event) => {
-      var form = document.createElement("form");
-      form.id = "observerforcer";
-      var username = document.createElement("input");
-      username.name = "testuser";
-      form.appendChild(username);
-      var password = document.createElement("input");
-      password.name = "testpass";
-      password.type = "password";
-      form.appendChild(password);
-
-      var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
-        var formLikeRoot = subject;
-        if (formLikeRoot.id !== "observerforcer") {
-          return;
-        }
-        SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
-        formLikeRoot.remove();
-        SimpleTest.executeSoon(() => {
-          var runTestEvent = new Event("runTests");
-          window.dispatchEvent(runTestEvent);
-          resolve();
-        });
-      });
-      SpecialPowers.addObserver(observer, "passwordmgr-processed-form");
-
-      document.body.appendChild(form);
-    });
-  });
-}
-
-const masterPassword = "omgsecret!";
-
-function enableMasterPassword() {
-  setMasterPassword(true);
-}
-
-function disableMasterPassword() {
-  setMasterPassword(false);
-}
-
-function setMasterPassword(enable) {
-  chromeScript.sendSyncMessage("setMasterPassword", { enable });
-}
-
-function isLoggedIn() {
-  return chromeScript.sendSyncMessage("isLoggedIn")[0][0];
-}
-
-function logoutMasterPassword() {
-  runInParent(function parent_logoutMasterPassword() {
-    const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
-    var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
-    sdr.logoutAndTeardown();
-  });
-}
-
-function dumpLogins(pwmgr) {
-  var logins = pwmgr.getAllLogins();
-  ok(true, "----- dumpLogins: have " + logins.length + " logins. -----");
+function dumpLogins() {
+  let logins = Services.logins.getAllLogins();
+  assert.ok(true, "----- dumpLogins: have " + logins.length + " logins. -----");
   for (var i = 0; i < logins.length; i++) {
     dumpLogin("login #" + i + " --- ", logins[i]);
   }
 }
 
 function dumpLogin(label, login) {
   var loginText = "";
   loginText += "host: ";
@@ -234,282 +76,83 @@ function dumpLogin(label, login) {
   loginText += " / user: ";
   loginText += login.username;
   loginText += " / pass: ";
   loginText += login.password;
   loginText += " / ufield: ";
   loginText += login.usernameField;
   loginText += " / pfield: ";
   loginText += login.passwordField;
-  ok(true, label + loginText);
-}
-
-function getRecipeParent() {
-  // eslint-disable-next-line no-shadow
-  var { LoginManagerParent } = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
-  if (!LoginManagerParent.recipeParentPromise) {
-    return null;
-  }
-  return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
-    return SpecialPowers.wrap(recipeParent);
-  });
-}
-
-/**
- * Resolves when a specified number of forms have been processed.
- */
-function promiseFormsProcessed(expectedCount = 1) {
-  var processedCount = 0;
-  return new Promise((resolve, reject) => {
-    function onProcessedForm(subject, topic, data) {
-      processedCount++;
-      if (processedCount == expectedCount) {
-        SpecialPowers.removeObserver(onProcessedForm, "passwordmgr-processed-form");
-        resolve(SpecialPowers.Cu.waiveXrays(subject), data);
-      }
-    }
-    SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form");
-  });
-}
-
-function loadRecipes(recipes) {
-  info("Loading recipes");
-  return new Promise(resolve => {
-    chromeScript.addMessageListener("loadedRecipes", function loaded() {
-      chromeScript.removeMessageListener("loadedRecipes", loaded);
-      resolve(recipes);
-    });
-    chromeScript.sendAsyncMessage("loadRecipes", recipes);
-  });
-}
-
-function resetRecipes() {
-  info("Resetting recipes");
-  return new Promise(resolve => {
-    chromeScript.addMessageListener("recipesReset", function reset() {
-      chromeScript.removeMessageListener("recipesReset", reset);
-      resolve();
-    });
-    chromeScript.sendAsyncMessage("resetRecipes");
-  });
-}
-
-function promiseStorageChanged(expectedChangeTypes) {
-  return new Promise((resolve, reject) => {
-    function onStorageChanged({ topic, data }) {
-      let changeType = expectedChangeTypes.shift();
-      is(data, changeType, "Check expected passwordmgr-storage-changed type");
-      if (expectedChangeTypes.length === 0) {
-        chromeScript.removeMessageListener("storageChanged", onStorageChanged);
-        resolve();
-      }
-    }
-    chromeScript.addMessageListener("storageChanged", onStorageChanged);
-  });
-}
-
-function promisePromptShown(expectedTopic) {
-  return new Promise((resolve, reject) => {
-    function onPromptShown({ topic, data }) {
-      is(topic, expectedTopic, "Check expected prompt topic");
-      chromeScript.removeMessageListener("promptShown", onPromptShown);
-      resolve();
-    }
-    chromeScript.addMessageListener("promptShown", onPromptShown);
-  });
-}
-
-/**
- * Run a function synchronously in the parent process and destroy it in the test cleanup function.
- * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
- *                                           or the URL to a JS file.
- * @return {Object} - the return value of loadChromeScript providing message-related methods.
- *                    @see loadChromeScript in specialpowersAPI.js
- */
-function runInParent(aFunctionOrURL) {
-  let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL);
-  SimpleTest.registerCleanupFunction(() => {
-    chromeScript.destroy();
-  });
-  return chromeScript;
-}
-
-/**
- * Run commonInit synchronously in the parent then run the test function after the runTests event.
- *
- * @param {Function} aFunction The test function to run
- */
-function runChecksAfterCommonInit(aFunction = null) {
-  SimpleTest.waitForExplicitFinish();
-  let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-  if (aFunction) {
-    window.addEventListener("runTests", aFunction);
-    pwmgrCommonScript.addMessageListener("registerRunTests", () => registerRunTests());
-  }
-  pwmgrCommonScript.sendSyncMessage("setupParent");
-  return pwmgrCommonScript;
+  assert.ok(true, label + loginText);
 }
 
-// Code to run when loaded as a chrome script in tests via loadChromeScript
-if (this.addMessageListener) {
-  var SpecialPowers = { Cc, Ci, Cr, Cu };
-  var ok, is;
-  // Ignore ok/is in commonInit since they aren't defined in a chrome script.
-  ok = is = () => {};
-
-  ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-  var {LoginHelper} = ChromeUtils.import("resource://gre/modules/LoginHelper.jsm");
-  var {LoginManagerParent} = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
-  var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+function onStorageChanged(subject, topic, data) {
+  sendAsyncMessage("storageChanged", {
+    topic,
+    data,
+  });
+}
+Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed");
 
-  function onStorageChanged(subject, topic, data) {
-    sendAsyncMessage("storageChanged", {
-      topic,
-      data,
-    });
-  }
-  Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed");
+function onPrompt(subject, topic, data) {
+  sendAsyncMessage("promptShown", {
+    topic,
+    data,
+  });
+}
+Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change");
+Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save");
 
-  function onPrompt(subject, topic, data) {
-    sendAsyncMessage("promptShown", {
-      topic,
-      data,
-    });
-  }
-  Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change");
-  Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save");
 
-  addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
-    commonInit(selfFilling);
-    sendAsyncMessage("doneSetup");
-  });
+// Begin message listeners
 
-  addMessageListener("loadRecipes", function(recipes) {
-    (async function() {
-      var recipeParent = await LoginManagerParent.recipeParentPromise;
-      await recipeParent.load(recipes);
-      sendAsyncMessage("loadedRecipes", recipes);
-    })();
-  });
+addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
+  commonInit(selfFilling);
+  sendAsyncMessage("doneSetup");
+});
 
-  addMessageListener("resetRecipes", function() {
-    (async function() {
-      let recipeParent = await LoginManagerParent.recipeParentPromise;
-      await recipeParent.reset();
-      sendAsyncMessage("recipesReset");
-    })();
-  });
+addMessageListener("loadRecipes", async function(recipes) {
+  var recipeParent = await LoginManagerParent.recipeParentPromise;
+  await recipeParent.load(recipes);
+  sendAsyncMessage("loadedRecipes", recipes);
+});
 
-  addMessageListener("proxyLoginManager", msg => {
-    // Recreate nsILoginInfo objects from vanilla JS objects.
-    let recreatedArgs = msg.args.map((arg, index) => {
-      if (msg.loginInfoIndices.includes(index)) {
-        return LoginHelper.vanillaObjectToLogin(arg);
-      }
-
-      return arg;
-    });
+addMessageListener("resetRecipes", async function() {
+  let recipeParent = await LoginManagerParent.recipeParentPromise;
+  await recipeParent.reset();
+  sendAsyncMessage("recipesReset");
+});
 
-    let rv = Services.logins[msg.methodName](...recreatedArgs);
-    if (rv instanceof Ci.nsILoginInfo) {
-      rv = LoginHelper.loginToVanillaObject(rv);
-    } else if (Array.isArray(rv) && rv.length > 0 && rv[0] instanceof Ci.nsILoginInfo) {
-      rv = rv.map(login => LoginHelper.loginToVanillaObject(login));
+addMessageListener("proxyLoginManager", msg => {
+  // Recreate nsILoginInfo objects from vanilla JS objects.
+  let recreatedArgs = msg.args.map((arg, index) => {
+    if (msg.loginInfoIndices.includes(index)) {
+      return LoginHelper.vanillaObjectToLogin(arg);
     }
-    return rv;
-  });
 
-  addMessageListener("isLoggedIn", () => {
-    // This can't use the LoginManager proxy below since it's not a method.
-    return Services.logins.isLoggedIn;
+    return arg;
   });
 
-  addMessageListener("setMasterPassword", ({ enable }) => {
-    let oldPW, newPW;
-    if (enable) {
-      oldPW = "";
-      newPW = masterPassword;
-    } else {
-      oldPW = masterPassword;
-      newPW = "";
-    }
-    // Set master password. Note that this logs in the user if no password was
-    // set before. But after logging out the next invocation of pwmgr can
-    // trigger a MP prompt.
-
-    var pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB);
-    var token = pk11db.getInternalKeyToken();
-    dump("MP change from " + oldPW + " to " + newPW + "\n");
-    token.changePassword(oldPW, newPW);
-    token.logoutSimple();
-  });
-
-  Services.mm.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) {
-    sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
-  });
-} else {
-  // Code to only run in the mochitest pages (not in the chrome script).
-  SimpleTest.registerCleanupFunction(() => {
-    SpecialPowers.popPrefEnv();
-    runInParent(function cleanupParent() {
-      // eslint-disable-next-line no-shadow
-      const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-      // eslint-disable-next-line no-shadow
-      const {LoginManagerParent} = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
-
-      // Remove all logins and disabled hosts
-      Services.logins.removeAllLogins();
-
-      let disabledHosts = Services.logins.getAllDisabledHosts();
-      disabledHosts.forEach(host => Services.logins.setLoginSavingEnabled(host, true));
-
-      let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].
-                    getService(Ci.nsIHttpAuthManager);
-      authMgr.clearAll();
+  let rv = Services.logins[msg.methodName](...recreatedArgs);
+  if (rv instanceof Ci.nsILoginInfo) {
+    rv = LoginHelper.loginToVanillaObject(rv);
+  } else if (Array.isArray(rv) && rv.length > 0 && rv[0] instanceof Ci.nsILoginInfo) {
+    rv = rv.map(login => LoginHelper.loginToVanillaObject(login));
+  }
+  return rv;
+});
 
-      if (LoginManagerParent._recipeManager) {
-        LoginManagerParent._recipeManager.reset();
-      }
-
-      // Cleanup PopupNotifications (if on a relevant platform)
-      let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-      if (chromeWin && chromeWin.PopupNotifications) {
-        let notes = chromeWin.PopupNotifications._currentNotifications;
-        if (notes.length > 0) {
-          dump("Removing " + notes.length + " popup notifications.\n");
-        }
-        for (let note of notes) {
-          note.remove();
-        }
-      }
-    });
-  });
-
+addMessageListener("isLoggedIn", () => {
+  // This can't use the LoginManager proxy in pwmgr_common.js since it's not a method.
+  return Services.logins.isLoggedIn;
+});
 
-  // eslint-disable-next-line no-shadow
-  let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {});
-  /**
-   * Proxy for Services.logins (nsILoginManager).
-   * Only supports arguments which support structured clone plus {nsILoginInfo}
-   * Assumes properties are methods.
-   */
-  this.LoginManager = new Proxy({}, {
-    get(target, prop, receiver) {
-      return (...args) => {
-        let loginInfoIndices = [];
-        let cloneableArgs = args.map((val, index) => {
-          if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) {
-            loginInfoIndices.push(index);
-            return LoginHelper.loginToVanillaObject(val);
-          }
+addMessageListener("setMasterPassword", ({ enable }) => {
+  if (enable) {
+    LoginTestUtils.masterPassword.enable();
+  } else {
+    LoginTestUtils.masterPassword.disable();
+  }
+});
 
-          return val;
-        });
-
-        return chromeScript.sendSyncMessage("proxyLoginManager", {
-          args: cloneableArgs,
-          loginInfoIndices,
-          methodName: prop,
-        })[0][0];
-      };
-    },
-  });
-}
+Services.mm.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) {
+  sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
+});
--- a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script>
 const chromeScript = runChecksAfterCommonInit(false);
 
 runInParent(function addLogins() {
--- a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html
@@ -1,24 +1,23 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test form field autocomplete in sandboxed documents (null principal)</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 
 <script>
-
 SpecialPowers.pushPrefEnv({"set": [["security.allow_eval_with_system_principal",
                                     true]]});
 
 var chromeScript = runChecksAfterCommonInit();
 
 var setupScript = runInParent(function setup() {
   const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script>
 const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
 const CROSS_ORIGIN_SECURE_PATH = TESTS_DIR + "mochitest/form_cross_origin_secure_action.html";
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
@@ -5,18 +5,17 @@
   <title>Test password-only forms should prefer a password-only login when present</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: Bug 444968
 <script>
-let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-pwmgrCommonScript.sendSyncMessage("setupParent", { selfFilling: true });
+PWMGR_COMMON_PARENT.sendSyncMessage("setupParent", { selfFilling: true });
 
 SimpleTest.waitForExplicitFinish();
 
 let chromeScript = runInParent(function chromeSetup() {
   const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
   let login1A  = Cc["@mozilla.org/login-manager/loginInfo;1"].
                  createInstance(Ci.nsILoginInfo);
--- a/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test form field autofill in sandboxed documents (null principal)</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 
 <script>
 var chromeScript = runChecksAfterCommonInit();
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test login autocomplete is activated when focused by js on load</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script>
 
 SpecialPowers.pushPrefEnv({"set": [["security.allow_eval_with_system_principal",
                                     true]]});
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
@@ -35,16 +35,17 @@ function startTest() {
   is($_(1, "qword").value, "", "Checking password 1B");
 
   // Fill in the username and password fields, for account creation.
   // Form 1
   $_(1, "uname").value = "newuser1";
   $_(1, "pword").value = "newpass1";
   $_(1, "qword").value = "newpass1";
 
+  // eslint-disable-next-line no-unused-vars
   var button = getFormSubmitButton(1);
 
   todo(false, "form submission disabled, can't auto-accept dialog yet");
   SimpleTest.finish();
 }
 
 
 // Called by each form's onsubmit handler.
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test basic login autocomplete</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: multiple login autocomplete
 
 <script>
 var chromeScript = runChecksAfterCommonInit();
@@ -51,16 +51,18 @@ var setupScript = runInParent(function s
 
   var login7  = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete4", null,
                                 "form8user", "form8pass", "uname", "pword");
 
   var login8A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
                                 "form9userAB", "form9pass", "uname", "pword");
   var login8B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
                                 "form9userAAB", "form9pass", "uname", "pword");
+  // Reference by `eval` in the message listeners below.
+  // eslint-disable-next-line no-unused-vars
   var login8C = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
                                 "form9userAABzz", "form9pass", "uname", "pword");
 
   var login10 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete7", null,
                                 "testuser10", "testpass10", "uname", "pword");
 
 
   // try/catch in case someone runs the tests manually, twice.
@@ -448,18 +450,17 @@ add_task(async function test_form1_delet
 
   // XXX tried sending character "t" before/during dropdown to test
   // filtering, but had no luck. Seemed like the character was getting lost.
   // Setting uname.value didn't seem to work either. This works with a human
   // driver, so I'm not sure what's up.
 
   // Delete the first entry (of 4), "tempuser1"
   synthesizeKey("KEY_ArrowDown");
-  var numLogins;
-  numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  let numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 5, "Correct number of logins before deleting one");
 
   let countChangedPromise = notifyMenuChanged(3);
   var deletionPromise = promiseStorageChanged(["removeLogin"]);
   // On OS X, shift-backspace and shift-delete work, just delete does not.
   // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
   synthesizeKey("KEY_Delete", {shiftKey: true});
   await deletionPromise;
@@ -492,17 +493,17 @@ add_task(async function test_form1_delet
   synthesizeKey("KEY_ArrowDown"); // open
   await shownPromise;
 
   // Delete the second entry (of 3), "testuser3"
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Delete", {shiftKey: true});
   checkACForm("", "");
-  numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  let numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 3, "Correct number of logins after deleting one");
   synthesizeKey("KEY_Enter");
   await promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(async function test_form1_first_after_deletion2() {
   restoreForm();
@@ -524,17 +525,17 @@ add_task(async function test_form1_delet
   await shownPromise;
 
   /* test 54 */
   // Delete the last entry (of 2), "zzzuser4"
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Delete", {shiftKey: true});
   checkACForm("", "");
-  numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  let numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 2, "Correct number of logins after deleting one");
   synthesizeKey("KEY_Enter");
   await promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(async function test_form1_first_after_3_deletions() {
   restoreForm();
@@ -555,17 +556,17 @@ add_task(async function test_form1_check
   synthesizeKey("KEY_ArrowDown"); // open
   await shownPromise;
 
   /* test 56 */
   // Delete the only remaining entry, "testuser2"
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Delete", {shiftKey: true});
   checkACForm("", "");
-  numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  let numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 1, "Correct number of logins after deleting one");
 
   // remove the login that's not shown in the list.
   setupScript.sendSyncMessage("removeLogin", "login0");
 });
 
 /* Tests for single-user forms for ignoring autocomplete=off */
 add_task(async function test_form2() {
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
@@ -9,24 +9,24 @@
 </head>
 <body>
 Login Manager test: html5 input types (email, tel, url, etc.)
 <script>
 runChecksAfterCommonInit(() => startTest());
 
 runInParent(function setup() {
   const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-  login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
-           createInstance(Ci.nsILoginInfo);
-  login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
-           createInstance(Ci.nsILoginInfo);
-  login3 = Cc["@mozilla.org/login-manager/loginInfo;1"].
-           createInstance(Ci.nsILoginInfo);
-  login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].
-           createInstance(Ci.nsILoginInfo);
+  let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+               createInstance(Ci.nsILoginInfo);
+  let login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+               createInstance(Ci.nsILoginInfo);
+  let login3 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+               createInstance(Ci.nsILoginInfo);
+  let login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+               createInstance(Ci.nsILoginInfo);
 
   login1.init("http://mochi.test:8888", "http://bug600551-1", null,
               "testuser@example.com", "testpass1", "", "");
   login2.init("http://mochi.test:8888", "http://bug600551-2", null,
               "555-555-5555", "testpass2", "", "");
   login3.init("http://mochi.test:8888", "http://bug600551-3", null,
               "http://mozilla.org", "testpass3", "", "");
   login4.init("http://mochi.test:8888", "http://bug600551-4", null,
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
@@ -16,18 +16,18 @@ runInParent(() => {
   var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
 
   // pwlogin1 uses a unique formSubmitURL, to check forms where no other logins
   // will apply. pwlogin2 uses the normal formSubmitURL, so that we can test
   // forms with a mix of username and non-username logins that might apply.
   //
   // Note: pwlogin2 is deleted at the end of the test.
 
-  pwlogin1 = new nsLoginInfo();
-  pwlogin2 = new nsLoginInfo();
+  let pwlogin1 = new nsLoginInfo();
+  let pwlogin2 = new nsLoginInfo();
 
   pwlogin1.init("http://mochi.test:8888", "http://mochi.test:1111", null,
                 "", "1234", "uname", "pword");
 
   pwlogin2.init("http://mochi.test:8888", "http://mochi.test:8888", null,
                 "", "1234", "uname", "pword");
 
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
@@ -75,18 +75,18 @@
 
     var pendingTests = [{expectedDialogs: 2, test: testNonAnonymousCredentials},
                         {expectedDialogs: 1, test: testAnonymousCredentials},
                         {expectedDialogs: 0, test: testAnonymousNoAuth}];
 
     let mm = runInParent(() => {
       const { classes: parentCc, interfaces: parentCi, utils: parentCu } = Components;
 
-      parentCu.import("resource://gre/modules/Services.jsm");
-      parentCu.import("resource://gre/modules/NetUtil.jsm");
+      let {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+      let {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
       parentCu.import("resource://gre/modules/Timer.jsm");
       parentCu.import("resource://gre/modules/XPCOMUtils.jsm");
 
       let channel = NetUtil.newChannel({
         uri: "http://example.com",
         loadUsingSystemPrincipal: true,
       });
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
@@ -35,17 +35,17 @@ function doxhr(URL, user, pass, next) {
     xhr.open("POST", URL, true);
   }
   xhr.onload = function() {
     is(xhr.status, 200, "Got status 200");
     next();
   };
   xhr.onerror = function() {
     ok(false, "request passed");
-    finishTest();
+    SimpleTest.finish();
   };
   xhr.send();
 }
 
 function startTest() {
   doxhr("auth2/authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", function() {
     doxhr("auth2", null, null, function() {
       doxhr("authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", SimpleTest.finish);
--- a/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test autocomplete due to multiple matching logins</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: autocomplete due to multiple matching logins
 
 <script>
 runChecksAfterCommonInit(false);
--- a/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
@@ -5,28 +5,26 @@
   <title>Test autofilling of fields outside of a form</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script type="application/javascript">
-let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 document.addEventListener("DOMContentLoaded", () => {
   document.getElementById("loginFrame").addEventListener("load", (evt) => {
     // Tell the parent to setup test logins.
-    chromeScript.sendAsyncMessage("setupParent", { selfFilling: true });
+    PWMGR_COMMON_PARENT.sendAsyncMessage("setupParent", { selfFilling: true });
   });
 });
 
 let doneSetupPromise = new Promise(resolve => {
   // When the setup is done, load a recipe for this test.
-  chromeScript.addMessageListener("doneSetup", function doneSetup() {
+  PWMGR_COMMON_PARENT.addMessageListener("doneSetup", function doneSetup() {
     resolve();
   });
 });
 
 add_task(async function setup() {
   info("Waiting for loads and setup");
   await doneSetupPromise;
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
@@ -8,18 +8,16 @@
   <script src="pwmgr_common.js"></script>
   <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script type="application/javascript">
 const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
 const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
 
-let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 let loadPromise = new Promise(resolve => {
   document.addEventListener("DOMContentLoaded", () => {
     document.getElementById("loginFrame").addEventListener("load", (evt) => {
       resolve();
     });
   });
 });
 
@@ -121,19 +119,19 @@ const TESTCASES = [
     newPasswordFieldValue: "pass2",
     oldPasswordFieldValue: null,
   },
 ];
 
 function getSubmitMessage() {
   info("getSubmitMessage");
   return new Promise((resolve, reject) => {
-    chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+    PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", function processed(...args) {
       info("got formSubmissionProcessed");
-      chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+      PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", processed);
       resolve(...args);
     });
   });
 }
 
 add_task(async function test() {
   let loginFrame = document.getElementById("loginFrame");
   let frameDoc = loginFrame.contentWindow.document;
--- a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
@@ -8,18 +8,16 @@
   <script src="pwmgr_common.js"></script>
   <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script type="application/javascript">
 const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
 const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
 
-let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 let loadPromise = new Promise(resolve => {
   document.addEventListener("DOMContentLoaded", () => {
     document.getElementById("loginFrame").addEventListener("load", (evt) => {
       resolve();
     });
   });
 });
 
@@ -120,19 +118,19 @@ const TESTCASES = [
     newPasswordFieldValue: "pass2",
     oldPasswordFieldValue: null,
   },
 ];
 
 function getSubmitMessage() {
   info("getSubmitMessage");
   return new Promise((resolve, reject) => {
-    chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+    PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", function processed(...args) {
       info("got formSubmissionProcessed");
-      chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+      PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", processed);
       resolve(...args);
     });
   });
 }
 
 add_task(async function test() {
   let loginFrame = document.getElementById("loginFrame");
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
@@ -10,18 +10,16 @@
 </head>
 <body>
 <script type="application/javascript">
 const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
 const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
 
 SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");
 
-let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 let loadPromise = new Promise(resolve => {
   document.addEventListener("DOMContentLoaded", () => {
     document.getElementById("loginFrame").addEventListener("load", (evt) => {
       resolve();
     });
   });
 });
 
@@ -35,20 +33,20 @@ add_task(async function setup() {
     set: [
       ["signon.formlessCapture.enabled", true],
     ],
   });
 
   info("Waiting for page and frame loads");
   await loadPromise;
 
-  chromeScript.addMessageListener("formSubmissionProcessed", submissionProcessed);
+  PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", submissionProcessed);
 
   SimpleTest.registerCleanupFunction(() => {
-    chromeScript.removeMessageListener("formSubmissionProcessed", submissionProcessed);
+    PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", submissionProcessed);
   });
 });
 
 const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
 const SCRIPTS = {
   PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
   WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
   WINDOW_LOCATION_RELOAD: `window.location.reload();`,
@@ -71,17 +69,18 @@ const TESTCASES = [
     document: `<input type=password value="pass2">`,
     wouldCapture: true,
   },
 ];
 
 add_task(async function test() {
   let loginFrame = document.getElementById("loginFrame");
 
-  var android = navigator.appVersion.includes("Android");
+  let waitTime;
+  let android = navigator.appVersion.includes("Android");
   if (android) {
     // intermittent failures on Android Debug at 5 seconds
     waitTime = 10000;
   } else {
     waitTime = 5000;
   }
 
   for (let tc of TESTCASES) {
--- a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test insecure form field autocomplete</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 
 <script>
 var chromeScript = runChecksAfterCommonInit();
 
@@ -508,17 +508,17 @@ add_task(async function test_form1_delet
   await shownPromise;
 
   synthesizeKey("KEY_ArrowDown"); // skip insecure warning
   // Delete the second entry (of 3), "testuser3"
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Delete", {shiftKey: true});
   checkACForm("", "");
-  numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  let numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 3, "Correct number of logins after deleting one");
   synthesizeKey("KEY_Enter");
   await promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(async function test_form1_first_after_deletion2() {
   restoreForm();
@@ -542,17 +542,17 @@ add_task(async function test_form1_delet
 
   synthesizeKey("KEY_ArrowDown"); // skip insecure warning
   // test 54
   // Delete the last entry (of 2), "zzzuser4"
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Delete", {shiftKey: true});
   checkACForm("", "");
-  numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  let numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 2, "Correct number of logins after deleting one");
   synthesizeKey("KEY_Enter");
   await promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(async function test_form1_first_after_3_deletions() {
   restoreForm();
@@ -575,17 +575,17 @@ add_task(async function test_form1_check
   await shownPromise;
 
   synthesizeKey("KEY_ArrowDown"); // skip insecure warning
   // test 56
   // Delete the only remaining entry, "testuser2"
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Delete", {shiftKey: true});
   checkACForm("", "");
-  numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  let numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 1, "Correct number of logins after deleting one");
 
   // remove the login that's not shown in the list.
   setupScript.sendSyncMessage("removeLogin", "login0");
 });
 
 // Tests for single-user forms for ignoring autocomplete=off
 add_task(async function test_form2() {
--- a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test basic login, contextual inscure password warning without saved logins</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: contextual inscure password warning without saved logins
 
 <script>
 let chromeScript = runChecksAfterCommonInit();
--- a/toolkit/components/passwordmgr/test/mochitest/test_master_password.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_master_password.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test for master password</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: master password.
 
 <p id="display"></p>
 
 <div id="content" style="display: none">
@@ -27,17 +27,16 @@ Login Manager test: master password.
 isTabModal = false;
 
 var exampleCom = "https://example.com/tests/toolkit/components/passwordmgr/test/mochitest/";
 var exampleOrg = "https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/";
 
 var chromeScript = runChecksAfterCommonInit();
 
 runInParent(() => {
-  const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
   const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
   var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
 
   var login1 = new nsLoginInfo();
   var login2 = new nsLoginInfo();
 
   login1.init("https://example.com", "https://example.com", null,
@@ -70,17 +69,17 @@ add_task(async function test_1() {
     checkHidden: true,
     checkMsg: "",
     checked: false,
     focused: "passField",
     defButton: "button0",
   };
   var action = {
     buttonClick: "ok",
-    passField: masterPassword,
+    passField: MASTER_PASSWORD,
   };
   var promptDone = handlePrompt(state, action);
 
   var logins = LoginManager.getAllLogins();
 
   await promptDone;
   is(logins.length, 3, "expected number of logins");
 
@@ -137,17 +136,17 @@ add_task(async function test_3() {
     checkHidden: true,
     checkMsg: "",
     checked: false,
     focused: "passField",
     defButton: "button0",
   };
   var action = {
     buttonClick: "ok",
-    passField: masterPassword,
+    passField: MASTER_PASSWORD,
   };
   var promptDone = handlePrompt(state, action);
 
   var fillPromise = promiseFormsProcessed();
 
   info("Load a single iframe to trigger a MP");
   iframe1.src = exampleCom + "subtst_master_pass.html";
 
@@ -242,17 +241,17 @@ add_task(async function test_4() {
 
   // Ok, now enter the MP. The MP prompt is already up.
 
   var fillPromise = promiseFormsProcessed(2);
 
   // fill existing MP dialog with MP.
   action = {
     buttonClick: "ok",
-    passField: masterPassword,
+    passField: MASTER_PASSWORD,
   };
   await handlePrompt(state, action);
   await fillPromise;
 
   // We shouldn't have to worry about iframe1's load event racing with
   // filling of iframe2's data. We notify observers synchronously, so
   // iframe2's observer will process iframe2 before iframe1 even finishes
   // processing the form.
--- a/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test basic login autocomplete</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: multiple login autocomplete
 
 <script>
 var chromeScript = runChecksAfterCommonInit();
--- a/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test prompter.{prompt,promptPassword,promptUsernameAndPassword}</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <p id="display"></p>
 
 <div id="content" style="display: none">
 </div>
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html
@@ -2,30 +2,33 @@
 <html>
 <head>
     <meta charset="utf-8">
     <title>Test for Async Auth Prompt</title>
     <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
     <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
     <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
     <script type="text/javascript" src="pwmgr_common.js"></script>
-    <script type="text/javascript" src="prompt_common.js"></script>
+    <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
     <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 
     <script class="testbody" type="text/javascript">
       const { NetUtil } = SpecialPowers.Cu.import("resource://gre/modules/NetUtil.jsm");
       const { TestUtils } = SpecialPowers.Cu.import("resource://testing-common/TestUtils.jsm");
       const EXAMPLE_COM = "http://example.com/tests/toolkit/components/passwordmgr/test/mochitest/";
       const EXAMPLE_ORG = "http://example.org/tests/toolkit/components/passwordmgr/test/mochitest/";
       let mozproxyOrigin;
 
       // Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
       // Used by prompt_common.js.
       isTabModal = false;
 
+      // These are magically defined on the window due to the iframe IDs
+      /* global iframe1, iframe2a, iframe2b */
+
       let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
 
       /**
        * Add a listener to add some logins to be autofilled in the HTTP/proxy auth. prompts later.
        */
       let pwmgrParent = runInParent(() => {
         const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
@@ -189,18 +192,18 @@
           passValue: "user2pass",
         });
         await handlePrompt(state, action);
 
         let iframe1Doc = await iframe1DocPromise;
         let iframe2aDoc = await iframe2aDocPromise;
         let iframe2bDoc = await iframe2bDocPromise;
 
-        authok1 = iframe1Doc.getElementById("ok").textContent;
-        proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+        let authok1 = iframe1Doc.getElementById("ok").textContent;
+        let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
 
         let authok2a = iframe2aDoc.getElementById("ok").textContent;
         let proxyok2a = iframe2aDoc.getElementById("proxy").textContent;
 
         let authok2b = iframe2bDoc.getElementById("ok").textContent;
         let proxyok2b = iframe2bDoc.getElementById("proxy").textContent;
 
         is(authok1, "PASS", "WWW Authorization OK, frame1");
@@ -212,18 +215,16 @@
       });
 
       add_task(async function test_threeSubframesWithSameProxyAndHTTPAuth() {
         // Load an iframe with 3 subpages all requiring the same login through
         // an authenticated proxy. We expect 2 dialogs, proxy authentication
         // and web authentication.
 
         let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
-        let iframe2aDocPromise = promiseLoadedContentDoc(iframe2a);
-        let iframe2bDocPromise = promiseLoadedContentDoc(iframe2b);
 
         iframe1.src = EXAMPLE_COM + "subtst_prompt_async.html";
         iframe2a.src = "about:blank";
         iframe2b.src = "about:blank";
 
         let state = {
           msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm2”`,
           title: "Authentication Required",
@@ -345,18 +346,18 @@
         });
         action = {
           buttonClick: "cancel",
         };
         await handlePrompt(state, action);
 
         let iframe1Doc = await iframe1DocPromise;
 
-        authok1 = iframe1Doc.getElementById("ok").textContent;
-        proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+        let authok1 = iframe1Doc.getElementById("ok").textContent;
+        let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
 
         is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
         is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
       });
 
       add_task(async function test_hugePayloadCancelled() {
         // Same as the previous two steps but let the server generate
         // huge content load to check http channel is capable to handle
@@ -437,19 +438,19 @@
         });
         action = {
           buttonClick: "cancel",
         };
         await handlePrompt(state, action);
 
         let iframe1Doc = await iframe1DocPromise;
 
-        authok1 = iframe1Doc.getElementById("ok").textContent;
-        proxyok1 = iframe1Doc.getElementById("proxy").textContent;
-        footnote = iframe1Doc.getElementById("footnote").textContent;
+        let authok1 = iframe1Doc.getElementById("ok").textContent;
+        let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+        let footnote = iframe1Doc.getElementById("footnote").textContent;
 
         is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
         is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
         is(footnote, "This is a footnote after the huge content fill",
            "Footnote present and loaded completely");
       });
 
       add_task(async function test_hugeProxySuccessWWWSuccess() {
@@ -476,31 +477,30 @@
           defButton: "button0",
         };
         let action = {
           buttonClick: "ok",
         };
         await handlePrompt(state, action);
 
         let iframe1Doc = await iframe1DocPromise;
-        authok1 = iframe1Doc.getElementById("ok").textContent;
-        proxyok1 = iframe1Doc.getElementById("proxy").textContent;
-        footnote = iframe1Doc.getElementById("footnote").textContent;
+        let authok1 = iframe1Doc.getElementById("ok").textContent;
+        let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+        let footnote = iframe1Doc.getElementById("footnote").textContent;
 
         is(authok1, "PASS", "WWW Authorization OK, frame1");
         is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
         is(footnote, "This is a footnote after the huge content fill",
            "Footnote present and loaded completely");
       });
 
       add_task(async function test_cancelSome() {
         // Check we process all challenges sent by server when
         // user cancels prompts
         let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
-        expectedDialogs = 5;
         iframe1.src = EXAMPLE_COM + "authenticate.sjs?" +
           "user=user6name&" +
           "pass=user6pass&" +
           "realm=mochirealm6&" +
           "proxy_user=proxy_user5&" +
           "proxy_pass=proxy_pass5&" +
           "proxy_realm=proxy_realm5&" +
           "huge=1&" +
@@ -548,19 +548,19 @@
         await handlePrompt(state, action);
 
         action = {
           buttonClick: "ok",
         };
         await handlePrompt(state, action);
 
         let iframe1Doc = await iframe1DocPromise;
-        authok1 = iframe1Doc.getElementById("ok").textContent;
-        proxyok1 = iframe1Doc.getElementById("proxy").textContent;
-        footnote = iframe1Doc.getElementById("footnote").textContent;
+        let authok1 = iframe1Doc.getElementById("ok").textContent;
+        let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+        let footnote = iframe1Doc.getElementById("footnote").textContent;
 
         is(authok1, "PASS", "WWW Authorization OK, frame1");
         is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
         is(footnote, "This is a footnote after the huge content fill",
            "Footnote present and loaded completely");
       });
     </script>
 </head>
--- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test HTTP auth prompts by loading authenticate.sjs</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <p id="display"></p>
 
 <div id="content" style="display: none">
   <iframe id="iframe"></iframe>
 </div>
@@ -20,20 +20,17 @@
 <script class="testbody" type="text/javascript">
 var iframe = document.getElementById("iframe");
 
 // Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
 isTabModal = false;
 
 const AUTHENTICATE_PATH = new URL("authenticate.sjs", window.location.href).pathname;
 
-let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 runInParent(() => {
-  // eslint-disable-next-line no-shadow
   const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
   let login3A, login3B, login4;
   login3A = Cc["@mozilla.org/login-manager/loginInfo;1"].
             createInstance(Ci.nsILoginInfo);
   login3B = Cc["@mozilla.org/login-manager/loginInfo;1"].
             createInstance(Ci.nsILoginInfo);
   login4  = Cc["@mozilla.org/login-manager/loginInfo;1"].
--- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
@@ -1,33 +1,31 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test HTTP auth prompts by loading authenticate.sjs with no window</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <p id="display"></p>
 
 <div id="content" style="display: none">
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 // Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
 isTabModal = false;
 
-let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 runInParent(() => {
   const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
   let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
               createInstance(Ci.nsILoginInfo);
   login.init("http://mochi.test:8888", null, "mochitest",
              "mochiuser1", "mochipass1", "", "");
   Services.logins.addLogin(login);
--- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test promptAuth prompts</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <p id="display"></p>
 
 <div id="content" style="display: none">
 </div>
 
@@ -29,18 +29,16 @@ var authinfo = {
   flags: Ci.nsIAuthInformation.AUTH_HOST,
   authenticationScheme: "basic",
   realm: "",
 };
 
 // Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
 isTabModal = false;
 
-let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 let prompterParent = runInParent(() => {
   const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
                     getService(Ci.nsIPromptFactory);
 
   const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
   let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
   let prompter2 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt2);
 
--- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test promptAuth proxy prompts</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <p id="display"></p>
 
 <div id="content" style="display: none">
   <iframe id="iframe"></iframe>
 </div>
@@ -180,19 +180,19 @@ add_task(async function test_autologin()
   // Enable the autologin pref.
   SpecialPowers.Services.prefs.setBoolPref("signon.autologin.proxy", true);
 
   proxyAuthinfo.username = "";
   proxyAuthinfo.password = "";
   proxyAuthinfo.realm    = "Proxy Realm";
   proxyAuthinfo.flags    = Ci.nsIAuthInformation.AUTH_PROXY;
 
-  time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+  let time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
   isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
-  time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+  let time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
 
   ok(isOk, "Checking dialog return value (accept)");
   isnot(time1, time2, "Checking that timeLastUsed was updated");
   is(proxyAuthinfo.username, "proxuser", "Checking returned username");
   is(proxyAuthinfo.password, "proxpass", "Checking returned password");
 });
 
 add_task(async function test_autologin_incorrect() {
@@ -216,21 +216,21 @@ add_task(async function test_autologin_i
     buttonClick: "ok",
   };
 
   proxyAuthinfo.username = "";
   proxyAuthinfo.password = "";
   proxyAuthinfo.realm    = "Proxy Realm";
   proxyAuthinfo.flags    = (Ci.nsIAuthInformation.AUTH_PROXY | Ci.nsIAuthInformation.PREVIOUS_FAILED);
 
-  time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+  let time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
   promptDone = handlePrompt(state, action);
   isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
   await promptDone;
-  time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+  let time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
 
   ok(isOk, "Checking dialog return value (accept)");
   isnot(time1, time2, "Checking that timeLastUsed was updated");
   is(proxyAuthinfo.username, "proxuser", "Checking returned username");
   is(proxyAuthinfo.password, "proxpass", "Checking returned password");
 });
 
 add_task(async function test_autologin_private() {
--- a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
@@ -2,37 +2,31 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test interaction between autocomplete and focus on username fields</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script>
-let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
-
 let readyPromise = registerRunTests();
 let chromeScript = runInParent(function chromeSetup() {
   const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
   let login1A  = Cc["@mozilla.org/login-manager/loginInfo;1"].
                  createInstance(Ci.nsILoginInfo);
-  let login1B  = Cc["@mozilla.org/login-manager/loginInfo;1"].
-                 createInstance(Ci.nsILoginInfo);
   let login2A  = Cc["@mozilla.org/login-manager/loginInfo;1"].
                  createInstance(Ci.nsILoginInfo);
   let login2B  = Cc["@mozilla.org/login-manager/loginInfo;1"].
                  createInstance(Ci.nsILoginInfo);
-  let login2C  = Cc["@mozilla.org/login-manager/loginInfo;1"].
-                 createInstance(Ci.nsILoginInfo);
 
   login1A.init("http://mochi.test:8888", "http://username-focus-1", null,
                "testuser1A", "testpass1A", "", "");
 
   login2A.init("http://mochi.test:8888", "http://username-focus-2", null,
                "testuser2A", "testpass2A", "", "");
   login2B.init("http://mochi.test:8888", "http://username-focus-2", null,
                "testuser2B", "testpass2B", "", "");
@@ -123,32 +117,32 @@ add_task(async function test_autofilled(
   info("Username and password already filled so don't show autocomplete");
   let noPopupPromise = promiseNoUnexpectedPopupShown();
   usernameField.focus();
   await noPopupPromise;
 
   removeFocus();
   usernameField.value = "testuser";
   info("Focus when we don't have an exact match");
-  shownPromise = promiseACShown();
+  let shownPromise = promiseACShown();
   usernameField.focus();
   await shownPromise;
 });
 
 add_task(async function test_autofilled_prefilled_un() {
   let usernameField = $_("-autofilled-prefilled-un", "uname");
   info("Username and password already filled so don't show autocomplete");
   let noPopupPromise = promiseNoUnexpectedPopupShown();
   usernameField.focus();
   await noPopupPromise;
 
   removeFocus();
   usernameField.value = "testuser";
   info("Focus when we don't have an exact match");
-  shownPromise = promiseACShown();
+  let shownPromise = promiseACShown();
   usernameField.focus();
   await shownPromise;
 });
 
 add_task(async function test_autofilled_focused_dynamic() {
   let usernameField = $_("-autofilled-focused-dynamic", "uname");
   let passwordField = $_("-autofilled-focused-dynamic", "pword");
   info("Username and password will be filled while username focused");
@@ -161,27 +155,27 @@ add_task(async function test_autofilled_
   await noPopupPromise;
 
   let popupState = await getPopupState();
   is(popupState.open, false, "Check popup is closed");
 
   removeFocus();
   passwordField.value = "test";
   info("Focus when we don't have an exact match");
-  shownPromise = promiseACShown();
+  let shownPromise = promiseACShown();
   usernameField.focus();
   await shownPromise;
 });
 
 // Begin testing forms that have multiple saved logins
 
 add_task(async function test_multiple() {
   let usernameField = $_("-multiple", "uname");
   info("Fields not filled due to multiple so autocomplete upon focus");
-  shownPromise = promiseACShown();
+  let shownPromise = promiseACShown();
   usernameField.focus();
   await shownPromise;
 });
 
 add_task(async function test_multiple_dynamic() {
   let usernameField = $_("-multiple-dynamic", "uname");
   let passwordField = $_("-multiple-dynamic", "pword");
   info("Fields not filled but username is focused upon marking so open");
@@ -200,32 +194,32 @@ add_task(async function test_multiple_pr
   info("Username and password already filled so don't show autocomplete");
   let noPopupPromise = promiseNoUnexpectedPopupShown();
   usernameField.focus();
   await noPopupPromise;
 
   removeFocus();
   usernameField.value = "testuser";
   info("Focus when we don't have an exact match");
-  shownPromise = promiseACShown();
+  let shownPromise = promiseACShown();
   usernameField.focus();
   await shownPromise;
 });
 
 add_task(async function test_multiple_prefilled_un2() {
   let usernameField = $_("-multiple-prefilled-un2", "uname");
   info("Username and password already filled so don't show autocomplete");
   let noPopupPromise = promiseNoUnexpectedPopupShown();
   usernameField.focus();
   await noPopupPromise;
 
   removeFocus();
   usernameField.value = "testuser";
   info("Focus when we don't have an exact match");
-  shownPromise = promiseACShown();
+  let shownPromise = promiseACShown();
   usernameField.focus();
   await shownPromise;
 });
 
 add_task(async function test_multiple_prefilled_focused_dynamic() {
   let usernameField = $_("-multiple-prefilled-focused-dynamic", "uname");
   let passwordField = $_("-multiple-prefilled-focused-dynamic", "pword");
   info("Username and password will be filled while username focused");
@@ -238,17 +232,17 @@ add_task(async function test_multiple_pr
   await noPopupPromise;
 
   let popupState = await getPopupState();
   is(popupState.open, false, "Check popup is closed");
 
   removeFocus();
   passwordField.value = "test";
   info("Focus when we don't have an exact match");
-  shownPromise = promiseACShown();
+  let shownPromise = promiseACShown();
   usernameField.focus();
   await shownPromise;
 });
 
 add_task(async function cleanup() {
   removeFocus();
 });
 </script>
rename from toolkit/components/passwordmgr/test/test_xhr.html
rename to toolkit/components/passwordmgr/test/mochitest/test_xhr.html
--- a/toolkit/components/passwordmgr/test/test_xhr.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr.html
@@ -1,199 +1,172 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test for XHR prompts</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: XHR prompt
 <p id="display"></p>
 
 <div id="content" style="display: none">
   <iframe id="iframe"></iframe>
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Login Manager: XHR prompts. **/
-var login1, login2;
-
-function initLogins() {
-  login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
-            createInstance(Ci.nsILoginInfo);
-  login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
-            createInstance(Ci.nsILoginInfo);
-
-  login1.init("http://mochi.test:8888", null, "xhr",
-              "xhruser1", "xhrpass1", "", "");
-  login2.init("http://mochi.test:8888", null, "xhr2",
-              "xhruser2", "xhrpass2", "", "");
-
-  SpecialPowers.Services.logins.addLogin(login1);
-  SpecialPowers.Services.logins.addLogin(login2);
-}
-
-function finishTest() {
-  ok(true, "finishTest removing testing logins...");
-  SpecialPowers.Services.logins.removeLogin(login1);
-  SpecialPowers.Services.logins.removeLogin(login2);
-
-  SimpleTest.finish();
+function makeRequest(uri) {
+  return new Promise((resolve, reject) => {
+    let request = new XMLHttpRequest();
+    request.open("GET", uri, true);
+    request.addEventListener("loadend", function onLoadEnd() {
+      let result = xhrLoad(request.responseXML);
+      resolve(result);
+    });
+    request.send(null);
+  });
 }
 
-function handleDialog(doc, testNum) {
-  ok(true, "handleDialog running for test " + testNum);
-
-  var clickOK = true;
-  var userfield = doc.getElementById("loginTextbox");
-  var passfield = doc.getElementById("password1Textbox");
-  var username = userfield.getAttribute("value");
-  var password = passfield.getAttribute("value");
-  var dialog    = doc.getElementById("commonDialog");
-
-  switch (testNum) {
-    case 1:
-      is(username, "xhruser1", "Checking provided username");
-      is(password, "xhrpass1", "Checking provided password");
-      break;
-
-    case 2:
-      is(username, "xhruser2", "Checking provided username");
-      is(password, "xhrpass2", "Checking provided password");
-
-      // Check that the dialog is modal, chrome and dependent;
-      // We can't just check window.opener because that'll be
-      // a content window, which therefore isn't exposed (it'll lie and
-      // be null).
-      var win = doc.defaultView;
-      var Ci = SpecialPowers.Ci;
-      var treeOwner = win.docShell.treeOwner;
-      treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
-      var flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
-      var wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
-      info("Flags: " + flags);
-      ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0,
-         "Dialog should be opened as chrome");
-      ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0,
-         "Dialog should be opened as a dialog");
-      ok((flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0,
-         "Dialog should be opened as dependent.");
-      ok(wbc.isWindowModal(), "Dialog should be modal");
-
-      // Check that the right tab is focused:
-      var browserWin = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser");
-      var spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
-      ok(spec.startsWith("http://mochi.test:8888"),
-         "Tab with remote URI (rather than about:blank) should be focused (" + spec + ")");
-
-
-      break;
-
-    default:
-      ok(false, "Uhh, unhandled switch for testNum #" + testNum);
-      break;
-  }
-
-  // Explicitly cancel the dialog and report a fail in this failure
-  // case, rather than letting the dialog get stuck due to an auth
-  // failure and having the test timeout.
-  if (!username && !password) {
-    ok(false, "No values prefilled");
-    clickOK = false;
-  }
-
-  if (clickOK) {
-    dialog.acceptDialog();
-  } else {
-    dialog.cancelDialog();
-  }
-
-  ok(true, "handleDialog done");
-  didDialog = true;
-}
-
-var newWin;
 function xhrLoad(xmlDoc) {
-  ok(true, "xhrLoad running for test " + testNum);
-
   // The server echos back the user/pass it received.
   var username = xmlDoc.getElementById("user").textContent;
   var password = xmlDoc.getElementById("pass").textContent;
   var authok = xmlDoc.getElementById("ok").textContent;
-
-
-  switch (testNum) {
-    case 1:
-      is(username, "xhruser1", "Checking provided username");
-      is(password, "xhrpass1", "Checking provided password");
-      break;
-
-    case 2:
-      is(username, "xhruser2", "Checking provided username");
-      is(password, "xhrpass2", "Checking provided password");
-
-      newWin.close();
-      break;
-
-    default:
-      ok(false, "Uhh, unhandled switch for testNum #" + testNum);
-      break;
-  }
-
-  doTest();
+  return {username, password, authok};
 }
 
-function doTest() {
-  switch (++testNum) {
-    case 1:
-      startCallbackTimer();
-      makeRequest("authenticate.sjs?user=xhruser1&pass=xhrpass1&realm=xhr");
-      break;
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+let prompterParent = runInParent(() => {
+  const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+  const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+                    getService(Ci.nsIPromptFactory);
+
+  let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+  let prompt = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt);
+
+  addMessageListener("proxyPrompter", function onMessage(msg) {
+    let rv = prompt[msg.methodName](...msg.args);
+    return {
+      rv,
+      // Send the args back to content so out/inout args can be checked.
+      args: msg.args,
+    };
+  });
+});
 
-    case 2:
-      // Test correct parenting, by opening another tab in the foreground,
-      // and making sure the prompt re-focuses the original tab when shown:
-      newWin = window.open();
-      newWin.focus();
-      startCallbackTimer();
-      makeRequest("authenticate.sjs?user=xhruser2&pass=xhrpass2&realm=xhr2");
-      break;
+let prompter1 = new PrompterProxy(prompterParent);
 
-    default:
-      finishTest();
-  }
-}
+add_task(function setup() {
+  runInParent(function initLogins() {
+    const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+    let nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+                                             Ci.nsILoginInfo, "init");
+    let login1 = new nsLoginInfo("http://mochi.test:8888", null, "xhr",
+                                 "xhruser1", "xhrpass1", "", "");
+    let login2 = new nsLoginInfo("http://mochi.test:8888", null, "xhr2",
+                                 "xhruser2", "xhrpass2", "", "");
+
+    try {
+      Services.logins.addLogin(login1);
+      Services.logins.addLogin(login2);
+    } catch (e) {
+      assert.ok(false, "addLogin threw: " + e);
+    }
+  });
+});
 
-function makeRequest(uri) {
-  var request = new XMLHttpRequest();
-  request.open("GET", uri, true);
-  request.onreadystatechange = function() {
-    if (request.readyState == 4) {
-      xhrLoad(request.responseXML);
-    }
+add_task(async function test1() {
+  let state = {
+    msg: "http://mochi.test:8888 is requesting your username and password. The site says: “xhr”",
+    title: "Authentication Required",
+    textValue: "xhruser1",
+    passValue: "xhrpass1",
+    iconClass: "authentication-icon question-icon",
+    titleHidden: true,
+    textHidden: false,
+    passHidden: false,
+    checkHidden: true,
+    checkMsg: "",
+    checked: false,
+    focused: "textField",
+    defButton: "button0",
   };
-  request.send(null);
-}
+  let action = {
+    buttonClick: "ok",
+  };
+  let promptDone = handlePrompt(state, action);
+  let requestPromise = makeRequest("authenticate.sjs?user=xhruser1&pass=xhrpass1&realm=xhr");
+  await promptDone;
+  let result = await requestPromise;
 
+  is(result.authok, "PASS", "Checking for successful authentication");
+  is(result.username, "xhruser1", "Checking for username");
+  is(result.password, "xhrpass1", "Checking for password");
+});
+
+add_task(async function test2() {
+  // Test correct parenting, by opening another tab in the foreground,
+  // and making sure the prompt re-focuses the original tab when shown:
+  let newWin = window.open();
+  newWin.focus();
 
-initLogins();
+  let state = {
+    msg: "http://mochi.test:8888 is requesting your username and password. The site says: “xhr2”",
+    title: "Authentication Required",
+    textValue: "xhruser2",
+    passValue: "xhrpass2",
+    iconClass: "authentication-icon question-icon",
+    titleHidden: true,
+    textHidden: false,
+    passHidden: false,
+    checkHidden: true,
+    checkMsg: "",
+    checked: false,
+    focused: "textField",
+    defButton: "button0",
+    // Check that the dialog is modal, chrome and dependent;
+    // We can't just check window.opener because that'll be
+    // a content window, which therefore isn't exposed (it'll lie and
+    // be null).
+    chrome: true,
+    dialog: true,
+    chromeDependent: true,
+    isWindowModal: true,
+  };
+  let action = {
+    buttonClick: "ok",
+  };
+  let promptDone = handlePrompt(state, action);
+  let requestPromise = makeRequest("authenticate.sjs?user=xhruser2&pass=xhrpass2&realm=xhr2");
+  await promptDone;
+  let result = await requestPromise;
 
-// clear plain HTTP auth sessions before the test, to allow
-// running them more than once.
-var authMgr = SpecialPowers.Cc["@mozilla.org/network/http-auth-manager;1"]
-                        .getService(SpecialPowers.Ci.nsIHttpAuthManager);
-authMgr.clearAll();
+  runInParent(() => {
+    const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-// start the tests
-testNum = 0;
-doTest();
+    // Check that the right tab is focused:
+    let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+    let spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+    assert.ok(spec.startsWith("http://mochi.test:8888"),
+              `Tab with remote URI (rather than about:blank)
+               should be focused (${spec})`);
+  });
 
-SimpleTest.waitForExplicitFinish();
+  is(result.authok, "PASS", "Checking for successful authentication");
+  is(result.username, "xhruser2", "Checking for username");
+  is(result.password, "xhrpass2", "Checking for password");
+
+  newWin.close();
+});
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/components/passwordmgr/test/mochitest/test_xml_load.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_xml_load.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test XML document prompts</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: XML prompt
 <p id="display"></p>
 
 <div id="content" style="display: none">
   <iframe id="iframe"></iframe>
@@ -109,24 +109,27 @@ add_task(async function test_backgroundT
     titleHidden: true,
     textHidden: false,
     passHidden: false,
     checkHidden: true,
     checkMsg: "",
     checked: false,
     focused: "textField",
     defButton: "button0",
+    chrome: true,
+    dialog: true,
+    chromeDependent: true,
+    isWindowModal: true,
   };
 
   let action = {
     buttonClick: "none",
   };
 
   await handlePrompt(state, action);
-  await checkPromptModal();
 
   await new Promise(resolve => {
     let focusScript = runInParent(checkWindowFocus);
     focusScript.addMessageListener("focusResult", (msg) => {
       focusScript.removeMessageListener("focusResult");
       ok(msg, "Dialog appears on the right tab");
       resolve(true);
     });
deleted file mode 100644
--- a/toolkit/components/passwordmgr/test/prompt_common.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * NOTE:
- * This file is currently only being used for tests which haven't been
- * fixed to work with e10s. Favor using the `prompt_common.js` file that
- * is in `toolkit/components/prompts/test/` instead.
- */
-/* eslint-disable mozilla/use-chromeutils-generateqi */
-
-var Ci = SpecialPowers.Ci;
-ok(Ci != null, "Access Ci");
-var Cc = SpecialPowers.Cc;
-ok(Cc != null, "Access Cc");
-
-var didDialog;
-
-var timer; // keep in outer scope so it's not GC'd before firing
-function startCallbackTimer() {
-  didDialog = false;
-
-  // Delay before the callback twiddles the prompt.
-  const dialogDelay = 10;
-
-  // Use a timer to invoke a callback to twiddle the authentication dialog
-  timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-  timer.init(observer, dialogDelay, Ci.nsITimer.TYPE_ONE_SHOT);
-}
-
-
-var observer = SpecialPowers.wrapCallbackObject({
-  QueryInterface(iid) {
-    const interfaces = [Ci.nsIObserver,
-                        Ci.nsISupports, Ci.nsISupportsWeakReference];
-
-    if (!interfaces.some(function(v) {
-      return iid.equals(v);
-    })) {
-      throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
-    }
-    return this;
-  },
-
-  observe(subject, topic, data) {
-    var doc = getDialogDoc();
-    if (doc) {
-      handleDialog(doc, testNum);
-    } else {
-      startCallbackTimer();
-    } // try again in a bit
-  },
-});
-
-function getDialogDoc() {
-  // Find the <browser> which contains notifyWindow, by looking
-  // through all the open windows and all the <browsers> in each.
-  // var enumerator = SpecialPowers.Services.wm.getEnumerator("navigator:browser");
-  for (let {docShell} of SpecialPowers.Services.wm.getXULWindowEnumerator(null)) {
-    var containedDocShells = docShell.getDocShellEnumerator(
-      docShell.typeChrome,
-      docShell.ENUMERATE_FORWARDS);
-    for (let childDocShell of containedDocShells) {
-      // We don't want it if it's not done loading.
-      if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) {
-        continue;
-      }
-      var childDoc = childDocShell.contentViewer.DOMDocument;
-
-      // ok(true, "Got window: " + childDoc.location.href);
-      if (childDoc.location.href == "chrome://global/content/commonDialog.xul") {
-        return childDoc;
-      }
-    }
-  }
-
-  return null;
-}
--- a/toolkit/components/passwordmgr/test/unit/test_telemetry.js
+++ b/toolkit/components/passwordmgr/test/unit/test_telemetry.js
@@ -120,17 +120,18 @@ add_task(function test_initialize() {
   }
 });
 
 /**
  * Tests the collection of statistics related to login metadata.
  */
 add_task(function test_logins_statistics() {
   // Repeat the operation twice to test that histograms are not accumulated.
-  for (let repeating of [false, true]) {
+  for (let pass of [1, 2]) {
+    info(`pass ${pass}`);
     triggerStatisticsCollection();
 
     // Should record 1 in the bucket corresponding to the number of passwords.
     testHistogram("PWMGR_NUM_SAVED_PASSWORDS",
                   { 10: 1 });
 
     // Should record 1 in the bucket corresponding to the number of passwords.
     testHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS",
--- a/toolkit/components/prompts/test/chromeScript.js
+++ b/toolkit/components/prompts/test/chromeScript.js
@@ -20,40 +20,16 @@ addMessageListener("handlePrompt", msg =
 function handlePromptWhenItAppears(action, isTabModal, isSelect) {
   let interval = setInterval(() => {
     if (handlePrompt(action, isTabModal, isSelect)) {
       clearInterval(interval);
     }
   }, 100);
 }
 
-addMessageListener("checkPromptModal", () => {
-  checkPromptIsModal();
-});
-
-function checkPromptIsModal() {
-  // Check that the dialog is modal, chrome and dependent;
-  // We can't just check window.opener because that'll be
-  // a content window, which therefore isn't exposed (it'll lie and
-  // be null).
-  let result = {};
-  let doc = getDialogDoc();
-  let win = doc.defaultView;
-  let treeOwner = win.docShell.treeOwner;
-  treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
-  let flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
-  let wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
-  result.chrome = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0;
-  result.dialog = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0;
-  result.chromeDependent = (flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0;
-  result.isWindowModal = wbc.isWindowModal();
-
-  sendAsyncMessage("checkPromptModalResult", result);
-}
-
 function handlePrompt(action, isTabModal, isSelect) {
   let ui;
 
   if (isTabModal) {
     let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
     let gBrowser = browserWin.gBrowser;
     let promptManager = gBrowser.getTabModalPromptBox(gBrowser.selectedBrowser);
     let prompts = promptManager.listPrompts();
@@ -147,16 +123,32 @@ function getPromptState(ui) {
   } else if (e.isSameNode(ui.password1Textbox.inputField)) {
     state.focused = "passField";
   } else if (ui.infoBody.isSameNode(e)) {
     state.focused = "infoBody";
   } else {
     state.focused = "ERROR: unexpected element focused: " + (e ? e.localName : "<null>");
   }
 
+  let treeOwner = ui.prompt &&
+                  ui.prompt.docShell &&
+                  ui.prompt.docShell.treeOwner;
+  if (treeOwner && treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)) {
+    // Check that the dialog is modal, chrome and dependent;
+    // We can't just check window.opener because that'll be
+    // a content window, which therefore isn't exposed (it'll lie and
+    // be null).
+    let flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
+    state.chrome = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0;
+    state.dialog = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0;
+    state.chromeDependent = (flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0;
+    let wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+    state.isWindowModal = wbc.isWindowModal();
+  }
+
   return state;
 }
 
 function dismissSelect(ui, action) {
   let dialog = ui.getElementsByTagName("dialog")[0];
   let listbox = ui.getElementById("list");
 
   if (action.selectItem) {
--- a/toolkit/components/prompts/test/prompt_common.js
+++ b/toolkit/components/prompts/test/prompt_common.js
@@ -30,30 +30,16 @@ function handlePrompt(state, action) {
       gChromeScript.removeMessageListener("promptHandled", handled);
       checkPromptState(msg.promptState, state);
       resolve(true);
     });
     gChromeScript.sendAsyncMessage("handlePrompt", { action, isTabModal});
   });
 }
 
-function checkPromptModal() {
-  return new Promise(resolve => {
-    gChromeScript.addMessageListener("checkPromptModalResult", function handled(result) {
-      gChromeScript.removeMessageListener("checkPromptModalResult", handled);
-      ok(result.chrome, "Dialog should be opened as chrome");
-      ok(result.dialog, "Dialog should be a dialog");
-      ok(result.chromeDependent, "Dialog should be chrome dependent");
-      ok(result.isWindowModal, "Dialog should be a window modal");
-      resolve(true);
-    });
-    gChromeScript.sendAsyncMessage("checkPromptModal");
-  });
-}
-
 function checkPromptState(promptState, expectedState) {
     info(`checkPromptState: ${expectedState.msg}`);
     // XXX check title? OS X has title in content
     is(promptState.msg, expectedState.msg, "Checking expected message");
     if (isOSX && !isTabModal)
       ok(!promptState.titleHidden, "Checking title always visible on OS X");
     else
       is(promptState.titleHidden, expectedState.titleHidden, "Checking title visibility");
@@ -87,16 +73,29 @@ function checkPromptState(promptState, e
     is(promptState.defButton1, expectedState.defButton == "button1", "checking button1 default");
     is(promptState.defButton2, expectedState.defButton == "button2", "checking button2 default");
 
     if (isOSX && expectedState.focused && expectedState.focused.startsWith("button")) {
         is(promptState.focused, "infoBody", "buttons don't focus on OS X, but infoBody does instead");
     } else {
         is(promptState.focused, expectedState.focused, "Checking focused element");
     }
+
+    if (expectedState.hasOwnProperty("chrome")) {
+        is(promptState.chrome, expectedState.chrome, "Dialog should be opened as chrome");
+    }
+    if (expectedState.hasOwnProperty("dialog")) {
+        is(promptState.dialog, expectedState.dialog, "Dialog should be opened as a dialog");
+    }
+    if (expectedState.hasOwnProperty("chromeDependent")) {
+        is(promptState.chromeDependent, expectedState.chromeDependent, "Dialog should be opened as dependent");
+    }
+    if (expectedState.hasOwnProperty("isWindowModal")) {
+        is(promptState.isWindowModal, expectedState.isWindowModal, "Dialog should be modal");
+    }
 }
 
 function checkEchoedAuthInfo(expectedState, doc) {
     // The server echos back the HTTP auth info it received.
     let username = doc.getElementById("user").textContent;
     let password = doc.getElementById("pass").textContent;
     let authok = doc.getElementById("ok").textContent;
 
--- a/toolkit/components/satchel/test/parent_utils.js
+++ b/toolkit/components/satchel/test/parent_utils.js
@@ -96,16 +96,26 @@ var ParentUtils = {
     ContentTaskUtils.waitForCondition(() => {
       return gAutocompletePopup.popupOpen &&
              gAutocompletePopup.selectedIndex === expectedIndex;
     }, "Checking selected index").then(() => {
       sendAsyncMessage("gotSelectedIndex");
     });
   },
 
+  testMenuEntry(index, statement) {
+    ContentTaskUtils.waitForCondition(() => {
+      let el = gAutocompletePopup.richlistbox.getItemAtIndex(index);
+      let testFunc = new Services.ww.activeWindow.Function("el", `return ${statement}`);
+      return gAutocompletePopup.popupOpen && el && testFunc(el);
+    }, "Testing menu entry").then(() => {
+      sendAsyncMessage("menuEntryTested");
+    });
+  },
+
   getPopupState() {
     sendAsyncMessage("gotPopupState", {
       open: gAutocompletePopup.popupOpen,
       selectedIndex: gAutocompletePopup.selectedIndex,
       direction: gAutocompletePopup.style.direction,
     });
   },
 
@@ -137,16 +147,19 @@ addMessageListener("countEntries", ({ na
 
 addMessageListener("waitForMenuChange", ({ expectedCount, expectedFirstValue }) => {
   ParentUtils.checkRowCount(expectedCount, expectedFirstValue);
 });
 
 addMessageListener("waitForSelectedIndex", ({ expectedIndex }) => {
   ParentUtils.checkSelectedIndex(expectedIndex);
 });
+addMessageListener("waitForMenuEntryTest", ({ index, statement }) => {
+  ParentUtils.testMenuEntry(index, statement);
+});
 
 addMessageListener("getPopupState", () => {
   ParentUtils.getPopupState();
 });
 
 addMessageListener("addObserver", () => {
   Services.obs.addObserver(ParentUtils, "satchel-storage-changed");
 });
--- a/toolkit/components/satchel/test/satchel_common.js
+++ b/toolkit/components/satchel/test/satchel_common.js
@@ -201,16 +201,26 @@ function notifySelectedIndex(expectedInd
       if (then) {
         then();
       }
       resolve();
     });
   });
 }
 
+function testMenuEntry(index, statement) {
+  return new Promise(resolve => {
+    gChromeScript.sendAsyncMessage("waitForMenuEntryTest", { index, statement });
+    gChromeScript.addMessageListener("menuEntryTested", function changed() {
+      gChromeScript.removeMessageListener("menuEntryTested", changed);
+      resolve();
+    });
+  });
+}
+
 function getPopupState(then = null) {
   return new Promise(resolve => {
     gChromeScript.sendAsyncMessage("getPopupState");
     gChromeScript.addMessageListener("gotPopupState", function listener(state) {
       gChromeScript.removeMessageListener("gotPopupState", listener);
       if (then) {
         then(state);
       }
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -308,16 +308,70 @@ const BaseControlMixin = Base => {
   }
 
   Base.implementCustomInterface(BaseControl,
                                 [Ci.nsIDOMXULControlElement]);
   return BaseControl;
 };
 MozElements.BaseControl = BaseControlMixin(MozXULElement);
 
+const BaseTextMixin = Base => class extends Base {
+  set label(val) {
+    this.setAttribute("label", val);
+    return val;
+  }
+
+  get label() {
+    return this.getAttribute("label");
+  }
+
+  set crop(val) {
+    this.setAttribute("crop", val);
+    return val;
+  }
+
+  get crop() {
+    return this.getAttribute("crop");
+  }
+
+  set image(val) {
+    this.setAttribute("image", val);
+    return val;
+  }
+
+  get image() {
+    return this.getAttribute("image");
+  }
+
+  set command(val) {
+    this.setAttribute("command", val);
+    return val;
+  }
+
+  get command() {
+    return this.getAttribute("command");
+  }
+
+  set accessKey(val) {
+    // Always store on the control
+    this.setAttribute("accesskey", val);
+    // If there is a label, change the accesskey on the labelElement
+    // if it's also set there
+    if (this.labelElement) {
+      this.labelElement.accessKey = val;
+    }
+    return val;
+  }
+
+  get accessKey() {
+    return this.labelElement ? this.labelElement.accessKey : this.getAttribute("accesskey");
+  }
+};
+MozElements.BaseText = BaseTextMixin(MozXULElement);
+
 // Attach the base class to the window so other scripts can use it:
 window.BaseControlMixin = BaseControlMixin;
 window.MozElementMixin = MozElementMixin;
 window.MozXULElement = MozXULElement;
 window.MozElements = MozElements;
 
 customElements.setElementCreationCallback("browser", () => {
   Services.scriptloader.loadSubScript("chrome://global/content/elements/browser-custom-element.js", window);
@@ -326,27 +380,28 @@ customElements.setElementCreationCallbac
 // For now, don't load any elements in the extension dummy document.
 // We will want to load <browser> when that's migrated (bug 1441935).
 const isDummyDocument = document.documentURI == "chrome://extensions/content/dummy.xul";
 if (!isDummyDocument) {
   for (let script of [
     "chrome://global/content/elements/general.js",
     "chrome://global/content/elements/notificationbox.js",
     "chrome://global/content/elements/radio.js",
+    "chrome://global/content/elements/richlistbox.js",
+    "chrome://global/content/elements/autocomplete-richlistitem.js",
     "chrome://global/content/elements/textbox.js",
     "chrome://global/content/elements/tabbox.js",
     "chrome://global/content/elements/tree.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
   for (let [tag, script] of [
     ["findbar", "chrome://global/content/elements/findbar.js"],
     ["menulist", "chrome://global/content/elements/menulist.js"],
-    ["richlistbox", "chrome://global/content/elements/richlistbox.js"],
     ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
     ["printpreview-toolbar", "chrome://global/content/printPreviewToolbar.js"],
     ["editor", "chrome://global/content/elements/editor.js"],
   ]) {
     customElements.setElementCreationCallback(tag, () => {
       Services.scriptloader.loadSubScript(script, window);
     });
   }
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -80,16 +80,17 @@ toolkit.jar:
    content/global/bindings/tabbox.xml          (widgets/tabbox.xml)
    content/global/bindings/text.xml            (widgets/text.xml)
 *  content/global/bindings/textbox.xml         (widgets/textbox.xml)
    content/global/bindings/timekeeper.js       (widgets/timekeeper.js)
    content/global/bindings/timepicker.js       (widgets/timepicker.js)
    content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/tree.xml            (widgets/tree.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
+   content/global/elements/autocomplete-richlistitem.js       (widgets/autocomplete-richlistitem.js)
    content/global/elements/browser-custom-element.js          (widgets/browser-custom-element.js)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js           (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/pluginProblem.js    (widgets/pluginProblem.js)
    content/global/elements/radio.js            (widgets/radio.js)
copy from toolkit/content/widgets/autocomplete.xml
copy to toolkit/content/widgets/autocomplete-richlistitem.js
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete-richlistitem.js
@@ -1,2296 +1,1027 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+/* 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/. */
 
-<bindings id="autocompleteBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:html="http://www.w3.org/1999/xhtml"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
+"use strict";
 
-  <binding id="autocomplete"
-           extends="chrome://global/content/bindings/textbox.xml#textbox">
-    <content sizetopopup="pref">
-      <children includes="image|box"/>
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-      <xul:moz-input-box anonid="moz-input-box" flex="1">
-        <children/>
-        <html:input anonid="input" class="textbox-input"
-                    allowevents="true"
-                    autocomplete="off"
-                    xbl:inherits="value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/>
-      </xul:moz-input-box>
-      <children includes="hbox"/>
-
-      <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/>
-    </content>
+MozElements.MozAutocompleteRichlistitem = class MozAutocompleteRichlistitem extends MozElements.MozRichlistitem {
+  constructor() {
+    super();
 
-    <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
-      <field name="mController">null</field>
-      <field name="mSearchNames">null</field>
-      <field name="mIgnoreInput">false</field>
-
-      <field name="_searchBeginHandler">null</field>
-      <field name="_searchCompleteHandler">null</field>
-      <field name="_textEnteredHandler">null</field>
-      <field name="_textRevertedHandler">null</field>
-
-      <constructor><![CDATA[
-        this.mController = Cc["@mozilla.org/autocomplete/controller;1"].
-          getService(Ci.nsIAutoCompleteController);
-
-        this._searchBeginHandler = this.initEventHandler("searchbegin");
-        this._searchCompleteHandler = this.initEventHandler("searchcomplete");
-        this._textEnteredHandler = this.initEventHandler("textentered");
-        this._textRevertedHandler = this.initEventHandler("textreverted");
-
-        // For security reasons delay searches on pasted values.
-        this.inputField.controllers.insertControllerAt(0, this._pasteController);
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        this.inputField.controllers.removeController(this._pasteController);
-      ]]></destructor>
-
-      <!-- =================== nsIAutoCompleteInput =================== -->
+    /**
+     * This overrides listitem's mousedown handler because we want to set the
+     * selected item even when the shift or accel keys are pressed.
+     */
+    this.addEventListener("mousedown", (event) => {
+      // Call this.control only once since it's not a simple getter.
+      let control = this.control;
+      if (!control || control.disabled) {
+        return;
+      }
+      if (!this.selected) {
+        control.selectItem(this);
+      }
+      control.currentItem = this;
+    });
 
-      <field name="_popup">null</field>
-      <property name="popup" readonly="true">
-        <getter><![CDATA[
-          // Memoize the result in a field rather than replacing this property,
-          // so that it can be reset along with the binding.
-          if (this._popup) {
-            return this._popup;
-          }
-
-          let popup = null;
-          let popupId = this.getAttribute("autocompletepopup");
-          if (popupId) {
-            popup = document.getElementById(popupId);
-          }
-          if (!popup) {
-            popup = document.createXULElement("panel");
-            popup.setAttribute("type", "autocomplete-richlistbox");
-            popup.setAttribute("noautofocus", "true");
-
-            let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset");
-            popupset.appendChild(popup);
-          }
-          popup.mInput = this;
-
-          return this._popup = popup;
-        ]]></getter>
-      </property>
-
-      <property name="controller" onget="return this.mController;" readonly="true"/>
-
-      <property name="popupOpen"
-                onget="return this.popup.popupOpen;"
-                onset="if (val) this.openPopup(); else this.closePopup();"/>
+    this.addEventListener("mouseover", (event) => {
+      // The point of implementing this handler is to allow drags to change
+      // the selected item.  If the user mouses down on an item, it becomes
+      // selected.  If they then drag the mouse to another item, select it.
+      // Handle all three primary mouse buttons: right, left, and wheel, since
+      // all three change the selection on mousedown.
+      let mouseDown = event.buttons & 0b111;
+      if (!mouseDown) {
+        return;
+      }
+      // Call this.control only once since it's not a simple getter.
+      let control = this.control;
+      if (!control || control.disabled) {
+        return;
+      }
+      if (!this.selected) {
+        control.selectItem(this);
+      }
+      control.currentItem = this;
+    });
 
-      <property name="disableAutoComplete"
-                onset="this.setAttribute('disableautocomplete', val); return val;"
-                onget="return this.getAttribute('disableautocomplete') == 'true';"/>
-
-      <property name="completeDefaultIndex"
-                onset="this.setAttribute('completedefaultindex', val); return val;"
-                onget="return this.getAttribute('completedefaultindex') == 'true';"/>
+    this.addEventListener("overflow", () => this._onOverflow());
+    this.addEventListener("underflow", () => this._onUnderflow());
+  }
 
-      <property name="completeSelectedIndex"
-                onset="this.setAttribute('completeselectedindex', val); return val;"
-                onget="return this.getAttribute('completeselectedindex') == 'true';"/>
-
-      <property name="forceComplete"
-                onset="this.setAttribute('forcecomplete', val); return val;"
-                onget="return this.getAttribute('forcecomplete') == 'true';"/>
+  connectedCallback() {
+    if (this.delayConnectedCallback()) {
+      return;
+    }
 
-      <property name="minResultsForPopup"
-                onset="this.setAttribute('minresultsforpopup', val); return val;"
-                onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/>
+    this.textContent = "";
+    this.appendChild(MozXULElement.parseXULToFragment(this._markup));
 
-      <property name="timeout"
-                onset="this.setAttribute('timeout', val); return val;">
-        <getter><![CDATA[
-          // For security reasons delay searches on pasted values.
-          if (this._valueIsPasted) {
-            let t = parseInt(this.getAttribute("pastetimeout"));
-            return isNaN(t) ? 1000 : t;
-          }
+    this._boundaryCutoff = null;
+    this._inOverflow = false;
 
-          let t = parseInt(this.getAttribute("timeout"));
-          return isNaN(t) ? 50 : t;
-        ]]></getter>
-      </property>
-
-      <property name="searchParam"
-                onget="return this.getAttribute('autocompletesearchparam') || '';"
-                onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
+    this._updateAttributes();
+    this._adjustAcItem();
+  }
 
-      <property name="searchCount" readonly="true"
-                onget="this.initSearchNames(); return this.mSearchNames.length;"/>
-
-      <property name="PrivateBrowsingUtils" readonly="true">
-        <getter><![CDATA[
-          let module = {};
-          ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", module);
-          Object.defineProperty(this, "PrivateBrowsingUtils", {
-            configurable: true,
-            enumerable: true,
-            writable: true,
-            value: module.PrivateBrowsingUtils,
-          });
-          return module.PrivateBrowsingUtils;
-        ]]></getter>
-      </property>
-
-      <property name="inPrivateContext" readonly="true"
-                onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/>
-
-      <property name="noRollupOnCaretMove" readonly="true"
-                onget="return this.popup.getAttribute('norolluponanchor') == 'true'"/>
-
-      <!-- This is the maximum number of drop-down rows we get when we
-            hit the drop marker beside fields that have it (like the URLbar).-->
-      <field name="maxDropMarkerRows" readonly="true">14</field>
+  static get observedAttributes() {
+    return [
+      "actiontype",
+      "current",
+      "selected",
+      "image",
+      "type",
+    ];
+  }
 
-      <method name="getSearchAt">
-        <parameter name="aIndex"/>
-        <body><![CDATA[
-          this.initSearchNames();
-          return this.mSearchNames[aIndex];
-        ]]></body>
-      </method>
+  get inheritedAttributeMap() {
+    if (!this.__inheritedAttributeMap) {
+      this.__inheritedAttributeMap = new Map([
+        [ this.querySelector(".ac-type-icon"), [ "selected", "current", "type" ] ],
+      [ this.querySelector(".ac-site-icon"), [ "src=image", "selected", "type" ] ],
+      [ this.querySelector(".ac-title"), [ "selected" ] ],
+      [ this.querySelector(".ac-title-text"), [ "selected" ] ],
+      [ this.querySelector(".ac-tags"), [ "selected" ] ],
+      [ this.querySelector(".ac-tags-text"), [ "selected" ] ],
+      [ this.querySelector(".ac-separator"), [ "selected", "actiontype", "type" ] ],
+      [ this.querySelector(".ac-url"), [ "selected", "actiontype" ] ],
+      [ this.querySelector(".ac-url-text"), [ "selected" ] ],
+      [ this.querySelector(".ac-action"), [ "selected", "actiontype" ] ],
+      [ this.querySelector(".ac-action-text"), [ "selected" ] ],
+      ]);
+    }
+    return this.__inheritedAttributeMap;
+  }
 
-      <method name="setTextValueWithReason">
-        <parameter name="aValue"/>
-        <parameter name="aReason"/>
-        <body><![CDATA[
-          if (aReason == Ci.nsIAutoCompleteInput
-                           .TEXTVALUE_REASON_COMPLETEDEFAULT) {
-            this._textValueSetByCompleteDefault = true;
-          }
-          this.textValue = aValue;
-          this._textValueSetByCompleteDefault = false;
-        ]]></body>
-      </method>
-
-      <property name="textValue">
-        <getter><![CDATA[
-          if (typeof this.onBeforeTextValueGet == "function") {
-            let result = this.onBeforeTextValueGet();
-            if (result) {
-              return result.value;
-            }
-          }
-          return this.value;
-        ]]></getter>
-        <setter><![CDATA[
-          if (typeof this.onBeforeTextValueSet == "function" &&
-              !this._textValueSetByCompleteDefault) {
-            val = this.onBeforeTextValueSet(val);
-          }
+  attributeChangedCallback(name, oldValue, newValue) {
+    if (this.isConnectedAndReady && oldValue != newValue &&
+        this.constructor.observedAttributes.includes(name)) {
+      this._updateAttributes();
+    }
+  }
 
-          // "input" event is automatically dispatched by the editor if
-          // necessary.
-          this._setValueInternal(val, true);
-
-          return this.value;
-        ]]></setter>
-      </property>
-
-      <method name="selectTextRange">
-        <parameter name="aStartIndex"/>
-        <parameter name="aEndIndex"/>
-        <body><![CDATA[
-          this.inputField.setSelectionRange(aStartIndex, aEndIndex);
-        ]]></body>
-      </method>
-
-      <method name="onSearchBegin">
-        <body><![CDATA[
-          if (this.popup && typeof this.popup.onSearchBegin == "function")
-            this.popup.onSearchBegin();
-          if (this._searchBeginHandler)
-            this._searchBeginHandler();
-        ]]></body>
-      </method>
-
-      <method name="onSearchComplete">
-        <body><![CDATA[
-          if (this.mController.matchCount == 0)
-            this.setAttribute("nomatch", "true");
-          else
-            this.removeAttribute("nomatch");
+  _updateAttributes() {
+    for (let [ el, attrs ] of this.inheritedAttributeMap.entries()) {
+      for (let attr of attrs) {
+        this.inheritAttribute(el, attr);
+      }
+    }
+  }
 
-          if (this.ignoreBlurWhileSearching && !this.focused) {
-            this.handleEnter();
-            this.detachController();
-          }
-
-          if (this._searchCompleteHandler)
-            this._searchCompleteHandler();
-        ]]></body>
-      </method>
+  get _markup() {
+    return `
+      <image class="ac-type-icon"></image>
+      <image class="ac-site-icon"></image>
+      <hbox class="ac-title" align="center">
+        <description class="ac-text-overflow-container">
+          <description class="ac-title-text"></description>
+        </description>
+      </hbox>
+      <hbox class="ac-tags" align="center">
+        <description class="ac-text-overflow-container">
+          <description class="ac-tags-text"></description>
+        </description>
+      </hbox>
+      <hbox class="ac-separator" align="center">
+        <description class="ac-separator-text"></description>
+      </hbox>
+      <hbox class="ac-url" align="center">
+        <description class="ac-text-overflow-container">
+          <description class="ac-url-text"></description>
+        </description>
+      </hbox>
+      <hbox class="ac-action" align="center">
+        <description class="ac-text-overflow-container">
+          <description class="ac-action-text"></description>
+        </description>
+      </hbox>
+    `;
+  }
 
-      <method name="onTextEntered">
-        <parameter name="event"/>
-        <body><![CDATA[
-          let rv = false;
-          if (this._textEnteredHandler) {
-            rv = this._textEnteredHandler(event);
-          }
-          return rv;
-        ]]></body>
-      </method>
-
-      <method name="onTextReverted">
-        <body><![CDATA[
-          if (this._textRevertedHandler)
-            return this._textRevertedHandler();
-          return false;
-        ]]></body>
-      </method>
-
-      <!-- =================== nsIDOMXULMenuListElement =================== -->
-
-      <property name="editable" readonly="true"
-                onget="return true;" />
-
-      <property name="crop"
-                onset="this.setAttribute('crop',val); return val;"
-                onget="return this.getAttribute('crop');"/>
+  get _typeIcon() {
+    return this.querySelector(".ac-type-icon");
+  }
 
-      <property name="open"
-                onget="return this.getAttribute('open') == 'true';">
-        <setter><![CDATA[
-          if (val)
-            this.showHistoryPopup();
-          else
-            this.closePopup();
-        ]]></setter>
-      </property>
+  get _titleText() {
+    return this.querySelector(".ac-title-text");
+  }
 
-      <!-- =================== PUBLIC MEMBERS =================== -->
+  get _tags() {
+    return this.querySelector(".ac-tags");
+  }
 
-      <field name="valueIsTyped">false</field>
-      <field name="_textValueSetByCompleteDefault">false</field>
-      <property name="value"
-                onset="return this._setValueInternal(val, false);">
-        <getter><![CDATA[
-          if (typeof this.onBeforeValueGet == "function") {
-            var result = this.onBeforeValueGet();
-            if (result)
-              return result.value;
-          }
-          return this.inputField.value;
-        ]]></getter>
-      </property>
+  get _tagsText() {
+    return this.querySelector(".ac-tags-text");
+  }
 
-      <property name="focused" readonly="true"
-                onget="return this.getAttribute('focused') == 'true';"/>
-
-      <!-- maximum number of rows to display at a time -->
-      <property name="maxRows"
-                onset="this.setAttribute('maxrows', val); return val;"
-                onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>
+  get _separator() {
+    return this.querySelector(".ac-separator");
+  }
 
-      <!-- option to allow scrolling through the list via the tab key, rather than
-           tab moving focus out of the textbox -->
-      <property name="tabScrolling"
-                onset="this.setAttribute('tabscrolling', val); return val;"
-                onget="return this.getAttribute('tabscrolling') == 'true';"/>
+  get _urlText() {
+    return this.querySelector(".ac-url-text");
+  }
 
-      <!-- option to completely ignore any blur events while searches are
-           still going on. -->
-      <property name="ignoreBlurWhileSearching"
-                onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
-                onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>
-
-      <!-- option to highlight entries that don't have any matches -->
-      <property name="highlightNonMatches"
-                onset="this.setAttribute('highlightnonmatches', val); return val;"
-                onget="return this.getAttribute('highlightnonmatches') == 'true';"/>
+  get _actionText() {
+    return this.querySelector(".ac-action-text");
+  }
 
-      <!-- =================== PRIVATE MEMBERS =================== -->
-
-      <!-- ::::::::::::: autocomplete controller ::::::::::::: -->
+  get label() {
+    // This property is a string that is read aloud by screen readers,
+    // so it must not contain anything that should not be user-facing.
 
-      <method name="attachController">
-        <body><![CDATA[
-          this.mController.input = this;
-        ]]></body>
-      </method>
-
-      <method name="detachController">
-        <body><![CDATA[
-          if (this.mController.input == this)
-            this.mController.input = null;
-        ]]></body>
-      </method>
-
-      <!-- ::::::::::::: popup opening ::::::::::::: -->
+    let parts = [
+      this.getAttribute("title"),
+      this.getAttribute("displayurl"),
+    ];
+    let label = parts.filter(str => str).join(" ");
 
-      <method name="openPopup">
-        <body><![CDATA[
-          if (this.focused)
-            this.popup.openAutocompletePopup(this, this);
-        ]]></body>
-      </method>
-
-      <method name="closePopup">
-        <body><![CDATA[
-          this.popup.closePopup();
-        ]]></body>
-      </method>
+    // allow consumers that have extended popups to override
+    // the label values for the richlistitems
+    let panel = this.parentNode.parentNode;
+    if (panel.createResultLabel) {
+      return panel.createResultLabel(this, label);
+    }
 
-      <method name="showHistoryPopup">
-        <body><![CDATA[
-          // Store our "normal" maxRows on the popup, so that it can reset the
-          // value when the popup is hidden.
-          this.popup._normalMaxRows = this.maxRows;
+    return label;
+  }
 
-          // Increase our maxRows temporarily, since we want the dropdown to
-          // be bigger in this case. The popup's popupshowing/popuphiding
-          // handlers will take care of resetting this.
-          this.maxRows = this.maxDropMarkerRows;
-
-          // Ensure that we have focus.
-          if (!this.focused)
-            this.focus();
-          this.attachController();
-          this.mController.startSearch("");
-        ]]></body>
-      </method>
+  get _stringBundle() {
+    if (!this.__stringBundle) {
+      this.__stringBundle = Services.strings.createBundle(
+        "chrome://global/locale/autocomplete.properties"
+      );
+    }
+    return this.__stringBundle;
+  }
 
-      <method name="toggleHistoryPopup">
-        <body><![CDATA[
-          if (!this.popup.popupOpen)
-            this.showHistoryPopup();
-          else
-            this.closePopup();
-        ]]></body>
-      </method>
-
-      <!-- ::::::::::::: event dispatching ::::::::::::: -->
+  get boundaryCutoff() {
+    if (!this._boundaryCutoff) {
+      this._boundaryCutoff = Services.prefs.
+        getIntPref("toolkit.autocomplete.richBoundaryCutoff");
+    }
+    return this._boundaryCutoff;
+  }
 
-      <method name="initEventHandler">
-        <parameter name="aEventType"/>
-        <body><![CDATA[
-          let handlerString = this.getAttribute("on" + aEventType);
-          if (handlerString) {
-            return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
-          }
-          return null;
-        ]]></body>
-      </method>
-
-      <!-- ::::::::::::: key handling ::::::::::::: -->
+  _cleanup() {
+    this.removeAttribute("url");
+    this.removeAttribute("image");
+    this.removeAttribute("title");
+    this.removeAttribute("text");
+    this.removeAttribute("displayurl");
+  }
 
-      <field name="_selectionDetails">null</field>
-      <method name="onKeyPress">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          return this.handleKeyPress(aEvent);
-        ]]></body>
-      </method>
-
-      <method name="handleKeyPress">
-        <parameter name="aEvent"/>
-        <parameter name="aOptions"/>
-        <body><![CDATA[
-          if (aEvent.target.localName != "textbox")
-            return true; // Let child buttons of autocomplete take input
+  _onOverflow() {
+    this._inOverflow = true;
+    this._handleOverflow();
+  }
 
-          // Re: urlbarDeferred, see the comment in urlbarBindings.xml.
-          if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) {
-            return false;
-          }
-
-          const isMac = /Mac/.test(navigator.platform);
-          var cancel = false;
+  _onUnderflow() {
+    this._inOverflow = false;
+    this._handleOverflow();
+  }
 
-          // Catch any keys that could potentially move the caret. Ctrl can be
-          // used in combination with these keys on Windows and Linux; and Alt
-          // can be used on OS X, so make sure the unused one isn't used.
-          let metaKey = isMac ? aEvent.ctrlKey : aEvent.altKey;
-          if (!metaKey) {
-            switch (aEvent.keyCode) {
-              case KeyEvent.DOM_VK_LEFT:
-              case KeyEvent.DOM_VK_RIGHT:
-              case KeyEvent.DOM_VK_HOME:
-                cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
-                break;
-            }
-          }
+  _getBoundaryIndices(aText, aSearchTokens) {
+    // Short circuit for empty search ([""] == "")
+    if (aSearchTokens == "")
+      return [0, aText.length];
 
-          // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt)
-          if (!aEvent.ctrlKey && !aEvent.altKey) {
-            switch (aEvent.keyCode) {
-              case KeyEvent.DOM_VK_TAB:
-                if (this.tabScrolling && this.popup.popupOpen)
-                  cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ?
-                                                                KeyEvent.DOM_VK_UP :
-                                                                KeyEvent.DOM_VK_DOWN);
-                else if (this.forceComplete && this.mController.matchCount >= 1)
-                  this.mController.handleTab();
-                break;
-              case KeyEvent.DOM_VK_UP:
-              case KeyEvent.DOM_VK_DOWN:
-              case KeyEvent.DOM_VK_PAGE_UP:
-              case KeyEvent.DOM_VK_PAGE_DOWN:
-                cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
-                break;
-            }
-          }
+    // Find which regions of text match the search terms
+    let regions = [];
+    for (let search of Array.prototype.slice.call(aSearchTokens)) {
+      let matchIndex = -1;
+      let searchLen = search.length;
 
-          // Handle readline/emacs-style navigation bindings on Mac.
-          if (isMac &&
-              this.popup.popupOpen &&
-              aEvent.ctrlKey &&
-              (aEvent.key === "n" || aEvent.key === "p")) {
-            const effectiveKey = (aEvent.key === "p") ?
-                                 KeyEvent.DOM_VK_UP :
-                                 KeyEvent.DOM_VK_DOWN;
-            cancel = this.mController.handleKeyNavigation(effectiveKey);
-          }
+      // Find all matches of the search terms, but stop early for perf
+      let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase();
+      while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) {
+        regions.push([matchIndex, matchIndex + searchLen]);
+      }
+    }
+
+    // Sort the regions by start position then end position
+    regions = regions.sort((a, b) => {
+      let start = a[0] - b[0];
+      return (start == 0) ? a[1] - b[1] : start;
+    });
 
-          // Handle keys we know aren't part of a shortcut, even with Alt or
-          // Ctrl.
-          switch (aEvent.keyCode) {
-            case KeyEvent.DOM_VK_ESCAPE:
-              cancel = this.mController.handleEscape();
-              break;
-            case KeyEvent.DOM_VK_RETURN:
-              if (isMac) {
-                // Prevent the default action, since it will beep on Mac
-                if (aEvent.metaKey)
-                  aEvent.preventDefault();
-              }
-              if (this.popup.selectedIndex >= 0) {
-                this._selectionDetails = {
-                  index: this.popup.selectedIndex,
-                  kind: "key",
-                };
-              }
-              cancel = this.handleEnter(aEvent, aOptions);
-              break;
-            case KeyEvent.DOM_VK_DELETE:
-              if (isMac && !aEvent.shiftKey) {
-                break;
-              }
-              cancel = this.handleDelete();
-              break;
-            case KeyEvent.DOM_VK_BACK_SPACE:
-              if (isMac && aEvent.shiftKey) {
-                cancel = this.handleDelete();
-              }
-              break;
-            case KeyEvent.DOM_VK_DOWN:
-            case KeyEvent.DOM_VK_UP:
-              if (aEvent.altKey)
-                this.toggleHistoryPopup();
-              break;
-            case KeyEvent.DOM_VK_F4:
-              if (!isMac) {
-                this.toggleHistoryPopup();
-              }
-              break;
-          }
+    // Generate the boundary indices from each region
+    let start = 0;
+    let end = 0;
+    let boundaries = [];
+    let len = regions.length;
+    for (let i = 0; i < len; i++) {
+      // We have a new boundary if the start of the next is past the end
+      let region = regions[i];
+      if (region[0] > end) {
+        // First index is the beginning of match
+        boundaries.push(start);
+        // Second index is the beginning of non-match
+        boundaries.push(end);
+
+        // Track the new region now that we've stored the previous one
+        start = region[0];
+      }
+
+      // Push back the end index for the current or new region
+      end = Math.max(end, region[1]);
+    }
 
-          if (cancel) {
-            aEvent.stopPropagation();
-            aEvent.preventDefault();
-          }
-
-          return true;
-        ]]></body>
-      </method>
+    // Add the last region
+    boundaries.push(start);
+    boundaries.push(end);
 
-      <method name="handleEnter">
-        <parameter name="event"/>
-        <body><![CDATA[
-          return this.mController.handleEnter(false, event || null);
-        ]]></body>
-      </method>
+    // Put on the end boundary if necessary
+    if (end < aText.length)
+      boundaries.push(aText.length);
 
-      <method name="handleDelete">
-        <body><![CDATA[
-          return this.mController.handleDelete();
-        ]]></body>
-      </method>
+    // Skip the first item because it's always 0
+    return boundaries.slice(1);
+  }
 
-      <!-- ::::::::::::: miscellaneous ::::::::::::: -->
+  _getSearchTokens(aSearch) {
+    let search = aSearch.toLowerCase();
+    return search.split(/\s+/);
+  }
 
-      <method name="initSearchNames">
-        <body><![CDATA[
-          if (!this.mSearchNames) {
-            var names = this.getAttribute("autocompletesearch");
-            if (!names)
-              this.mSearchNames = [];
-            else
-              this.mSearchNames = names.split(" ");
-          }
-        ]]></body>
-      </method>
-
-      <method name="_focus">
-        <!-- doesn't reset this.mController -->
-        <body><![CDATA[
-          this._dontBlur = true;
-          this.focus();
-          this._dontBlur = false;
-        ]]></body>
-      </method>
+  _setUpDescription(aDescriptionElement, aText, aNoEmphasis) {
+    // Get rid of all previous text
+    if (!aDescriptionElement) {
+      return;
+    }
+    while (aDescriptionElement.hasChildNodes())
+      aDescriptionElement.firstChild.remove();
 
-      <method name="resetActionType">
-        <body><![CDATA[
-          if (this.mIgnoreInput)
-            return;
-          this.removeAttribute("actiontype");
-        ]]></body>
-      </method>
+    // If aNoEmphasis is specified, don't add any emphasis
+    if (aNoEmphasis) {
+      aDescriptionElement.appendChild(document.createTextNode(aText));
+      return;
+    }
+
+    // Get the indices that separate match and non-match text
+    let search = this.getAttribute("text");
+    let tokens = this._getSearchTokens(search);
+    let indices = this._getBoundaryIndices(aText, tokens);
 
-      <field name="_valueIsPasted">false</field>
-      <field name="_pasteController"><![CDATA[
-        ({
-          _autocomplete: this,
-          _kGlobalClipboard: Ci.nsIClipboard.kGlobalClipboard,
-          supportsCommand: aCommand => aCommand == "cmd_paste",
-          doCommand(aCommand) {
-            this._autocomplete._valueIsPasted = true;
-            this._autocomplete.editor.paste(this._kGlobalClipboard);
-            this._autocomplete._valueIsPasted = false;
-          },
-          isCommandEnabled(aCommand) {
-            return this._autocomplete.editor.isSelectionEditable &&
-                   this._autocomplete.editor.canPaste(this._kGlobalClipboard);
-          },
-          onEvent() {},
-        })
-      ]]></field>
+    this._appendDescriptionSpans(indices, aText, aDescriptionElement,
+      aDescriptionElement);
+  }
+
+  _appendDescriptionSpans(indices, text, spansParentElement, descriptionElement) {
+    let next;
+    let start = 0;
+    let len = indices.length;
+    // Even indexed boundaries are matches, so skip the 0th if it's empty
+    for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
+      next = indices[i];
+      let spanText = text.substr(start, next - start);
+      start = next;
 
-      <method name="_setValueInternal">
-        <parameter name="aValue"/>
-        <parameter name="aIsUserInput"/>
-        <body><![CDATA[
-          this.mIgnoreInput = true;
+      if (i % 2 == 0) {
+        // Emphasize the text for even indices
+        let span = spansParentElement.appendChild(
+          document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+        this._setUpEmphasisSpan(span, descriptionElement);
+        span.textContent = spanText;
+      } else {
+        // Otherwise, it's plain text
+        spansParentElement.appendChild(document.createTextNode(spanText));
+      }
+    }
+  }
 
-          if (typeof this.onBeforeValueSet == "function")
-            aValue = this.onBeforeValueSet(aValue);
+  _setUpTags(tags) {
+    while (this._tagsText.hasChildNodes()) {
+      this._tagsText.firstChild.remove();
+    }
 
-          if (typeof this.trimValue == "function" &&
-              !this._textValueSetByCompleteDefault)
-            aValue = this.trimValue(aValue);
+    let anyTagsMatch = false;
 
-          this.valueIsTyped = false;
-          if (aIsUserInput) {
-            this.inputField.setUserInput(aValue);
-          } else {
-            this.inputField.value = aValue;
-          }
+    // Include only tags that match the search string.
+    for (let tag of tags) {
+      // Check if the tag matches the search string.
+      let search = this.getAttribute("text");
+      let tokens = this._getSearchTokens(search);
+      let indices = this._getBoundaryIndices(tag, tokens);
 
-          if (typeof this.formatValue == "function")
-            this.formatValue();
+      if (indices.length == 2 &&
+        indices[0] == 0 &&
+        indices[1] == tag.length) {
+        // The tag doesn't match the search string, so don't include it.
+        continue;
+      }
 
-          this.mIgnoreInput = false;
-          var event = document.createEvent("Events");
-          event.initEvent("ValueChange", true, true);
-          this.inputField.dispatchEvent(event);
-          return aValue;
-        ]]></body>
-      </method>
+      anyTagsMatch = true;
+
+      let tagSpan =
+        document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+      tagSpan.classList.add("ac-tag");
+      this._tagsText.appendChild(tagSpan);
 
-      <method name="onInput">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          if (!this.mIgnoreInput && this.mController.input == this) {
-            this.valueIsTyped = true;
-            this.mController.handleText();
-          }
-          this.resetActionType();
-        ]]></body>
-      </method>
-    </implementation>
+      this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText);
+    }
 
-    <handlers>
-      <handler event="input"><![CDATA[
-        this.onInput(event);
-      ]]></handler>
+    return anyTagsMatch;
+  }
 
-      <handler event="keypress" phase="capturing" group="system"
-               action="return this.onKeyPress(event);"/>
-
-      <handler event="compositionstart" phase="capturing"
-               action="if (this.mController.input == this) this.mController.handleStartComposition();"/>
-
-      <handler event="compositionend" phase="capturing"
-               action="if (this.mController.input == this) this.mController.handleEndComposition();"/>
-
-      <handler event="focus" phase="capturing"><![CDATA[
-        this.attachController();
-        if (window.gBrowser && window.gBrowser.selectedBrowser.hasAttribute("usercontextid")) {
-          this.userContextId = parseInt(window.gBrowser.selectedBrowser.getAttribute("usercontextid"));
-        } else {
-          this.userContextId = 0;
-        }
-      ]]></handler>
+  _setUpEmphasisSpan(aSpan, aDescriptionElement) {
+    aSpan.classList.add("ac-emphasize-text");
+    switch (aDescriptionElement) {
+      case this._titleText:
+        aSpan.classList.add("ac-emphasize-text-title");
+        break;
+      case this._tagsText:
+        aSpan.classList.add("ac-emphasize-text-tag");
+        break;
+      case this._urlText:
+        aSpan.classList.add("ac-emphasize-text-url");
+        break;
+      case this._actionText:
+        aSpan.classList.add("ac-emphasize-text-action");
+        break;
+    }
+  }
 
-      <handler event="blur" phase="capturing"><![CDATA[
-        if (!this._dontBlur) {
-          if (this.forceComplete && this.mController.matchCount >= 1) {
-            // If forceComplete is requested, we need to call the enter processing
-            // on blur so the input will be forced to the closest match.
-            // Thunderbird is the only consumer of forceComplete and this is used
-            // to force an recipient's email to the exact address book entry.
-            this.mController.handleEnter(true);
-          }
-          if (!this.ignoreBlurWhileSearching)
-            this.detachController();
-        }
-      ]]></handler>
-    </handlers>
-  </binding>
+  /**
+   * This will generate an array of emphasis pairs for use with
+   * _setUpEmphasisedSections(). Each pair is a tuple (array) that
+   * represents a block of text - containing the text of that block, and a
+   * boolean for whether that block should have an emphasis styling applied
+   * to it.
+   *
+   * These pairs are generated by parsing a localised string (aSourceString)
+   * with parameters, in the format that is used by
+   * nsIStringBundle.formatStringFromName():
+   *
+   * "textA %1$S textB textC %2$S"
+   *
+   * Or:
+   *
+   * "textA %S"
+   *
+   * Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided
+   * replacement strings. These are specified an array of tuples
+   * (aReplacements), each containing the replacement text and a boolean for
+   * whether that text should have an emphasis styling applied. This is used
+   * as a 1-based array - ie, "%1$S" is replaced by the item in the first
+   * index of aReplacements, "%2$S" by the second, etc. "%S" will always
+   * match the first index.
+   */
+  _generateEmphasisPairs(aSourceString, aReplacements) {
+    let pairs = [];
 
-  <binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/popup.xml#popup">
-    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
-      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/>
-      <xul:hbox>
-        <children/>
-      </xul:hbox>
-    </content>
+    // Split on %S, %1$S, %2$S, etc. ie:
+    //   "textA %S"
+    //     becomes ["textA ", "%S"]
+    //   "textA %1$S textB textC %2$S"
+    //     becomes ["textA ", "%1$S", " textB textC ", "%2$S"]
+    let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/);
 
-    <implementation implements="nsIAutoCompletePopup">
-      <field name="mInput">null</field>
-      <field name="mPopupOpen">false</field>
-      <field name="_currentIndex">0</field>
-
-      <constructor><![CDATA[
-        if (!this.listEvents) {
-          this.listEvents = {
-            handleEvent: event => {
-              if (!this.parentNode) {
-                return;
-              }
+    for (let part of parts) {
+      // The above regex will actually give us an empty string at the
+      // end - we don't want that, as we don't want to later generate an
+      // empty text node for it.
+      if (part.length === 0)
+        continue;
 
-              switch (event.type) {
-                case "mouseup":
-                  // Don't call onPopupClick for the scrollbar buttons, thumb,
-                  // slider, etc. If we hit the richlistbox and not a
-                  // richlistitem, we ignore the event.
-                  if (event.target.closest("richlistbox,richlistitem")
-                                  .localName == "richlistitem") {
-                    this.onPopupClick(event);
-                  }
-                  break;
-                case "mousemove":
-                  if (Date.now() - this.mLastMoveTime <= 30) {
-                    return;
-                  }
+      // Determine if this token is a replacement token or a normal text
+      // token. If it is a replacement token, we want to extract the
+      // numerical number. However, we still want to match on "$S".
+      let match = part.match(/^%(?:([0-9]+)\$)?S$/);
+
+      if (match) {
+        // "%S" doesn't have a numerical number in it, but will always
+        // be assumed to be 1. Furthermore, the input string specifies
+        // these with a 1-based index, but we want a 0-based index.
+        let index = (match[1] || 1) - 1;
 
-                  let item = event.target.closest("richlistbox,richlistitem");
-
-                  // If we hit the richlistbox and not a richlistitem, we ignore
-                  // the event.
-                  if (item.localName == "richlistbox") {
-                    return;
-                  }
+        if (index >= 0 && index < aReplacements.length) {
+          pairs.push([...aReplacements[index]]);
+        }
+      } else {
+        pairs.push([part]);
+      }
+    }
 
-                  let index = this.richlistbox.getIndexOfItem(item);
-
-                  this.mousedOverIndex = index;
-
-                  if (item.selectedByMouseOver) {
-                    this.richlistbox.selectedIndex = index;
-                  }
+    return pairs;
+  }
 
-                  this.mLastMoveTime = Date.now();
-                  break;
-              }
-            },
-          };
-          this.richlistbox.addEventListener("mouseup", this.listEvents);
-          this.richlistbox.addEventListener("mousemove", this.listEvents);
-        }
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        if (this.listEvents) {
-          this.richlistbox.removeEventListener("mouseup", this.listEvents);
-          this.richlistbox.removeEventListener("mousemove", this.listEvents);
-          delete this.listEvents;
-        }
-      ]]></destructor>
-
-      <!-- =================== nsIAutoCompletePopup =================== -->
+  /**
+   * _setUpEmphasisedSections() has the same use as _setUpDescription,
+   * except instead of taking a string and highlighting given tokens, it takes
+   * an array of pairs generated by _generateEmphasisPairs(). This allows
+   * control over emphasising based on specific blocks of text, rather than
+   * search for substrings.
+   */
+  _setUpEmphasisedSections(aDescriptionElement, aTextPairs) {
+    // Get rid of all previous text
+    while (aDescriptionElement.hasChildNodes())
+      aDescriptionElement.firstChild.remove();
 
-      <property name="input" readonly="true"
-                onget="return this.mInput"/>
-
-      <property name="overrideValue" readonly="true"
-                onget="return null;"/>
-
-      <property name="popupOpen" readonly="true"
-                onget="return this.mPopupOpen;"/>
+    for (let [text, emphasise] of aTextPairs) {
+      if (emphasise) {
+        let span = aDescriptionElement.appendChild(
+          document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+        span.textContent = text;
+        switch (emphasise) {
+          case "match":
+            this._setUpEmphasisSpan(span, aDescriptionElement);
+            break;
+        }
+      } else {
+        aDescriptionElement.appendChild(document.createTextNode(text));
+      }
+    }
+  }
 
-      <method name="closePopup">
-        <body>
-          <![CDATA[
-          if (this.mPopupOpen) {
-            this.hidePopup();
-            this.removeAttribute("width");
-          }
-        ]]>
-        </body>
-      </method>
+  _unescapeUrl(url) {
+    return Services.textToSubURI.unEscapeURIForUI("UTF-8", url);
+  }
+
+  _reuseAcItem() {
+    let action = this._parseActionUrl(this.getAttribute("url"));
+    let popup = this.parentNode.parentNode;
+
+    // If the item is a searchengine action, then it should
+    // only be reused if the engine name is the same as the
+    // popup's override engine name, if any.
+    if (!action ||
+      action.type != "searchengine" ||
+      !popup.overrideSearchEngineName ||
+      action.params.engineName == popup.overrideSearchEngineName) {
+      this.collapsed = false;
 
-      <!-- This is the default number of rows that we give the autocomplete
-           popup when the textbox doesn't have a "maxrows" attribute
-           for us to use. -->
-      <field name="defaultMaxRows" readonly="true">6</field>
+      // The popup may have changed size between now and the last
+      // time the item was shown, so always handle over/underflow.
+      let dwu = window.windowUtils;
+      let popupWidth = dwu.getBoundsWithoutFlushing(this.parentNode).width;
+      if (!this._previousPopupWidth || this._previousPopupWidth != popupWidth) {
+        this._previousPopupWidth = popupWidth;
+        this.handleOverUnderflow();
+      }
 
-      <!-- In some cases (e.g. when the input's dropmarker button is clicked),
-           the input wants to display a popup with more rows. In that case, it
-           should increase its maxRows property and store the "normal" maxRows
-           in this field. When the popup is hidden, we restore the input's
-           maxRows to the value stored in this field.
-
-           This field is set to -1 between uses so that we can tell when it's
-           been set by the input and when we need to set it in the popupshowing
-           handler. -->
-      <field name="_normalMaxRows">-1</field>
+      return true;
+    }
 
-      <property name="maxRows" readonly="true">
-        <getter>
-          <![CDATA[
-          return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
-        ]]>
-        </getter>
-      </property>
+    return false;
+  }
 
-      <method name="getNextIndex">
-        <parameter name="aReverse"/>
-        <parameter name="aAmount"/>
-        <parameter name="aIndex"/>
-        <parameter name="aMaxRow"/>
-        <body><![CDATA[
-          if (aMaxRow < 0)
-            return -1;
+  _adjustAcItem() {
+    let originalUrl = this.getAttribute("ac-value");
+    let title = this.getAttribute("ac-comment");
+    this.setAttribute("url", originalUrl);
+    this.setAttribute("image", this.getAttribute("ac-image"));
+    this.setAttribute("title", title);
+    this.setAttribute("text", this.getAttribute("ac-text"));
+
+    let popup = this.parentNode.parentNode;
+    let titleLooksLikeUrl = false;
+    let displayUrl = originalUrl;
+    let emphasiseUrl = true;
+    let trimDisplayUrl = true;
 
-          var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount;
-          if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow)
-            newIdx = aMaxRow;
-          else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0)
-            newIdx = 0;
+    let type = this.getAttribute("originaltype");
+    let types = new Set(type.split(/\s+/));
+    let initialTypes = new Set(types);
+    // Remove types that should ultimately not be in the `type` string.
+    types.delete("action");
+    types.delete("autofill");
+    types.delete("heuristic");
+    type = [...types][0] || "";
 
-          if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow)
-            aIndex = -1;
-          else
-            aIndex = newIdx;
+    let action;
 
-          return aIndex;
-        ]]></body>
-      </method>
+    if (initialTypes.has("autofill") && !initialTypes.has("action")) {
+      // Treat autofills as visiturl actions, unless they are already also
+      // actions.
+      action = {
+        type: "visiturl",
+        params: { url: title },
+      };
+    }
 
-      <method name="onPopupClick">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          this.input.controller.handleEnter(true, aEvent);
-        ]]></body>
-      </method>
+    this.removeAttribute("actiontype");
+    this.classList.remove(
+      "overridable-action",
+      "emptySearchQuery",
+      "aliasOffer"
+    );
 
-      <property name="selectedIndex"
-                onget="return this.richlistbox.selectedIndex;">
-        <setter>
-          <![CDATA[
-          if (val != this.richlistbox.selectedIndex) {
-            this._previousSelectedIndex = this.richlistbox.selectedIndex;
-          }
-          this.richlistbox.selectedIndex = val;
-          // Since ensureElementIsVisible may cause an expensive Layout flush,
-          // invoke it only if there may be a scrollbar, so if we could fetch
-          // more results than we can show at once.
-          // maxResults is the maximum number of fetched results, maxRows is the
-          // maximum number of rows we show at once, without a scrollbar.
-          if (this.mPopupOpen && this.maxResults > this.maxRows) {
-            // when clearing the selection (val == -1, so selectedItem will be
-            // null), we want to scroll back to the top.  see bug #406194
-            this.richlistbox.ensureElementIsVisible(
-              this.richlistbox.selectedItem || this.richlistbox.firstElementChild);
+    // If the type includes an action, set up the item appropriately.
+    if (initialTypes.has("action") || action) {
+      action = action || this._parseActionUrl(originalUrl);
+      this.setAttribute("actiontype", action.type);
+
+      switch (action.type) {
+        case "switchtab":
+          {
+            this.classList.add("overridable-action");
+            displayUrl = action.params.url;
+            let desc = this._stringBundle.GetStringFromName("switchToTab2");
+            this._setUpDescription(this._actionText, desc, true);
+            break;
           }
-          return val;
-        ]]>
-        </setter>
-      </property>
-
-      <field name="_previousSelectedIndex">-1</field>
-      <field name="mLastMoveTime">Date.now()</field>
-      <field name="mousedOverIndex">-1</field>
-
-      <method name="onSearchBegin">
-        <body><![CDATA[
-          this.mousedOverIndex = -1;
-
-          if (typeof this._onSearchBegin == "function") {
-            this._onSearchBegin();
-          }
-        ]]></body>
-      </method>
-
-      <method name="openAutocompletePopup">
-        <parameter name="aInput"/>
-        <parameter name="aElement"/>
-        <body>
-          <![CDATA[
-          // until we have "baseBinding", (see bug #373652) this allows
-          // us to override openAutocompletePopup(), but still call
-          // the method on the base class
-          this._openAutocompletePopup(aInput, aElement);
-        ]]>
-        </body>
-      </method>
-
-      <method name="_openAutocompletePopup">
-        <parameter name="aInput"/>
-        <parameter name="aElement"/>
-        <body>
-          <![CDATA[
-          if (!this.mPopupOpen) {
-            // It's possible that the panel is hidden initially
-            // to avoid impacting startup / new window performance
-            aInput.popup.hidden = false;
-
-            this.mInput = aInput;
-            // clear any previous selection, see bugs 400671 and 488357
-            this.selectedIndex = -1;
-
-            var width = aElement.getBoundingClientRect().width;
-            this.setAttribute("width", width > 100 ? width : 100);
-            // invalidate() depends on the width attribute
-            this._invalidate();
-
-            this.openPopup(aElement, "after_start", 0, 0, false, false);
+        case "remotetab":
+          {
+            displayUrl = action.params.url;
+            let desc = action.params.deviceName;
+            this._setUpDescription(this._actionText, desc, true);
+            break;
           }
-        ]]>
-        </body>
-      </method>
-
-      <method name="invalidate">
-        <parameter name="reason"/>
-        <body>
-          <![CDATA[
-          // Don't bother doing work if we're not even showing
-          if (!this.mPopupOpen)
-            return;
-
-          this._invalidate(reason);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_invalidate">
-        <parameter name="reason"/>
-        <body>
-          <![CDATA[
-          // collapsed if no matches
-          this.richlistbox.collapsed = (this.matchCount == 0);
-
-          // Update the richlistbox height.
-          if (this._adjustHeightRAFToken) {
-            cancelAnimationFrame(this._adjustHeightRAFToken);
-            this._adjustHeightRAFToken = null;
-          }
-
-          if (this.mPopupOpen) {
-            delete this._adjustHeightOnPopupShown;
-            this._adjustHeightRAFToken = requestAnimationFrame(() => this.adjustHeight());
-          } else {
-            this._adjustHeightOnPopupShown = true;
-          }
-
-          this._currentIndex = 0;
-          if (this._appendResultTimeout) {
-            clearTimeout(this._appendResultTimeout);
-          }
-          this._appendCurrentResult(reason);
-        ]]>
-        </body>
-      </method>
-
-      <property name="maxResults" readonly="true">
-        <getter>
-          <![CDATA[
-            // This is how many richlistitems will be kept around.
-            // Note, this getter may be overridden, or instances
-            // can have the nomaxresults attribute set to have no
-            // limit.
-            if (this.getAttribute("nomaxresults") == "true") {
-              return Infinity;
-            }
-
-            return 20;
-          ]]>
-        </getter>
-      </property>
-
-      <property name="matchCount" readonly="true">
-        <getter>
-          <![CDATA[
-          return Math.min(this.mInput.controller.matchCount, this.maxResults);
-          ]]>
-        </getter>
-      </property>
-
-      <method name="_collapseUnusedItems">
-        <body>
-          <![CDATA[
-            let existingItemsCount = this.richlistbox.children.length;
-            for (let i = this.matchCount; i < existingItemsCount; ++i) {
-              let item = this.richlistbox.children[i];
-
-              item.collapsed = true;
-              if (typeof item._onCollapse == "function") {
-                item._onCollapse();
-              }
-            }
-          ]]>
-        </body>
-      </method>
+        case "searchengine":
+          {
+            emphasiseUrl = false;
 
-      <method name="adjustHeight">
-        <body>
-          <![CDATA[
-          // Figure out how many rows to show
-          let rows = this.richlistbox.children;
-          let numRows = Math.min(this.matchCount, this.maxRows, rows.length);
-
-          // Default the height to 0 if we have no rows to show
-          let height = 0;
-          if (numRows) {
-            let firstRowRect = rows[0].getBoundingClientRect();
-            if (this._rlbPadding == undefined) {
-              let style = window.getComputedStyle(this.richlistbox);
-              let paddingTop = parseInt(style.paddingTop) || 0;
-              let paddingBottom = parseInt(style.paddingBottom) || 0;
-              this._rlbPadding = paddingTop + paddingBottom;
-            }
-
-            // The class `forceHandleUnderflow` is for the item might need to
-            // handle OverUnderflow or Overflow when the height of an item will
-            // be changed dynamically.
-            for (let i = 0; i < numRows; i++) {
-              if (rows[i].classList.contains("forceHandleUnderflow")) {
-                rows[i].handleOverUnderflow();
-              }
-            }
-
-            let lastRowRect = rows[numRows - 1].getBoundingClientRect();
-            // Calculate the height to have the first row to last row shown
-            height = lastRowRect.bottom - firstRowRect.top +
-                     this._rlbPadding;
-          }
-
-          let currentHeight = this.richlistbox.getBoundingClientRect().height;
-          if (height <= currentHeight) {
-            this._collapseUnusedItems();
-          }
-          this.richlistbox.style.removeProperty("height");
-          this.richlistbox.height = height;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_appendCurrentResult">
-        <parameter name="invalidateReason"/>
-        <body>
-          <![CDATA[
-          var controller = this.mInput.controller;
-          var matchCount = this.matchCount;
-          var existingItemsCount = this.richlistbox.children.length;
-
-          // Process maxRows per chunk to improve performance and user experience
-          for (let i = 0; i < this.maxRows; i++) {
-            if (this._currentIndex >= matchCount) {
-              break;
-            }
-            let item;
-            let reusable = false;
-            let itemExists = this._currentIndex < existingItemsCount;
+            // The order here is not localizable, we default to appending
+            // "- Search with Engine" to the search string, to be able to
+            // properly generate emphasis pairs. That said, no localization
+            // changed the order while it was possible, so doesn't look like
+            // there's a strong need for that.
+            let {
+              engineName,
+              searchSuggestion,
+              searchQuery,
+              alias,
+            } = action.params;
 
-            let originalValue, originalText, originalType;
-            let style = controller.getStyleAt(this._currentIndex);
-            let value =
-              style && style.includes("autofill") ?
-              controller.getFinalCompleteValueAt(this._currentIndex) :
-              controller.getValueAt(this._currentIndex);
-            let label = controller.getLabelAt(this._currentIndex);
-            let comment = controller.getCommentAt(this._currentIndex);
-            let image = controller.getImageAt(this._currentIndex);
-            // trim the leading/trailing whitespace
-            let trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
-
-            if (itemExists) {
-              item = this.richlistbox.children[this._currentIndex];
-
-              // Url may be a modified version of value, see _adjustAcItem().
-              originalValue = item.getAttribute("url") || item.getAttribute("ac-value");
-              originalText = item.getAttribute("ac-text");
-              originalType = item.getAttribute("originaltype");
-
-              // The styles on the list which have different <content> structure and overrided
-              // _adjustAcItem() are unreusable.
-              const UNREUSEABLE_STYLES = [
-                "autofill-profile",
-                "autofill-footer",
-                "autofill-clear-button",
-                "autofill-insecureWarning",
-              ];
-              // Reuse the item when its style is exactly equal to the previous style or
-              // neither of their style are in the UNREUSEABLE_STYLES.
-              reusable = originalType === style ||
-                !(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
-            } else {
-              // need to create a new item
-              item = document.createXULElement("richlistitem");
-            }
-
-            item.setAttribute("dir", this.style.direction);
-            item.setAttribute("ac-image", image);
-            item.setAttribute("ac-value", value);
-            item.setAttribute("ac-label", label);
-            item.setAttribute("ac-comment", comment);
-            item.setAttribute("ac-text", trimmedSearchString);
-
-            // Completely reuse the existing richlistitem for invalidation
-            // due to new results, but only when: the item is the same, *OR*
-            // we are about to replace the currently moused-over item, to
-            // avoid surprising the user.
-            let iface = Ci.nsIAutoCompletePopup;
-            if (reusable &&
-                originalText == trimmedSearchString &&
-                invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
-                (originalValue == value ||
-                 this.mousedOverIndex === this._currentIndex)) {
-              // try to re-use the existing item
-              let reused = item._reuseAcItem();
-              if (reused) {
-                this._currentIndex++;
-                continue;
-              }
-            } else {
-              if (typeof item._cleanup == "function") {
-                item._cleanup();
-              }
-              item.setAttribute("originaltype", style);
-            }
-
-            if (itemExists) {
-              // Adjust only when the result's type is reusable for existing
-              // item's. Otherwise, we might insensibly call old _adjustAcItem()
-              // as new binding has not been attached yet.
-              // We don't need to worry about switching to new binding, since
-              // _adjustAcItem() will fired by its own constructor accordingly.
-              if (reusable) {
-                item._adjustAcItem();
-              }
-              item.collapsed = false;
-            } else {
-              // set the class at the end so we can use the attributes
-              // in the xbl constructor
-              item.className = "autocomplete-richlistitem";
-              this.richlistbox.appendChild(item);
+            // Override the engine name if the popup defines an override.
+            let override = popup.overrideSearchEngineName;
+            if (override && override != engineName) {
+              engineName = override;
+              action.params.engineName = override;
+              let newURL =
+                PlacesUtils.mozActionURI(action.type, action.params);
+              this.setAttribute("url", newURL);
             }
 
-            this._currentIndex++;
-          }
-
-          if (typeof this.onResultsAdded == "function") {
-            // The items bindings may not be attached yet, so we must delay this
-            // before we can properly handle items properly without breaking
-            // the richlistbox.
-            Services.tm.dispatchToMainThread(() => this.onResultsAdded());
-          }
-
-          if (this._currentIndex < matchCount) {
-            // yield after each batch of items so that typing the url bar is
-            // responsive
-            this._appendResultTimeout = setTimeout(() => this._appendCurrentResult(), 0);
-          }
-        ]]>
-        </body>
-      </method>
-
-      <property name="overflowPadding"
-                onget="return Number(this.getAttribute('overflowpadding'))"
-                readonly="true" />
-
-      <method name="selectBy">
-        <parameter name="aReverse"/>
-        <parameter name="aPage"/>
-        <body>
-          <![CDATA[
-          try {
-            var amount = aPage ? 5 : 1;
-
-            // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
-            this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.matchCount - 1);
-            if (this.selectedIndex == -1) {
-              this.input._focus();
-            }
-          } catch (ex) {
-            // do nothing - occasionally timer-related js errors happen here
-            // e.g. "this.selectedIndex has no properties", when you type fast and hit a
-            // navigation key before this popup has opened
-          }
-            ]]>
-        </body>
-      </method>
-
-      <field name="richlistbox">
-        document.getAnonymousElementByAttribute(this, "anonid", "richlistbox");
-      </field>
-
-      <property name="view"
-                onget="return this.mInput.controller;"
-                onset="return val;"/>
-
-    </implementation>
-    <handlers>
-      <handler event="popupshowing"><![CDATA[
-        // If normalMaxRows wasn't already set by the input, then set it here
-        // so that we restore the correct number when the popup is hidden.
-
-        // Null-check this.mInput; see bug 1017914
-        if (this._normalMaxRows < 0 && this.mInput) {
-          this._normalMaxRows = this.mInput.maxRows;
-        }
-
-        // Set an attribute for styling the popup based on the input.
-        let inputID = "";
-        if (this.mInput && this.mInput.ownerDocument &&
-            this.mInput.ownerDocument.documentURIObject.schemeIs("chrome")) {
-          inputID = this.mInput.id;
-          // Take care of elements with no id that are inside xbl bindings
-          if (!inputID) {
-            let bindingParent = this.mInput.ownerDocument.getBindingParent(this.mInput);
-            if (bindingParent) {
-              inputID = bindingParent.id;
-            }
-          }
-        }
-        this.setAttribute("autocompleteinput", inputID);
-
-        this.mPopupOpen = true;
-      ]]></handler>
-
-      <handler event="popupshown">
-        <![CDATA[
-          if (this._adjustHeightOnPopupShown) {
-            delete this._adjustHeightOnPopupShown;
-            this.adjustHeight();
-          }
-      ]]>
-      </handler>
-
-      <handler event="popuphiding"><![CDATA[
-        var isListActive = true;
-        if (this.selectedIndex == -1)
-          isListActive = false;
-        this.input.controller.stopSearch();
-
-        this.removeAttribute("autocompleteinput");
-        this.mPopupOpen = false;
-
-        // Reset the maxRows property to the cached "normal" value (if there's
-        // any), and reset normalMaxRows so that we can detect whether it was set
-        // by the input when the popupshowing handler runs.
-
-        // Null-check this.mInput; see bug 1017914
-        if (this.mInput && this._normalMaxRows > 0) {
-          this.mInput.maxRows = this._normalMaxRows;
-        }
-        this._normalMaxRows = -1;
-        // If the list was being navigated and then closed, make sure
-        // we fire accessible focus event back to textbox
-
-        // Null-check this.mInput; see bug 1017914
-        if (isListActive && this.mInput) {
-          this.mInput.mIgnoreFocus = true;
-          this.mInput._focus();
-          this.mInput.mIgnoreFocus = false;
-        }
-      ]]></handler>
-    </handlers>
-  </binding>
-
-  <binding id="autocomplete-richlistitem-insecure-field" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
-    <content align="center"
-             onoverflow="this._onOverflow();"
-             onunderflow="this._onUnderflow();">
-      <xul:image anonid="type-icon"
-                 class="ac-type-icon"
-                 xbl:inherits="selected,current,type"/>
-      <xul:image anonid="site-icon"
-                 class="ac-site-icon"
-                 xbl:inherits="src=image,selected,type"/>
-      <xul:vbox class="ac-title"
-                align="left"
-                xbl:inherits="">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="title-text"
-                           class="ac-title-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:vbox>
-      <xul:hbox anonid="tags"
-                class="ac-tags"
-                align="center"
-                xbl:inherits="selected">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="tags-text"
-                           class="ac-tags-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox anonid="separator"
-                class="ac-separator"
-                align="center"
-                xbl:inherits="selected,actiontype,type">
-        <xul:description class="ac-separator-text">—</xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-url"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="url-text"
-                           class="ac-url-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-action"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="action-text"
-                           class="ac-action-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-    </content>
-
-    <handlers>
-      <handler event="click" button="0"><![CDATA[
-        let baseURL = this.Services.urlFormatter.formatURLPref("app.support.baseURL");
-        window.openTrustedLinkIn(baseURL + "insecure-password", "tab", {
-          relatedToCurrent: true,
-        });
-      ]]></handler>
-    </handlers>
-
-    <implementation>
-      <constructor><![CDATA[
-        // Unlike other autocomplete items, the height of the insecure warning
-        // increases by wrapping. So "forceHandleUnderflow" is for container to
-        // recalculate an item's height and width.
-        this.classList.add("forceHandleUnderflow");
-      ]]></constructor>
-
-      <property name="_learnMoreString">
-        <getter><![CDATA[
-          if (!this.__learnMoreString) {
-            this.__learnMoreString =
-              this.Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties").
-              GetStringFromName("insecureFieldWarningLearnMore");
-          }
-          return this.__learnMoreString;
-        ]]></getter>
-      </property>
-
-      <!-- Override _getSearchTokens to have the Learn More text emphasized -->
-      <method name="_getSearchTokens">
-        <parameter name="aSearch"/>
-        <body>
-          <![CDATA[
-            return [this._learnMoreString.toLowerCase()];
-          ]]>
-        </body>
-      </method>
-
-    </implementation>
-  </binding>
-
-  <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-
-    <content align="center"
-             onoverflow="this._onOverflow();"
-             onunderflow="this._onUnderflow();">
-      <xul:image anonid="type-icon"
-                 class="ac-type-icon"
-                 xbl:inherits="selected,current,type"/>
-      <xul:image anonid="site-icon"
-                 class="ac-site-icon"
-                 xbl:inherits="src=image,selected,type"/>
-      <xul:hbox class="ac-title"
-                align="center"
-                xbl:inherits="selected">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="title-text"
-                           class="ac-title-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox anonid="tags"
-                class="ac-tags"
-                align="center"
-                xbl:inherits="selected">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="tags-text"
-                           class="ac-tags-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox anonid="separator"
-                class="ac-separator"
-                align="center"
-                xbl:inherits="selected,actiontype,type">
-        <xul:description class="ac-separator-text">—</xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-url"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="url-text"
-                           class="ac-url-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-action"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="action-text"
-                           class="ac-action-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-    </content>
-
-    <implementation implements="nsIDOMXULSelectControlItemElement">
-      <constructor>
-        <![CDATA[
-          this._typeIcon = document.getAnonymousElementByAttribute(
-            this, "anonid", "type-icon"
-          );
-          this._siteIcon = document.getAnonymousElementByAttribute(
-            this, "anonid", "site-icon"
-          );
-          this._titleText = document.getAnonymousElementByAttribute(
-            this, "anonid", "title-text"
-          );
-          this._tags = document.getAnonymousElementByAttribute(
-            this, "anonid", "tags"
-          );
-          this._tagsText = document.getAnonymousElementByAttribute(
-            this, "anonid", "tags-text"
-          );
-          this._separator = document.getAnonymousElementByAttribute(
-            this, "anonid", "separator"
-          );
-          this._urlText = document.getAnonymousElementByAttribute(
-            this, "anonid", "url-text"
-          );
-          this._actionText = document.getAnonymousElementByAttribute(
-            this, "anonid", "action-text"
-          );
-          this._adjustAcItem();
-        ]]>
-      </constructor>
-
-      <property name="Services" readonly="true">
-        <getter><![CDATA[
-          let module = {};
-          if (window.Services) {
-            module.Services = window.Services;
-          } else {
-            ChromeUtils.import("resource://gre/modules/Services.jsm", module);
-          }
-          Object.defineProperty(this, "Services", {
-            configurable: true,
-            enumerable: true,
-            writable: true,
-            value: module.Services,
-          });
-          return module.Services;
-        ]]></getter>
-      </property>
-
-      <method name="_cleanup">
-        <body>
-        <![CDATA[
-          this.removeAttribute("url");
-          this.removeAttribute("image");
-          this.removeAttribute("title");
-          this.removeAttribute("text");
-          this.removeAttribute("displayurl");
-        ]]>
-        </body>
-      </method>
-
-      <property name="label" readonly="true">
-        <getter>
-          <![CDATA[
-            // This property is a string that is read aloud by screen readers,
-            // so it must not contain anything that should not be user-facing.
-
-            let parts = [
-              this.getAttribute("title"),
-              this.getAttribute("displayurl"),
-            ];
-            let label = parts.filter(str => str).join(" ");
-
-            // allow consumers that have extended popups to override
-            // the label values for the richlistitems
-            let panel = this.parentNode.parentNode;
-            if (panel.createResultLabel) {
-              return panel.createResultLabel(this, label);
-            }
-
-            return label;
-          ]]>
-        </getter>
-      </property>
-
-      <property name="_stringBundle">
-        <getter><![CDATA[
-          if (!this.__stringBundle) {
-            this.__stringBundle = this.Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
-          }
-          return this.__stringBundle;
-        ]]></getter>
-      </property>
-
-      <field name="_boundaryCutoff">null</field>
-
-      <property name="boundaryCutoff" readonly="true">
-        <getter>
-          <![CDATA[
-          if (!this._boundaryCutoff) {
-            this._boundaryCutoff =
-              Cc["@mozilla.org/preferences-service;1"].
-              getService(Ci.nsIPrefBranch).
-              getIntPref("toolkit.autocomplete.richBoundaryCutoff");
-          }
-          return this._boundaryCutoff;
-          ]]>
-        </getter>
-      </property>
-
-      <field name="_inOverflow">false</field>
-
-      <method name="_onOverflow">
-        <body>
-          <![CDATA[
-          this._inOverflow = true;
-          this._handleOverflow();
-          ]]>
-        </body>
-      </method>
-
-      <method name="_onUnderflow">
-        <body>
-          <![CDATA[
-          this._inOverflow = false;
-          this._handleOverflow();
-          ]]>
-        </body>
-      </method>
-
-      <method name="_getBoundaryIndices">
-        <parameter name="aText"/>
-        <parameter name="aSearchTokens"/>
-        <body>
-          <![CDATA[
-          // Short circuit for empty search ([""] == "")
-          if (aSearchTokens == "")
-            return [0, aText.length];
-
-          // Find which regions of text match the search terms
-          let regions = [];
-          for (let search of Array.prototype.slice.call(aSearchTokens)) {
-            let matchIndex = -1;
-            let searchLen = search.length;
-
-            // Find all matches of the search terms, but stop early for perf
-            let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase();
-            while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) {
-              regions.push([matchIndex, matchIndex + searchLen]);
-            }
-          }
-
-          // Sort the regions by start position then end position
-          regions = regions.sort((a, b) => {
-            let start = a[0] - b[0];
-            return (start == 0) ? a[1] - b[1] : start;
-          });
+            let engineStr =
+              this._stringBundle.formatStringFromName("searchWithEngine", [engineName], 1);
+            this._setUpDescription(this._actionText, engineStr, true);
 
-          // Generate the boundary indices from each region
-          let start = 0;
-          let end = 0;
-          let boundaries = [];
-          let len = regions.length;
-          for (let i = 0; i < len; i++) {
-            // We have a new boundary if the start of the next is past the end
-            let region = regions[i];
-            if (region[0] > end) {
-              // First index is the beginning of match
-              boundaries.push(start);
-              // Second index is the beginning of non-match
-              boundaries.push(end);
-
-              // Track the new region now that we've stored the previous one
-              start = region[0];
-            }
-
-            // Push back the end index for the current or new region
-            end = Math.max(end, region[1]);
-          }
-
-          // Add the last region
-          boundaries.push(start);
-          boundaries.push(end);
-
-          // Put on the end boundary if necessary
-          if (end < aText.length)
-            boundaries.push(aText.length);
-
-          // Skip the first item because it's always 0
-          return boundaries.slice(1);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_getSearchTokens">
-        <parameter name="aSearch"/>
-        <body>
-          <![CDATA[
-          let search = aSearch.toLowerCase();
-          return search.split(/\s+/);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_setUpDescription">
-        <parameter name="aDescriptionElement"/>
-        <parameter name="aText"/>
-        <parameter name="aNoEmphasis"/>
-        <body>
-          <![CDATA[
-          // Get rid of all previous text
-          if (!aDescriptionElement) {
-            return;
-          }
-          while (aDescriptionElement.hasChildNodes())
-            aDescriptionElement.firstChild.remove();
-
-          // If aNoEmphasis is specified, don't add any emphasis
-          if (aNoEmphasis) {
-            aDescriptionElement.appendChild(document.createTextNode(aText));
-            return;
-          }
-
-          // Get the indices that separate match and non-match text
-          let search = this.getAttribute("text");
-          let tokens = this._getSearchTokens(search);
-          let indices = this._getBoundaryIndices(aText, tokens);
-
-          this._appendDescriptionSpans(indices, aText, aDescriptionElement,
-                                       aDescriptionElement);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_appendDescriptionSpans">
-        <parameter name="indices"/>
-        <parameter name="text"/>
-        <parameter name="spansParentElement"/>
-        <parameter name="descriptionElement"/>
-        <body>
-          <![CDATA[
-          let next;
-          let start = 0;
-          let len = indices.length;
-          // Even indexed boundaries are matches, so skip the 0th if it's empty
-          for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
-            next = indices[i];
-            let spanText = text.substr(start, next - start);
-            start = next;
-
-            if (i % 2 == 0) {
-              // Emphasize the text for even indices
-              let span = spansParentElement.appendChild(
-                document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
-              this._setUpEmphasisSpan(span, descriptionElement);
-              span.textContent = spanText;
-            } else {
-              // Otherwise, it's plain text
-              spansParentElement.appendChild(document.createTextNode(spanText));
-            }
-          }
-          ]]>
-        </body>
-      </method>
-
-      <method name="_setUpTags">
-        <parameter name="tags"/>
-        <body>
-          <![CDATA[
-          while (this._tagsText.hasChildNodes()) {
-            this._tagsText.firstChild.remove();
-          }
-
-          let anyTagsMatch = false;
-
-          // Include only tags that match the search string.
-          for (let tag of tags) {
-            // Check if the tag matches the search string.
-            let search = this.getAttribute("text");
-            let tokens = this._getSearchTokens(search);
-            let indices = this._getBoundaryIndices(tag, tokens);
-
-            if (indices.length == 2 &&
-                indices[0] == 0 &&
-                indices[1] == tag.length) {
-              // The tag doesn't match the search string, so don't include it.
-              continue;
-            }
-
-            anyTagsMatch = true;
-
-            let tagSpan =
-              document.createElementNS("http://www.w3.org/1999/xhtml", "span");
-            tagSpan.classList.add("ac-tag");
-            this._tagsText.appendChild(tagSpan);
-
-            this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText);
-          }
-
-          return anyTagsMatch;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_setUpEmphasisSpan">
-        <parameter name="aSpan"/>
-        <parameter name="aDescriptionElement"/>
-        <body>
-          <![CDATA[
-          aSpan.classList.add("ac-emphasize-text");
-          switch (aDescriptionElement) {
-            case this._titleText:
-              aSpan.classList.add("ac-emphasize-text-title");
-              break;
-            case this._tagsText:
-              aSpan.classList.add("ac-emphasize-text-tag");
-              break;
-            case this._urlText:
-              aSpan.classList.add("ac-emphasize-text-url");
-              break;
-            case this._actionText:
-              aSpan.classList.add("ac-emphasize-text-action");
-              break;
-          }
-          ]]>
-        </body>
-      </method>
-
-      <!--
-        This will generate an array of emphasis pairs for use with
-        _setUpEmphasisedSections(). Each pair is a tuple (array) that
-        represents a block of text - containing the text of that block, and a
-        boolean for whether that block should have an emphasis styling applied
-        to it.
-
-        These pairs are generated by parsing a localised string (aSourceString)
-        with parameters, in the format that is used by
-        nsIStringBundle.formatStringFromName():
-
-          "textA %1$S textB textC %2$S"
-
-        Or:
-
-          "textA %S"
-
-        Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided
-        replacement strings. These are specified an array of tuples
-        (aReplacements), each containing the replacement text and a boolean for
-        whether that text should have an emphasis styling applied. This is used
-        as a 1-based array - ie, "%1$S" is replaced by the item in the first
-        index of aReplacements, "%2$S" by the second, etc. "%S" will always
-        match the first index.
-      -->
-      <method name="_generateEmphasisPairs">
-        <parameter name="aSourceString"/>
-        <parameter name="aReplacements"/>
-        <body>
-          <![CDATA[
-            let pairs = [];
-
-            // Split on %S, %1$S, %2$S, etc. ie:
-            //   "textA %S"
-            //     becomes ["textA ", "%S"]
-            //   "textA %1$S textB textC %2$S"
-            //     becomes ["textA ", "%1$S", " textB textC ", "%2$S"]
-            let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/);
-
-            for (let part of parts) {
-              // The above regex will actually give us an empty string at the
-              // end - we don't want that, as we don't want to later generate an
-              // empty text node for it.
-              if (part.length === 0)
-                continue;
-
-              // Determine if this token is a replacement token or a normal text
-              // token. If it is a replacement token, we want to extract the
-              // numerical number. However, we still want to match on "$S".
-              let match = part.match(/^%(?:([0-9]+)\$)?S$/);
-
-              if (match) {
-                // "%S" doesn't have a numerical number in it, but will always
-                // be assumed to be 1. Furthermore, the input string specifies
-                // these with a 1-based index, but we want a 0-based index.
-                let index = (match[1] || 1) - 1;
-
-                if (index >= 0 && index < aReplacements.length) {
-                  pairs.push([...aReplacements[index]]);
-                }
-              } else {
-                pairs.push([part]);
-              }
-            }
-
-            return pairs;
-          ]]>
-        </body>
-      </method>
-
-      <!--
-        _setUpEmphasisedSections() has the same use as _setUpDescription,
-        except instead of taking a string and highlighting given tokens, it takes
-        an array of pairs generated by _generateEmphasisPairs(). This allows
-        control over emphasising based on specific blocks of text, rather than
-        search for substrings.
-      -->
-      <method name="_setUpEmphasisedSections">
-        <parameter name="aDescriptionElement"/>
-        <parameter name="aTextPairs"/>
-        <body>
-          <![CDATA[
-          // Get rid of all previous text
-          while (aDescriptionElement.hasChildNodes())
-            aDescriptionElement.firstChild.remove();
-
-          for (let [text, emphasise] of aTextPairs) {
-            if (emphasise) {
-              let span = aDescriptionElement.appendChild(
-                document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
-              span.textContent = text;
-              switch (emphasise) {
-                case "match":
-                  this._setUpEmphasisSpan(span, aDescriptionElement);
-                  break;
-              }
-            } else {
-              aDescriptionElement.appendChild(document.createTextNode(text));
-            }
-          }
-          ]]>
-        </body>
-      </method>
-
-      <method name="_unescapeUrl">
-        <parameter name="url"/>
-        <body>
-          <![CDATA[
-          return this.Services.textToSubURI.unEscapeURIForUI("UTF-8", url);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_reuseAcItem">
-        <body>
-          <![CDATA[
-            let action = this._parseActionUrl(this.getAttribute("url"));
-            let popup = this.parentNode.parentNode;
-
-            // If the item is a searchengine action, then it should
-            // only be reused if the engine name is the same as the
-            // popup's override engine name, if any.
-            if (!action ||
-                action.type != "searchengine" ||
-                !popup.overrideSearchEngineName ||
-                action.params.engineName == popup.overrideSearchEngineName) {
-              this.collapsed = false;
-
-              // The popup may have changed size between now and the last
-              // time the item was shown, so always handle over/underflow.
-              let dwu = window.windowUtils;
-              let popupWidth = dwu.getBoundsWithoutFlushing(this.parentNode).width;
-              if (!this._previousPopupWidth || this._previousPopupWidth != popupWidth) {
-                this._previousPopupWidth = popupWidth;
-                this.handleOverUnderflow();
-              }
-
-              return true;
-            }
-
-            return false;
-          ]]>
-        </body>
-      </method>
-
-
-      <method name="_adjustAcItem">
-        <body>
-          <![CDATA[
-          let originalUrl = this.getAttribute("ac-value");
-          let title = this.getAttribute("ac-comment");
-          this.setAttribute("url", originalUrl);
-          this.setAttribute("image", this.getAttribute("ac-image"));
-          this.setAttribute("title", title);
-          this.setAttribute("text", this.getAttribute("ac-text"));
-
-          let popup = this.parentNode.parentNode;
-          let titleLooksLikeUrl = false;
-          let displayUrl = originalUrl;
-          let emphasiseUrl = true;
-          let trimDisplayUrl = true;
-
-          let type = this.getAttribute("originaltype");
-          let types = new Set(type.split(/\s+/));
-          let initialTypes = new Set(types);
-          // Remove types that should ultimately not be in the `type` string.
-          types.delete("action");
-          types.delete("autofill");
-          types.delete("heuristic");
-          type = [...types][0] || "";
-
-          let action;
-
-          if (initialTypes.has("autofill") && !initialTypes.has("action")) {
-            // Treat autofills as visiturl actions, unless they are already also
-            // actions.
-            action = {
-              type: "visiturl",
-              params: { url: title },
-            };
-          }
-
-          this.removeAttribute("actiontype");
-          this.classList.remove(
-            "overridable-action",
-            "emptySearchQuery",
-            "aliasOffer"
-          );
-
-          // If the type includes an action, set up the item appropriately.
-          if (initialTypes.has("action") || action) {
-            action = action || this._parseActionUrl(originalUrl);
-            this.setAttribute("actiontype", action.type);
-
-            switch (action.type) {
-            case "switchtab": {
-              this.classList.add("overridable-action");
-              displayUrl = action.params.url;
-              let desc = this._stringBundle.GetStringFromName("switchToTab2");
-              this._setUpDescription(this._actionText, desc, true);
-              break;
-            }
-            case "remotetab": {
-              displayUrl = action.params.url;
-              let desc = action.params.deviceName;
-              this._setUpDescription(this._actionText, desc, true);
-              break;
-            }
-            case "searchengine": {
-              emphasiseUrl = false;
-
-              // The order here is not localizable, we default to appending
-              // "- Search with Engine" to the search string, to be able to
-              // properly generate emphasis pairs. That said, no localization
-              // changed the order while it was possible, so doesn't look like
-              // there's a strong need for that.
-              let {
-                engineName,
-                searchSuggestion,
-                searchQuery,
-                alias,
-              } = action.params;
-
-              // Override the engine name if the popup defines an override.
-              let override = popup.overrideSearchEngineName;
-              if (override && override != engineName) {
-                engineName = override;
-                action.params.engineName = override;
-                let newURL =
-                  PlacesUtils.mozActionURI(action.type, action.params);
-                this.setAttribute("url", newURL);
-              }
-
-              let engineStr =
-                this._stringBundle.formatStringFromName("searchWithEngine",
-                                                        [engineName], 1);
-              this._setUpDescription(this._actionText, engineStr, true);
-
-              // Make the title by generating an array of pairs and its
-              // corresponding interpolation string (e.g., "%1$S") to pass to
-              // _generateEmphasisPairs.
-              let pairs;
-              if (searchSuggestion) {
-                // Check if the search query appears in the suggestion.  It may
-                // not.  If it does, then emphasize the query in the suggestion
-                // and otherwise just include the suggestion without emphasis.
-                let idx = searchSuggestion.indexOf(searchQuery);
-                if (idx >= 0) {
-                  pairs = [
-                    [searchSuggestion.substring(0, idx), ""],
-                    [searchQuery, "match"],
-                    [searchSuggestion.substring(idx + searchQuery.length), ""],
-                  ];
-                } else {
-                  pairs = [
-                    [searchSuggestion, ""],
-                  ];
-                }
-              } else if (alias &&
-                         !searchQuery.trim() &&
-                         !initialTypes.has("heuristic")) {
-                // For non-heuristic alias results that have an empty query, we
-                // want to show "@engine -- Search with Engine" to make it clear
-                // that the user can search by selecting the result and using
-                // the alias.  Normally we hide the "Search with Engine" part
-                // until the result is selected or moused over, but not here.
-                // Add the aliasOffer class so we can detect this in the CSS.
-                this.classList.add("aliasOffer");
+            // Make the title by generating an array of pairs and its
+            // corresponding interpolation string (e.g., "%1$S") to pass to
+            // _generateEmphasisPairs.
+            let pairs;
+            if (searchSuggestion) {
+              // Check if the search query appears in the suggestion.  It may
+              // not.  If it does, then emphasize the query in the suggestion
+              // and otherwise just include the suggestion without emphasis.
+              let idx = searchSuggestion.indexOf(searchQuery);
+              if (idx >= 0) {
                 pairs = [
-                  [alias, ""],
+                  [searchSuggestion.substring(0, idx), ""],
+                  [searchQuery, "match"],
+                  [searchSuggestion.substring(idx + searchQuery.length), ""],
                 ];
               } else {
-                // Add the emptySearchQuery class if the search query is the
-                // empty string.  We use it to hide .ac-separator in CSS.
-                if (!searchQuery.trim()) {
-                  this.classList.add("emptySearchQuery");
-                }
                 pairs = [
-                  [searchQuery, ""],
+                  [searchSuggestion, ""],
                 ];
               }
-              let interpStr = pairs.map((pair, i) => `%${i + 1}$S`).join("");
-              title = this._generateEmphasisPairs(interpStr, pairs);
-
-              // If this is a default search match, we remove the image so we
-              // can style it ourselves with a generic search icon.
-              // We don't do this when matching an aliased search engine,
-              // because the icon helps with recognising which engine will be
-              // used (when using the default engine, we don't need that
-              // recognition).
-              if (!action.params.alias && !initialTypes.has("favicon")) {
-                this.removeAttribute("image");
+            } else if (alias &&
+              !searchQuery.trim() &&
+              !initialTypes.has("heuristic")) {
+              // For non-heuristic alias results that have an empty query, we
+              // want to show "@engine -- Search with Engine" to make it clear
+              // that the user can search by selecting the result and using
+              // the alias.  Normally we hide the "Search with Engine" part
+              // until the result is selected or moused over, but not here.
+              // Add the aliasOffer class so we can detect this in the CSS.
+              this.classList.add("aliasOffer");
+              pairs = [
+                [alias, ""],
+              ];
+            } else {
+              // Add the emptySearchQuery class if the search query is the
+              // empty string.  We use it to hide .ac-separator in CSS.
+              if (!searchQuery.trim()) {
+                this.classList.add("emptySearchQuery");
               }
-              break;
+              pairs = [
+                [searchQuery, ""],
+              ];
             }
-            case "visiturl": {
-              emphasiseUrl = false;
-              displayUrl = action.params.url;
-              titleLooksLikeUrl = true;
-              let visitStr = this._stringBundle.GetStringFromName("visit");
-              this._setUpDescription(this._actionText, visitStr, true);
-              break;
-            }
-            case "extension": {
-              let content = action.params.content;
-              displayUrl = content;
-              trimDisplayUrl = false;
-              this._setUpDescription(this._actionText, content, true);
-              break;
-            }
-            }
-          }
+            let interpStr = pairs.map((pair, i) => `%${i + 1}$S`).join("");
+            title = this._generateEmphasisPairs(interpStr, pairs);
 
-          if (trimDisplayUrl) {
-            let input = popup.input;
-            if (typeof input.trimValue == "function")
-              displayUrl = input.trimValue(displayUrl);
-            displayUrl = this._unescapeUrl(displayUrl);
-          }
-          // For performance reasons we may want to limit the displayUrl size.
-          if (popup.textRunsMaxLen && displayUrl) {
-            displayUrl = displayUrl.substr(0, popup.textRunsMaxLen);
-          }
-          this.setAttribute("displayurl", displayUrl);
-
-          // Show the domain as the title if we don't have a title.
-          if (!title) {
-            titleLooksLikeUrl = true;
-            try {
-              let uri = this.Services.io.newURI(originalUrl);
-              // Not all valid URLs have a domain.
-              if (uri.host)
-                title = uri.host;
-            } catch (e) {}
-            if (!title)
-              title = displayUrl;
-          }
-
-          this._tags.setAttribute("empty", "true");
-
-          if (type == "tag" || type == "bookmark-tag") {
-            // The title is separated from the tags by an endash
-            let tags;
-            [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
-
-            // Each tag is split by a comma in an undefined order, so sort it
-            let sortedTags = tags.split(/\s*,\s*/).sort((a, b) => {
-              return a.localeCompare(a);
-            });
-
-            let anyTagsMatch = this._setUpTags(sortedTags);
-            if (anyTagsMatch) {
-              this._tags.removeAttribute("empty");
-            }
-            if (type == "bookmark-tag") {
-              type = "bookmark";
+            // If this is a default search match, we remove the image so we
+            // can style it ourselves with a generic search icon.
+            // We don't do this when matching an aliased search engine,
+            // because the icon helps with recognising which engine will be
+            // used (when using the default engine, we don't need that
+            // recognition).
+            if (!action.params.alias && !initialTypes.has("favicon")) {
+              this.removeAttribute("image");
             }
-          } else if (type == "keyword") {
-            // Note that this is a moz-action with action.type == keyword.
+            break;
+          }
+        case "visiturl":
+          {
             emphasiseUrl = false;
-            let keywordArg = this.getAttribute("text").replace(/^[^\s]+\s*/, "");
-            if (!keywordArg) {
-              // Treat keyword searches without arguments as visiturl actions.
-              type = "visiturl";
-              this.setAttribute("actiontype", "visiturl");
-              let visitStr = this._stringBundle.GetStringFromName("visit");
-              this._setUpDescription(this._actionText, visitStr, true);
-            } else {
-              let pairs = [[title, ""], [keywordArg, "match"]];
-              let interpStr =
-                this._stringBundle.GetStringFromName("bookmarkKeywordSearch");
-              title = this._generateEmphasisPairs(interpStr, pairs);
-              // The action box will be visible since this is a moz-action, but
-              // we want it to appear as if it were not visible, so set its text
-              // to the empty string.
-              this._setUpDescription(this._actionText, "", false);
-            }
+            displayUrl = action.params.url;
+            titleLooksLikeUrl = true;
+            let visitStr = this._stringBundle.GetStringFromName("visit");
+            this._setUpDescription(this._actionText, visitStr, true);
+            break;
+          }
+        case "extension":
+          {
+            let content = action.params.content;
+            displayUrl = content;
+            trimDisplayUrl = false;
+            this._setUpDescription(this._actionText, content, true);
+            break;
           }
+      }
+    }
 
-          this.setAttribute("type", type);
+    if (trimDisplayUrl) {
+      let input = popup.input;
+      if (typeof input.trimValue == "function")
+        displayUrl = input.trimValue(displayUrl);
+      displayUrl = this._unescapeUrl(displayUrl);
+    }
+    // For performance reasons we may want to limit the displayUrl size.
+    if (popup.textRunsMaxLen && displayUrl) {
+      displayUrl = displayUrl.substr(0, popup.textRunsMaxLen);
+    }
+    this.setAttribute("displayurl", displayUrl);
 
-          if (titleLooksLikeUrl) {
-            this._titleText.setAttribute("lookslikeurl", "true");
-          } else {
-            this._titleText.removeAttribute("lookslikeurl");
-          }
+    // Show the domain as the title if we don't have a title.
+    if (!title) {
+      titleLooksLikeUrl = true;
+      try {
+        let uri = Services.io.newURI(originalUrl);
+        // Not all valid URLs have a domain.
+        if (uri.host)
+          title = uri.host;
+      } catch (e) {}
+      if (!title)
+        title = displayUrl;
+    }
+
+    this._tags.setAttribute("empty", "true");
+
+    if (type == "tag" || type == "bookmark-tag") {
+      // The title is separated from the tags by an endash
+      let tags;
+      [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
+
+      // Each tag is split by a comma in an undefined order, so sort it
+      let sortedTags = tags.split(/\s*,\s*/).sort((a, b) => {
+        return a.localeCompare(a);
+      });
 
-          if (Array.isArray(title)) {
-            // For performance reasons we may want to limit the title size.
-            if (popup.textRunsMaxLen) {
-              title.forEach(t => {
-                // Limit all the even items.
-                for (let i = 0; i < t.length; i += 2) {
-                  t[i] = t[i].substr(0, popup.textRunsMaxLen);
-                }
-              });
-            }
-            this._setUpEmphasisedSections(this._titleText, title);
-          } else {
-            // For performance reasons we may want to limit the title size.
-            if (popup.textRunsMaxLen && title) {
-              title = title.substr(0, popup.textRunsMaxLen);
-            }
-            this._setUpDescription(this._titleText, title, false);
-          }
-          this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl);
+      let anyTagsMatch = this._setUpTags(sortedTags);
+      if (anyTagsMatch) {
+        this._tags.removeAttribute("empty");
+      }
+      if (type == "bookmark-tag") {
+        type = "bookmark";
+      }
+    } else if (type == "keyword") {
+      // Note that this is a moz-action with action.type == keyword.
+      emphasiseUrl = false;
+      let keywordArg = this.getAttribute("text").replace(/^[^\s]+\s*/, "");
+      if (!keywordArg) {
+        // Treat keyword searches without arguments as visiturl actions.
+        type = "visiturl";
+        this.setAttribute("actiontype", "visiturl");
+        let visitStr = this._stringBundle.GetStringFromName("visit");
+        this._setUpDescription(this._actionText, visitStr, true);
+      } else {
+        let pairs = [
+          [title, ""],
+          [keywordArg, "match"],
+        ];
+        let interpStr =
+          this._stringBundle.GetStringFromName("bookmarkKeywordSearch");
+        title = this._generateEmphasisPairs(interpStr, pairs);
+        // The action box will be visible since this is a moz-action, but
+        // we want it to appear as if it were not visible, so set its text
+        // to the empty string.
+        this._setUpDescription(this._actionText, "", false);
+      }
+    }
+
+    this.setAttribute("type", type);
+
+    if (titleLooksLikeUrl) {
+      this._titleText.setAttribute("lookslikeurl", "true");
+    } else {
+      this._titleText.removeAttribute("lookslikeurl");
+    }
 
-          // Removing the max-width may be jarring when the item is visible, but
-          // we have no other choice to properly crop the text.
-          // Removing max-widths may cause overflow or underflow events, that
-          // will set the _inOverflow property. In case both the old and the new
-          // text are overflowing, the overflow event won't happen, and we must
-          // enforce an _handleOverflow() call to update the max-widths.
-          let wasInOverflow = this._inOverflow;
-          this._removeMaxWidths();
-          if (wasInOverflow && this._inOverflow) {
-            this._handleOverflow();
+    if (Array.isArray(title)) {
+      // For performance reasons we may want to limit the title size.
+      if (popup.textRunsMaxLen) {
+        title.forEach(t => {
+          // Limit all the even items.
+          for (let i = 0; i < t.length; i += 2) {
+            t[i] = t[i].substr(0, popup.textRunsMaxLen);
           }
-          ]]>
-        </body>
-      </method>
+        });
+      }
+      this._setUpEmphasisedSections(this._titleText, title);
+    } else {
+      // For performance reasons we may want to limit the title size.
+      if (popup.textRunsMaxLen && title) {
+        title = title.substr(0, popup.textRunsMaxLen);
+      }
+      this._setUpDescription(this._titleText, title, false);
+    }
+    this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl);
+
+    // Removing the max-width may be jarring when the item is visible, but
+    // we have no other choice to properly crop the text.
+    // Removing max-widths may cause overflow or underflow events, that
+    // will set the _inOverflow property. In case both the old and the new
+    // text are overflowing, the overflow event won't happen, and we must
+    // enforce an _handleOverflow() call to update the max-widths.
+    let wasInOverflow = this._inOverflow;
+    this._removeMaxWidths();
+    if (wasInOverflow && this._inOverflow) {
+      this._handleOverflow();
+    }
+  }
 
-      <method name="_removeMaxWidths">
-        <body>
-          <![CDATA[
-          if (this._hasMaxWidths) {
-            this._titleText.style.removeProperty("max-width");
-            this._tagsText.style.removeProperty("max-width");
-            this._urlText.style.removeProperty("max-width");
-            this._actionText.style.removeProperty("max-width");
-            this._hasMaxWidths = false;
-          }
-          ]]>
-        </body>
-      </method>
+  _removeMaxWidths() {
+    if (this._hasMaxWidths) {
+      this._titleText.style.removeProperty("max-width");
+      this._tagsText.style.removeProperty("max-width");
+      this._urlText.style.removeProperty("max-width");
+      this._actionText.style.removeProperty("max-width");
+      this._hasMaxWidths = false;
+    }
+  }
+
+  /**
+   * This method truncates the displayed strings as necessary.
+   */
+  _handleOverflow() {
+    let itemRect = this.parentNode.getBoundingClientRect();
+    let titleRect = this._titleText.getBoundingClientRect();
+    let tagsRect = this._tagsText.getBoundingClientRect();
+    let separatorRect = this._separator.getBoundingClientRect();
+    let urlRect = this._urlText.getBoundingClientRect();
+    let actionRect = this._actionText.getBoundingClientRect();
+    let separatorURLActionWidth =
+      separatorRect.width + Math.max(urlRect.width, actionRect.width);
+
+    // Total width for the title and URL/action is the width of the item
+    // minus the start of the title text minus a little optional extra padding.
+    // This extra padding amount is basically arbitrary but keeps the text
+    // from getting too close to the popup's edge.
+    let dir = this.getAttribute("dir");
+    let titleStart = dir == "rtl" ? itemRect.right - titleRect.right :
+      titleRect.left - itemRect.left;
 
-      <!-- This method truncates the displayed strings as necessary. -->
-      <method name="_handleOverflow">
-        <body><![CDATA[
-          let itemRect = this.parentNode.getBoundingClientRect();
-          let titleRect = this._titleText.getBoundingClientRect();
-          let tagsRect = this._tagsText.getBoundingClientRect();
-          let separatorRect = this._separator.getBoundingClientRect();
-          let urlRect = this._urlText.getBoundingClientRect();
-          let actionRect = this._actionText.getBoundingClientRect();
-          let separatorURLActionWidth =
-            separatorRect.width + Math.max(urlRect.width, actionRect.width);
+    let popup = this.parentNode.parentNode;
+    let itemWidth = itemRect.width - titleStart - popup.overflowPadding -
+      (popup.margins ? popup.margins.end : 0);
+
+    if (this._tags.hasAttribute("empty")) {
+      // The tags box is not displayed in this case.
+      tagsRect.width = 0;
+    }
+
+    let titleTagsWidth = titleRect.width + tagsRect.width;
+    if (titleTagsWidth + separatorURLActionWidth > itemWidth) {
+      // Title + tags + URL/action overflows the item width.
 
-          // Total width for the title and URL/action is the width of the item
-          // minus the start of the title text minus a little optional extra padding.
-          // This extra padding amount is basically arbitrary but keeps the text
-          // from getting too close to the popup's edge.
-          let dir = this.getAttribute("dir");
-          let titleStart = dir == "rtl" ? itemRect.right - titleRect.right
-                                        : titleRect.left - itemRect.left;
-
-          let popup = this.parentNode.parentNode;
-          let itemWidth = itemRect.width - titleStart - popup.overflowPadding -
-                          (popup.margins ? popup.margins.end : 0);
+      // The percentage of the item width allocated to the title and tags.
+      let titleTagsPct = 0.66;
 
-          if (this._tags.hasAttribute("empty")) {
-            // The tags box is not displayed in this case.
-            tagsRect.width = 0;
-          }
+      let titleTagsAvailable = itemWidth - separatorURLActionWidth;
+      let titleTagsMaxWidth = Math.max(
+        titleTagsAvailable,
+        itemWidth * titleTagsPct
+      );
+      if (titleTagsWidth > titleTagsMaxWidth) {
+        // Title + tags overflows the max title + tags width.
 
-          let titleTagsWidth = titleRect.width + tagsRect.width;
-          if (titleTagsWidth + separatorURLActionWidth > itemWidth) {
-            // Title + tags + URL/action overflows the item width.
-
-            // The percentage of the item width allocated to the title and tags.
-            let titleTagsPct = 0.66;
+        // The percentage of the title + tags width allocated to the
+        // title.
+        let titlePct = 0.33;
 
-            let titleTagsAvailable = itemWidth - separatorURLActionWidth;
-            let titleTagsMaxWidth = Math.max(
-              titleTagsAvailable,
-              itemWidth * titleTagsPct
-            );
-            if (titleTagsWidth > titleTagsMaxWidth) {
-              // Title + tags overflows the max title + tags width.
+        let titleAvailable = titleTagsMaxWidth - tagsRect.width;
+        let titleMaxWidth = Math.max(
+          titleAvailable,
+          titleTagsMaxWidth * titlePct
+        );
+        let tagsAvailable = titleTagsMaxWidth - titleRect.width;
+        let tagsMaxWidth = Math.max(
+          tagsAvailable,
+          titleTagsMaxWidth * (1 - titlePct)
+        );
+        this._titleText.style.maxWidth = titleMaxWidth + "px";
+        this._tagsText.style.maxWidth = tagsMaxWidth + "px";
+      }
+      let urlActionMaxWidth = Math.max(
+        itemWidth - titleTagsWidth,
+        itemWidth * (1 - titleTagsPct)
+      );
+      urlActionMaxWidth -= separatorRect.width;
+      this._urlText.style.maxWidth = urlActionMaxWidth + "px";
+      this._actionText.style.maxWidth = urlActionMaxWidth + "px";
+      this._hasMaxWidths = true;
+    }
+  }
 
-              // The percentage of the title + tags width allocated to the
-              // title.
-              let titlePct = 0.33;
+  handleOverUnderflow() {
+    this._removeMaxWidths();
+    this._handleOverflow();
+  }
+
+  _parseActionUrl(aUrl) {
+    if (!aUrl.startsWith("moz-action:"))
+      return null;
 
-              let titleAvailable = titleTagsMaxWidth - tagsRect.width;
-              let titleMaxWidth = Math.max(
-                titleAvailable,
-                titleTagsMaxWidth * titlePct
-              );
-              let tagsAvailable = titleTagsMaxWidth - titleRect.width;
-              let tagsMaxWidth = Math.max(
-                tagsAvailable,
-                titleTagsMaxWidth * (1 - titlePct)
-              );
-              this._titleText.style.maxWidth = titleMaxWidth + "px";
-              this._tagsText.style.maxWidth = tagsMaxWidth + "px";
-            }
-            let urlActionMaxWidth = Math.max(
-              itemWidth - titleTagsWidth,
-              itemWidth * (1 - titleTagsPct)
-            );
-            urlActionMaxWidth -= separatorRect.width;
-            this._urlText.style.maxWidth = urlActionMaxWidth + "px";
-            this._actionText.style.maxWidth = urlActionMaxWidth + "px";
-            this._hasMaxWidths = true;
-          }
-        ]]></body>
-      </method>
+    // URL is in the format moz-action:ACTION,PARAMS
+    // Where PARAMS is a JSON encoded object.
+    let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
+
+    let action = {
+      type,
+    };
+
+    try {
+      action.params = JSON.parse(params);
+      for (let key in action.params) {
+        action.params[key] = decodeURIComponent(action.params[key]);
+      }
+    } catch (e) {
+      // If this failed, we assume that params is not a JSON object, and
+      // is instead just a flat string. This may happen for legacy
+      // search components.
+      action.params = {
+        url: params,
+      };
+    }
+
+    return action;
+  }
+};
+
+MozXULElement.implementCustomInterface(
+  MozElements.MozAutocompleteRichlistitem,
+  [Ci.nsIDOMXULSelectControlItemElement]
+);
+
+class MozAutocompleteRichlistitemInsecureWarning extends MozElements.MozAutocompleteRichlistitem {
+  constructor() {
+    super();
+
+    this.addEventListener("click", (event) => {
+      if (event.button != 0) {
+        return;
+      }
 
-      <method name="handleOverUnderflow">
-        <body>
-          <![CDATA[
-          this._removeMaxWidths();
-          this._handleOverflow();
-          ]]>
-        </body>
-      </method>
+      let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+      window.openTrustedLinkIn(baseURL + "insecure-password", "tab", {
+        relatedToCurrent: true,
+      });
+    });
+  }
 
-      <method name="_parseActionUrl">
-        <parameter name="aUrl"/>
-        <body><![CDATA[
-          if (!aUrl.startsWith("moz-action:"))
-            return null;
+  connectedCallback() {
+    if (this.delayConnectedCallback()) {
+      return;
+    }
+
+    super.connectedCallback();
+
+    // Unlike other autocomplete items, the height of the insecure warning
+    // increases by wrapping. So "forceHandleUnderflow" is for container to
+    // recalculate an item's height and width.
+    this.classList.add("forceHandleUnderflow");
+  }
 
-          // URL is in the format moz-action:ACTION,PARAMS
-          // Where PARAMS is a JSON encoded object.
-          let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
-
-          let action = {
-            type,
-          };
+  static get observedAttributes() {
+    return [
+      "actiontype",
+      "current",
+      "selected",
+      "image",
+      "type",
+    ];
+  }
 
-          try {
-            action.params = JSON.parse(params);
-            for (let key in action.params) {
-              action.params[key] = decodeURIComponent(action.params[key]);
-            }
-          } catch (e) {
-            // If this failed, we assume that params is not a JSON object, and
-            // is instead just a flat string. This may happen for legacy
-            // search components.
-            action.params = {
-              url: params,
-            };
-          }
+  get inheritedAttributeMap() {
+    if (!this.__inheritedAttributeMap) {
+      this.__inheritedAttributeMap = new Map([
+        [ this.querySelector(".ac-type-icon"), [ "selected", "current", "type" ] ],
+        [ this.querySelector(".ac-site-icon"), [ "src=image", "selected", "type" ] ],
+        [ this.querySelector(".ac-title-text"), [ "selected" ] ],
+        [ this.querySelector(".ac-tags-text"), [ "selected" ] ],
+        [ this.querySelector(".ac-separator"), [ "selected", "actiontype", "type" ] ],
+        [ this.querySelector(".ac-url"), [ "selected", "actiontype" ] ],
+        [ this.querySelector(".ac-url-text"), [ "selected" ] ],
+        [ this.querySelector(".ac-action"), [ "selected", "actiontype" ] ],
+        [ this.querySelector(".ac-action-text"), [ "selected" ] ],
+      ]);
+    }
+    return this.__inheritedAttributeMap;
+  }
 
-          return action;
-        ]]></body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <!--
-        This overrides listitem's mousedown handler because we want to set the
-        selected item even when the shift or accel keys are pressed.
-      -->
-      <handler event="mousedown"><![CDATA[
-        // Call this.control only once since it's not a simple getter.
-        let control = this.control;
-        if (!control || control.disabled) {
-          return;
-        }
-        if (!this.selected) {
-          control.selectItem(this);
-        }
-        control.currentItem = this;
-      ]]></handler>
+  get _markup() {
+    return `
+      <image class="ac-type-icon"></image>
+      <image class="ac-site-icon"></image>
+      <vbox class="ac-title" align="left">
+        <description class="ac-text-overflow-container">
+          <description class="ac-title-text"></description>
+        </description>
+      </vbox>
+      <hbox class="ac-tags" align="center">
+        <description class="ac-text-overflow-container">
+          <description class="ac-tags-text"></description>
+        </description>
+      </hbox>
+      <hbox class="ac-separator" align="center">
+        <description class="ac-separator-text"></description>
+      </hbox>
+      <hbox class="ac-url" align="center">
+        <description class="ac-text-overflow-container">
+          <description class="ac-url-text"></description>
+        </description>
+      </hbox>
+      <hbox class="ac-action" align="center">
+        <description class="ac-text-overflow-container">
+          <description class="ac-action-text"></description>
+        </description>
+      </hbox>
+    `;
+  }
 
-      <handler event="mouseover"><![CDATA[
-        // The point of implementing this handler is to allow drags to change
-        // the selected item.  If the user mouses down on an item, it becomes
-        // selected.  If they then drag the mouse to another item, select it.
-        // Handle all three primary mouse buttons: right, left, and wheel, since
-        // all three change the selection on mousedown.
-        let mouseDown = event.buttons & 0b111;
-        if (!mouseDown) {
-          return;
-        }
-        // Call this.control only once since it's not a simple getter.
-        let control = this.control;
-        if (!control || control.disabled) {
-          return;
-        }
-        if (!this.selected) {
-          control.selectItem(this);
-        }
-        control.currentItem = this;
-      ]]></handler>
-    </handlers>
-  </binding>
-</bindings>
+  get _learnMoreString() {
+    if (!this.__learnMoreString) {
+      this.__learnMoreString = Services.strings.createBundle(
+        "chrome://passwordmgr/locale/passwordmgr.properties"
+      ).
+      GetStringFromName("insecureFieldWarningLearnMore");
+    }
+    return this.__learnMoreString;
+  }
+
+  /**
+   * Override _getSearchTokens to have the Learn More text emphasized
+   */
+  _getSearchTokens(aSearch) {
+    return [this._learnMoreString.toLowerCase()];
+  }
+}
+
+customElements.define("autocomplete-richlistitem", MozElements.MozAutocompleteRichlistitem, {
+  extends: "richlistitem",
+});
+
+customElements.define("autocomplete-richlistitem-insecure-warning", MozAutocompleteRichlistitemInsecureWarning, {
+  extends: "richlistitem",
+});
+}
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1009,31 +1009,31 @@
           var existingItemsCount = this.richlistbox.children.length;
 
           // Process maxRows per chunk to improve performance and user experience
           for (let i = 0; i < this.maxRows; i++) {
             if (this._currentIndex >= matchCount) {
               break;
             }
             let item;
-            let reusable = false;
             let itemExists = this._currentIndex < existingItemsCount;
 
             let originalValue, originalText, originalType;
             let style = controller.getStyleAt(this._currentIndex);
             let value =
               style && style.includes("autofill") ?
               controller.getFinalCompleteValueAt(this._currentIndex) :
               controller.getValueAt(this._currentIndex);
             let label = controller.getLabelAt(this._currentIndex);
             let comment = controller.getCommentAt(this._currentIndex);
             let image = controller.getImageAt(this._currentIndex);
             // trim the leading/trailing whitespace
             let trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
 
+            let reusable = false;
             if (itemExists) {
               item = this.richlistbox.children[this._currentIndex];
 
               // Url may be a modified version of value, see _adjustAcItem().
               originalValue = item.getAttribute("url") || item.getAttribute("ac-value");
               originalText = item.getAttribute("ac-text");
               originalType = item.getAttribute("originaltype");
 
@@ -1044,19 +1044,36 @@
                 "autofill-footer",
                 "autofill-clear-button",
                 "autofill-insecureWarning",
               ];
               // Reuse the item when its style is exactly equal to the previous style or
               // neither of their style are in the UNREUSEABLE_STYLES.
               reusable = originalType === style ||
                 !(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
-            } else {
-              // need to create a new item
-              item = document.createXULElement("richlistitem");
+            }
+
+            // If no reusable item available, then create a new item.
+            if (!reusable) {
+              let options = null;
+              switch (style) {
+                case "autofill-profile":
+                case "autofill-footer":
+                case "autofill-clear-button":
+                case "autofill-insecureWarning":
+                  // implemented via XBL bindings, no CE for them
+                  break;
+                case "insecureWarning":
+                  options = { is: "autocomplete-richlistitem-insecure-warning" };
+                  break;
+                default:
+                  options = { is: "autocomplete-richlistitem" };
+              }
+              item = document.createXULElement("richlistitem", options);
+              item.className = "autocomplete-richlistitem";
             }
 
             item.setAttribute("dir", this.style.direction);
             item.setAttribute("ac-image", image);
             item.setAttribute("ac-value", value);
             item.setAttribute("ac-label", label);
             item.setAttribute("ac-comment", comment);
             item.setAttribute("ac-text", trimmedSearchString);
@@ -1079,30 +1096,28 @@
               }
             } else {
               if (typeof item._cleanup == "function") {
                 item._cleanup();
               }
               item.setAttribute("originaltype", style);
             }
 
-            if (itemExists) {
+            if (reusable) {
               // Adjust only when the result's type is reusable for existing
               // item's. Otherwise, we might insensibly call old _adjustAcItem()
               // as new binding has not been attached yet.
               // We don't need to worry about switching to new binding, since
               // _adjustAcItem() will fired by its own constructor accordingly.
-              if (reusable) {
-                item._adjustAcItem();
-              }
+              item._adjustAcItem();
               item.collapsed = false;
+            } else if (itemExists) {
+              let oldItem = this.richlistbox.children[this._currentIndex];
+              this.richlistbox.replaceChild(item, oldItem);
             } else {
-              // set the class at the end so we can use the attributes
-              // in the xbl constructor
-              item.className = "autocomplete-richlistitem";
               this.richlistbox.appendChild(item);
             }
 
             this._currentIndex++;
           }
 
           if (typeof this.onResultsAdded == "function") {
             // The items bindings may not be attached yet, so we must delay this
@@ -1217,1080 +1232,9 @@
         if (isListActive && this.mInput) {
           this.mInput.mIgnoreFocus = true;
           this.mInput._focus();
           this.mInput.mIgnoreFocus = false;
         }
       ]]></handler>
     </handlers>
   </binding>
-
-  <binding id="autocomplete-richlistitem-insecure-field" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
-    <content align="center"
-             onoverflow="this._onOverflow();"
-             onunderflow="this._onUnderflow();">
-      <xul:image anonid="type-icon"
-                 class="ac-type-icon"
-                 xbl:inherits="selected,current,type"/>
-      <xul:image anonid="site-icon"
-                 class="ac-site-icon"
-                 xbl:inherits="src=image,selected,type"/>
-      <xul:vbox class="ac-title"
-                align="left"
-                xbl:inherits="">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="title-text"
-                           class="ac-title-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:vbox>
-      <xul:hbox anonid="tags"
-                class="ac-tags"
-                align="center"
-                xbl:inherits="selected">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="tags-text"
-                           class="ac-tags-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox anonid="separator"
-                class="ac-separator"
-                align="center"
-                xbl:inherits="selected,actiontype,type">
-        <xul:description class="ac-separator-text">—</xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-url"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="url-text"
-                           class="ac-url-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-action"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="action-text"
-                           class="ac-action-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-    </content>
-
-    <handlers>
-      <handler event="click" button="0"><![CDATA[
-        let baseURL = this.Services.urlFormatter.formatURLPref("app.support.baseURL");
-        window.openTrustedLinkIn(baseURL + "insecure-password", "tab", {
-          relatedToCurrent: true,
-        });
-      ]]></handler>
-    </handlers>
-
-    <implementation>
-      <constructor><![CDATA[
-        // Unlike other autocomplete items, the height of the insecure warning
-        // increases by wrapping. So "forceHandleUnderflow" is for container to
-        // recalculate an item's height and width.
-        this.classList.add("forceHandleUnderflow");
-      ]]></constructor>
-
-      <property name="_learnMoreString">
-        <getter><![CDATA[
-          if (!this.__learnMoreString) {
-            this.__learnMoreString =
-              this.Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties").
-              GetStringFromName("insecureFieldWarningLearnMore");
-          }
-          return this.__learnMoreString;
-        ]]></getter>
-      </property>
-
-      <!-- Override _getSearchTokens to have the Learn More text emphasized -->
-      <method name="_getSearchTokens">
-        <parameter name="aSearch"/>
-        <body>
-          <![CDATA[
-            return [this._learnMoreString.toLowerCase()];
-          ]]>
-        </body>
-      </method>
-
-    </implementation>
-  </binding>
-
-  <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-
-    <content align="center"
-             onoverflow="this._onOverflow();"
-             onunderflow="this._onUnderflow();">
-      <xul:image anonid="type-icon"
-                 class="ac-type-icon"
-                 xbl:inherits="selected,current,type"/>
-      <xul:image anonid="site-icon"
-                 class="ac-site-icon"
-                 xbl:inherits="src=image,selected,type"/>
-      <xul:hbox class="ac-title"
-                align="center"
-                xbl:inherits="selected">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="title-text"
-                           class="ac-title-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox anonid="tags"
-                class="ac-tags"
-                align="center"
-                xbl:inherits="selected">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="tags-text"
-                           class="ac-tags-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox anonid="separator"
-                class="ac-separator"
-                align="center"
-                xbl:inherits="selected,actiontype,type">
-        <xul:description class="ac-separator-text">—</xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-url"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="url-text"
-                           class="ac-url-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-      <xul:hbox class="ac-action"
-                align="center"
-                xbl:inherits="selected,actiontype">
-        <xul:description class="ac-text-overflow-container">
-          <xul:description anonid="action-text"
-                           class="ac-action-text"
-                           xbl:inherits="selected"/>
-        </xul:description>
-      </xul:hbox>
-    </content>
-
-    <implementation implements="nsIDOMXULSelectControlItemElement">
-      <constructor>
-        <![CDATA[
-          this._typeIcon = document.getAnonymousElementByAttribute(
-            this, "anonid", "type-icon"
-          );
-          this._siteIcon = document.getAnonymousElementByAttribute(
-            this, "anonid", "site-icon"
-          );
-          this._titleText = document.getAnonymousElementByAttribute(
-            this, "anonid", "title-text"
-          );
-          this._tags = document.getAnonymousElementByAttribute(
-            this, "anonid", "tags"
-          );
-          this._tagsText = document.getAnonymousElementByAttribute(
-            this, "anonid", "tags-text"
-          );
-          this._separator = document.getAnonymousElementByAttribute(
-            this, "anonid", "separator"
-          );
-          this._urlText = document.getAnonymousElementByAttribute(
-            this, "anonid", "url-text"
-          );
-          this._actionText = document.getAnonymousElementByAttribute(
-            this, "anonid", "action-text"
-          );
-          this._adjustAcItem();
-        ]]>
-      </constructor>
-
-      <property name="Services" readonly="true">
-        <getter><![CDATA[
-          let module = {};
-          if (window.Services) {
-            module.Services = window.Services;
-          } else {
-            ChromeUtils.import("resource://gre/modules/Services.jsm", module);
-          }
-          Object.defineProperty(this, "Services", {
-            configurable: true,
-            enumerable: true,
-            writable: true,
-            value: module.Services,
-          });
-          return module.Services;
-        ]]></getter>
-      </property>
-
-      <method name="_cleanup">
-        <body>
-        <![CDATA[
-          this.removeAttribute("url");
-          this.removeAttribute("image");
-          this.removeAttribute("title");
-          this.removeAttribute("text");
-          this.removeAttribute("displayurl");
-        ]]>
-        </body>
-      </method>
-
-      <property name="label" readonly="true">
-        <getter>
-          <![CDATA[
-            // This property is a string that is read aloud by screen readers,
-            // so it must not contain anything that should not be user-facing.
-
-            let parts = [
-              this.getAttribute("title"),
-              this.getAttribute("displayurl"),
-            ];
-            let label = parts.filter(str => str).join(" ");
-
-            // allow consumers that have extended popups to override
-            // the label values for the richlistitems
-            let panel = this.parentNode.parentNode;
-            if (panel.createResultLabel) {
-              return panel.createResultLabel(this, label);
-            }
-
-            return label;
-          ]]>
-        </getter>
-      </property>
-
-      <property name="_stringBundle">
-        <getter><![CDATA[
-          if (!this.__stringBundle) {
-            this.__stringBundle = this.Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
-          }
-          return this.__stringBundle;
-        ]]></getter>
-      </property>
-
-      <field name="_boundaryCutoff">null</field>
-
-      <property name="boundaryCutoff" readonly="true">
-        <getter>
-          <![CDATA[
-          if (!this._boundaryCutoff) {
-            this._boundaryCutoff =
-              Cc["@mozilla.org/preferences-service;1"].
-              getService(Ci.nsIPrefBranch).
-              getIntPref("toolkit.autocomplete.richBoundaryCutoff");
-          }
-          return this._boundaryCutoff;
-          ]]>
-        </getter>
-      </property>
-
-      <field name="_inOverflow">false</field>
-
-      <method name="_onOverflow">
-        <body>
-          <![CDATA[
-          this._inOverflow = true;
-          this._handleOverflow();
-          ]]>
-        </body>
-      </method>
-
-      <method name="_onUnderflow">
-        <body>
-          <![CDATA[
-          this._inOverflow = false;
-          this._handleOverflow();
-          ]]>
-        </body>
-      </method>
-
-      <method name="_getBoundaryIndices">
-        <parameter name="aText"/>
-        <parameter name="aSearchTokens"/>
-        <body>
-          <![CDATA[
-          // Short circuit for empty search ([""] == "")
-          if (aSearchTokens == "")
-            return [0, aText.length];
-
-          // Find which regions of text match the search terms
-          let regions = [];
-          for (let search of Array.prototype.slice.call(aSearchTokens)) {
-            let matchIndex = -1;
-            let searchLen = search.length;
-
-            // Find all matches of the search terms, but stop early for perf
-            let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase();
-            while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) {
-              regions.push([matchIndex, matchIndex + searchLen]);
-            }
-          }
-
-          // Sort the regions by start position then end position
-          regions = regions.sort((a, b) => {
-            let start = a[0] - b[0];
-            return (start == 0) ? a[1] - b[1] : start;
-          });
-
-          // Generate the boundary indices from each region
-          let start = 0;
-          let end = 0;
-          let boundaries = [];
-          let len = regions.length;
-          for (let i = 0; i < len; i++) {
-            // We have a new boundary if the start of the next is past the end
-            let region = regions[i];
-            if (region[0] > end) {
-              // First index is the beginning of match
-              boundaries.push(start);
-              // Second index is the beginning of non-match
-              boundaries.push(end);
-
-              // Track the new region now that we've stored the previous one
-              start = region[0];
-            }
-
-            // Push back the end index for the current or new region
-            end = Math.max(end, region[1]);
-          }
-
-          // Add the last region
-          boundaries.push(start);
-          boundaries.push(end);
-
-          // Put on the end boundary if necessary
-          if (end < aText.length)
-            boundaries.push(aText.length);
-
-          // Skip the first item because it's always 0
-          return boundaries.slice(1);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_getSearchTokens">
-        <parameter name="aSearch"/>
-        <body>
-          <![CDATA[
-          let search = aSearch.toLowerCase();
-          return search.split(/\s+/);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_setUpDescription">
-        <parameter name="aDescriptionElement"/>
-        <parameter name="aText"/>
-        <parameter name="aNoEmphasis"/>
-        <body>
-          <![CDATA[
-          // Get rid of all previous text
-          if (!aDescriptionElement) {
-            return;
-          }
-          while (aDescriptionElement.hasChildNodes())
-            aDescriptionElement.firstChild.remove();
-
-          // If aNoEmphasis is specified, don't add any emphasis
-          if (aNoEmphasis) {
-            aDescriptionElement.appendChild(document.createTextNode(aText));
-            return;
-          }
-
-          // Get the indices that separate match and non-match text
-          let search = this.getAttribute("text");
-          let tokens = this._getSearchTokens(search);
-          let indices = this._getBoundaryIndices(aText, tokens);
-
-          this._appendDescriptionSpans(indices, aText, aDescriptionElement,
-                                       aDescriptionElement);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_appendDescriptionSpans">
-        <parameter name="indices"/>
-        <parameter name="text"/>
-        <parameter name="spansParentElement"/>
-        <parameter name="descriptionElement"/>
-        <body>
-          <![CDATA[
-          let next;
-          let start = 0;
-          let len = indices.length;
-          // Even indexed boundaries are matches, so skip the 0th if it's empty
-          for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
-            next = indices[i];
-            let spanText = text.substr(start, next - start);
-            start = next;
-
-            if (i % 2 == 0) {
-              // Emphasize the text for even indices
-              let span = spansParentElement.appendChild(
-                document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
-              this._setUpEmphasisSpan(span, descriptionElement);
-              span.textContent = spanText;
-            } else {
-              // Otherwise, it's plain text
-              spansParentElement.appendChild(document.createTextNode(spanText));
-            }
-          }
-          ]]>
-        </body>
-      </method>
-
-      <method name="_setUpTags">
-        <parameter name="tags"/>
-        <body>
-          <![CDATA[
-          while (this._tagsText.hasChildNodes()) {
-            this._tagsText.firstChild.remove();
-          }
-
-          let anyTagsMatch = false;
-
-          // Include only tags that match the search string.
-          for (let tag of tags) {
-            // Check if the tag matches the search string.
-            let search = this.getAttribute("text");
-            let tokens = this._getSearchTokens(search);
-            let indices = this._getBoundaryIndices(tag, tokens);
-
-            if (indices.length == 2 &&
-                indices[0] == 0 &&
-                indices[1] == tag.length) {
-              // The tag doesn't match the search string, so don't include it.
-              continue;
-            }
-
-            anyTagsMatch = true;
-
-            let tagSpan =
-              document.createElementNS("http://www.w3.org/1999/xhtml", "span");
-            tagSpan.classList.add("ac-tag");
-            this._tagsText.appendChild(tagSpan);
-
-            this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText);
-          }
-
-          return anyTagsMatch;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_setUpEmphasisSpan">
-        <parameter name="aSpan"/>
-        <parameter name="aDescriptionElement"/>
-        <body>
-          <![CDATA[
-          aSpan.classList.add("ac-emphasize-text");
-          switch (aDescriptionElement) {
-            case this._titleText:
-              aSpan.classList.add("ac-emphasize-text-title");
-              break;
-            case this._tagsText:
-              aSpan.classList.add("ac-emphasize-text-tag");
-              break;
-            case this._urlText:
-              aSpan.classList.add("ac-emphasize-text-url");
-              break;
-            case this._actionText:
-              aSpan.classList.add("ac-emphasize-text-action");
-              break;
-          }
-          ]]>
-        </body>
-      </method>
-
-      <!--
-        This will generate an array of emphasis pairs for use with
-        _setUpEmphasisedSections(). Each pair is a tuple (array) that
-        represents a block of text - containing the text of that block, and a
-        boolean for whether that block should have an emphasis styling applied
-        to it.
-
-        These pairs are generated by parsing a localised string (aSourceString)
-        with parameters, in the format that is used by
-        nsIStringBundle.formatStringFromName():
-
-          "textA %1$S textB textC %2$S"
-
-        Or:
-
-          "textA %S"
-
-        Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided
-        replacement strings. These are specified an array of tuples
-        (aReplacements), each containing the replacement text and a boolean for
-        whether that text should have an emphasis styling applied. This is used
-        as a 1-based array - ie, "%1$S" is replaced by the item in the first
-        index of aReplacements, "%2$S" by the second, etc. "%S" will always
-        match the first index.
-      -->
-      <method name="_generateEmphasisPairs">
-        <parameter name="aSourceString"/>
-        <parameter name="aReplacements"/>
-        <body>
-          <![CDATA[
-            let pairs = [];
-
-            // Split on %S, %1$S, %2$S, etc. ie:
-            //   "textA %S"
-            //     becomes ["textA ", "%S"]
-            //   "textA %1$S textB textC %2$S"
-            //     becomes ["textA ", "%1$S", " textB textC ", "%2$S"]
-            let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/);
-
-            for (let part of parts) {
-              // The above regex will actually give us an empty string at the
-              // end - we don't want that, as we don't want to later generate an
-              // empty text node for it.
-              if (part.length === 0)
-                continue;
-
-              // Determine if this token is a replacement token or a normal text
-              // token. If it is a replacement token, we want to extract the
-              // numerical number. However, we still want to match on "$S".
-              let match = part.match(/^%(?:([0-9]+)\$)?S$/);
-
-              if (match) {
-                // "%S" doesn't have a numerical number in it, but will always
-                // be assumed to be 1. Furthermore, the input string specifies
-                // these with a 1-based index, but we want a 0-based index.
-                let index = (match[1] || 1) - 1;
-
-                if (index >= 0 && index < aReplacements.length) {
-                  pairs.push([...aReplacements[index]]);
-                }
-              } else {
-                pairs.push([part]);
-              }
-            }
-
-            return pairs;
-          ]]>
-        </body>
-      </method>
-
-      <!--
-        _setUpEmphasisedSections() has the same use as _setUpDescription,
-        except instead of taking a string and highlighting given tokens, it takes
-        an array of pairs generated by _generateEmphasisPairs(). This allows
-        control over emphasising based on specific blocks of text, rather than
-        search for substrings.
-      -->
-      <method name="_setUpEmphasisedSections">
-        <parameter name="aDescriptionElement"/>
-        <parameter name="aTextPairs"/>
-        <body>
-          <![CDATA[
-          // Get rid of all previous text
-          while (aDescriptionElement.hasChildNodes())
-            aDescriptionElement.firstChild.remove();
-
-          for (let [text, emphasise] of aTextPairs) {
-            if (emphasise) {
-              let span = aDescriptionElement.appendChild(
-                document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
-              span.textContent = text;
-              switch (emphasise) {
-                case "match":
-                  this._setUpEmphasisSpan(span, aDescriptionElement);
-                  break;
-              }
-            } else {
-              aDescriptionElement.appendChild(document.createTextNode(text));
-            }
-          }
-          ]]>
-        </body>
-      </method>
-
-      <method name="_unescapeUrl">
-        <parameter name="url"/>
-        <body>
-          <![CDATA[
-          return this.Services.textToSubURI.unEscapeURIForUI("UTF-8", url);
-          ]]>
-        </body>
-      </method>
-
-      <method name="_reuseAcItem">
-        <body>
-          <![CDATA[
-            let action = this._parseActionUrl(this.getAttribute("url"));
-            let popup = this.parentNode.parentNode;
-
-            // If the item is a searchengine action, then it should
-            // only be reused if the engine name is the same as the
-            // popup's override engine name, if any.
-            if (!action ||
-                action.type != "searchengine" ||
-                !popup.overrideSearchEngineName ||
-                action.params.engineName == popup.overrideSearchEngineName) {
-              this.collapsed = false;
-
-              // The popup may have changed size between now and the last
-              // time the item was shown, so always handle over/underflow.
-              let dwu = window.windowUtils;
-              let popupWidth = dwu.getBoundsWithoutFlushing(this.parentNode).width;
-              if (!this._previousPopupWidth || this._previousPopupWidth != popupWidth) {
-                this._previousPopupWidth = popupWidth;
-                this.handleOverUnderflow();
-              }
-
-              return true;
-            }
-
-            return false;
-          ]]>
-        </body>
-      </method>
-
-
-      <method name="_adjustAcItem">
-        <body>
-          <![CDATA[
-          let originalUrl = this.getAttribute("ac-value");
-          let title = this.getAttribute("ac-comment");
-          this.setAttribute("url"