Bug 1296638 - Refactor some autocomplete tests to be more reliable. r?MattN draft
authorMike Conley <mconley@mozilla.com>
Fri, 16 Sep 2016 18:01:41 -0400
changeset 417401 085293d0264780076ce69c88447df1227bfc80fd
parent 416647 4478bba9238a6463e351c9595c5123610b861745
child 417402 c577add776acf2fff9858d810a82c456cb0d3b2c
push id30395
push usermconley@mozilla.com
push dateSun, 25 Sep 2016 02:19:47 +0000
reviewersMattN
bugs1296638
milestone52.0a1
Bug 1296638 - Refactor some autocomplete tests to be more reliable. r?MattN MozReview-Commit-ID: LBx729EWiiX
toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
toolkit/components/satchel/test/satchel_common.js
toolkit/components/satchel/test/test_form_autocomplete.html
toolkit/components/satchel/test/test_form_autocomplete_with_list.html
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
@@ -203,16 +203,23 @@ function sendFakeAutocompleteEvent(eleme
   acEvent.initEvent("DOMAutoComplete", true, false);
   element.dispatchEvent(acEvent);
 }
 
 function spinEventLoop() {
   return Promise.resolve();
 }
 
+function promisePopupClosedAndFormsProcessed() {
+  return Promise.all([
+    promiseFormsProcessed(),
+    closeAutoCompletePopup(),
+  ]);
+}
+
 add_task(function* setup() {
   listenForUnexpectedPopupShown();
 });
 
 add_task(function* test_form1_initial_empty() {
   yield SimpleTest.promiseFocus(window);
 
   // Make sure initial form is empty.
@@ -220,119 +227,100 @@ add_task(function* test_form1_initial_em
   let popupState = yield getPopupState();
   is(popupState.open, false, "Check popup is initially closed");
 });
 
 add_task(function* test_form1_first_entry() {
   yield SimpleTest.promiseFocus(window);
   // Trigger autocomplete popup
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   let popupState = yield getPopupState();
   is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
 
   doKey("down"); // first
   checkACForm("", ""); // value shouldn't update just by selecting
-  doKey("return"); // not "enter"!
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("tempuser1", "temppass1");
 });
 
 add_task(function* test_form1_second_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   doKey("down"); // first
   doKey("down"); // second
-  doKey("return"); // not "enter"!
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_third_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   doKey("down"); // first
   doKey("down"); // second
   doKey("down"); // third
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser3", "testpass3");
 });
 
 add_task(function* test_form1_fourth_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   doKey("down"); // first
   doKey("down"); // second
   doKey("down"); // third
   doKey("down"); // fourth
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(function* test_form1_wraparound_first_entry() {
   // Trigger autocomplete popup
   restoreForm();
   yield spinEventLoop(); // let focus happen
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   doKey("down"); // first
   doKey("down"); // second
   doKey("down"); // third
   doKey("down"); // fourth
   doKey("down"); // deselects
   doKey("down"); // first
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("tempuser1", "temppass1");
 });
 
 add_task(function* test_form1_wraparound_up_last_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   doKey("up"); // last (fourth)
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(function* test_form1_wraparound_down_up_up() {
   // Trigger autocomplete popup
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
   doKey("down"); // select first entry
   doKey("up");   // selects nothing!
   doKey("up");   // select last entry
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(function* test_form1_wraparound_up_last() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
@@ -340,73 +328,60 @@ add_task(function* test_form1_wraparound
   doKey("down");
   doKey("up"); // deselects
   doKey("up"); // last entry
   doKey("up");
   doKey("up");
   doKey("up"); // first entry
   doKey("up"); // deselects
   doKey("up"); // last entry
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(function* test_form1_fill_username_without_autofill_right() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Set first entry w/o triggering autocomplete
   doKey("down"); // first
-  doKey("right");
-  yield spinEventLoop();
+  yield closeAutoCompletePopup("right");
   checkACForm("tempuser1", ""); // empty password
 });
 
 add_task(function* test_form1_fill_username_without_autofill_left() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
-
+  yield openAutoCompletePopup();
   // Set first entry w/o triggering autocomplete
   doKey("down"); // first
-  doKey("left");
+  yield closeAutoCompletePopup("left");
   checkACForm("tempuser1", ""); // empty password
 });
 
 add_task(function* test_form1_pageup_first() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check first entry (page up)
   doKey("down"); // first
   doKey("down"); // second
   doKey("page_up"); // first
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("tempuser1", "temppass1");
 });
 
 add_task(function* test_form1_pagedown_last() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   /* test 13 */
   // Check last entry (page down)
   doKey("down"); // first
   doKey("page_down"); // last
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(function* test_form1_untrusted_event() {
   restoreForm();
   yield spinEventLoop();
 
   // Send a fake (untrusted) event.
@@ -414,19 +389,17 @@ add_task(function* test_form1_untrusted_
   uname.value = "zzzuser4";
   sendFakeAutocompleteEvent(uname);
   yield spinEventLoop();
   checkACForm("zzzuser4", "");
 });
 
 add_task(function* test_form1_delete() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // 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"
   doKey("down");
@@ -439,102 +412,84 @@ add_task(function* test_form1_delete() {
   // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
   doKey("delete", shiftModifier);
   yield deletionPromise;
 
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 4, "Correct number of logins after deleting one");
   notifyMenuChanged(4);
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_first_after_deletion() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check the new first entry (of 3)
   doKey("down");
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_delete_second() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Delete the second entry (of 3), "testuser3"
   doKey("down");
   doKey("down");
   doKey("delete", shiftModifier);
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 3, "Correct number of logins after deleting one");
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(function* test_form1_first_after_deletion2() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check the new first entry (of 2)
   doKey("down");
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_delete_last() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   /* test 54 */
   // Delete the last entry (of 2), "zzzuser4"
   doKey("down");
   doKey("down");
   doKey("delete", shiftModifier);
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 2, "Correct number of logins after deleting one");
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_first_after_3_deletions() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check the only remaining entry
   doKey("down");
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_check_only_entry_remaining() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   /* test 56 */
   // Delete the only remaining entry, "testuser2"
   doKey("down");
   doKey("delete", shiftModifier);
   //doKey("return");
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
@@ -547,76 +502,64 @@ add_task(function* test_form1_check_only
 /* Tests for single-user forms for ignoring autocomplete=off */
 add_task(function* test_form2() {
   // Turn our attention to form2
   uname = $_(2, "uname");
   pword = $_(2, "pword");
   checkACForm("singleuser5", "singlepass5");
 
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
-  doKey("return"); // not "enter"!
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
 add_task(function* test_form3() {
   uname = $_(3, "uname");
   pword = $_(3, "pword");
   checkACForm("singleuser5", "singlepass5");
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
-  doKey("return"); // not "enter"!
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
 add_task(function* test_form4() {
   uname = $_(4, "uname");
   pword = $_(4, "pword");
   checkACForm("singleuser5", "singlepass5");
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
-  doKey("return"); // not "enter"!
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
 add_task(function* test_form5() {
   uname = $_(5, "uname");
   pword = $_(5, "pword");
   checkACForm("singleuser5", "singlepass5");
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
-  doKey("return"); // not "enter"!
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
 add_task(function* test_form6() {
   // (this is a control, w/o autocomplete=off, to ensure the login
   // that was being suppressed would have been filled in otherwise)
   uname = $_(6, "uname");
   pword = $_(6, "pword");
@@ -653,24 +596,22 @@ add_task(function* test_form7() {
   // Delete login6B. It was created just to prevent filling in a login
   // automatically, removing it makes it more likely that we'll catch a
   // future regression with form filling here.
   setupScript.sendSyncMessage("removeLogin", "login6B");
 });
 
 add_task(function* test_form7_2() {
   restoreForm();
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
-  doKey("return"); // not "enter"!
+  yield closeAutoCompletePopup();
   // The form changes, so we expect the old username field to get the
   // selected autocomplete value, but neither the new username field nor
   // the password field should have any values filled in.
   yield spinEventLoop();
   checkACForm("form7user1", "");
   is($_(7, "uname2").value, "", "Verifying empty uname2");
   restoreForm(); // clear field, so reloading test doesn't fail
 
@@ -700,32 +641,28 @@ add_task(function* test_form8_3() {
   setupScript.sendSyncMessage("removeLogin", "login7");
 });
 
 add_task(function* test_form9_filtering() {
   // Turn our attention to form9 to test the dropdown - bug 497541
   uname = $_(9, "uname");
   pword = $_(9, "pword");
   uname.focus();
-  let shownPromise = promiseACShown();
-  sendString("form9userAB");
-  yield shownPromise;
+  yield openAutoCompletePopup("form9userAB");
 
   checkACForm("form9userAB", "");
   uname.focus();
-  doKey("left");
-  shownPromise = promiseACShown();
-  sendChar("A");
-  let results = yield shownPromise;
+  yield closeAutoCompletePopup("left");
+
+  let results = yield openAutoCompletePopup("A");
 
   checkACForm("form9userAAB", "");
   checkArrayValues(results, ["form9userAAB"], "Check dropdown is updated after inserting 'A'");
   doKey("down");
-  doKey("return");
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("form9userAAB", "form9pass");
 });
 
 add_task(function* test_form9_autocomplete_cache() {
   // Note that this addLogin call will only be seen by the autocomplete
   // attempt for the sendChar if we do not successfully cache the
   // autocomplete results.
   setupScript.sendSyncMessage("addLogin", "login8C");
@@ -757,24 +694,21 @@ add_task(function* test_form11_recipes()
 
   // First test DOMAutocomplete
   // Switch the password field to type=password so _fillForm marks the username
   // field for autocomplete.
   pword.type = "password";
   yield promiseFormsProcessed();
   restoreForm();
   checkACForm("", "");
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   doKey("down");
   checkACForm("", ""); // value shouldn't update
-  doKey("return"); // not "enter"!
-  yield promiseFormsProcessed();
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser10", "testpass10");
 
   // Now test recipes with blur on the username field.
   restoreForm();
   checkACForm("", "");
   uname.value = "testuser10";
   checkACForm("testuser10", "");
   doKey("tab");
@@ -784,24 +718,20 @@ add_task(function* test_form11_recipes()
 });
 
 add_task(function* test_form12_formless() {
   // Test form-less autocomplete
   uname = $_(12, "uname");
   pword = $_(12, "pword");
   restoreForm();
   checkACForm("", "");
-  let shownPromise = promiseACShown();
-  doKey("down"); // open
-  yield shownPromise;
+  yield openAutoCompletePopup();
 
   // Trigger autocomplete
   doKey("down");
   checkACForm("", ""); // value shouldn't update
-  let processedPromise = promiseFormsProcessed();
-  doKey("return"); // not "enter"!
-  yield processedPromise;
+  yield promisePopupClosedAndFormsProcessed();
   checkACForm("testuser", "testpass");
 });
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/components/satchel/test/satchel_common.js
+++ b/toolkit/components/satchel/test/satchel_common.js
@@ -243,25 +243,254 @@ function promiseACShown() {
   return new Promise(resolve => {
     gChromeScript.addMessageListener("onpopupshown", ({ results }) => {
       gPopupShownExpected = false;
       resolve(results);
     });
   });
 }
 
+/**
+ * Clears all FormHistory entries.
+ *
+ * @returns {Promise}
+ */
+function clearFormHistory() {
+  return new Promise(resolve => {
+    updateFormHistory([
+      { op : "remove" },
+    ], resolve);
+  });
+}
+
+/**
+ * Given a field name, add some form autocomplete entries for
+ * that field.
+ *
+ * @param {string} fieldname
+ *        The name attribute of the field that the values are
+ *        being set for.
+ * @param {Array<string>} values
+ *        An array of strings to set as autocomplete entries
+ *        for the field.
+ *
+ *        Example:
+ *          yield addFormHistory("myInput", [
+ *            "value1",
+ *            "value2",
+ *          ]);
+ *
+ * @returns {Promise}
+ */
+function addFormHistory(fieldname, values) {
+  let ops = [];
+  for (let value of values) {
+    ops.push({
+      op: "add",
+      fieldname,
+      value,
+    });
+  }
+
+  if (ops.length) {
+    return new Promise(resolve => {
+      updateFormHistory(ops, resolve);
+    });
+  }
+
+  return Promise.resolve();
+}
+
+/**
+ * Repeats the keyboard input key a certain number of times on whatever is
+ * currently focused.
+ *
+ * @param {string} key
+ *        The suffix to DOM_VK_ to send as the key character.
+ *        Examples: "return", "down", "k".
+ * @param {int} times
+ *        How many times to repeat the key input.
+ * @param {int, optional} modifier
+ *        A bitmask indicating what modifiers should  be applied to this key
+ *        event. See the nsIDOMWindowUtils.MODIFIER_* list for examples.
+ */
+function repeatKey(key, times, modifier = null) {
+  for (let i = 0; i < times; ++i) {
+    doKey(key, modifier);
+  }
+}
+
+/**
+ * Given some autocomplete results (from openAutoCompletePopup or
+ * notifyMenuChanged), compare them against an array of expected
+ * results, and fail the test if they don't match (both elements
+ * and ordering).
+ *
+ * @param {Array<string>} results
+ *        The results that were displayed to the user in the
+ *        autocomplete popup.
+ * @param {Array<string>} expectedResults
+ *        The list of strings to compare against.
+ */
+function assertResultsMatch(results, expectedResults) {
+  let compareArrays = (a, b) => {
+    if (a.length == b.length) {
+      for (let i = 0; i < a.length; ++i) {
+        if (a[i] != b[i]) {
+          return false;
+        }
+      }
+
+      return true;
+    }
+    return false;
+  };
+
+  info("Comparing results: " + JSON.stringify(results) +
+       " with expected: " + JSON.stringify(expectedResults));
+  ok(compareArrays(results, expectedResults),
+     "Got expected autocomplete results");
+}
+
+/**
+ * Prepares an input with some autocomplete values, focuses
+ * that input, runs a testing function, and then cleans up,
+ * bluring the focus on the input. This is a generator function
+ * that yields Promises.
+ *
+ * @param {int} formNum
+ *        The number of the form that the input is inside. The
+ *        form is expected to have an ID attribute set to
+ *        "form<id>".
+ * @param {string} fieldName
+ *        The name attribute of the input element to be tested.
+ * @param {Array<string>} values
+ *        A list of entries to pre-populate as autocomplete
+ *        values for the input element.
+ * @param {function*} testFunction
+ *        A generator function that may or may not yield
+ *        Promises that will then test the input. The generator
+ *        is passed the input as an argument.
+ */
+function* prepareTest(formNum, fieldName, values, testFunction) {
+  yield addFormHistory(fieldName, values);
+
+  let input = $_(formNum, fieldName);
+  input.focus();
+  input.value = "";
+
+  yield testFunction(input);
+
+  input.value = "";
+  yield clearFormHistory();
+  input.blur();
+}
+
+/**
+ * Presses the down cursor, and then returns a Promise that resolves
+ * once the Form AutoComplete popup is open. The Promise will resolve
+ * with the strings that the popup displays to the user.
+ *
+ * @param {string, optional} openString
+ *        The characters to send to the input that might open the
+ *        popup with autocomplete results. If omitted, the down
+ *        cursor will be sent instead, which will open the popup
+ *        without adding anything to the input.
+ * @returns {Promise}
+ *          Resolves with Array<string> of the results displayed to
+ *          the user in the popup.
+ */
+function openAutoCompletePopup(openString) {
+  let popupPromise = new Promise(resolve => {
+    gChromeScript.addMessageListener("onpopupshown", function onShown(msg) {
+      gChromeScript.removeMessageListener("onpopupshown", onShown);
+      gPopupShownExpected = false;
+      resolve(msg.results);
+    });
+  });
+
+  gPopupShownExpected = true;
+  if (openString) {
+    sendString(openString)
+  } else {
+    doKey("down");
+  }
+
+  return popupPromise;
+}
+
+/**
+ * Closes an autocomplete popup by pressing a key. Returns a Promise
+ * once the popup is completely closed.
+ *
+ * @param {string} key
+ *        The suffix to DOM_VK_ to send as the key character.
+ *        Examples: "return", "left", "escape".
+ * @returns {Promise}
+ */
+function closeAutoCompletePopup(key) {
+  let acInput =
+    SpecialPowers.Cc["@mozilla.org/satchel/form-fill-controller;1"]
+                 .getService(SpecialPowers.Ci.nsIAutoCompleteInput);
+
+  let closed = new Promise(resolve => {
+    SimpleTest.waitForCondition(() => {
+      return !acInput.popupOpen;
+    }, resolve, "Timed out waiting for autocomplete popup to be closed.");
+  });
+  doKey(key || "return");
+  return closed;
+}
+
+/**
+ * Returns a Promise that resolves once a scroll event
+ * occurs on the Y axis.
+ *
+ * @returns {Promise}
+ */
+function waitForScroll() {
+  return new Promise(resolve => {
+    addEventListener("scroll", function onScroll() {
+      if (!window.pageYOffset) {
+        return;
+      }
+
+      removeEventListener("scroll", onScroll);
+      resolve();
+    });
+  });
+}
+
+/**
+ * Return a Promise that resolves once an event of type
+ * "input" fires on a given input element.
+ *
+ * @param {DOM input element} input
+ *        The input element to watch.
+ * @returns {Promise}
+ */
+function waitForInputEvent(input) {
+  return new Promise(resolve => {
+    input.addEventListener("input", function onInput(e) {
+      input.removeEventListener("input", onInput);
+      ok(e.bubbles, "Input event should bubble.");
+      ok(e.cancelable, "Input event should be cancelable.");
+      resolve();
+    });
+  });
+}
+
 function satchelCommonSetup() {
   var chromeURL = SimpleTest.getTestFileURL("parent_utils.js");
   gChromeScript = SpecialPowers.loadChromeScript(chromeURL);
   gChromeScript.addMessageListener("onpopupshown", ({ results }) => {
     gLastAutoCompleteResults = results;
     if (gPopupShownListener)
       gPopupShownListener();
   });
 
   SimpleTest.registerCleanupFunction(() => {
     gChromeScript.sendAsyncMessage("cleanup");
     gChromeScript.destroy();
   });
 }
 
-
 satchelCommonSetup();
--- a/toolkit/components/satchel/test/test_form_autocomplete.html
+++ b/toolkit/components/satchel/test/test_form_autocomplete.html
@@ -1,14 +1,15 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for Form History 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/SpawnTask.js"></script>
   <script type="text/javascript" src="satchel_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Form History test: form field autocomplete
 <p id="display"></p>
 
 <!-- We presumably can't hide the content for this test. The large top padding is to allow
@@ -129,940 +130,792 @@ Form History test: form field autocomple
     <button type="submit">Submit</button>
   </form>
 
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-/** Test for Form History autocomplete **/
+/** Tests for Form History autocomplete **/
 
-var input = $_(1, "field1");
-const shiftModifier = Event.SHIFT_MASK;
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({
+    set: [
+      ["browser.formfill.debug", true],
+    ],
+  });
+  yield clearFormHistory();
+  listenForUnexpectedPopupShown();
+});
 
-function setupFormHistory(aCallback) {
-  updateFormHistory([
-    { op : "remove" },
-    { op : "add", fieldname : "field1", value : "value1" },
-    { op : "add", fieldname : "field1", value : "value2" },
-    { op : "add", fieldname : "field1", value : "value3" },
-    { op : "add", fieldname : "field1", value : "value4" },
-    { op : "add", fieldname : "field2", value : "value1" },
-    { op : "add", fieldname : "field3", value : "a" },
-    { op : "add", fieldname : "field3", value : "aa" },
-    { op : "add", fieldname : "field3", value : "aaz" },
-    { op : "add", fieldname : "field3", value : "aa\xe6" }, // 0xae == latin ae pair (0xc6 == AE)
-    { op : "add", fieldname : "field3", value : "az" },
-    { op : "add", fieldname : "field3", value : "z" },
-    { op : "add", fieldname : "field4", value : "a\xe6" },
-    { op : "add", fieldname : "field4", value : "aa a\xe6" },
-    { op : "add", fieldname : "field4", value : "aba\xe6" },
-    { op : "add", fieldname : "field4", value : "bc d\xe6" },
-    { op : "add", fieldname : "field5", value : "1" },
-    { op : "add", fieldname : "field5", value : "12" },
-    { op : "add", fieldname : "field5", value : "123" },
-    { op : "add", fieldname : "field5", value : "1234" },
-    { op : "add", fieldname : "field6", value : "value" },
-    { op : "add", fieldname : "field7", value : "value" },
-    { op : "add", fieldname : "field8", value : "value" },
-    { op : "add", fieldname : "field9", value : "value" },
-    { op : "add", fieldname : "field10", value : "42" },
-    { op : "add", fieldname : "field11", value : "2010-10-10" },
-    { op : "add", fieldname : "field12", value : "21:21" },
-    { op : "add", fieldname : "field13", value : "32" },  // not used, since type=range doesn't have a drop down menu
-    { op : "add", fieldname : "field14", value : "#ffffff" }, // not used, since type=color doesn't have autocomplete currently
-    { op : "add", fieldname : "field15", value : "2016-08" },
-    { op : "add", fieldname : "field16", value : "2016-W32" },
-    { op : "add", fieldname : "searchbar-history", value : "blacklist test" },
-  ], aCallback);
-}
+/**
+ * Given an input with 4 autocomplete values, ensures that we can select each
+ * one and have the input take that value.
+ */
+add_task(function* test_basic_selection() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "value1",
+    "value2",
+    "value3",
+    "value4",
+  ];
 
-// All these non-implemeted types might need autocomplete tests in the future.
-var todoTypes = [ "datetime", "datetime-local" ];
-var todoInput = document.createElement("input");
-for (var type of todoTypes) {
-  todoInput.type = type;
-  todo_is(todoInput.type, type, type + " type shouldn't be implemented");
-}
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    // This will loop through each value, ensure that we can reach it in the
+    // popup, and have it become the field value if we press enter while it's
+    // selected.
+    for (let i = 0; i < VALUES.length; ++i) {
+      let items = yield openAutoCompletePopup();
+      is(items.length, VALUES.length,
+         `Should still be showing ${VALUES.length} entries.`);
 
+      // Now select the first item...
+      doKey("down");
+      // And ensure that the field has not updated
+      is(input.value, "", "Field should still be blank.");
+
+      // Depending on which VALUE we're on, we need to hit down (i - 1) number
+      // of times in order to hit it.
+      for (let j = 0; j < i; ++j) {
+        doKey("down");
+      }
 
-function setForm(value) {
-  input.value = value;
-  input.focus();
-}
-
-// Restore the form to the default state.
-function restoreForm() {
-  setForm("");
-}
+      yield closeAutoCompletePopup();
+      is(input.value, VALUES[i],
+         "Should have updated the field to the right value.");
+      input.value = "";
+    }
+  });
+});
 
-// Check for expected form data.
-function checkForm(expectedValue) {
-  var formID = input.parentNode.id;
-  is(input.value, expectedValue, "Checking " + formID + " input");
-}
-
-var testNum = 0;
-var expectingPopup = false;
-
-function expectPopup()
-{
-  info("expecting popup for test " + testNum);
-  expectingPopup = true;
-}
+/**
+ * Tests that if we have some entries in an autocomplete popup, that when we
+ * cursor through to the bottom, that we wrap around back to the top. Also
+ * tests that we can then wraparound from the top back to the bottom.
+ */
+add_task(function* test_wraparounds() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "value1",
+    "value2",
+    "value3",
+  ];
 
-function popupShownListener()
-{
-  info("popup shown for test " + testNum);
-  if (expectingPopup) {
-    expectingPopup = false;
-    SimpleTest.executeSoon(runTest);
-  }
-  else {
-    ok(false, "Autocomplete popup not expected during test " + testNum);
-  }
-}
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    yield openAutoCompletePopup();
 
-registerPopupShownListener(popupShownListener);
+    // Initially, nothing is selected. We want to try to wrap around, which means
+    // pressing down 5 times (note that the 4th time will deselect everything in
+    // the popup).
+    repeatKey("down", 5);
+    yield closeAutoCompletePopup();
+    // We should have wrapped around back to the first entry, so the field should
+    // be at the first entry now.
+    is(input.value, VALUES[0],
+       "Should have wrapped around and selected the first entry.");
 
-/*
- * Main section of test...
- *
- * This is a bit hacky, as many operations happen asynchronously.
- * Various mechanisms call runTests as a result of operations:
- *   - set expectingPopup to true, and the next test will occur when the autocomplete popup is shown
- *   - call waitForMenuChange(x) to run the next test when the autocomplete popup to have x items in it
- *   - addEntry calls runs the test when an entry has been added
- *   - some tests scroll the window. This is because the form fill controller happens to scroll
- *     the field into view near the end of the search, and there isn't any other good notification
- *     to listen to for when the search is complete.
- *   - some items still use setTimeout
- */
-function runTest() {
-  testNum++;
+    // Now let's try going the other direction, without anything initially
+    // selected...
+    input.value = "";
+    yield openAutoCompletePopup();
 
-  ok(true, "Starting test #" + testNum);
+    doKey("up");
+    yield closeAutoCompletePopup();
+
+    // We should have wrapped around over the top and gone back to the bottom to
+    // the last entry.
+    is(input.value, VALUES[2],
+       "Should have wrapped around and selected the last entry.");
 
-  switch (testNum) {
-    case 1:
-        // Make sure initial form is empty.
-        checkForm("");
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Now let's try going the same direction again, but after having first
+    // selected the first item.
+    input.value = "";
+    yield openAutoCompletePopup();
+
+    // Select the first item
+    doKey("down");
+    // Selects nothing, and then the last item
+    repeatKey("up", 2);
 
-    case 2:
-        checkMenuEntries(["value1", "value2", "value3", "value4"], testNum);
-        // Check first entry
-        doKey("down");
-        checkForm(""); // value shouldn't update
-        doKey("return"); // not "enter"!
-        checkForm("value1");
+    yield closeAutoCompletePopup();
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // We should have wrapped around over the top and gone back to the bottom to
+    // the last entry.
+    is(input.value, VALUES[2],
+       "Should have wrapped around and selected the last entry.");
+
+    // And finally, let's see if we can go around twice.
+    input.value = "";
+    yield openAutoCompletePopup();
 
-    case 3:
-        // Check second entry
-        doKey("down");
-        doKey("down");
-        doKey("return"); // not "enter"!
-        checkForm("value2");
+    // Select the first item
+    doKey("down");
+    // Selects nothing, then the last item, then goes all the way
+    // back to the first item, unselects, and then selects the last
+    // item again. That should be 6 up key events in total.
+    repeatKey("up", 6);
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    yield closeAutoCompletePopup();
 
-    case 4:
-        // Check third entry
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("return");
-        checkForm("value3");
+    // We should have wrapped around over the top and gone back to the bottom to
+    // the last entry.
+    is(input.value, VALUES[2],
+       "Should have wrapped around and selected the last entry.");
+  });
+});
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests that we can select an item using the left and right cursor keys.
+ */
+add_task(function* test_left_and_right_cursors() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "value1",
+  ];
 
-    case 5:
-        // Check fourth entry
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("return");
-        checkForm("value4");
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    yield openAutoCompletePopup();
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Select the first item...
+    doKey("down");
+    // using the right cursor key.
+    yield closeAutoCompletePopup("right");
+
+    is(input.value, VALUES[0],
+       "Should have selected the first item.");
+
+    input.value = "";
 
-    case 6:
-        // Check first entry (wraparound)
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("down"); // deselects
-        doKey("down");
-        doKey("return");
-        checkForm("value1");
+    // Now do this again, but with the left cursor.
+    yield openAutoCompletePopup();
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Select the first item...
+    doKey("down");
+    yield closeAutoCompletePopup("left");
 
-    case 7:
-        // Check the last entry via arrow-up
-        doKey("up");
-        doKey("return");
-        checkForm("value4");
+    is(input.value, VALUES[0],
+       "Should have selected the first item.");
+  });
+});
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 8:
-        // Check the last entry via arrow-up
-        doKey("down"); // select first entry
-        doKey("up");   // selects nothing!
-        doKey("up");   // select last entry
-        doKey("return");
-        checkForm("value4");
+/**
+ * Tests that we can get to the top or bottom of the page of items
+ * using Page Up and Page Down.
+ */
+add_task(function* test_page_up_page_down() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "value1",
+    "value2",
+    "value3",
+    "value4",
+  ];
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    yield openAutoCompletePopup();
+
+    // Select the second item to start...
+    repeatKey("down", 2);
 
-    case 9:
-        // Check the last entry via arrow-up (wraparound)
-        doKey("down");
-        doKey("up"); // deselects
-        doKey("up"); // last entry
-        doKey("up");
-        doKey("up");
-        doKey("up"); // first entry
-        doKey("up"); // deselects
-        doKey("up"); // last entry
-        doKey("return");
-        checkForm("value4");
+    // Then hitting Page Up should select the first item.
+    doKey("page_up");
+    yield closeAutoCompletePopup();
+
+    is(input.value, VALUES[0],
+       "Should have selected the first item.");
+
+    input.value = "";
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Now let's try Page Down. This should select the
+    // last item in the currently displayed list of
+    // results.
 
-    case 10:
-        // Set first entry w/o triggering autocomplete
-        doKey("down");
-        doKey("right");
-        checkForm("value1");
+    yield openAutoCompletePopup();
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    doKey("page_down");
+    yield closeAutoCompletePopup();
 
-    case 11:
-        // Set first entry w/o triggering autocomplete
-        doKey("down");
-        doKey("left");
-        checkForm("value1");
+    is(input.value, VALUES[3],
+       "Should have selected the last item.");
+  });
+});
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests that items can be deleted from the form autocomplete popup.
+ */
+add_task(function* test_deletion() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  // A const Array is still mutable (it just can't be re-assigned). In this
+  // test, note that items will be removed from this Array.
+  const VALUES = [
+    "value1",
+    "value2",
+    "value3",
+    "value4",
+  ];
+  const DUMMY_VALUE = "init";
+  const DELETION_ORDER = [
+    "value1",
+    "value3",
+    "value4",
+    "value2",
+  ];
 
-    case 12:
-        // Check first entry (page up)
-        doKey("down");
-        doKey("down");
-        doKey("page_up");
-        doKey("return");
-        checkForm("value1");
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    // This loop is going to open up the autocomplete popup, and then remove
+    // items in the list according to DELETION_ORDER. It will then ensure that
+    // the selected item is correctly set after the deletion, if applicable.
+    for (let toDelete of DELETION_ORDER) {
+      input.value = "";
+      info("Values is: " + JSON.stringify(VALUES) + " and we're deleting " + toDelete);
+      let indexToDelete = VALUES.indexOf(toDelete);
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 13:
-        // Check last entry (page down)
-        doKey("down");
-        doKey("page_down");
-        doKey("return");
-        checkForm("value4");
+      let results = yield openAutoCompletePopup();
 
-        // Trigger autocomplete popup
-        testNum = 49;
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+      // Make sure we're showing what's expected
+      assertResultsMatch(results, VALUES);
 
-    /* Test removing entries from the dropdown */
+      // If we add 1 to the indexToDelete, that's how many times we need to
+      // hit the down arrow key to select the value we're going to get rid
+      // of.
+      repeatKey("down", indexToDelete + 1);
 
-    case 50:
-        checkMenuEntries(["value1", "value2", "value3", "value4"], testNum);
-        // Delete the first entry (of 4)
-        setForm("value");
-        doKey("down");
+      // Now delete the value!
+      doKey("delete", Event.SHIFT_MASK);
+
+      info("Waiting for menu change to length " + (VALUES.length - 1) + "\n");
+      yield notifyMenuChanged(VALUES.length - 1);
 
-        // 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.
-        if (SpecialPowers.OS == "Darwin")
-          doKey("back_space", shiftModifier);
-        else
-          doKey("delete", shiftModifier);
+      // Now let's make sure that the item was deleted from FormHistory.
+      let count = yield countEntries(FIELD_NAME, toDelete);
+      is(count, 0, "Should have removed the autocomplete value.");
 
-        // This tests that on OS X shift-backspace didn't delete the last character
-        // in the input (bug 480262).
-        waitForMenuChange(3);
-        break;
+      // Update our internal notion of what is being displayed.
+      VALUES.splice(indexToDelete, 1);
 
-    case 51:
-        checkForm("value");
-        countEntries("field1", "value1",
-          function (num) {
-            ok(!num, testNum + " checking that f1/v1 was deleted");
-            runTest();
-          });
-        break;
+      // If there is an item after the one we just deleted, it should
+      // now be selected. Otherwise, it'll be the one before, unless
+      // there's nothing left.
+      let expected;
+      if (VALUES.length) {
+        expected = VALUES[indexToDelete] || VALUES[indexToDelete - 1];
+      }
 
-    case 52:
-        doKey("return");
-        checkForm("value2");
+      yield closeAutoCompletePopup();
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+      if (expected) {
+        is(input.value, expected,
+         "Should have selected the right value now that one was " +
+         "deleted.");
+      }
 
-    case 53:
-        checkMenuEntries(["value2", "value3", "value4"], testNum);
-        // Check the new first entry (of 3)
-        doKey("down");
-        doKey("return");
-        checkForm("value2");
-
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+      // We only need to do the following if there are any items left
+      // over - otherwise, no autocomplete popups are expected anymore.
+      if (VALUES.length) {
+        // Now if we re-open the popup, that first value should not be there.
+        results = yield openAutoCompletePopup();
+        assertResultsMatch(results, VALUES);
+        yield closeAutoCompletePopup("escape");
+      }
+    }
+  });
+});
 
-    case 54:
-        // Delete the second entry (of 3)
-        doKey("down");
-        doKey("down");
-        doKey("delete", shiftModifier);
-        waitForMenuChange(2);
-        break;
+/**
+ * Tests that we do not show an autocomplete popup for fields
+ * and forms that have the autocomplete attribute set to "off".
+ */
+add_task(function* test_autocomplete_off() {
+  const FORM_NUM = 2;
+  const FIELD_NAME = "field2";
+  const VALUES = [
+    "value1"
+  ];
 
-    case 55:
-        checkForm("");
-        countEntries("field1", "value3",
-          function (num) {
-            ok(!num, testNum + " checking that f1/v3 was deleted");
-            runTest();
-          });
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    yield openAutoCompletePopup();
+    // Select the first entry
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, VALUES[0], "Should have selected the first value.")
 
-    case 56:
-        doKey("return");
-        checkForm("value4")
-
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Look at form 3, try to trigger autocomplete popup on an item
+    // with the same name.
+    let otherInput = $_(3, "field2");
+    otherInput.value = "";
+    otherInput.focus();
 
-    case 57:
-        checkMenuEntries(["value2", "value4"], testNum);
-        // Check the new first entry (of 2)
-        doKey("down");
-        doKey("return");
-        checkForm("value2");
+    doKey("down");
+    yield notifyMenuChanged(0);
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Ensure there's no autocomplete dropdown (autocomplete=off is present on
+    // the input).
+    doKey("down");
+    doKey("return");
+    is(otherInput.value, "", "Should not have filled in this input.");
 
-    case 58:
-        // Delete the last entry (of 2)
-        doKey("down");
-        doKey("down");
-        doKey("delete", shiftModifier);
-        checkForm("");
-        waitForMenuChange(1);
-        break;
+    // Look at form 4, try to trigger autocomplete popup
+    otherInput = $_(4, "field2");
+    otherInput.value = "";
+    otherInput.focus();
+
+    doKey("down");
+    yield notifyMenuChanged(0);
 
-    case 59:
-        countEntries("field1", "value4",
-          function (num) {
-            ok(!num, testNum + " checking that f1/v4 was deleted");
-            runTest();
-          });
-        break;
-
-    case 60:
-        doKey("return");
-        checkForm("value2");
+    // Ensure there's no autocomplete dropdown (autocomplete=off is present on
+    // the form this time).
+    doKey("down");
+    doKey("return");
+    is(otherInput.value, "", "Should not have filled in this input.");
+  });
+});
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 61:
-        checkMenuEntries(["value2"], testNum);
-        // Check the new first entry (of 1)
-        doKey("down");
-        doKey("return");
-        checkForm("value2");
+/**
+ * Tests that the autocomplete popup will update itself if the
+ * user types in more characters after it has been opened.
+ */
+add_task(function* test_autocomplete_while_typing() {
+  const FORM_NUM = 5;
+  const FIELD_NAME = "field3";
+  const VALUES = [
+    "a",
+    "aa",
+    "aaz",
+    "aa\xe6", // 0xae == latin ae pair (0xc6 == AE)
+    "az",
+    "z",
+  ];
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 62:
-        // Delete the only remaining entry
-        doKey("down");
-        doKey("delete", shiftModifier);
-        waitForMenuChange(0);
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    // open the autocomplete popup with "a"
+    let results = yield openAutoCompletePopup("a");
+    assertResultsMatch(results, ["a", "aa", "aaz", "aa\xe6", "az"]);
 
-    case 63:
-        checkForm("");
-        countEntries("field1", "value2",
-          function (num) {
-            ok(!num, testNum + " checking that f1/v2 was deleted");
-            runTest();
-          });
-        break;
+    sendChar("a");
+    // input is now "aa";
+    results = yield notifyMenuChanged(3);
+    assertResultsMatch(results, ["aa", "aaz", "aa\xe6"]);
 
-    case 64:
-        // Look at form 2, trigger autocomplete popup
-        input = $_(2, "field2");
-        testNum = 99;
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    sendChar("\xc6");
+    // input is now "aa\xc6"
+
+    results = yield notifyMenuChanged(1);
+    assertResultsMatch(results, ["aa\xe6"]);
 
-    /* Test entries with autocomplete=off */
-
-    case 100:
-        // Select first entry
-        doKey("down");
-        doKey("return");
-        checkForm("value1");
+    doKey("back_space");
+    // input is now "aa"
+    results = yield notifyMenuChanged(3);
+    assertResultsMatch(results, ["aa", "aaz", "aa\xe6"]);
 
-        // Look at form 3, try to trigger autocomplete popup
-        input = $_(3, "field2");
-        restoreForm();
-        // Sometimes, this will fail if scrollTo(0, 0) is called, so that doesn't
-        // happen here. Fortunately, a different input is used from the last test,
-        // so a scroll should still occur.
-        doKey("down");
-        waitForScroll();
-        break;
+    doKey("back_space");
+    // input is now "a"
+    results = yield notifyMenuChanged(5);
+    assertResultsMatch(results, ["a", "aa", "aaz", "aa\xe6", "az"]);
+
+    sendChar("z");
+    // input is now "az"
+    results = yield notifyMenuChanged(2);
+    assertResultsMatch(results, ["az", "aaz"]);
+
+    yield closeAutoCompletePopup("left");
+    // input is still "az", but carat is after the "a"
 
-    case 101:
-        // Ensure there's no autocomplete dropdown (autocomplete=off is present)
-        doKey("down");
-        doKey("return");
-        checkForm("");
-
-        // Look at form 4, try to trigger autocomplete popup
-        input = $_(4, "field2");
-        restoreForm();
-        doKey("down");
-        waitForMenuChange(0);
-        break;
-
-    case 102:
-        // Ensure there's no autocomplete dropdown (autocomplete=off is present)
-        doKey("down");
-        doKey("return");
-        checkForm("");
-
-        // Look at form 5, try to trigger autocomplete popup
-        input = $_(5, "field3");
-        restoreForm();
-        testNum = 199;
-        expectPopup();
-        input.focus();
-        sendChar("a");
-        break;
+    // Check case-insensitivity.
+    results = yield openAutoCompletePopup("A");
+    // input is now "aAz"
+    assertResultsMatch(results, ["aaz"]);
 
-    /* Test filtering as characters are typed. */
-
-    case 200:
-        checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"], testNum);
-        input.focus();
-        sendChar("a");
-        waitForMenuChange(3);
-        break;
+    yield addFormHistory("field3", ["aazq"]);
+    // check that results were cached
+    input.focus();
+    yield closeAutoCompletePopup("right");
+    // input is still "aAz" but carat is after "z"
+    sendChar("q");
+    // input is now "aAzq"
+    yield notifyMenuChanged(0);
+    yield addFormHistory("field3", ["aazqq"]);
+    input.focus();
+    window.scrollTo(0, 0);
+    sendChar("q");
+    // check that empty results were cached - bug 496466
+    yield notifyMenuChanged(0);
+    doKey("escape");
+  });
+});
 
