Bug 1371131 - Part 3. Create address and credit card result subclasses. r=lchang,steveck
authorRay Lin <ralin@mozilla.com>
Mon, 24 Jul 2017 11:50:58 +0800
changeset 419430 1f85c1bfd4bdb1dc53633096b1ef6062869aeda8
parent 419429 6efa0b5aaabe5bd6b5e7c657897c520347e72968
child 419431 91fd8e1b78e39652bdb1de06e221784986db5f13
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslchang, steveck
bugs1371131
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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\./);
+      }
+    });
   });
 });