Bug 1162338 - Part 2 - Fix test_bug_511615.html to work in e10s. r=mak draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 14 Mar 2016 13:26:58 +0000
changeset 339949 e21355092abaf1267aa2bc3a42df2a4f211b3dcd
parent 339948 a152300122c34fa96ea464fe1bec4e4353bed905
child 339951 3703431540b1ce1a6a7540929b272b1072f6255b
push id12840
push userpaolo.mozmail@amadzone.org
push dateMon, 14 Mar 2016 13:54:49 +0000
reviewersmak
bugs1162338
milestone48.0a1
Bug 1162338 - Part 2 - Fix test_bug_511615.html to work in e10s. r=mak MozReview-Commit-ID: GiwGq2BUdUn
toolkit/components/satchel/test/mochitest.ini
toolkit/components/satchel/test/parent_utils.js
toolkit/components/satchel/test/satchel_common.js
toolkit/components/satchel/test/test_bug_511615.html
toolkit/components/satchel/test/test_popup_direction.html
--- a/toolkit/components/satchel/test/mochitest.ini
+++ b/toolkit/components/satchel/test/mochitest.ini
@@ -1,19 +1,18 @@
 [DEFAULT]
-skip-if = toolkit == 'android' || buildapp == 'b2g' || os == 'linux' # linux - bug 947531
+skip-if = toolkit == 'android' || buildapp == 'b2g' || os == 'linux' # linux - bug 1022386
 support-files =
   satchel_common.js
   subtst_form_submission_1.html
   subtst_privbrowsing.html
   parent_utils.js
 
 [test_bug_511615.html]
-skip-if = e10s # bug 1162338 (needs refactoring to talk to the autocomplete popup)
 [test_bug_787624.html]
-skip-if = e10s # bug 1162338 (needs refactoring to talk to the autocomplete popup)
 [test_datalist_with_caching.html]
 [test_form_autocomplete.html]
 [test_form_autocomplete_with_list.html]
 [test_form_submission.html]
 [test_form_submission_cap.html]
 [test_form_submission_cap2.html]
+[test_popup_direction.html]
 [test_popup_enter_event.html]
--- a/toolkit/components/satchel/test/parent_utils.js
+++ b/toolkit/components/satchel/test/parent_utils.js
@@ -75,16 +75,33 @@ var ParentUtils = {
           gAutocompletePopup.tree.view.getCellText(0, gAutocompletePopup.tree.columns[0]) ===
           expectedFirstValue);
     }).then(() => {
       let results = this.getMenuEntries();
       sendAsyncMessage("gotMenuChange", { results });
     });
   },
 
