Bug 1380279 - Add an ability to retrieve multiple collection type in startSearch to support credit cards autofilling. r=seanlee draft
authorRay Lin <ralin@mozilla.com>
Thu, 20 Jul 2017 12:13:37 +0800
changeset 611885 ba476aeddc873bba2956487f5ad29784d5cd5701
parent 611714 eb1d92b2b6a4161492561250f51bae5bafeda68a
child 638251 50b40cd2381f379e32414afbd7f55b3c47d0bb53
push id69320
push userbmo:ralin@mozilla.com
push dateThu, 20 Jul 2017 04:18:12 +0000
reviewersseanlee
bugs1380279
milestone56.0a1
Bug 1380279 - Add an ability to retrieve multiple collection type in startSearch to support credit cards autofilling. r=seanlee MozReview-Commit-ID: EeV3W8fZylh
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/content/manageProfiles.js
browser/extensions/formautofill/test/browser/browser_manageProfilesDialog.js
browser/extensions/formautofill/test/browser/head.js
browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
browser/extensions/formautofill/test/unit/test_getRecords.js
browser/extensions/formautofill/test/unit/xpcshell.ini
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -100,68 +100,73 @@ AutofillProfileAutoCompleteSearch.protot
         onSearchResult: (search, result) => {
           listener.onSearchResult(this, result);
           ProfileAutocomplete.setProfileAutoCompleteResult(result);
         },
       });
       return;
     }
 
-    this._getAddresses({info, searchString}).then((addresses) => {
+    let collectionName = FormAutofillUtils.isAddressField(info.fieldName) ?
+      "addresses" : "creditCards";
+
+    this._getRecords({collectionName, info, searchString}).then((records) => {
       if (this.forceStop) {
         return;
       }
       // Sort addresses by timeLastUsed for showing the lastest used address at top.
-      addresses.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
+      records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
 
       let handler = FormAutofillContent.getFormHandler(focusedInput);
-      let adaptedAddresses = handler.getAdaptedProfiles(addresses);
+      let adaptedRecords = handler.getAdaptedProfiles(records);
 
       let allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput);
       let result = new ProfileAutoCompleteResult(searchString,
                                                  info.fieldName,
                                                  allFieldNames,
-                                                 adaptedAddresses,
+                                                 adaptedRecords,
                                                  {});
 
       listener.onSearchResult(this, result);
       ProfileAutocomplete.setProfileAutoCompleteResult(result);
     });
   },
 
   /**
    * Stops an asynchronous search that is in progress
    */
   stopSearch() {
     ProfileAutocomplete.setProfileAutoCompleteResult(null);
     this.forceStop = true;
   },
 
   /**
-   * Get the address data from parent process for AutoComplete result.
+   * Get the records from parent process for AutoComplete result.
    *
    * @private
    * @param  {Object} data
    *         Parameters for querying the corresponding result.
+   * @param  {string} data.collectionName
+   *         The name used to specify which collection to retrieve records.
    * @param  {string} data.searchString
-   *         The typed string for filtering out the matched address.
+   *         The typed string for filtering out the matched records.
    * @param  {string} data.info
    *         The input autocomplete property's information.
    * @returns {Promise}
    *          Promise that resolves when addresses returned from parent process.
    */