-    case 201:
-        checkMenuEntries(["aa", "aaz", "aa\xe6"], testNum);
-        input.focus();
-        sendChar("\xc6");
-        waitForMenuChange(1);
-        break;
-
-    case 202:
-        checkMenuEntries(["aa\xe6"], testNum);
-        doKey("back_space");
-        waitForMenuChange(3);
-        break;
-
-    case 203:
-        checkMenuEntries(["aa", "aaz", "aa\xe6"], testNum);
-        doKey("back_space");
-        waitForMenuChange(5);
-        break;
+/**
+ * Tests that autocomplete popups should be shown if the inputted
+ * characters match any part of the form history strings for a
+ * named field.
+ */
+add_task(function* test_substring_matches() {
+  const FORM_NUM = 6;
+  const FIELD_NAME = "field4";
+  const VALUES = [
+    "a\xe6",
+    "aa a\xe6",
+    "aba\xe6",
+    "bc d\xe6",
+  ];
 
-    case 204:
-        checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"], testNum);
-        input.focus();
-        sendChar("z");
-        waitForMenuChange(2);
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup("a");
+    // Input should now be "a";
+    // We should get alphabetical results for the first character
+    assertResultsMatch(results, ["aa a\xe6", "aba\xe6", "a\xe6"]);
 
-    case 205:
-        checkMenuEntries(["az", "aaz"], testNum);
-        input.focus();
-        doKey("left");
-        expectPopup();
-        // Check case-insensitivity.
-        sendChar("A");
-        break;
+    sendChar("\xe6");
+    // Input should now be "a\xe6"
+    results = yield notifyMenuChanged(3, "a\xe6");
+    // prefix match comes first, then word boundary match
+    // followed by substring match
+    assertResultsMatch(results, ["a\xe6", "aa a\xe6", "aba\xe6"]);
 
-    case 206:
-        checkMenuEntries(["aaz"], testNum);
-        addEntry("field3", "aazq");
-        break;
-
-    case 207:
-        // check that results were cached
-        input.focus();
-        doKey("right");
-        sendChar("q");
-        waitForMenuChange(0);
-        break;
+    input.value = "";
+    sendChar("b");
+    // Input should now be "b"
+    results = yield notifyMenuChanged(1, "bc d\xe6");
+    assertResultsMatch(results, ["bc d\xe6"]);
 
-    case 208:
-        // check that results were cached
-        checkMenuEntries([], testNum);
-        addEntry("field3", "aazqq");
-        break;
+    sendChar(" ");
+    // Input should now be "b "
+    results = yield notifyMenuChanged(1);
+    assertResultsMatch(results, ["bc d\xe6"]);
 
-    case 209:
-        input.focus();
-        window.scrollTo(0, 0);
-        sendChar("q");
-        waitForMenuChange(0);
-        break;
+    // check multi-word substring matches
+    sendChar("\xc6");
+    // Input should now be "b \xc6"
+    results = yield notifyMenuChanged(2);
+    assertResultsMatch(results, ["bc d\xe6", "aba\xe6"]);
 
-    case 210:
-        // check that empty results were cached - bug 496466
-        checkMenuEntries([], testNum);
-        doKey("escape");
+    yield closeAutoCompletePopup("left");
+    // Input is still "b \xc6", but the carat is just after the " "
+    results = yield openAutoCompletePopup("d");
+    // Input should now be "b d\xc6"
+    assertResultsMatch(results, ["bc d\xe6"]);
 
-        // Look at form 6, try to trigger autocomplete popup
-        input = $_(6, "field4");
-        restoreForm();
-        testNum = 249;
-        expectPopup();
-        input.focus();
-        sendChar("a");
-        break;
-
-    /* Test substring matches and word boundary bonuses */
+    // Now check inserting in multi-word searches
+    sendChar("z");
+    // Input should now be "b dz\xc6"
+    results = yield notifyMenuChanged(0);
+    assertResultsMatch(results, []);
+    yield closeAutoCompletePopup();
+  });
+});
 
