Bug 1371131 - Part 3. Create address and credit card result subclasses. r=lchang, steveck draft
authorRay Lin <ralin@mozilla.com>
Mon, 24 Jul 2017 11:50:58 +0800
changeset 614934 51f404fbb834adcb012b468b6cfc6b8cc2963cc3
parent 614119 d5bec265d7e395316db3e5c856c7224abdbcddb2
child 614935 cef3c5ca39b6cd68200360783078a710b5d6bbfd
push id70183
push userbmo:ralin@mozilla.com
push dateTue, 25 Jul 2017 07:22:33 +0000
reviewerslchang, steveck
bugs1371131
milestone56.0a1
Bug 1371131 - Part 3. Create address and credit card result subclasses. r=lchang, steveck MozReview-Commit-ID: 25TNvIQL6ob
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -14,17 +14,19 @@ this.EXPORTED_SYMBOLS = ["FormAutofillCo
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components;
 
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ProfileAutoCompleteResult",
+XPCOMUtils.defineLazyModuleGetter(this, "AddressResult",
+                                  "resource://formautofill/ProfileAutoCompleteResult.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CreditCardResult",
                                   "resource://formautofill/ProfileAutoCompleteResult.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillHandler",
                                   "resource://formautofill/FormAutofillHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
                                   "resource://gre/modules/FormLikeFactory.jsm");
 
 const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"]
                              .getService(Ci.nsIFormFillController);
@@ -114,22 +116,30 @@ AutofillProfileAutoCompleteSearch.protot
       }
       // Sort addresses by timeLastUsed for showing the lastest used address at top.
       records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
 
       let handler = FormAutofillContent.getFormHandler(focusedInput);
       let adaptedRecords = handler.getAdaptedProfiles(records);
 
       let allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput);
-      let result = new ProfileAutoCompleteResult(searchString,
-                                                 info.fieldName,
-                                                 allFieldNames,
-                                                 adaptedRecords,
-                                                 {});
-
+      let result = null;
+      if (collectionName == "addresses") {
+        result = new AddressResult(searchString,
+                                   info.fieldName,
+                                   allFieldNames,
+                                   adaptedRecords,
+                                   {});
+      } else {
+        result = new CreditCardResult(searchString,
+                                      info.fieldName,
+                                      allFieldNames,
+                                      adaptedRecords,
+                                      {});
+      }
       listener.onSearchResult(this, result);
       ProfileAutocomplete.setProfileAutoCompleteResult(result);
     });
   },
 
   /**
    * Stops an asynchronous search that is in progress
    */
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["ProfileAutoCompleteResult"];
+this.EXPORTED_SYMBOLS = ["AddressResult", "CreditCardResult"]; /* exported AddressResult, CreditCardResult */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
@@ -42,24 +42,16 @@ class ProfileAutoCompleteResult {
     } else {
       this.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
     }
 
     // An array of primary and secondary labels for each profile.
     this._popupLabels = this._generateLabels(this._focusedFieldName,
                                              this._allFieldNames,
                                              this._matchingProfiles);
-    // Add an empty result entry for footer. Its content will come from
-    // the footer binding, so don't assign any value to it.
-    this._popupLabels.push({
-      primary: "",
-      secondary: "",
-      categories: FormAutofillUtils.getCategoriesFromFieldNames(allFieldNames),
-      focusedCategory: FormAutofillUtils.getCategoryFromFieldName(focusedFieldName),
-    });
   }
 
   /**
    * @returns {number} The number of results
    */
   get matchCount() {
     return this._popupLabels.length;
   }
