Bug 1373130 - Send message after identifyAutofillFields being invoked to indicate that formautofill is ready to open popup. r=lchang draft
authorRay Lin <ralin@mozilla.com>
Mon, 16 Oct 2017 18:10:02 +0800
changeset 680807 f66ac796307ce042389b7f29aa78672e028da19c
parent 680726 89ab122de8a0a3057e2f91fa65d9e1e446e87ddc
child 735974 23475062ae391a48efd4c07d371c81d18e42aa98
push id84635
push userbmo:ralin@mozilla.com
push dateMon, 16 Oct 2017 10:35:22 +0000
reviewerslchang
bugs1373130
milestone58.0a1
Bug 1373130 - Send message after identifyAutofillFields being invoked to indicate that formautofill is ready to open popup. r=lchang MozReview-Commit-ID: 4dZzgYNahEq
browser/extensions/formautofill/content/FormAutofillFrameScript.js
browser/extensions/formautofill/test/browser/head.js
browser/extensions/formautofill/test/mochitest/formautofill_common.js
browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
browser/extensions/formautofill/test/mochitest/test_on_address_submission.html
--- a/browser/extensions/formautofill/content/FormAutofillFrameScript.js
+++ b/browser/extensions/formautofill/content/FormAutofillFrameScript.js
@@ -31,16 +31,19 @@ var FormAutofillFrameScript = {
       return;
     }
     this._hasPendingTask = true;
 
     setTimeout(() => {
       FormAutofillContent.identifyAutofillFields(this._nextHandleElement);
       this._hasPendingTask = false;
       this._nextHandleElement = null;
+      // This is for testing purpose only which sends a message to indicate that the
+      // form has been identified, and ready to open popup.
+      sendAsyncMessage("FormAutofill:FieldsIdentified");
     });
   },
 
   init() {
     addEventListener("focusin", this);
     addMessageListener("FormAutofill:PreviewProfile", this);
     addMessageListener("FormAutoComplete:PopupClosed", this);
     addMessageListener("FormAutoComplete:PopupOpened", this);
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -109,34 +109,46 @@ async function expectPopupOpen(browser) 
       return (item.getAttribute("originaltype") == "autofill-profile" ||
              item.getAttribute("originaltype") == "insecureWarning" ||
              item.getAttribute("originaltype") == "autofill-footer") &&
              item.hasAttribute("formautofillattached");
     });
   }, "The popup should be a form autofill one");
 }
 