-    case 250:
-        // alphabetical results for first character
-        checkMenuEntries(["aa a\xe6", "aba\xe6", "a\xe6"], testNum);
-        input.focus();
-
-        sendChar("\xe6");
-        waitForMenuChange(3, "a\xe6");
-        break;
+/**
+ * Tests that autocomplete popups will only return results
+ * that abide by the maxLength attribute on the input field.
+ */
+add_task(function* test_max_length() {
+  const FORM_NUM = 7;
+  const FIELD_NAME = "field5";
+  const VALUES = [
+    "1",
+    "12",
+    "123",
+    "1234",
+  ];
 
-    case 251:
-        // prefix match comes first, then word boundary match
-        // followed by substring match
-        checkMenuEntries(["a\xe6", "aa a\xe6", "aba\xe6"], testNum);
-
-        restoreForm();
-        input.focus();
-        sendChar("b");
-        waitForMenuChange(1, "bc d\xe6");
-        break;
-
-    case 252:
-        checkMenuEntries(["bc d\xe6"], testNum);
-        input.focus();
-        sendChar(" ");
-        waitForMenuChange(1);
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, ["1", "12", "123", "1234"]);
+    yield closeAutoCompletePopup();
 
-    case 253:
-        // check that trailing space has no effect after single char.
-        checkMenuEntries(["bc d\xe6"], testNum);
-        input.focus();
-        sendChar("\xc6");
-        waitForMenuChange(2);
-        break;
+    // This first loop reduces the maxLength on the input from
+    // VALUES.length until 1, and ensures that only the strings
+    // with the right lengths appear in the popup.
+    for (let maxLength = VALUES.length; maxLength > 0; --maxLength) {
+      input.maxLength = maxLength;
+      let expectedResults = VALUES.filter(s => (s.length <= maxLength));
+      input.focus();
+      let results = yield openAutoCompletePopup();
+      assertResultsMatch(results, expectedResults);
+      yield closeAutoCompletePopup();
+      input.blur();
+    }
 
-    case 254:
-        // check multi-word substring matches
-        checkMenuEntries(["bc d\xe6", "aba\xe6"]);
-        input.focus();
-        expectPopup();
-        doKey("left");
-        sendChar("d");
-        break;
+    // The 0-case - we expect no popup if maxLength is 0.
+    input.maxLength = 0;
+    input.focus();
+    yield notifyMenuChanged(0);
+    input.blur();
 
-    case 255:
-        // check inserting in multi-word searches
-        checkMenuEntries(["bc d\xe6"], testNum);
-        input.focus();
-        sendChar("z");
-        waitForMenuChange(0);
-        break;
-
-    case 256:
-        checkMenuEntries([], testNum);
+    // This second loop works the input maxLength back up from 1 to
+    // VALUES.length, ensuring that only the strings with the right
+    // lengths appear in the popup.
+    for (let maxLength = 1; maxLength <= VALUES.length; ++maxLength) {
+      input.maxLength = maxLength;
+      let expectedResults = VALUES.filter(s => (s.length <= maxLength));
+      input.focus();
+      let results = yield openAutoCompletePopup();
+      assertResultsMatch(results, expectedResults);
+      yield closeAutoCompletePopup();
+      input.blur();
+    }
+  });
+});
 