-  _getAddresses(data) {
-    this.log.debug("_getAddresses with data:", data);
+  _getRecords(data) {
+    this.log.debug("_getRecords with data:", data);
     return new Promise((resolve) => {
-      Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) {
-        Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult);
+      Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) {
+        Services.cpmm.removeMessageListener("FormAutofill:Records", getResult);
         resolve(result.data);
       });
 
-      Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", data);
+      Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data);
     });
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AutofillProfileAutoCompleteSearch]);
 
 let ProfileAutocomplete = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -75,17 +75,17 @@ FormAutofillParent.prototype = {
   _active: null,
 
   /**
    * Initializes ProfileStorage and registers the message handler.
    */
   async init() {
     Services.obs.addObserver(this, "advanced-pane-loaded");
     Services.ppmm.addMessageListener("FormAutofill:InitStorage", this);
-    Services.ppmm.addMessageListener("FormAutofill:GetAddresses", this);
+    Services.ppmm.addMessageListener("FormAutofill:GetRecords", this);
     Services.ppmm.addMessageListener("FormAutofill:SaveAddress", this);
     Services.ppmm.addMessageListener("FormAutofill:RemoveAddresses", this);
     Services.ppmm.addMessageListener("FormAutofill:OpenPreferences", this);
     Services.mm.addMessageListener("FormAutofill:OnFormSubmit", this);
 
     // Observing the pref and storage changes
     Services.prefs.addObserver(ENABLED_PREF, this);
     Services.obs.addObserver(this, "formautofill-storage-changed");
@@ -176,18 +176,18 @@ FormAutofillParent.prototype = {
    * @param   {nsIFrameMessageManager} message.target Caller's message manager.
    */
   receiveMessage({name, data, target}) {
     switch (name) {
       case "FormAutofill:InitStorage": {
         this.profileStorage.initialize();
         break;
       }
-      case "FormAutofill:GetAddresses": {
-        this._getAddresses(data, target);
+      case "FormAutofill:GetRecords": {
+        this._getRecords(data, target);
         break;
       }
       case "FormAutofill:SaveAddress": {
         if (data.guid) {
           this.profileStorage.addresses.update(data.guid, data.address);
         } else {
           this.profileStorage.addresses.add(data.address);
         }
@@ -212,61 +212,68 @@ FormAutofillParent.prototype = {
    * Uninitializes FormAutofillParent. This is for testing only.
    *
    * @private
    */
   _uninit() {
     this.profileStorage._saveImmediately();
 
     Services.ppmm.removeMessageListener("FormAutofill:InitStorage", this);
-    Services.ppmm.removeMessageListener("FormAutofill:GetAddresses", this);
+    Services.ppmm.removeMessageListener("FormAutofill:GetRecords", this);
     Services.ppmm.removeMessageListener("FormAutofill:SaveAddress", this);
     Services.ppmm.removeMessageListener("FormAutofill:RemoveAddresses", this);
     Services.obs.removeObserver(this, "advanced-pane-loaded");
     Services.prefs.removeObserver(ENABLED_PREF, this);
   },
 
   /**
-   * Get the address data from profile store and return addresses back to content
+   * Get the records from profile store and return results back to content
    * process.
    *
    * @private
+   * @param  {string} data.collectionName
+   *         The name used to specify which collection to retrieve records.
    * @param  {string} data.searchString
-   *         The typed string for filtering out the matched address.
+   *         The typed string for filtering out the matched records.
    * @param  {string} data.info
    *         The input autocomplete property's information.
    * @param  {nsIFrameMessageManager} target
    *         Content's message manager.
    */
-  _getAddresses({searchString, info}, target) {
-    let addresses = [];
+  _getRecords({collectionName, searchString, info}, target) {
+    let records;
+    let collection = this.profileStorage[collectionName];
 
-    if (info && info.fieldName) {
-      addresses = this.profileStorage.addresses.getByFilter({searchString, info});
+    if (!collection) {
+      records = [];
+    } else if (info && info.fieldName) {
+      records = collection.getByFilter({searchString, info});
     } else {
-      addresses = this.profileStorage.addresses.getAll();
+      records = collection.getAll();
     }
 
-    target.sendAsyncMessage("FormAutofill:Addresses", addresses);
+    target.sendAsyncMessage("FormAutofill:Records", records);
   },
 
   _updateSavedFieldNames() {
     log.debug("_updateSavedFieldNames");
     if (!Services.ppmm.initialProcessData.autofillSavedFieldNames) {
       Services.ppmm.initialProcessData.autofillSavedFieldNames = new Set();
     } else {
       Services.ppmm.initialProcessData.autofillSavedFieldNames.clear();
     }
 
-    this.profileStorage.addresses.getAll().forEach((address) => {
-      Object.keys(address).forEach((fieldName) => {
-        if (!address[fieldName]) {
-          return;
-        }
-        Services.ppmm.initialProcessData.autofillSavedFieldNames.add(fieldName);
+    ["addresses", "creditCards"].forEach(c => {
+      this.profileStorage[c].getAll().forEach((record) => {
+        Object.keys(record).forEach((fieldName) => {
+          if (!record[fieldName]) {
+            return;
+          }
+          Services.ppmm.initialProcessData.autofillSavedFieldNames.add(fieldName);
+        });
       });
     });
 
     // Remove the internal guid and metadata fields.
     this.profileStorage.INTERNAL_FIELDS.forEach((fieldName) => {
       Services.ppmm.initialProcessData.autofillSavedFieldNames.delete(fieldName);
     });
 
--- a/browser/extensions/formautofill/content/manageProfiles.js
+++ b/browser/extensions/formautofill/content/manageProfiles.js
@@ -58,37 +58,47 @@ ManageProfileDialog.prototype = {
   },
 
   /**
    * Load addresses and render them.
    *
    * @returns {promise}
    */
   loadAddresses() {
-    return this.getAddresses().then(addresses => {
+    return this.getRecords({collectionName: "addresses"}).then(addresses => {
       log.debug("addresses:", addresses);
       // Sort by last modified time starting with most recent
       addresses.sort((a, b) => b.timeLastModified - a.timeLastModified);
       this.renderAddressElements(addresses);
       this.updateButtonsStates(this._selectedOptions.length);
     });
   },
 
   /**
-   * Get addresses from storage.
+   * Get records from storage.
    *
-   * @returns {promise}
+   * @private
+   * @param  {Object} data
+   *         Parameters for querying the corresponding result.
+   * @param  {string} data.collectionName
+   *         The name used to specify which collection to retrieve records.
+   * @param  {string} data.searchString
+   *         The typed string for filtering out the matched records.
+   * @param  {string} data.info
+   *         The input autocomplete property's information.
+   * @returns {Promise}
+   *          Promise that resolves when addresses returned from parent process.
    */
-  getAddresses() {
+  getRecords(data) {
     return new Promise(resolve => {
-      Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) {
-        Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult);
+      Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) {
+        Services.cpmm.removeMessageListener("FormAutofill:Records", getResult);
         resolve(result.data);
       });
-      Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {});
+      Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data);
     });
   },
 
   /**
    * Render the addresses onto the page while maintaining selected options if
    * they still exist.
    *
    * @param  {array<object>} addresses
--- a/browser/extensions/formautofill/test/browser/browser_manageProfilesDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_manageProfilesDialog.js
@@ -4,20 +4,20 @@ const TEST_SELECTORS = {
   selAddresses: "#profiles",
   btnRemove: "#remove",
   btnAdd: "#add",
   btnEdit: "#edit",
 };
 
 const DIALOG_SIZE = "width=600,height=400";
 
-function waitForAddresses() {
+function waitForRecords() {
   return new Promise(resolve => {
-    Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) {
-      Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult);
+    Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) {
+      Services.cpmm.removeMessageListener("FormAutofill:Records", getResult);
       // Wait for the next tick for elements to get rendered.
       SimpleTest.executeSoon(resolve.bind(null, result.data));
     });
   });
 }
 
 add_task(async function test_manageProfilesInitialState() {
   await BrowserTestUtils.withNewTab({gBrowser, url: MANAGE_PROFILES_DIALOG_URL}, async function(browser) {
@@ -49,50 +49,50 @@ add_task(async function test_cancelManag
 });
 
 add_task(async function test_removingSingleAndMultipleProfiles() {
   await saveAddress(TEST_ADDRESS_1);
   await saveAddress(TEST_ADDRESS_2);
   await saveAddress(TEST_ADDRESS_3);
 
   let win = window.openDialog(MANAGE_PROFILES_DIALOG_URL, null, DIALOG_SIZE);
-  await waitForAddresses();
+  await waitForRecords();
 
   let selAddresses = win.document.querySelector(TEST_SELECTORS.selAddresses);
   let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
   let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
 
   is(selAddresses.length, 3, "Three addresses");
 
   EventUtils.synthesizeMouseAtCenter(selAddresses.children[0], {}, win);
   is(btnRemove.disabled, false, "Remove button enabled");
   is(btnEdit.disabled, false, "Edit button enabled");
   EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
-  await waitForAddresses();
+  await waitForRecords();
   is(selAddresses.length, 2, "Two addresses left");
 
   EventUtils.synthesizeMouseAtCenter(selAddresses.children[0], {}, win);
   EventUtils.synthesizeMouseAtCenter(selAddresses.children[1],
                                      {shiftKey: true}, win);
   is(btnEdit.disabled, true, "Edit button disabled when multi-select");
 
   EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
-  await waitForAddresses();
+  await waitForRecords();
   is(selAddresses.length, 0, "All addresses are removed");
 
   win.close();
 });
 
 add_task(async function test_profilesDialogWatchesStorageChanges() {
   let win = window.openDialog(MANAGE_PROFILES_DIALOG_URL, null, DIALOG_SIZE);
-  await waitForAddresses();
+  await waitForRecords();
 
   let selAddresses = win.document.querySelector(TEST_SELECTORS.selAddresses);
 
   await saveAddress(TEST_ADDRESS_1);
-  let addresses = await waitForAddresses();
+  let addresses = await waitForRecords();
   is(selAddresses.length, 1, "One address is shown");
 
   await removeAddresses([addresses[0].guid]);
-  await waitForAddresses();
+  await waitForRecords();
   is(selAddresses.length, 0, "Address is removed");
   win.close();
 });
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -59,26 +59,30 @@ async function openPopupOn(browser, sele
   await ContentTask.spawn(browser, {selector}, async function({selector}) {
     content.document.querySelector(selector).focus();
   });
   await sleep(2000);
   await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
   await expectPopupOpen(browser);
 }
 
-function getAddresses() {
+function getRecords(data) {
   return new Promise(resolve => {
-    Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) {
-      Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult);
+    Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) {
+      Services.cpmm.removeMessageListener("FormAutofill:Records", getResult);
       resolve(result.data);
     });
-    Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {});
+    Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data);
   });
 }
 
+function getAddresses() {
+  return getRecords({collectionName: "addresses"});
+}
+
 function saveAddress(address) {
   Services.cpmm.sendAsyncMessage("FormAutofill:SaveAddress", {address});
   return TestUtils.topicObserved("formautofill-storage-changed");
 }
 
 function removeAddresses(guids) {
   Services.cpmm.sendAsyncMessage("FormAutofill:RemoveAddresses", {guids});
   return TestUtils.topicObserved("formautofill-storage-changed");
--- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -6,25 +6,25 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 let {profileStorage} = Cu.import("resource://formautofill/ProfileStorage.jsm", {});
 
 var ParentUtils = {
   cleanUpAddress() {
-    Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) {
-      Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult);
+    Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) {
+      Services.cpmm.removeMessageListener("FormAutofill:Records", getResult);
 
       let addresses = result.data;
       Services.cpmm.sendAsyncMessage("FormAutofill:RemoveAddresses",
                                      {guids: addresses.map(address => address.guid)});
     });
 
-    Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {searchString: ""});
+    Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", {searchString: "", collectionName: "addresses"});
   },
 
   updateAddress(type, chromeMsg, msgData, contentMsg) {
     Services.cpmm.sendAsyncMessage(chromeMsg, msgData);
     Services.obs.addObserver(function observer(subject, topic, data) {
       if (data != type) {
         return;
       }
@@ -55,18 +55,18 @@ var ParentUtils = {
       if (field in addressA && field in addressB && (addressA[field] !== addressB[field])) {
         return false;
       }
     }
     return true;
   },
 
   checkAddresses({expectedAddresses}) {
-    Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) {
-      Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult);
+    Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) {
+      Services.cpmm.removeMessageListener("FormAutofill:Records", getResult);
       let addresses = result.data;
       if (addresses.length !== expectedAddresses.length) {
         sendAsyncMessage("FormAutofillTest:areAddressesMatching", false);
         return;
       }
 
       for (let address of addresses) {
         let matching = expectedAddresses.some((expectedAddress) => {
@@ -77,17 +77,17 @@ var ParentUtils = {
           sendAsyncMessage("FormAutofillTest:areAddressesMatching", false);
           return;
         }
       }
 
       sendAsyncMessage("FormAutofillTest:areAddressesMatching", true);
     });
 
-    Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {searchString: ""});
+    Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", {searchString: "", collectionName: "addresses"});
   },
 };
 
 Services.obs.addObserver(ParentUtils, "formautofill-storage-changed");
 
 addMessageListener("FormAutofillTest:AddAddress", (msg) => {
   ParentUtils.updateAddress("add", "FormAutofill:SaveAddress", msg, "FormAutofillTest:AddressAdded");
 });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getRecords.js
@@ -0,0 +1,50 @@
+/*
+ * Test for make sure getRecords can retrieve right collection from storag.
+ */
+
+"use strict";
+
+Cu.import("resource://formautofill/FormAutofillParent.jsm");
+Cu.import("resource://formautofill/ProfileStorage.jsm");
+
+add_task(async function test_getRecords() {
+  let formAutofillParent = new FormAutofillParent();
+
+  await formAutofillParent.init();
+  await formAutofillParent.profileStorage.initialize();
+
+  let fakeResult = {
+    addresses: [{
+      "given-name": "Timothy",
+      "additional-name": "John",
+      "family-name": "Berners-Lee",
+      "organization": "World Wide Web Consortium",
+    }],
+    creditCards: [{
+      "cc-name": "John Doe",
+      "cc-number": "1234567812345678",
+      "cc-exp-month": 4,
+      "cc-exp-year": 2017,
+    }],
+  };
+
+  ["addresses", "creditCards", "nonExisting"].forEach(collectionName => {
+    let collection = profileStorage[collectionName];
+    let expectedResult = fakeResult[collectionName] || [];
+    let target = {
+      sendAsyncMessage: function sendAsyncMessage(msg, payload) {},
+    };
+    let mock = sinon.mock(target);
+    mock.expects("sendAsyncMessage").once().withExactArgs("FormAutofill:Records", expectedResult);
+
+    if (collection) {
+      sinon.stub(collection, "getAll");
+      collection.getAll.returns(expectedResult);
+    }
+    formAutofillParent._getRecords({collectionName}, target);
+    mock.verify();
+    if (collection) {
+      do_check_eq(collection.getAll.called, true);
+    }
+  });
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -22,16 +22,17 @@ support-files =
 [test_collectFormFields.js]
 [test_creditCardRecords.js]
 [test_extractLabelStrings.js]
 [test_findLabelElements.js]
 [test_getAdaptedProfiles.js]
 [test_getCategoriesFromFieldNames.js]
 [test_getFormInputDetails.js]
 [test_getInfo.js]
+[test_getRecords.js]
 [test_isCJKName.js]
 [test_isFieldEligibleForAutofill.js]
 [test_markAsAutofillField.js]
 [test_migrateRecords.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
 [test_profileAutocompleteResult.js]
 [test_phoneNumber.js]