+  checkSelectedIndex(expectedIndex) {
+    ContentTaskUtils.waitForCondition(() => {
+      return gAutocompletePopup.popupOpen &&
+             gAutocompletePopup.selectedIndex === expectedIndex;
+    }).then(() => {
+      sendAsyncMessage("gotSelectedIndex");
+    });
+  },
+
+  getPopupState() {
+    sendAsyncMessage("gotPopupState", {
+      open: gAutocompletePopup.popupOpen,
+      selectedIndex: gAutocompletePopup.selectedIndex,
+      direction: gAutocompletePopup.style.direction,
+    });
+  },
+
   observe(subject, topic, data) {
     assert.ok(topic === "satchel-storage-changed");
     sendAsyncMessage("satchel-storage-changed", { subject: null, topic, data });
   },
 
   cleanup() {
     gAutocompletePopup.removeEventListener("popupshown", this._popupshownListener);
     this.cleanUpFormHist();
@@ -103,16 +120,24 @@ addMessageListener("updateFormHistory", 
 addMessageListener("countEntries", ({ name, value }) => {
   ParentUtils.countEntries(name, value);
 });
 
 addMessageListener("waitForMenuChange", ({ expectedCount, expectedFirstValue }) => {
   ParentUtils.checkRowCount(expectedCount, expectedFirstValue);
 });
 
+addMessageListener("waitForSelectedIndex", ({ expectedIndex }) => {
+  ParentUtils.checkSelectedIndex(expectedIndex);
+});
+
+addMessageListener("getPopupState", () => {
+  ParentUtils.getPopupState();
+});
+
 addMessageListener("addObserver", () => {
   Services.obs.addObserver(ParentUtils, "satchel-storage-changed", false);
 });
 addMessageListener("removeObserver", () => {
   Services.obs.removeObserver(ParentUtils, "satchel-storage-changed");
 });
 
 addMessageListener("cleanup", () => {
--- a/toolkit/components/satchel/test/satchel_common.js
+++ b/toolkit/components/satchel/test/satchel_common.js
@@ -120,33 +120,16 @@ var checkObserver = {
       });
   }
 };
 
 function checkForSave(name, value, message) {
   checkObserver.verifyStack.push({ name : name, value: value, message: message });
 }
 
-function NonE10SgetAutocompletePopup() {
-  var Ci = SpecialPowers.Ci;
-  chromeWin = SpecialPowers.wrap(window)
-                .QueryInterface(Ci.nsIInterfaceRequestor)
-                .getInterface(Ci.nsIWebNavigation)
-                .QueryInterface(Ci.nsIDocShellTreeItem)
-                .rootTreeItem
-                .QueryInterface(Ci.nsIInterfaceRequestor)
-                .getInterface(Ci.nsIDOMWindow)
-                .QueryInterface(Ci.nsIDOMChromeWindow);
-  autocompleteMenu = chromeWin.document.getElementById("PopupAutoComplete");
-  ok(autocompleteMenu, "Got autocomplete popup");
-
-  return autocompleteMenu;
-}
-
-
 function getFormSubmitButton(formNum) {
   var form = $("form" + formNum); // by id, not name
   ok(form != null, "getting form " + formNum);
 
   // we can't just call form.submit(), because that doesn't seem to
   // invoke the form onsubmit handler.
   var button = form.firstChild;
   while (button && button.type != "submit") { button = button.nextSibling; }
@@ -192,16 +175,32 @@ function notifyMenuChanged(expectedCount
                             expectedFirstValue });
   script.addMessageListener("gotMenuChange", function changed({ results }) {
     script.removeMessageListener("gotMenuChange", changed);
     gLastAutoCompleteResults = results;
     then();
   });
 }
 
+function notifySelectedIndex(expectedIndex, then) {
+  script.sendAsyncMessage("waitForSelectedIndex", { expectedIndex });
+  script.addMessageListener("gotSelectedIndex", function changed() {
+    script.removeMessageListener("gotSelectedIndex", changed);
+    then();
+  });
+}
+
+function getPopupState(then) {
+  script.sendAsyncMessage("getPopupState");
+  script.addMessageListener("gotPopupState", function listener(state) {
+    script.removeMessageListener("gotPopupState", listener);
+    then(state);
+  });
+}
+
 var chromeURL = SimpleTest.getTestFileURL("parent_utils.js");
 var script = SpecialPowers.loadChromeScript(chromeURL);
 script.addMessageListener("onpopupshown", ({ results }) => {
   gLastAutoCompleteResults = results;
   if (gPopupShownListener)
     gPopupShownListener();
 });
 
--- a/toolkit/components/satchel/test/test_bug_511615.html
+++ b/toolkit/components/satchel/test/test_bug_511615.html
@@ -1,389 +1,194 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <title>Test for Form History Autocomplete</title>
+  <title>Test for Form History Autocomplete Untrusted Events: Bug 511615</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.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
+Test for Form History Autocomplete Untrusted Events: Bug 511615
 <p id="display"></p>
 
 <!-- we presumably can't hide the content for this test. -->