-        // Look at form 7, try to trigger autocomplete popup
-        input = $_(7, "field5");
-        testNum = 299;
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests autocomplete on a field of type "email".
+ */
+add_task(function* test_email_field() {
+  const FORM_NUM = 8;
+  const FIELD_NAME = "field6";
+  // Non email values are also currently allowed.
+  const VALUES = [
+    "123",
+    "holtzmann@ghostbusters.org",
+    "mconley@mozilla.com",
+    "value",
+  ];
 
-    case 300:
-        checkMenuEntries(["1", "12", "123", "1234"], testNum);
-        input.maxLength = 4;
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
-
-    case 301:
-        checkMenuEntries(["1", "12", "123", "1234"], testNum);
-        input.maxLength = 3;
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    yield closeAutoCompletePopup();
+  });
+});
 
-    case 302:
-        checkMenuEntries(["1", "12", "123"], testNum);
-        input.maxLength = 2;
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
-
-    case 303:
-        checkMenuEntries(["1", "12"], testNum);
-        input.maxLength = 1;
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
+/**
+ * Tests autocomplete on a field of type "tel".
+ */
+add_task(function* test_tel_field() {
+  const FORM_NUM = 9;
+  const FIELD_NAME = "field7";
+  // Non phonenumber values are also allowed.
+  const VALUES = [
+    "1-800-267-2001", // Alaaaaaaarm Force
+    "555-555-5555",
+    "Ask me about LOOM!",
+    "value",
+  ];
 
-    case 304:
-        checkMenuEntries(["1"], testNum);
-        input.maxLength = 0;
-        doKey("escape");
-        doKey("down");
-        waitForMenuChange(0);
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    yield closeAutoCompletePopup();
+  });
+});
 