@@ -74,16 +66,106 @@ class ProfileAutoCompleteResult {
    * Get the secondary label based on the focused field name and related field names
    * in the same form.
    * @param   {string} focusedFieldName The field name of the focused input
    * @param   {Array<Object>} allFieldNames The field names in the same section
    * @param   {object} profile The profile providing the labels to show.
    * @returns {string} The secondary label
    */
   _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
+    return "";
+  }
+
+  _generateLabels(focusedFieldName, allFieldNames, profiles) {}
+
+  /**
+   * Retrieves a result
+   * @param   {number} index The index of the result requested
+   * @returns {string} The result at the specified index
+   */
+  getValueAt(index) {
+    this._checkIndexBounds(index);
+    return this._popupLabels[index].primary;
+  }
+
+  getLabelAt(index) {
+    this._checkIndexBounds(index);
+    return JSON.stringify(this._popupLabels[index]);
+  }
+
+  /**
+   * Retrieves a comment (metadata instance)
+   * @param   {number} index The index of the comment requested
+   * @returns {string} The comment at the specified index
+   */
+  getCommentAt(index) {
+    this._checkIndexBounds(index);
+    return JSON.stringify(this._matchingProfiles[index]);
+  }
+
+  /**
+   * Retrieves a style hint specific to a particular index.
+   * @param   {number} index The index of the style hint requested
+   * @returns {string} The style hint at the specified index
+   */
+  getStyleAt(index) {
+    this._checkIndexBounds(index);
+    if (index == this.matchCount - 1) {
+      return "autofill-footer";
+    }
+    return "autofill-profile";
+  }
+
+  /**
+   * Retrieves an image url.
+   * @param   {number} index The index of the image url requested
+   * @returns {string} The image url at the specified index
+   */
+  getImageAt(index) {
+    this._checkIndexBounds(index);
+    return "";
+  }
+
+  /**
+   * Retrieves a result
+   * @param   {number} index The index of the result requested
+   * @returns {string} The result at the specified index
+   */
+  getFinalCompleteValueAt(index) {
+    return this.getValueAt(index);
+  }
+
+  /**
+   * Removes a result from the resultset
+   * @param {number} index The index of the result to remove
+   * @param {boolean} removeFromDatabase TRUE for removing data from DataBase
+   *                                     as well.
+   */
+  removeValueAt(index, removeFromDatabase) {
+    // There is no plan to support removing profiles via autocomplete.
+  }
+}
+
+class AddressResult extends ProfileAutoCompleteResult {
+  constructor(...args) {
+    super(...args);
+
+    // Add an empty result entry for footer. Its content will come from
+    // the footer binding, so don't assign any value to it.
+    // The additional properties: categories and focusedCategory are required of
+    // the popup to generate autofill hint on the footer.
+    this._popupLabels.push({
+      primary: "",
+      secondary: "",
+      categories: FormAutofillUtils.getCategoriesFromFieldNames(this._allFieldNames),
+      focusedCategory: FormAutofillUtils.getCategoryFromFieldName(this._focusedFieldName),
+    });
+  }
+
+  _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
     // We group similar fields into the same field name so we won't pick another
     // field in the same group as the secondary label.
     const GROUP_FIELDS = {
       "name": [
         "name",
         "given-name",
         "additional-name",
         "family-name",
@@ -155,75 +237,85 @@ class ProfileAutoCompleteResult {
         secondary: this._getSecondaryLabel(focusedFieldName,
                                            allFieldNames,
                                            profile),
       };
     });
   }
 
 
-  /**
-   * Retrieves a result
-   * @param   {number} index The index of the result requested
-   * @returns {string} The result at the specified index
-   */
-  getValueAt(index) {
-    this._checkIndexBounds(index);
-    return this._popupLabels[index].primary;
-  }
+}
 