+async function waitForFieldsIdentified() {
+  return new Promise(resolve => {
+    Services.mm.addMessageListener("FormAutofill:FieldsIdentified", function onIdentified() {
+      Services.mm.removeMessageListener("FormAutofill:FieldsIdentified", onIdentified);
+      resolve();
+    });
+  });
+}
+
 async function openPopupOn(browser, selector) {
   await SimpleTest.promiseFocus(browser);
-  /* eslint no-shadow: ["error", { "allow": ["selector"] }] */
-  const identified = await ContentTask.spawn(browser, {selector}, async function({selector}) {
+  /* eslint no-shadow: ["error", { "allow": ["selector", "focused", "identified"] }] */
+  const {focused, identified} = await ContentTask.spawn(browser, {selector}, async function({selector}) {
     const input = content.document.querySelector(selector);
     const forms = content.document.getElementsByTagName("form");
     const rootElement = [...forms].find(form => form.contains(input)) || content.document.body;
 
     input.focus();
-    if (rootElement.hasAttribute("test-formautofill-identified")) {
-      return true;
-    }
+    const focused = content.document.activeElement == input;
+    const identified = rootElement.hasAttribute("test-formautofill-identified");
+
     rootElement.setAttribute("test-formautofill-identified", "true");
-    return false;
+    return {focused, identified};
   });
-  // Wait 2 seconds for identifyAutofillFields if the form hasn't been identified yet.
+  // Wait 200ms for identifyAutofillFields if the form hasn't been identified yet.
   if (!identified) {
-    await sleep(2000);
+    if (!focused) {
+      await waitForFieldsIdentified();
+    }
+    await sleep(200);
   }
   await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
   await expectPopupOpen(browser);
 }
 
 async function expectPopupClose(browser) {
   await BrowserTestUtils.waitForCondition(() => !browser.autoCompletePopup.popupOpen,
     "popup should have closed");
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -9,28 +9,40 @@ let expectingPopup = null;
 
 const {FormAutofillUtils} = SpecialPowers.Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 async function sleep(ms = 500, reason = "Intentionally wait for UI ready") {
   SimpleTest.requestFlakyTimeout(reason);
   await new Promise(resolve => setTimeout(resolve, ms));
 }
 
+async function waitForFieldsIdentified() {
+  return new Promise(resolve => {
+    formFillChromeScript.addMessageListener("FormAutofillTest:FieldsIdentified", function onIdentified() {
+      formFillChromeScript.removeMessageListener("FormAutofillTest:FieldsIdentified", onIdentified);
+      resolve();
+    });
+  });
+}
+
 async function setInput(selector, value) {
   let input = document.querySelector("input" + selector);
   input.value = value;
   input.focus();
 
+  if (document.activeElement != input) {
+    await waitForFieldsIdentified();
+  }
   // "identifyAutofillFields" is invoked asynchronously in "focusin" event. We
   // should make sure fields are ready for popup before doing tests.
   //
   // TODO: "sleep" is used here temporarily because there's no event to
   //       notify us of the state of "identifyAutofillFields" for now. We should
   //       figure out a better way after the heuristics land.
-  await sleep(500, "Guarantee asynchronous identifyAutofillFields is invoked");
+  await sleep(200, "Guarantee asynchronous identifyAutofillFields is invoked");
   return input;
 }
 
 function clickOnElement(selector) {
   let element = document.querySelector(selector);
 
   if (!element) {
     throw new Error("Can not find the element");
--- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -164,16 +164,20 @@ var ParentUtils = {
   observe(subject, topic, data) {
     assert.ok(topic === "formautofill-storage-changed");
     sendAsyncMessage("formautofill-storage-changed", {subject: null, topic, data});
   },
 };
 
 Services.obs.addObserver(ParentUtils, "formautofill-storage-changed");
 
+Services.mm.addMessageListener("FormAutofill:FieldsIdentified", () => {
+  sendAsyncMessage("FormAutofillTest:FieldsIdentified");
+});
+
 addMessageListener("FormAutofillTest:AddAddress", (msg) => {
   ParentUtils.operateAddress("add", msg, "FormAutofillTest:AddressAdded");
 });
 
 addMessageListener("FormAutofillTest:RemoveAddress", (msg) => {
   ParentUtils.operateAddress("remove", msg, "FormAutofillTest:AddressRemoved");
 });
 
--- a/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html
+++ b/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html
@@ -26,44 +26,46 @@ let TEST_ADDRESSES = [{
 }, {
   organization: "Mozilla",
   "street-address": "331 E. Evelyn Avenue",
   tel: "+16509030800",
 }];
 
 initPopupListener();
 
+async function fillMockProfilesSequentially(profile) {
+  return Object.entries(profile)
+    .map(([key, value]) => setInput("#" + key, value))
+    .reduce((p, c) => p.then(c), Promise.resolve());
+}
+
 // Submit first address for saving.
 add_task(async function check_storage_after_form_submitted() {
   // We already verified the first time use case in browser test
   await SpecialPowers.pushPrefEnv({
     set: [["extensions.formautofill.firstTimeUse", false]],
   });
 
-  for (let key in TEST_ADDRESSES[0]) {
-    await setInput("#" + key, TEST_ADDRESSES[0][key]);
-  }
+  await fillMockProfilesSequentially(TEST_ADDRESSES[0]);
 
   clickOnElement("input[type=submit]");
 
   let expectedAddresses = TEST_ADDRESSES.slice(0, 1);
   await onStorageChanged("add");
   // Check if timesUsed is set correctly
   expectedAddresses[0].timesUsed = 1;
   let matching = await checkAddresses(expectedAddresses);
   ok(matching, "Address saved as expected");
   delete expectedAddresses[0].timesUsed;
 });
 
 // Submit another new address.
 add_task(async function check_storage_after_another_address_submitted() {
   document.querySelector("form").reset();
-  for (let key in TEST_ADDRESSES[1]) {
-    await setInput("#" + key, TEST_ADDRESSES[1][key]);
-  }
+  await fillMockProfilesSequentially(TEST_ADDRESSES[1]);
 
   clickOnElement("input[type=submit]");
 
   // The 2nd test address should be on the top since it's the last used one.
   let addressesInMenu = TEST_ADDRESSES.slice(1);
   addressesInMenu.push(TEST_ADDRESSES[0]);
 
   // let expectedAddresses = TEST_ADDRESSES.slice(0);
@@ -77,19 +79,17 @@ add_task(async function check_storage_af
   checkMenuEntries(addressesInMenu.map(address =>
     JSON.stringify({primary: address.organization, secondary: address["street-address"]})
   ));
 });
 
 // Submit another new address that is mergeable.
 add_task(async function new_address_submitted_and_merged() {
   document.querySelector("form").reset();
-  for (let key in TEST_ADDRESSES[0]) {
-    await setInput("#" + key, TEST_ADDRESSES[0][key]);
-  }
+  await fillMockProfilesSequentially(TEST_ADDRESSES[0]);
   // Add country to first address in storage
   await setInput("#country", "US");
   TEST_ADDRESSES[0].country = "US";
   clickOnElement("input[type=submit]");
 
   let expectedAddresses = TEST_ADDRESSES.slice(0);
   // Check if timesUsed is set correctly
   expectedAddresses[0].timesUsed = 2;