-    case 305:
-        checkMenuEntries([], testNum);
-        input.maxLength = 4;
-
-        // now again with a character typed
-        input.focus();
-        sendChar("1");
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
+/**
+ * Tests autocomplete on a field of type "url".
+ */
+add_task(function* test_url_field() {
+  const FORM_NUM = 10;
+  const FIELD_NAME = "field8";
+  // Non URL values are also allowed.
+  const VALUES = [
+    "ftp://ftp.mozilla.org",
+    "http://www.mozilla.org",
+    "https://www.mozilla.org",
+    "value",
+  ];
 
-    case 306:
-        checkMenuEntries(["1", "12", "123", "1234"], testNum);
-        input.maxLength = 3;
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
-
-    case 307:
-        checkMenuEntries(["1", "12", "123"], testNum);
-        input.maxLength = 2;
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
-
-    case 308:
-        checkMenuEntries(["1", "12"], testNum);
-        input.maxLength = 1;
-        expectPopup();
-        doKey("escape");
-        doKey("down");
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    yield closeAutoCompletePopup();
+  });
+});
 
-    case 309:
-        checkMenuEntries(["1"], testNum);
-        input.maxLength = 0;
-        doKey("escape");
-        doKey("down");
-        waitForMenuChange(0);
-        break;
-
-    case 310:
-        checkMenuEntries([], testNum);
+/**
+ * Tests autocomplete on a field of type "search".
+ */
+add_task(function* test_search_field() {
+  const FORM_NUM = 11;
+  const FIELD_NAME = "field9";
+  const VALUES = [
+    "how do I add Content Security Policy to apache?",
+    "how do I contact yahoo",
+    "how do I delete my facebook account?",
+    "how do I know if I'm running e10s",
+    "value",
+  ];
 
-        input = $_(8, "field6");
-        testNum = 399;
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    yield closeAutoCompletePopup();
+  });
+});
 