-  getLabelAt(index) {
-    this._checkIndexBounds(index);
-    return JSON.stringify(this._popupLabels[index]);
+class CreditCardResult extends ProfileAutoCompleteResult {
+  constructor(...args) {
+    super(...args);
+
+    // Add an empty result entry for footer.
+    this._popupLabels.push({primary: "", secondary: ""});
   }
 
-  /**
-   * Retrieves a comment (metadata instance)
-   * @param   {number} index The index of the comment requested
-   * @returns {string} The comment at the specified index
-   */
-  getCommentAt(index) {
-    this._checkIndexBounds(index);
-    return JSON.stringify(this._matchingProfiles[index]);
+  _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
+    const GROUP_FIELDS = {
+      "cc-name": [
+        "cc-name",
+        "cc-given-name",
+        "cc-additional-name",
+        "cc-family-name",
+      ],
+      "cc-exp": [
+        "cc-exp",
+        "cc-exp-month",
+        "cc-exp-year",
+      ],
+    };
+
+    const secondaryLabelOrder = [
+      "cc-number",       // Credit card number
+      "cc-name",         // Full name
+      "cc-exp",          // Expiration date
+    ];
+
+    for (let field in GROUP_FIELDS) {
+      if (GROUP_FIELDS[field].includes(focusedFieldName)) {
+        focusedFieldName = field;
+        break;
+      }
+    }
+
+    for (const currentFieldName of secondaryLabelOrder) {
+      if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
+        continue;
+      }
+
+      let matching = GROUP_FIELDS[currentFieldName] ?
+        allFieldNames.some(fieldName => GROUP_FIELDS[currentFieldName].includes(fieldName)) :
+        allFieldNames.includes(currentFieldName);
+
+      if (matching) {
+        return profile[currentFieldName];
+      }
+    }
+
+    return ""; // Nothing matched.
   }
 
-  /**
-   * Retrieves a style hint specific to a particular index.
-   * @param   {number} index The index of the style hint requested
-   * @returns {string} The style hint at the specified index
-   */
-  getStyleAt(index) {
-    this._checkIndexBounds(index);
-    if (index == this.matchCount - 1) {
-      return "autofill-footer";
-    }
-    return "autofill-profile";
+  _generateLabels(focusedFieldName, allFieldNames, profiles) {
+    // Skip results without a primary label.
+    return profiles.filter(profile => {
+      return !!profile[focusedFieldName];
+    }).map(profile => {
+      return {
+        primary: profile[focusedFieldName],
+        secondary: this._getSecondaryLabel(focusedFieldName,
+                                           allFieldNames,
+                                           profile),
+      };
+    });
   }
 
-  /**
-   * Retrieves an image url.
-   * @param   {number} index The index of the image url requested
-   * @returns {string} The image url at the specified index
-   */
-  getImageAt(index) {
+  // Always return empty string for credit card result. Since the decryption might
+  // be required of users' input, we have to to suppress AutoCompleteController
+  // from filling encrypted data directly.
+  getValueAt(index) {
     this._checkIndexBounds(index);
     return "";
   }
-
-  /**
-   * Retrieves a result
-   * @param   {number} index The index of the result requested
-   * @returns {string} The result at the specified index
-   */
-  getFinalCompleteValueAt(index) {
-    return this.getValueAt(index);
-  }
-
-  /**
-   * Removes a result from the resultset
-   * @param {number} index The index of the result to remove
-   * @param {boolean} removeFromDatabase TRUE for removing data from DataBase
-   *                                     as well.
-   */
-  removeValueAt(index, removeFromDatabase) {
-    // There is no plan to support removing profiles via autocomplete.
-  }
 }
--- a/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
+++ b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
@@ -1,10 +1,11 @@
 "use strict";
 
+/* global AddressResult, CreditCardResult */
 Cu.import("resource://formautofill/ProfileAutoCompleteResult.jsm");
 
 let matchingProfiles = [{
   guid: "test-guid-1",
   "given-name": "Timothy",
   "family-name": "Berners-Lee",
   name: "Timothy Berners-Lee",
   organization: "Sesame Street",
@@ -37,17 +38,17 @@ let allFieldNames = [
   "street-address",
   "address-line1",
   "address-line2",
   "address-line3",
   "organization",
   "tel",
 ];
 
-let testCases = [{
+let addressTestCases = [{
   description: "Focus on an `organization` field",
   options: {},
   matchingProfiles,
   allFieldNames,
   searchString: "",
   fieldName: "organization",
   expected: {
     searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
@@ -210,47 +211,178 @@ let testCases = [{
   fieldName: "",
   expected: {
     searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE,
     defaultIndex: 0,
     items: [],
   },
 }];
 
-add_task(async function test_all_patterns() {
-  testCases.forEach(testCase => {
-    do_print("Starting testcase: " + testCase.description);
-    let actual = new ProfileAutoCompleteResult(testCase.searchString,
-                                               testCase.fieldName,
-                                               testCase.allFieldNames,
-                                               testCase.matchingProfiles,
-                                               testCase.options);
-    let expectedValue = testCase.expected;
-    let expectedItemLength = expectedValue.items.length;
-    // If the last item shows up as a footer, we expect one more item
-    // than expected.
-    if (actual.getStyleAt(actual.matchCount - 1) == "autofill-footer") {
-      expectedItemLength++;
-    }
+matchingProfiles = [{
+  guid: "test-guid-1",
+  "cc-name": "Timothy Berners-Lee",
+  "cc-number": "************6785",
+  "cc-exp-month": 12,
+  "cc-exp-year": 2014,
+}, {
+  guid: "test-guid-2",
+  "cc-name": "John Doe",
+  "cc-number": "************1234",
+  "cc-exp-month": 4,
+  "cc-exp-year": 2014,
+}, {
+  guid: "test-guid-3",
+  "cc-number": "************5678",
+  "cc-exp-month": 8,
+  "cc-exp-year": 2018,
+}];
+
+allFieldNames = [
+  "cc-name",
+  "cc-number",
+  "cc-exp-month",
+  "cc-exp-year",
+];
 
-    equal(actual.searchResult, expectedValue.searchResult);
-    equal(actual.defaultIndex, expectedValue.defaultIndex);
-    equal(actual.matchCount, expectedItemLength);
-    expectedValue.items.forEach((item, index) => {
-      equal(actual.getValueAt(index), item.value);
-      equal(actual.getCommentAt(index), item.comment);
-      equal(actual.getLabelAt(index), item.label);
-      equal(actual.getStyleAt(index), item.style);
-      equal(actual.getImageAt(index), item.image);
-    });
+let creditCardTestCases = [{
+  description: "Focus on a `cc-name` field",
+  options: {},
+  matchingProfiles,
+  allFieldNames,
+  searchString: "",
+  fieldName: "cc-name",
+  expected: {
+    searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
+    defaultIndex: 0,
+    items: [{
+      value: "",
+      style: "autofill-profile",
+      comment: JSON.stringify(matchingProfiles[0]),
+      label: JSON.stringify({
+        primary: "Timothy Berners-Lee",
+        secondary: "************6785",
+      }),
+      image: "",
+    }, {
+      value: "",
+      style: "autofill-profile",
+      comment: JSON.stringify(matchingProfiles[1]),
+      label: JSON.stringify({
+        primary: "John Doe",
+        secondary: "************1234",
+      }),
+      image: "",
+    }],
+  },
+}, {
+  description: "Focus on a `cc-number` field",
+  options: {},
+  matchingProfiles,
+  allFieldNames,
+  searchString: "",
+  fieldName: "cc-number",
+  expected: {
+    searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
+    defaultIndex: 0,
+    items: [{
+      value: "",
+      style: "autofill-profile",
+      comment: JSON.stringify(matchingProfiles[0]),
+      label: JSON.stringify({
+        primary: "************6785",
+        secondary: "Timothy Berners-Lee",
+      }),
+      image: "",
+    }, {
+      value: "",
+      style: "autofill-profile",
+      comment: JSON.stringify(matchingProfiles[1]),
+      label: JSON.stringify({
+        primary: "************1234",
+        secondary: "John Doe",
+      }),
+      image: "",
+    }, {
+      value: "",
+      style: "autofill-profile",
+      comment: JSON.stringify(matchingProfiles[2]),
+      label: JSON.stringify({
+        primary: "************5678",
+        secondary: "",
+      }),
+      image: "",
+    }],
+  },
+}, {
+  description: "No matching profiles",
+  options: {},
+  matchingProfiles: [],
+  allFieldNames,
+  searchString: "",
+  fieldName: "",
+  expected: {
+    searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
+    defaultIndex: 0,
+    items: [],
+  },
+}, {
+  description: "Search with failure",
+  options: {resultCode: Ci.nsIAutoCompleteResult.RESULT_FAILURE},
+  matchingProfiles: [],
+  allFieldNames,
+  searchString: "",
+  fieldName: "",
+  expected: {
+    searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE,
+    defaultIndex: 0,
+    items: [],
+  },
+}];
 
-    if (expectedValue.items.length != 0) {
-      Assert.throws(() => actual.getValueAt(expectedItemLength),
-        /Index out of range\./);
+let testSets = [{
+  collectionConstructor: AddressResult,
+  testCases: addressTestCases,
+}, {
+  collectionConstructor: CreditCardResult,
+  testCases: creditCardTestCases,
+}];
 
-      Assert.throws(() => actual.getLabelAt(expectedItemLength),
-        /Index out of range\./);
+add_task(async function test_all_patterns() {
+  testSets.forEach(({collectionConstructor, testCases}) => {
+    testCases.forEach(testCase => {
+      do_print("Starting testcase: " + testCase.description);
+      let actual = new collectionConstructor(testCase.searchString,
+                                             testCase.fieldName,
+                                             testCase.allFieldNames,
+                                             testCase.matchingProfiles,
+                                             testCase.options);
+      let expectedValue = testCase.expected;
+      let expectedItemLength = expectedValue.items.length;
+      // If the last item shows up as a footer, we expect one more item
+      // than expected.
+      if (actual.getStyleAt(actual.matchCount - 1) == "autofill-footer") {
+        expectedItemLength++;
+      }
 
-      Assert.throws(() => actual.getCommentAt(expectedItemLength),
-        /Index out of range\./);
-    }
+      equal(actual.searchResult, expectedValue.searchResult);
+      equal(actual.defaultIndex, expectedValue.defaultIndex);
+      equal(actual.matchCount, expectedItemLength);
+      expectedValue.items.forEach((item, index) => {
+        equal(actual.getValueAt(index), item.value);
+        equal(actual.getCommentAt(index), item.comment);
+        equal(actual.getLabelAt(index), item.label);
+        equal(actual.getStyleAt(index), item.style);
+        equal(actual.getImageAt(index), item.image);
+      });
+
+      if (expectedValue.items.length != 0) {
+        Assert.throws(() => actual.getValueAt(expectedItemLength),
+          /Index out of range\./);
+
+        Assert.throws(() => actual.getLabelAt(expectedItemLength),
+          /Index out of range\./);
+
+        Assert.throws(() => actual.getCommentAt(expectedItemLength),
+          /Index out of range\./);
+      }
+    });
   });
 });