-<div id="content" style="direction: rtl;">
-  <!-- unused -->
-  <form id="unused" onsubmit="return false;">
-    <input  type="text" name="field1" value="unused">
-  </form>
-
+<div id="content">
   <!-- normal, basic form -->
   <form id="form1" onsubmit="return false;">
     <input  type="text" name="field1">
     <button type="submit">Submit</button>
   </form>
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-/** Test for Form History autocomplete **/
-var autocompletePopup = NonE10SgetAutocompletePopup();
-autocompletePopup.style.direction = "ltr";
+var resolvePopupShownListener;
+registerPopupShownListener(() => resolvePopupShownListener());
+
+function waitForNextPopup() {
+  return new Promise(resolve => { resolvePopupShownListener = resolve; });
+}
+
+/**
+ * Indicates the time to wait before checking that the state of the autocomplete
+ * popup, including whether it is open, has not changed in response to events.
+ *
+ * Manual testing on a fast machine revealed that 80ms was still unreliable,
+ * while 100ms detected a simulated failure reliably. Unfortunately, this means
+ * that to take into account slower machines we should use a value like 300ms.
+ *
+ * Note that if a machine takes more than this time to show the popup, this
+ * would not cause a failure, conversely the machine would not be able to detect
+ * whether the test should have failed. In other words, this use of timeouts is
+ * never expected to cause intermittent failures with test automation.
+ */
+const POPUP_RESPONSE_WAIT_TIME_MS = 200;
+
+SimpleTest.requestFlakyTimeout("Must ensure that an event does not happen.");
+
+/**
+ * Checks that the popup does not open in response to the given function.
+ */
+function expectPopupDoesNotOpen(triggerFn) {
+  let popupShown = waitForNextPopup();
+  triggerFn();
+  return Promise.race([
+    popupShown.then(() => Promise.reject("Popup opened unexpectedly.")),
+    new Promise(resolve => setTimeout(resolve, POPUP_RESPONSE_WAIT_TIME_MS)),
+  ]);
+}
+
+/**
+ * Checks that the selected index in the popup still matches the given value.
+ */
+function checkSelectedIndexAfterResponseTime(expectedIndex) {
+  return new Promise(resolve => {
+    setTimeout(() => getPopupState(resolve), POPUP_RESPONSE_WAIT_TIME_MS);
+  }).then(popupState => {
+    is(popupState.open, true, "Popup should still be open.");
+    is(popupState.selectedIndex, expectedIndex, "Selected index should match.");
+  });
+}
+
+function doKeyUnprivileged(key) {
+  var keyName = "DOM_VK_" + key.toUpperCase();
+  var keycode, charcode, alwaysVal;
+
+  if (key.length == 1) {
+      keycode = 0;
+      charcode = key.charCodeAt(0);
+      alwaysval = charcode;
+  } else {
+      keycode = KeyEvent[keyName];
+      if (!keycode)
+          throw "invalid keyname in test";
+      charcode = 0;
+      alwaysval = keycode;
+  }
+
+  var dnEvent = document.createEvent('KeyboardEvent');
+  var prEvent = document.createEvent('KeyboardEvent');
+  var upEvent = document.createEvent('KeyboardEvent');
+
+  dnEvent.initKeyEvent("keydown",  true, true, null, false, false, false, false, alwaysval, 0);
+  prEvent.initKeyEvent("keypress", true, true, null, false, false, false, false, keycode, charcode);
+  upEvent.initKeyEvent("keyup",    true, true, null, false, false, false, false, alwaysval, 0);
+
+  input.dispatchEvent(dnEvent);
+  input.dispatchEvent(prEvent);
+  input.dispatchEvent(upEvent);
+}
+
+function doClickWithMouseEventUnprivileged() {
+  var dnEvent = document.createEvent('MouseEvent');
+  var upEvent = document.createEvent('MouseEvent');
+  var ckEvent = document.createEvent('MouseEvent');
+
+  dnEvent.initMouseEvent("mousedown",  true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+  upEvent.initMouseEvent("mouseup",    true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+  ckEvent.initMouseEvent("mouseclick", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+
+  input.dispatchEvent(dnEvent);
+  input.dispatchEvent(upEvent);
+  input.dispatchEvent(ckEvent);
+}
 
 var input = $_(1, "field1");
 
-// Get the form history service
-function setupFormHistory(aCallback) {
-  updateFormHistory([
+add_task(function* test_initialize() {
+  yield new Promise(resolve => 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 : "field1", value : "value5" },
     { op : "add", fieldname : "field1", value : "value6" },
     { op : "add", fieldname : "field1", value : "value7" },
     { op : "add", fieldname : "field1", value : "value8" },
     { op : "add", fieldname : "field1", value : "value9" },
-  ], aCallback);
-}
-
-function checkForm(expectedValue) {
-  var formID = input.parentNode.id;
-  is(input.value, expectedValue, "Checking " + formID + " input");
-}
-
-function checkPopupOpen(isOpen, expectedIndex) {
-    var actuallyOpen = autocompletePopup.popupOpen;
-    var actualIndex = autocompletePopup.selectedIndex;
-    is(actuallyOpen, isOpen, "popup should be " + (isOpen ? "open" : "closed"));
-    if (isOpen)
-        is(actualIndex, expectedIndex, "checking selected index");
-
-    // Correct state if needed, so following tests run as expected.
-    if (actuallyOpen != isOpen) {
-        if (isOpen)
-            autocompletePopup.openPopup();
-        else
-            autocompletePopup.closePopup();
-    }
-    if (isOpen && actualIndex != expectedIndex)
-        autocompletePopup.selectedIndex = expectedIndex;
-}
-
-function doKeyUnprivileged(key) {
-    var keyName = "DOM_VK_" + key.toUpperCase();
-    var keycode, charcode, alwaysVal;
-
-    if (key.length == 1) {
-        keycode = 0;
-        charcode = key.charCodeAt(0);
-        alwaysval = charcode;
-    } else {
-        keycode = KeyEvent[keyName];
-        if (!keycode)
-            throw "invalid keyname in test";
-        charcode = 0;
-        alwaysval = keycode;
-    }
-
-    var dnEvent = document.createEvent('KeyboardEvent');
-    var prEvent = document.createEvent('KeyboardEvent');
-    var upEvent = document.createEvent('KeyboardEvent');
-
-    dnEvent.initKeyEvent("keydown",  true, true, null, false, false, false, false, alwaysval, 0);
-    prEvent.initKeyEvent("keypress", true, true, null, false, false, false, false, keycode, charcode);
-    upEvent.initKeyEvent("keyup",    true, true, null, false, false, false, false, alwaysval, 0);
-
-    input.dispatchEvent(dnEvent);
-    input.dispatchEvent(prEvent);
-    input.dispatchEvent(upEvent);
-}
-
-function doClickUnprivileged() {
-    var dnEvent = document.createEvent('MouseEvent');
-    var upEvent = document.createEvent('MouseEvent');
-    var ckEvent = document.createEvent('MouseEvent');
-
-    dnEvent.initMouseEvent("mousedown",  true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
-    upEvent.initMouseEvent("mouseup",    true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
-    ckEvent.initMouseEvent("mouseclick", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
-
-    input.dispatchEvent(dnEvent);
-    input.dispatchEvent(upEvent);
-    input.dispatchEvent(ckEvent);
-}
-
-var testNum = 0;
-var expectingPopup = false;
-
-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" + testNum);
-  }
-}
-
-SpecialPowers.addAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
-
-/*
- * Main section of test...
- *
- * This is a bit hacky, because the events are either being sent or
- * processes asynchronously, so we need to interrupt our flow with lots of
- * setTimeout() calls. The case statements are executed in order, one per
- * timeout.
- */
-function runTest() {
-  testNum++;
-
-  ok(true, "Starting test #" + testNum);
-
-  switch(testNum) {
-    //
-    // Check initial state
-    //
-    case 1:
-        input.value = "";
-        checkForm("");
-        is(autocompletePopup.popupOpen, false, "popup should be initially closed");
-        break;
+  ], resolve));
+});
 
-    //
-    // Try to open the autocomplete popup from untrusted events.
-    //
-    // try a focus()
-    case 2:
-        input.focus();
-        break;
-    case 3:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // try a click()
-    case 4:
-        input.click();
-        break;
-    case 5:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // try a mouseclick event
-    case 6:
-        doClickUnprivileged();
-        break;
-    case 7:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // try a down-arrow
-    case 8:
-        doKeyUnprivileged("down");
-        break;
-    case 9:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // try a page-down
-    case 10:
-        doKeyUnprivileged("page_down");
-        break;
-    case 11:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // try a return
-    case 12:
-// XXX this causes later tests to fail for some reason.
-//        doKeyUnprivileged("return"); // not "enter"!
-        break;
-    case 13:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // try a keypress
-    case 14:
-        doKeyUnprivileged('v');
-        break;
-    case 15:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // try a space
-    case 16:
-        doKeyUnprivileged(" ");
-        break;
-    case 17:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    // backspace
-    case 18:
-        doKeyUnprivileged("back_space");
-        break;
-    case 19:
-        checkPopupOpen(false);
-        checkForm("");
-        break;
-    case 20:
-        // We're privileged for this test, so open the popup.
-        checkPopupOpen(false);
-        checkForm("");
-        expectPopup();
-        doKey("down");
-        return;
-        break;
-    case 21:
-        checkPopupOpen(true, -1);
-        checkForm("");
-        testNum = 99;
-        break;
-
-    //
-    // Try to change the selected autocomplete item from untrusted events
-    //
-
-    // try a down-arrow
-    case 100:
-        doKeyUnprivileged("down");
-        break;
-    case 101:
-        checkPopupOpen(true, -1);
-        checkForm("");
-        break;
-    // try a page-down
-    case 102:
-        doKeyUnprivileged("page_down");
-        break;
-    case 103:
-        checkPopupOpen(true, -1);
-        checkForm("");
-        break;
-    // really adjust the index
-    case 104:
-        // (We're privileged for this test.) Try a privileged down-arrow.
-        doKey("down");
-        break;
-    case 105:
-        checkPopupOpen(true, 0);
-        checkForm("");
-        break;
-    // try a down-arrow
-    case 106:
-        doKeyUnprivileged("down");
-        break;
-    case 107:
-        checkPopupOpen(true, 0);
-        checkForm("");
-        break;
-    // try a page-down
-    case 108:
-        doKeyUnprivileged("page_down");
-        break;
-    case 109:
-        checkPopupOpen(true, 0);
-        checkForm("");
-    // try a keypress
-    case 110:
-        // XXX this causes the popup to close, and makes the value "vaa" (sic)
-        //doKeyUnprivileged('a');
-        break;
-    case 111:
-        checkPopupOpen(true, 0);
-        checkForm("");
-        testNum = 199;
-        break;
-
-    //
-    // Try to use the selected autocomplete item from untrusted events
-    //
-    // try a right-arrow
-    case 200:
-        doKeyUnprivileged("right");
-        break;
-    case 201:
-        checkPopupOpen(true, 0);
-        checkForm("");
-        break;
-    // try a space
-    case 202:
-        doKeyUnprivileged(" ");
-        break;
-    case 203:
-        // XXX we should ignore this input while popup is open?
-        checkPopupOpen(true, 0);
-        checkForm("");
-        break;
-    // backspace
-    case 204:
-        doKeyUnprivileged("back_space");
-        break;
-    case 205:
-        // XXX we should ignore this input while popup is open?
-        checkPopupOpen(true, 0);
-        checkForm("");
-        break;
-    case 206:
-        // (this space intentionally left blank)
-        break;
-    case 207:
-        checkPopupOpen(true, 0);
-        checkForm("");
-        break;
-    // try a return
-    case 208:
-// XXX this seems to cause problems with reliably closing the popup
-//        doKeyUnprivileged("return"); // not "enter"!
-        break;
-    case 209:
-        checkPopupOpen(true, 0);
-        checkForm("");
-        break;
-    // Send a real Escape to ensure popup closed at end of test.
-    case 210:
-        // Need to use doKey(), even though this test is not privileged.
-        doKey("escape");
-        break;
-    case 211:
-        checkPopupOpen(false);
-        checkForm("");
-        is(autocompletePopup.style.direction, "rtl", "direction should have been changed from ltr to rtl");
-
-        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
-        SimpleTest.finish();
-        return;
-
-    default:
-        ok(false, "Unexpected invocation of test #" + testNum);
-        SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
-        SimpleTest.finish();
-        return;
+add_task(function* test_untrusted_events_ignored() {
+  // The autocomplete popup should not open from untrusted events.
+  for (let triggerFn of [
+    () => input.focus(),
+    () => input.click(),
+    () => doClickWithMouseEventUnprivileged(),
+    () => doKeyUnprivileged("down"),
+    () => doKeyUnprivileged("page_down"),
+    () => doKeyUnprivileged("return"),
+    () => doKeyUnprivileged("v"),
+    () => doKeyUnprivileged(" "),
+    () => doKeyUnprivileged("back_space"),
+  ]) {
+    // We must wait for the entire timeout for each individual test, because the
+    // next event in the list might prevent the popup from opening.
+    yield expectPopupDoesNotOpen(triggerFn);
   }
 
-  SimpleTest.executeSoon(runTest);
-}
+  // A privileged key press will actually open the popup.
+  let popupShown = waitForNextPopup();
+  doKey("down");
+  yield popupShown;
+
+  // The selected autocomplete item should not change from untrusted events.
+  for (let triggerFn of [
+    () => doKeyUnprivileged("down"),
+    () => doKeyUnprivileged("page_down"),
+  ]) {
+    triggerFn();
+    yield checkSelectedIndexAfterResponseTime(-1);
+  }
+
+  // A privileged key press will actually change the selected index.
+  let indexChanged = new Promise(resolve => notifySelectedIndex(0, resolve));
+  doKey("down");
+  yield indexChanged;
 
-function startTest() {
-  setupFormHistory(runTest);
-}
+  // The selected autocomplete item should not change and it should not be
+  // possible to use it from untrusted events.
+  for (let triggerFn of [
+    () => doKeyUnprivileged("down"),
+    () => doKeyUnprivileged("page_down"),
+    () => doKeyUnprivileged("right"),
+    () => doKeyUnprivileged(" "),
+    () => doKeyUnprivileged("back_space"),
+    () => doKeyUnprivileged("back_space"),
+    () => doKeyUnprivileged("return"),
+  ]) {
+    triggerFn();
+    yield checkSelectedIndexAfterResponseTime(0);
+    is(input.value, "", "The selected item should not have been used.");
+  }
 
-SimpleTest.waitForFocus(startTest);
-
-SimpleTest.waitForExplicitFinish();
+  // Close the popup.
+  doKey("escape");
+});
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/test/test_popup_direction.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Popup Direction</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.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>
+Test for Popup Direction
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+  <!-- normal, basic form -->
+  <form id="form1" onsubmit="return false;">
+    <input  type="text" name="field1">
+    <button type="submit">Submit</button>
+  </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var resolvePopupShownListener;
+registerPopupShownListener(() => resolvePopupShownListener());
+
+function waitForNextPopup() {
+  return new Promise(resolve => { resolvePopupShownListener = resolve; });
+}
+
+add_task(function* test_popup_direction() {
+  var input = $_(1, "field1");
+
+  yield new Promise(resolve => updateFormHistory([
+    { op : "remove" },
+    { op : "add", fieldname : "field1", value : "value1" },
+    { op : "add", fieldname : "field1", value : "value2" },
+  ], resolve));
+
+  for (let direction of ["ltr", "rtl"]) {
+    document.getElementById("content").style.direction = direction;
+
+    let popupShown = waitForNextPopup();
+    input.focus();
+    doKey("down");
+    yield popupShown;
+
+    let popupState = yield new Promise(resolve => getPopupState(resolve));
+    is(popupState.direction, direction, "Direction should match.");
+
+    // Close the popup.
+    doKey("escape");
+  }
+});
+
+</script>
+</pre>
+</body>
+</html>