-    case 400:
-    case 401:
-    case 402:
-    case 403:
-        checkMenuEntries(["value"], testNum);
-        doKey("down");
-        doKey("return");
-        checkForm("value");
+/**
+ * Tests autocomplete on a field of type "date".
+ */
+add_task(function* test_date_field() {
+  const FORM_NUM = 14;
+  const FIELD_NAME = "field11";
+  const VALUES = [
+    "2010-10-10",
+    "2011-01-01",
+  ];
 
-        if (testNum == 400) {
-          input = $_(9, "field7");
-        } else if (testNum == 401) {
-          input = $_(10, "field8");
-        } else if (testNum == 402) {
-          input = $_(11, "field9");
-        } else if (testNum == 403) {
-          todo(false, "Fix input type=number");
-          input = $_(12, "field10");
-        }
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    repeatKey("down", 2);
+    yield closeAutoCompletePopup();
+    is(input.value, VALUES[1], "Should have selected the second value.");
+  });
+});
 
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 404:
-        checkMenuEntries(["42"], testNum);
-        doKey("down");
-        doKey("return");
-        checkForm("42");
-
-        input = $_(14, "field11");
-        restoreForm();
-        expectPopup();
-        doKey("down");
-        break;
+/**
+ * Tests autocomplete on a field of type "time".
+ */
+add_task(function* test_time_field() {
+  const FORM_NUM = 15;
+  const FIELD_NAME = "field12";
+  const VALUES = [
+    "01:13",
+    "21:21",
+  ];
 
-    case 405:
-        checkMenuEntries(["2010-10-10"]);
-        doKey("down");
-        doKey("return");
-        checkForm("2010-10-10");
-
-        input = $_(15, "field12");
-        restoreForm();
-        expectPopup();
-        doKey("down");
-        break;
-
-    case 406:
-        checkMenuEntries(["21:21"]);
-        doKey("down");
-        doKey("return");
-        checkForm("21:21");
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, VALUES[0], "Should have selected the first value.");
+  });
+});
 
-        input = $_(16, "field13");
-        restoreForm();
-        doKey("down");
-        waitForMenuChange(0);
-        break;
+/**
+ * Tests that there is no autocomplete on a field of type "range".
+ */
+add_task(function* test_range_field() {
+  const FORM_NUM = 16;
+  const FIELD_NAME = "field13";
+  const VALUES = [
+    "32", // Should never be visible, since "range" has no autocomplete.
+  ];
 
-    case 407:
-        checkMenuEntries([]); // type=range does not have a drop down menu
-        doKey("down");
-        doKey("return");
-        checkForm("30"); // default (midway between minimum (0) and maximum (64)) - step
-
-        input = $_(17, "field14");
-        restoreForm();
-        waitForMenuChange(0);
-        break;
-
-    case 408:
-        checkMenuEntries([]); // type=color does not have a drop down menu
-        checkForm("#000000"); // default color value
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    doKey("down");
+    yield notifyMenuChanged(0);
+    doKey("down");
+    doKey("return");
+    // This range should have a max of 64, and start at the default
+    // midway of 32. Two presses of "down" with a step of 1 should
+    // decrease the value to 30.
+    is(input.value, "30", "Got expected value on the range.");
+  });
+});
 
-        input = $_(18, "field15");
-        restoreForm();
-        expectPopup();
-        doKey("down");
-        break;
-
-    case 409:
-        checkMenuEntries(["2016-08"]);
-        doKey("down");
-        doKey("return");
-        checkForm("2016-08");
+/**
+ * Tests that there is no autocomplete on a field of type "color".
+ */
+add_task(function* test_color_field() {
+  const FORM_NUM = 17;
+  const FIELD_NAME = "field14";
+  const VALUES = [
+    "#ffffff", // Should never be visible, since "color" has no autocomplete.
+  ];
 
-        input = $_(19, "field16");
-        restoreForm();
-        expectPopup();
-        doKey("down");
-        break;
-
-    case 410:
-        checkMenuEntries(["2016-W32"]);
-        doKey("down");
-        doKey("return");
-        checkForm("2016-W32");
-
-        addEntry("field1", "value1");
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    doKey("down");
+    yield notifyMenuChanged(0);
+    // This range should have a max of 64, and start at the default
+    // midway of 32. Two presses of "down" with a step of 1 should
+    // decrease the value to 30.
+    is(input.value, "#000000", "Got expected default value on the color.");
+  });
+});
 
-    case 411:
-        input = $_(1, "field1");
-        // Go to test 500.
-        testNum = 499;
-
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests autocomplete on a field of type "month".
+ */
+add_task(function* test_month_field() {
+  const FORM_NUM = 18;
+  const FIELD_NAME = "field15";
+  const VALUES = [
+    "2016-08",
+  ];
 
-    // Check that the input event is fired.
-    case 500:
-      input.addEventListener("input", function(event) {
-        input.removeEventListener("input", arguments.callee, false);
-        ok(true, testNum + " oninput should have been received");
-        ok(event.bubbles, testNum + " input event should bubble");
-        ok(event.cancelable, testNum + " input event should be cancelable");
-      }, false);
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, VALUES[0], "Should have selected the first value.");
+  });
+});
 
-      doKey("down");
-      checkForm("");
-      doKey("return");
-      checkForm("value1");
-      testNum = 599;
-      setTimeout(runTest, 100);
-      break;
-
-    case 600:
-        // check we don't show autocomplete for searchbar-history
-        input = $_(13, "searchbar-history");
+/**
+ * Tests autocomplete on a field of type "week".
+ */
+add_task(function* test_week_field() {
+  const FORM_NUM = 19;
+  const FIELD_NAME = "field16";
+  const VALUES = [
+    "2016-W32",
+    "2016-W44",
+  ];
 
-        // Trigger autocomplete popup
-        checkForm("");
-        restoreForm();
-        doKey("down");
-        waitForMenuChange(0);
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, VALUES[0], "Should have selected the first value.");
+  });
+});
 
-    case 601:
-        checkMenuEntries([], testNum);
-        input.blur();
-        SimpleTest.finish();
-        return;
+/**
+ * Tests that "input" events are fired when selecting an item
+ * from an autocomplete list.
+ */
+add_task(function* test_input_event_is_fired() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "value",
+  ];
 
-    default:
-        ok(false, "Unexpected invocation of test #" + testNum);
-        SimpleTest.finish();
-        return;
-  }
-}
-
-function addEntry(name, value)
-{
-  updateFormHistory({ op : "add", fieldname : name, value: value }, runTest);
-}
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, VALUES);
+    doKey("down");
 
-// Runs the next test when scroll event occurs
-function waitForScroll()
-{
-  addEventListener("scroll", function() {
-    if (!window.pageYOffset)
-      return;
-
-    removeEventListener("scroll", arguments.callee, false);
-    setTimeout(runTest, 100);
-  }, false);
-}
-
-function waitForMenuChange(expectedCount, expectedFirstValue)
-{
-    notifyMenuChanged(expectedCount, expectedFirstValue, runTest);
-}
+    let inputEventPromise = waitForInputEvent(input);
+    // closeAutoCompletePopup defaults to closing with "return",
+    // which should select the first value.
+    yield closeAutoCompletePopup();
+    yield inputEventPromise;
+    is(input.value, VALUES[0], "Should have selected the first value.");
+  });
+});
 
-function checkMenuEntries(expectedValues, testNum) {
-    var actualValues = getMenuEntries();
-    is(actualValues.length, expectedValues.length, testNum + " Checking length of expected menu");
-    for (var i = 0; i < expectedValues.length; i++)
-        is(actualValues[i], expectedValues[i], testNum + " Checking menu entry #"+i);
-}
+/**
+ * Tests that the field name "searchbar-history" is blacklisted
+ * from autocomplete popups (See bug 461820).
+ */
+add_task(function* test_searchbar_history_blacklist() {
+  const FORM_NUM = 13;
+  const FIELD_NAME = "searchbar-history";
+  const VALUES = [
+    "definitely",
+    "should",
+    "not",
+    "see",
+    "any",
+    "of",
+    "these.",
+  ];
 
-function startTest() {
-    setupFormHistory(function() {
-        runTest();
-    });
-}
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    doKey("down");
+    yield notifyMenuChanged(0);
+  });
+});
 
-window.onload = startTest;
-
-SimpleTest.waitForExplicitFinish();
-SimpleTest.requestFlakyTimeout("untriaged");
 </script>
 </pre>
 </body>
 </html>
 
--- a/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
+++ b/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
@@ -1,14 +1,15 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for Form History 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/SpawnTask.js"></script>
   <script type="text/javascript" src="satchel_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Form History test: form field autocomplete
 <p id="display"></p>
 
 <!-- we presumably can't hide the content for this test. -->
@@ -37,470 +38,355 @@ Form History test: form field autocomple
     <option value="Reddit">PASS2</option>
     <option value="final"></option>
   </datalist>
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-/** Test for Form History autocomplete **/
+/** Test for Form History autocomplete with datalists **/
 
-var input = $_(1, "field1");
-const shiftModifier = Components.interfaces.nsIDOMEvent.SHIFT_MASK;
+let gDataListLabels;
+let gDataListValues;
 
-function setupFormHistory(aCallback) {
-  updateFormHistory([
-    { op : "remove" },
-    { op : "add", fieldname : "field1", value : "historyvalue" },
-    { op : "add", fieldname : "field2", value : "othervalue" },
-  ], aCallback);
-}
-
-function setForm(value) {
-    input.value = value;
-    input.focus();
-}
-
-// Restore the form to the default state.
-function restoreForm() {
-    setForm("");
-}
+add_task(function* setup() {
+  let datalist = document.getElementById("suggest");
+  gDataListLabels = Array.from(datalist.options).map((option) => {
+    // The autocomplete popup will show the label if one exists,
+    // and fallback to the value if one doesn't.
+    return option.label || option.value;
+  });
 
-// Check for expected form data.
-function checkForm(expectedValue) {
-    var formID = input.parentNode.id;
-    is(input.value, expectedValue, "Checking " + formID + " input");
-}
+  gDataListValues = Array.from(datalist.options).map((option) => {
+    // The autocomplete popup will show the label if one exists,
+    // and fallback to the value if one doesn't.
+    return option.value;
+  });
 
-var testNum = 0;
-var prevValue;
-var expectingPopup = false;
+  listenForUnexpectedPopupShown();
+});
 
-function expectPopup() {
-  info("expecting popup for test " + testNum);
-  expectingPopup = true;
-}
-
-function popupShownListener() {
-  info("popup shown for test " + testNum);
-  if (expectingPopup) {
-    expectingPopup = false;
-    SimpleTest.executeSoon(runTest);
-  }
-  else {
-    ok(false, "Autocomplete popup not expected during test " + testNum);
-  }
-}
-
-registerPopupShownListener(popupShownListener);
+/**
+ * Tests that autocomplete will show Form History entries
+ * along with datalist entries.
+ */
+add_task(function* test_form_history_datalist_mix() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "historyvalue",
+  ];
 
-/*
-* Main section of test...
-*
-* This is a bit hacky, as many operations happen asynchronously.
-* Various mechanisms call runTests as a result of operations:
-*   - set expectingPopup to true, and the next test will occur when the autocomplete popup is shown
-*   - call waitForMenuChange(x) to run the next test when the autocomplete popup to have x items in it
-*/
-function runTest() {
-    testNum++;
-
-    info("Starting test #" + testNum);
-
-    switch (testNum) {
-    case 1:
-        // Make sure initial form is empty.
-        checkForm("");
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-    case 2:
-        checkMenuEntries(["historyvalue", "PASS1", "PASS2", "final"], testNum);
-        // Check first entry
-        doKey("down");
-        checkForm(""); // value shouldn't update
-        doKey("return"); // not "enter"!
-        checkForm("historyvalue");
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    // Form History entries should always be displayed before datalist
+    // entries. Note that we're showing the labels in the popup, which
+    // might not equal their values.
+    assertResultsMatch(results, [...VALUES, ...gDataListLabels]);
+    doKey("down");
+    is(input.value, "", "Field should not have updated yet.");
+    yield closeAutoCompletePopup();
+    is(input.value, VALUES[0], "Should have selected the first value.");
+    input.value = "";
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 3:
-        // Check second entry
-        doKey("down");
-        doKey("down");
-        doKey("return"); // not "enter"!
-        checkForm("Google");
+    for (let i = 0; i < gDataListLabels.length; ++i) {
+      yield openAutoCompletePopup();
+      // Skip past the first Form History entry, and select the first
+      // datalist entry.
+      repeatKey("down", 2);
+      // Then skip down to the datalist entry we want.
+      repeatKey("down", i);
+      is(input.value, "", "Field should not have updated yet.");
+      yield closeAutoCompletePopup();
+      is(input.value, gDataListValues[i],
+         "Should have selected the right datalist value.");
+      input.value = "";
+    }
+  });
+});
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests that Form History and datalist entries can be deleted
+ * from the autocomplete popup.
+ */
+add_task(function* test_form_history_datalist_deletion() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "historyvalue1",
+    "historyvalue2",
+  ];
 
-    case 4:
-        // Check third entry
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("return");
-        checkForm("Reddit");
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, [...VALUES, ...gDataListLabels]);
+    let total = results.length;
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Select the first item...
+    doKey("down");
 
-    case 5:
-        // Check fourth entry
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("return");
-        checkForm("final");
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 6:
-        //Delete the first entry (of 3)
-        doKey("down");
-        doKey("delete", shiftModifier);
-        waitForMenuChange(3);
-        break;
+    // Delete each entry one by one.
+    for (let i = 1; i <= total; ++i) {
+      doKey("delete", Event.SHIFT_MASK);
+      yield notifyMenuChanged(total - i);
+    }
 
-    case 7:
-        checkForm("");
-        countEntries("field1", "historyvalue",
-          function (num) {
-            ok(!num, testNum + " checking that form history value was deleted");
-            runTest();
-          });
-        break;
-
-    case 8:
-        doKey("return");
-        checkForm("Google")
+    // With the last value gone, the popup should be
+    // closed. If we blur and refocus, the datalist entries
+    // should be back.
+    input.blur();
+    input.focus();
+    results = yield openAutoCompletePopup();
+    // Should only be showing the datalist entries now.
+    assertResultsMatch(results, gDataListLabels);
+    yield closeAutoCompletePopup();
+  });
+});
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 9:
-        //Test deletion
-        checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
-        // Check the new first entry (of 3)
-        doKey("down");
-        doKey("return");
-        checkForm("Google");
-
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests that a autocomplete results list composed of Form History
+ * and datalist entries will update as characters are put into the
+ * input and the search is refined.
+ */
+add_task(function* test_search_datalist_values() {
+  const FORM_NUM = 1;
+  const FIELD_NAME = "field1";
+  const VALUES = [
+    "historyvalue1",
+    "historyvalue2",
+    "historyvalue3",
+  ];
 
-    case 10:
-        // Test autocompletion of datalists with cached results.
-        sendString("PAS");
-        waitForMenuChange(2);
-        break;
-
-    case 11:
-        // Continuation of test 10
-        sendString("S1");
-        waitForMenuChange(1);
-        break;
-
-    case 12:
-        doKey("down");
-        doKey("return");
-        checkForm("Google");
-
-        // Trigger autocomplete popup
-        // Look at form 3, try to trigger autocomplete popup
-        input.value = "";
-        input = $_(3, "field2");
-        testNum = 99;
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup("PAS");
+    // Input should now be "PAS"
+    is(results.length, 2, "Expect 2 entries for 'PAS'.");
+    sendString("S2");
+    // Input should now be "PASS2", which should only match one entry.
+    yield notifyMenuChanged(1);
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, "Reddit",
+       "Should have selected the only matching datalist value.");
+  });
+});
 
-    case 100:
-        checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
-        // Check first entry
-        doKey("down");
-        checkForm(""); // value shouldn't update
-        doKey("return"); // not "enter"!
-        checkForm("Google");
-
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests that if a field has autocomplete="off" set, that datalist
+ * entries will appear, but no Form History entries. Also tests
+ * that the user can select datalist entries from the popup despite
+ * autocomplete being disabled.
+ */
+add_task(function* test_autocomplete_off() {
+  const FORM_NUM = 3;
+  const FIELD_NAME = "field2";
+  const VALUES = [
+    "this",
+    "should",
+    "never",
+    "appear!",
+  ];
 
-    case 101:
-        // Check second entry
-        doKey("down");
-        doKey("down");
-        doKey("return"); // not "enter"!
-        checkForm("Reddit");
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, gDataListLabels);
+    yield closeAutoCompletePopup();
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
-
-    case 102:
-        // Check third entry
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("return");
-        checkForm("final");
+    for (let i = 0; i < gDataListValues.length; ++i) {
+      yield openAutoCompletePopup();
+      // Select the first datalist value in the list.
+      doKey("down");
+      // And maybe go down further if we're selecting a
+      // later entry.
+      repeatKey("down", i);
+      yield closeAutoCompletePopup();
+      is(input.value, gDataListValues[i],
+         "Should have selected the right datalist value.");
+      input.value = "";
+    }
+  });
+});
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+/**
+ * Tests what occurs to autocomplete popups when JavaScript dynamically
+ * manipulates datalist entries. This test will remove and change entries
+ * from the "suggest" datalist, but will restore the DOM to its original state
+ * before finishing.
+ */
+add_task(function* test_dynamic_options() {
+  const FORM_NUM = 3;
+  const FIELD_NAME = "field2";
+  const VALUES = [];
 
-    case 103:
-        checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
-        // Check first entry
-        doKey("down");
-        checkForm(""); // value shouldn't update
-        doKey("return"); // not "enter"!
-        checkForm("Google");
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    let results = yield openAutoCompletePopup();
+    assertResultsMatch(results, [...VALUES, ...gDataListLabels]);
 
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // Select the first datalist value.
+    doKey("down");
 
-    case 104:
-        // Check second entry
-        doKey("down");
-        doKey("down");
-        doKey("return"); // not "enter"!
-        checkForm("Reddit");
-
-        // Trigger autocomplete popup
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    let datalist = document.getElementById("suggest");
+    // Now dynamically remove the second value.
+    let toRemove = datalist.children[1];
+    toRemove.remove();
+    yield notifyMenuChanged(gDataListLabels.length - 1);
+    // Note that selection is lost when the popup is updated, so currently
+    // nothing is selected. We'll select the second displayed entry, which
+    // should be the third entry from the original list, pre-removal.
+    repeatKey("down", 2);
+    yield closeAutoCompletePopup();
+    is(input.value, gDataListValues[2],
+       "Should have selected the third datalist entry.");
 
-    case 105:
-        // Check third entry
-        doKey("down");
-        doKey("down");
-        doKey("down");
-        doKey("return");
-        checkForm("final");
+    // Now restore the removed element.
+    datalist.insertBefore(toRemove, datalist.children[1]);
+    input.value = "";
 
-        testNum = 199;
-        expectPopup();
-        restoreForm();
-        doKey("down");
-        break;
+    // We have to blur and re-focus the element in order to
+    // clear the autocomplete cache.
+    input.blur();
+    input.focus();
 
-    // Test dynamic updates.
-    // For some reasons, when there is an update of the list, the selection is
-    // lost so we need to go down like if we were at the beginning of the list
-    // again.
-    case 200:
-      // Removing the second element while on the first then going down and
-      // push enter. Value should be one from the third suggesion.
-      doKey("down");
-      var datalist = document.getElementById('suggest');
-      var toRemove = datalist.children[1]
-      datalist.removeChild(toRemove);
+    // Next, let's try adding an <option> while the popup is opened. Strangely,
+    // initially the results returned by openAutoCompletePopup has only a
+    // single entry in non-e10s mode, but this quickly updates itself to
+    // display all entries.
+    yield openAutoCompletePopup();
+    yield results =
+      yield notifyMenuChanged(VALUES.length + gDataListLabels.length);
+    assertResultsMatch(results, [...VALUES, ...gDataListLabels]);
 
-      SimpleTest.executeSoon(function() {
-        doKey("down");
-        doKey("down");
-        doKey("return");
-        checkForm("final");
-
-        // Restore the element.
-        datalist.insertBefore(toRemove, datalist.children[1]);
-        expectPopup();
-        restoreForm();
-        doKey("down");
-      });
-      break;
+    // Select the first value.
+    doKey("down");
+    let added = new Option("Foo");
+    datalist.insertBefore(added, datalist.children[1]);
+    yield notifyMenuChanged(4);
 
-    case 201:
-      // Adding an attribute after the first one while on the first then going
-      // down and push enter. Value should be the on from the new suggestion.
-      doKey("down");
-      datalist = document.getElementById('suggest');
-      var added = new Option("Foo");
-      datalist.insertBefore(added, datalist.children[1]);
-      waitForMenuChange(4);
-      break;
+    // Adding the new <option> cleared our selection. Let's select
+    // the new <option>, which should be the second value.
+    repeatKey("down", 2);
+    yield closeAutoCompletePopup();
+    is(input.value, "Foo", "Should have selected the newly added value.");
+    // Now remove the element.
+    added.remove();
+    // The input is still "Foo", which shouldn't match anything.
+    yield notifyMenuChanged(0);
 
-    case 202:
-      doKey("down");
-      doKey("down");
-      doKey("return");
-      checkForm("Foo");
+    input.value = "";
 
-      // Remove the element.
-      datalist = document.getElementById('suggest');
-      datalist.removeChild(datalist.children[1]);
-      waitForMenuChange(0);
-      break;
+    // Now let's programmatically change some values.
+    let option = datalist.children[0];
+    let oldValue = option.value;
+    let oldLabel = option.label;
+    option.value = "apple";
 
-    case 203:
-      // Change the first element value attribute.
-      restoreForm();
-      datalist = document.getElementById('suggest');
-      prevValue = datalist.children[0].value;
-      datalist.children[0].value = "foo";
-      expectPopup();
-      break;
+    results = yield openAutoCompletePopup();
+    assertResultsMatch(results, gDataListLabels);
+    yield notifyMenuChanged(3);
 
-    case 204:
-      doKey("down");
-      doKey("return");
-      checkForm("foo");
+    // Select the first value, which is the one we modified.
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, "apple",
+       "Should have gotten the dynamically modified option.")
 
-      datalist = document.getElementById('suggest');
-      datalist.children[0].value = prevValue;
-      waitForMenuChange(0);
-      break;
+    // Now that "apple" is selected, nothing should match.
+    yield notifyMenuChanged(0);
 
-    case 205:
-      // Change the textContent to update the value attribute.
-      restoreForm();
-      datalist = document.getElementById('suggest');
-      prevValue = datalist.children[0].getAttribute('value');
-      datalist.children[0].removeAttribute('value');
-      datalist.children[0].textContent = "foobar";
-      expectPopup();
-      break;
+    input.value = "";
+    // Now we'll change the textContent to update the value attribute.
+    option.removeAttribute("label");
+    option.removeAttribute("value");
+    option.textContent = "orange";
 
-    case 206:
-      doKey("down");
-      doKey("return");
-      checkForm("foobar");
+    input.blur();
+    input.focus();
+
+    yield openAutoCompletePopup();
 
-      datalist = document.getElementById('suggest');
-      datalist.children[0].setAttribute('value', prevValue);
-      testNum = 299;
-      waitForMenuChange(0);
-      break;
+    // Now select the option we just modified...
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, "orange",
+       "Got the value we modified by setting the textContent.");
 
-    // Tests for filtering (or not).
-    case 300:
-      // Filters with first letter of the word.
-      restoreForm();
-      synthesizeKey("f", {});
-      expectPopup();
-      break;
-
-    case 301:
-      doKey("down");
-      doKey("return");
-      checkForm("final");
-      expectPopup();
-      restoreForm();
-      doKey("down");
-      break;
+    // And now clean up.
+    input.blur();
+    option.textContent = "";
+    option.label = oldLabel;
+    option.value = oldValue;
+  });
+});
 
-    case 302:
-      // Filter with a letter in the middle of the word.
-      synthesizeKey("i", {});
-      synthesizeKey("n", {});
-      waitForMenuChange(1);
-      break;
+/**
+ * Tests filtering of datalists, as well as the special "mozNoFilter"
+ * input attribute that disables filtering.
+ */
+add_task(function* test_filtering() {
+  const FORM_NUM = 3;
+  const FIELD_NAME = "field2";
+  const VALUES = [];
 
-    case 303:
-      // Continuation of test 302.
-      doKey("down");
-      doKey("return");
-      checkForm("final");
-      expectPopup();
-      restoreForm();
-      doKey("down");
-      break;
+  // Expects that there's a "final" value in the datalist.
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    yield openAutoCompletePopup("f");
+    // Input is now "f". There should only be one result.
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, "final", "Got expected datalist entry after filtering.");
+    input.value = "";
 
-    case 304:
-      // Filter is disabled with mozNoFilter.
-      input.setAttribute('mozNoFilter', 'true');
-      synthesizeKey("f", {});
-      waitForMenuChange(3); // no change
-      break;
+    // Now try matching just part of the word.
+    yield openAutoCompletePopup();
+    sendChar("i");
+    sendChar("n");
+    yield notifyMenuChanged(1);
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, "final", "Got expected datalist entry after filtering.");
+    input.value = "";
 
-    case 305:
-      // Continuation of test 304.
-      doKey("down");
-      doKey("return");
-      checkForm("Google");
-      input.removeAttribute('mozNoFilter');
-      testNum = 399;
-      expectPopup();
-      restoreForm();
-      doKey("down");
-      break;
+    // Now test that filtering is disabled with the special "mozNoFilter"
+    // attribute.
+    input.setAttribute("mozNoFilter", "true");
+    yield openAutoCompletePopup("f");
+    // Shouldn't have done any filtering - we should still be showing
+    // the full list.
+    yield notifyMenuChanged(3);
 
-    case 400:
-      // Check that the input event is fired.
-      input.addEventListener("input", function(event) {
-        input.removeEventListener("input", arguments.callee, false);
-        ok(true, "oninput should have been received");
-        ok(event.bubbles, "input event should bubble");
-        ok(event.cancelable, "input event should be cancelable");
-        checkForm("Google");
-        input.blur();
-        SimpleTest.finish();
-      }, false);
+    doKey("down");
+    yield closeAutoCompletePopup();
+    is(input.value, gDataListValues[0],
+       "Selected the first value after disabling filtering.");
 
-      doKey("down");
-      checkForm("");
-      doKey("return");
-      break;
+    // And now clean up.
+    input.removeAttribute("mozNoFilter");
+  });
+});
 
-    default:
-      ok(false, "Unexpected invocation of test #" + testNum);
-      SimpleTest.finish();
-      return;
-    }
-}
+/**
+ * Tests that "input" events are fired when selecting an item
+ * from an autocomplete list with datalist values.
+ */
+add_task(function*() {
+  const FORM_NUM = 3;
+  const FIELD_NAME = "field2";
+  const VALUES = [];
 
-function waitForMenuChange(expectedCount) {
-    notifyMenuChanged(expectedCount, null, runTest);
-}
+  // Expects that there's a "final" value in the datalist.
+  yield prepareTest(FORM_NUM, FIELD_NAME, VALUES, function*(input) {
+    yield openAutoCompletePopup();
+    doKey("down");
+    // Just a double-check that the input value hasn't changed yet.
+    is(input.value, "",
+       "The input value should not have changed just yet.");
+    let inputPromise = waitForInputEvent(input);
+    yield closeAutoCompletePopup();
+    yield inputPromise;
+    is(input.value, gDataListValues[0], "Got the first datalist value.");
+  });
+});
 
-function checkMenuEntries(expectedValues, testNum) {
-    var actualValues = getMenuEntries();
-    is(actualValues.length, expectedValues.length, testNum + " Checking length of expected menu");
-    for (var i = 0; i < expectedValues.length; i++)
-        is(actualValues[i], expectedValues[i], testNum + " Checking menu entry #"+i);
-}
-
-function startTest() {
-    setupFormHistory(runTest);
-}
-
-window.onload = startTest;
-
-SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 </body>
 </html>