Bug 970456 - "Be" doesn't autocomplete to my friend "Ben" in my compose window anymore (because I have frequently-contacted holBErt contacts, with "be" in the middle of their last name). r=neil, a=mkmelin
authorMagnus Melin <mkmelin+mozilla@iki.fi>
Thu, 13 Nov 2014 23:12:16 +0200
changeset 21301 98414962486a411d1d503855a00d672b545ef203
parent 21300 27b383faf2e6c0272d7e4de2186754367212b471
child 21302 5c7a3bfe09cc963027d9863d28f7aeca5d97ec50
push id1274
push usermbanner@mozilla.com
push dateMon, 12 Jan 2015 19:54:49 +0000
treeherdercomm-beta@baea280adc1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersneil, mkmelin
bugs970456
Bug 970456 - "Be" doesn't autocomplete to my friend "Ben" in my compose window anymore (because I have frequently-contacted holBErt contacts, with "be" in the middle of their last name). r=neil, a=mkmelin
mailnews/addrbook/src/nsAbAutoCompleteSearch.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
mailnews/addrbook/test/unit/xpcshell.ini
--- a/mailnews/addrbook/src/nsAbAutoCompleteSearch.js
+++ b/mailnews/addrbook/src/nsAbAutoCompleteSearch.js
@@ -120,16 +120,52 @@ nsAbAutoCompleteSearch.prototype = {
           Components.utils.reportError(ex);
         }
       }
     }
     return popularityIndex;
   },
 
   /**
+   * Gets the score of the (full) address, given the search input. We want
+   * results that match the beginning of a "word" in the result to score better
+   * than a result that matches only in the middle of the word.
+   *
+   * @param aAddress - full lower-cased address, including display name and address
+   * @param aSearchString - search string provided by user
+   * @return a score; a higher score is better than a lower one
+   */
+  _getScore: function(aAddress, aSearchString) {
+    const BEST = 100;
+    // We'll do this case-insensitively and ignore the domain.
+    let atIdx = aAddress.lastIndexOf("@");
+    if (atIdx != -1) // mail lists don't have an @
+      aAddress = aAddress.substr(0, atIdx);
+    aSearchString = aSearchString.toLocaleLowerCase();
+    let idx = aAddress.indexOf(aSearchString);
+    if (idx == 0)
+      return BEST;
+    if (idx == -1)
+      return 0;
+
+    // We want to treat firstname, lastname and word boundary(ish) parts of
+    // the email address the same. E.g. for "John Doe (:xx) <jd.who@example.com>"
+    // all of these should score (almost) the same: "John", "Doe", "xx",
+    // ":xx:", "jd", "who".
+    let prevCh = aAddress.charAt(idx - 1);
+    if (/[ :."'(\-_<&]/.test(prevCh)) {
+      // -1, so exact begins-with match will still be the first hit.
+      return BEST - 1;
+    }
+
+    // The match was inside a word -> we don't care about the position.
+    return 0;
+  },
+
+  /**
    * Searches cards in the given directory. If a card is matched (and isn't
    * a mailing list) then the function will add a result for each email address
    * that exists.
    *
    * @param searchQuery  The boolean search query to use.
    * @param directory    An nsIAbDirectory to search.
    * @param result       The result element to append results to.
    */
@@ -196,40 +232,36 @@ nsAbAutoCompleteSearch.prototype = {
   /**
    * Checks to see if an emailAddress (name/address) is a duplicate of an
    * existing entry already in the results. If the emailAddress is found, it
    * will remove the existing element if the popularity of the new card is
    * higher than the previous card.
    *
    * @param directory       The directory that the card is in.
    * @param card            The card that could be a duplicate.
-   * @param emailAddress    The emailAddress (name/address combination) to check
-   *                        for duplicates against.
+   * @param lcEmailAddress  The emailAddress (name/address combination) to check
+   *                        for duplicates against. Lowercased.
    * @param currentResults  The current results list.
    */
-  _checkDuplicate: function _checkDuplicate(directory, card, emailAddress,
-                                            currentResults) {
-    let lcEmailAddress = emailAddress.toLocaleLowerCase();
+  _checkDuplicate: function (directory, card, lcEmailAddress, currentResults) {
     let existingResult = currentResults._collectedValues.get(lcEmailAddress);
-    let popIndex = this._getPopularityIndex(directory, card);
+    if (!existingResult)
+      return false;
 
-    if (existingResult) {
-      // It's a duplicate, is the new one more popular?
-      if (popIndex > existingResult.popularity) {
-        // Yes it is, so delete this element, return false and allow
-        // _addToResult to sort the new element into the correct place.
-        currentResults._collectedValues.delete(lcEmailAddress);
-        return false;
-      }
-      // Not more popular, but still a duplicate. Return true and _addToResult
-      // will just forget about it.
-      return true;
+    let popIndex = this._getPopularityIndex(directory, card);
+    // It's a duplicate, is the new one more popular?
+    if (popIndex > existingResult.popularity) {
+      // Yes it is, so delete this element, return false and allow
+      // _addToResult to sort the new element into the correct place.
+      currentResults._collectedValues.delete(lcEmailAddress);
+      return false;
     }
-
-    return false;
+    // Not more popular, but still a duplicate. Return true and _addToResult
+    // will just forget about it.
+    return true;
   },
 
   /**
    * Adds a card to the results list if it isn't a duplicate. The function will
    * order the results by popularity.
    *
    * @param commentColumn  The text to be displayed in the comment column
    *                       (if any).
@@ -245,29 +277,31 @@ nsAbAutoCompleteSearch.prototype = {
                          emailToUse, isPrimaryEmail, result) {
     let mbox = this._parser.makeMailboxObject(card.displayName,
       card.isMailList ? card.getProperty("Notes", "") || card.displayName :
                         emailToUse);
     if (!mbox.email)
       return;
 
     let emailAddress = mbox.toString();
+    let lcEmailAddress = emailAddress.toLocaleLowerCase();
 
     // If it is a duplicate, then just return and don't add it. The
     // _checkDuplicate function deals with it all for us.
-    if (this._checkDuplicate(directory, card, emailAddress, result))
+    if (this._checkDuplicate(directory, card, lcEmailAddress, result))
       return;
 
-    result._collectedValues.set(emailAddress.toLocaleLowerCase(), {
+    result._collectedValues.set(lcEmailAddress, {
       value: emailAddress,
       comment: commentColumn,
       card: card,
       isPrimaryEmail: isPrimaryEmail,
       emailToUse: emailToUse,
-      popularity: this._getPopularityIndex(directory, card)
+      popularity: this._getPopularityIndex(directory, card),
+      score: this._getScore(lcEmailAddress, result.searchString)
     });
   },
 
   // nsIAutoCompleteSearch
 
   /**
    * Starts a search based on the given parameters.
    *
@@ -356,25 +390,26 @@ nsAbAutoCompleteSearch.prototype = {
         let dir = allABs.getNext();
         if (dir instanceof Components.interfaces.nsIAbDirectory &&
             dir.useForAutocomplete(params.idKey)) {
           this._searchCards(searchQuery, dir, result);
         }
       }
 
       result._searchResults = [...result._collectedValues.values()];
-      // Order by descending popularity, then primary email before secondary
-      // for the same card, then for differing cards sort by email.
-      let order_by_popularity_and_email = function(a, b) {
-        return (b.popularity - a.popularity) ||
+      result._searchResults.sort(function(a, b) {
+        // Order by 1) descending score, then 2) descending popularity,
+        // then 3) primary email before secondary for the same card, then
+        // 4) by differing cards sort by email.
+        return (b.score - a.score) ||
+               (b.popularity - a.popularity) ||
                ((a.card == b.card && a.isPrimaryEmail) ? -1 : 0) ||
                ((a.value < b.value) ? -1 : (a.value == b.value) ? 0 : 1);
         // TODO: this should actually use a.value.localeCompare(b.value) .
-      }
-      result._searchResults.sort(order_by_popularity_and_email);
+      });
     }
 
     if (result.matchCount) {
       result.searchResult = ACR.RESULT_SUCCESS;
       result.defaultIndex = 0;
     }
 
     aListener.onSearchResult(this, result);
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
@@ -7,37 +7,37 @@
 
 const ACR = Components.interfaces.nsIAutoCompleteResult;
 
 // Input and results arrays for the autocomplete tests. This are potentially
 // more complicated than really required, but it was easier to do them
 // on a pattern rather just doing the odd spot check.
 //
 // Note the expected arrays are in expected sort order as well.
-const results = [ { email: "d <ema@foo.invalid>", dirName: kPABData.dirName },
-                  { email: "di <emai@foo.invalid>", dirName: kPABData.dirName },
-                  { email: "dis <email@foo.invalid>", dirName: kPABData.dirName },
-                  { email: "disp <e@foo.invalid>", dirName: kPABData.dirName },
-                  { email: "displ <em@foo.invalid>", dirName: kPABData.dirName },
-                  { email: "DisplayName1 <PrimaryEmail1@test.invalid>",
+const results = [ { email: "d <ema@foo.invalid>", dirName: kPABData.dirName }, // 0
+                  { email: "di <emai@foo.invalid>", dirName: kPABData.dirName }, // 1
+                  { email: "dis <email@foo.invalid>", dirName: kPABData.dirName }, // 2
+                  { email: "disp <e@foo.invalid>", dirName: kPABData.dirName }, // 3
+                  { email: "displ <em@foo.invalid>", dirName: kPABData.dirName }, // 4
+                  { email: "DisplayName1 <PrimaryEmail1@test.invalid>", // 5
                     dirName: kCABData.dirName },
-                  { email: "t <list>", dirName: kPABData.dirName },
-                  { email: "te <lis>", dirName: kPABData.dirName },
-                  { email: "tes <li>", dirName: kPABData.dirName },
+                  { email: "t <list>", dirName: kPABData.dirName }, // 6
+                  { email: "te <lis>", dirName: kPABData.dirName }, // 7
+                  { email: "tes <li>", dirName: kPABData.dirName }, // 8
                    // this contact has a nickname of "abcdef"
-                  { email: "test <l>", dirName: kPABData.dirName } ];
-
+                  { email: "test <l>", dirName: kPABData.dirName } // 9
+                ];
 const firstNames = [ { search: "f",      expected: [5, 0, 1, 2, 3, 4, 9] },
                      { search: "fi",     expected: [5, 0, 1, 3, 4] },
                      { search: "fir",    expected: [5, 0, 1, 4] },
                      { search: "firs",   expected: [5, 0, 1] },
                      { search: "first",  expected: [5, 1] },
                      { search: "firstn", expected: [5] } ];
 
-const lastNames = [ { search: "l",      expected: [5, 0, 1, 2, 3, 4, 6, 7, 8, 9] },
+const lastNames = [ { search: "l",      expected: [6, 7, 8, 9, 5, 0, 1, 2, 3, 4] },
                     { search: "la",     expected: [5, 0, 2, 3, 4] },
                     { search: "las",    expected: [5, 0, 3, 4] },
                     { search: "last",   expected: [5, 0, 4] },
                     { search: "lastn",  expected: [5, 0] },
                     { search: "lastna", expected: [5]} ];
 
 const displayNames = [ { search: "d",      expected: [5, 0, 1, 2, 3, 4, 9] },
                        { search: "di",     expected: [5, 1, 2, 3, 4] },
@@ -48,30 +48,30 @@ const displayNames = [ { search: "d",   
 
 const nickNames = [ { search: "n",      expected: [5, 0, 1, 2, 3, 4] },
                     { search: "ni",     expected: [5, 0, 1, 2, 3] },
                     { search: "nic",    expected: [5, 1, 2, 3] },
                     { search: "nick",   expected: [5, 2, 3] },
                     { search: "nickn",  expected: [5, 3] },
                     { search: "nickna", expected: [5] } ];
 
-const emails = [ { search: "e",     expected: [5, 0, 1, 2, 3, 4, 7, 8, 9] },
-                 { search: "em",    expected: [5, 0, 1, 2, 4] },
-                 { search: "ema",   expected: [5, 0, 1, 2] },
-                 { search: "emai",  expected: [5, 1, 2] },
-                 { search: "email", expected: [5, 2] } ];
+const emails = [ { search: "e",     expected: [0, 1, 2, 3, 4, 5, 7, 8, 9] },
+                 { search: "em",    expected: [0, 1, 2, 4, 5] },
+                 { search: "ema",   expected: [0, 1, 2, 5] },
+                 { search: "emai",  expected: [1, 2, 5] },
+                 { search: "email", expected: [2, 5] } ];
 
 // "l" case tested above
-const lists = [ { search: "li", expected: [5, 0, 1, 2, 3, 4, 6, 7, 8] },
+const lists = [ { search: "li", expected: [6, 7, 8, 5, 0, 1, 2, 3, 4] },
                 { search: "lis", expected: [6, 7] },
                 { search: "list", expected: [6] },
-                { search: "t", expected: [5, 0, 1, 4, 6, 7, 8, 9] },
-                { search: "te", expected: [5, 7, 8, 9] },
-                { search: "tes", expected: [5, 8, 9] },
-                { search: "test", expected: [5, 9] },
+                { search: "t", expected: [6, 7, 8, 9, 5, 0, 1, 4] },
+                { search: "te", expected: [7, 8, 9, 5] },
+                { search: "tes", expected: [8, 9, 5] },
+                { search: "test", expected: [9, 5] },
                 { search: "abcdef", expected: [9] } // Bug 441586
               ];
 
 const bothNames = [ { search: "f l",            expected: [5, 0, 1, 2, 3, 4, 9] },
                     { search: "l f",            expected: [5, 0, 1, 2, 3, 4, 9] },
                     { search: "firstn lastna",  expected: [5] },
                     { search: "lastna firstna", expected: [5] } ];
 
@@ -167,18 +167,18 @@ function run_test() {
 
   do_check_eq(obs._search, acs);
   do_check_eq(obs._result.searchString, "email");
   do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
   do_check_eq(obs._result.errorDescription, null);
   do_check_eq(obs._result.matchCount, 2);
   do_check_eq(obs._result.defaultIndex, 0);
 
-  do_check_eq(obs._result.getValueAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
-  do_check_eq(obs._result.getLabelAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
+  do_check_eq(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+  do_check_eq(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
   do_check_eq(obs._result.getCommentAt(0), "");
   do_check_eq(obs._result.getStyleAt(0), "local-abook");
   do_check_eq(obs._result.getImageAt(0), "");
 
   // quick-check that nothing is found for addr_newsgroups
   acs.startSearch("email", paramNews, null, obsNews);
   do_check_true(obsNews._result == null || obsNews._result.matchCount == 0);
 
@@ -193,44 +193,53 @@ function run_test() {
 
   do_check_eq(obs._search, acs);
   do_check_eq(obs._result.searchString, "email");
   do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
   do_check_eq(obs._result.errorDescription, null);
   do_check_eq(obs._result.matchCount, 2);
   do_check_eq(obs._result.defaultIndex, 0);
 
-  do_check_eq(obs._result.getValueAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
-  do_check_eq(obs._result.getLabelAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
-  do_check_eq(obs._result.getCommentAt(0), kCABData.dirName);
+  do_check_eq(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+  do_check_eq(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+  do_check_eq(obs._result.getCommentAt(0), kPABData.dirName);
   do_check_eq(obs._result.getStyleAt(0), "local-abook");
   do_check_eq(obs._result.getImageAt(0), "");
 
   // Check input with different case
   acs.startSearch("EMAIL", param, null, obs);
 
   do_check_eq(obs._search, acs);
   do_check_eq(obs._result.searchString, "EMAIL");
   do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
   do_check_eq(obs._result.errorDescription, null);
   do_check_eq(obs._result.matchCount, 2);
   do_check_eq(obs._result.defaultIndex, 0);
 
-  do_check_eq(obs._result.getValueAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
-  do_check_eq(obs._result.getLabelAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
-  do_check_eq(obs._result.getCommentAt(0), kCABData.dirName);
+  do_check_eq(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+  do_check_eq(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+  do_check_eq(obs._result.getCommentAt(0), kPABData.dirName);
   do_check_eq(obs._result.getStyleAt(0), "local-abook");
   do_check_eq(obs._result.getImageAt(0), "");
 
 
   // Now check multiple matches
   function checkInputItem(element, index, array) {
-    print("Checking " + element.search);
+    print("Search #" + index + ": search=" + element.search);
     acs.startSearch(element.search, param, null, obs);
 
+    for (var i = 0; i < obs._result.matchCount; i++) {
+      print("... got " + i + ": " + obs._result.getValueAt(i));
+    }
+
+    for (var i = 0; i < element.expected.length; i++) {
+      print("... expected " + i + " (result " + element.expected[i] + "): " +
+            results[element.expected[i]].email);
+    }
+
     do_check_eq(obs._search, acs);
     do_check_eq(obs._result.searchString, element.search);
     do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
     do_check_eq(obs._result.errorDescription, null);
     do_check_eq(obs._result.matchCount, element.expected.length);
     do_check_eq(obs._result.defaultIndex, 0);
 
     for (var i = 0; i < element.expected.length; ++i) {
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
@@ -25,17 +25,17 @@ const cards = [
     displayName: "Mr. (Bracket)", value: "Mr. (Bracket) <bracket@not.invalid>",
     popularityIndex: 2 },
   { email: "mr@(bracket).not.invalid", secondEmail: "bracket@not.invalid",  firstName: "Mr.",
     displayName: "Mr. Bracket", value: "Mr. Bracket <mr@(bracket).not.invalid>",
     popularityIndex: 1 }
 ];
 
 const duplicates = [
-  { search: "test", expected: [2, 1] },
+  { search: "test", expected: [1, 2] },
   { search: "first", expected: [6, 5, 3] },
   { search: "(bracket)", expected: [7, 8] }
 ];
 
 
 function acObserver() {}
 
 acObserver.prototype = {
@@ -75,28 +75,34 @@ function run_test()
   // Test - duplicate elements
 
   var acs = Components.classes["@mozilla.org/autocomplete/search;1?name=addrbook"]
     .getService(Components.interfaces.nsIAutoCompleteSearch);
 
   var obs = new acObserver();
 
   function checkInputItem(element, index, array) {
-    print("Checking " + element.search);
+    print("Search #" + index + ": search=" + element.search);
     acs.startSearch(element.search, JSON.stringify({ type: "addr_to"  }), null, obs);
 
+    for (var i = 0; i < obs._result.matchCount; i++) {
+      print("... got " + i + ": " + obs._result.getValueAt(i));
+    }
+
+    for (var i = 0; i < element.expected.length; i++) {
+      print("... expected " + i + " (card " + element.expected[i] + "): " +
+            cards[element.expected[i]].value);
+    }
+
     do_check_eq(obs._search, acs);
     do_check_eq(obs._result.searchString, element.search);
     do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
     do_check_eq(obs._result.errorDescription, null);
     do_check_eq(obs._result.matchCount, element.expected.length);
 
-    for (var i = 0; i < element.expected.length; ++i)
-      print(obs._result.getValueAt(i));
-
     for (var i = 0; i < element.expected.length; ++i) {
       do_check_eq(obs._result.getValueAt(i), cards[element.expected[i]].value);
       do_check_eq(obs._result.getLabelAt(i), cards[element.expected[i]].value);
       do_check_eq(obs._result.getCommentAt(i), "");
       do_check_eq(obs._result.getStyleAt(i), "local-abook");
       do_check_eq(obs._result.getImageAt(i), "");
       obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
       do_check_eq(obs._result.getCardAt(i).firstName,
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
@@ -37,37 +37,37 @@ const cards = [
   { displayName: "primaryX" }
 ];
 
 // These are for the initial search
 const searches = [ "primary", "second", "firstName", "thename", "sortbasic",
                    "testsort", "2testsort", "3testsort" ];
 
 const expectedResults = [ [ "primary@test.invalid",
-                            "second@test.invalid" ], // searching for primary/second returns
-                          [ "primary@test.invalid",  // both the emails as the new search query
-                            "second@test.invalid" ], // looks in both the fields.
+                            "second@test.invalid"], // searching for primary/second returns
+                          [ "second@test.invalid",  // both the emails as the new search query
+                            "primary@test.invalid" ], // looks in both the fields.
                           [ "test1@test.invalid",
                             "test2@test.invalid" ],
-                          [ "name@test.invalid",
-                            "thename@test.invalid" ],
+                          [ "thename@test.invalid",
+                            "name@test.invalid"],
                           [ "sortbasic <foo_b@test.invalid>",
                             "sortbasic <foo_a@test.invalid>" ],
-                          [ "3testsort <j@test.invalid>",
+                          [ "testsort <c@test.invalid>",
+                            "testsort <a@test.invalid>",
+                            "testsort <d@test.invalid>",
+                            "testsort <e@test.invalid>",
+                            "3testsort <j@test.invalid>",
                             "3testsort <h@test.invalid>",
                             "3testsort <g@test.invalid>",
                             "3testsort <f@test.invalid>",
                             "2testsort <c@test.invalid>",
                             "2testsort <a@test.invalid>",
                             "2testsort <d@test.invalid>",
-                            "2testsort <e@test.invalid>",
-                            "testsort <c@test.invalid>",
-                            "testsort <a@test.invalid>",
-                            "testsort <d@test.invalid>",
-                            "testsort <e@test.invalid>" ],
+                            "2testsort <e@test.invalid>"],
                           [ "2testsort <c@test.invalid>",
                             "2testsort <a@test.invalid>",
                             "2testsort <d@test.invalid>",
                             "2testsort <e@test.invalid>" ],
                           [ "3testsort <j@test.invalid>",
                             "3testsort <h@test.invalid>",
                             "3testsort <g@test.invalid>",
                             "3testsort <f@test.invalid>" ] ];
@@ -126,17 +126,17 @@ function run_test()
   var acs = Components.classes["@mozilla.org/autocomplete/search;1?name=addrbook"]
     .getService(Components.interfaces.nsIAutoCompleteSearch);
 
   var obs = new acObserver();
 
   print("Checking Initial Searches");
 
   function checkSearch(element, index, array) {
-    print("Checking " + element);
+    print("Search #" + index + ": search=" + element);
     acs.startSearch(element, JSON.stringify({ type: "addr_to"  }), null, obs);
 
     do_check_eq(obs._search, acs);
     do_check_eq(obs._result.searchString, element);
     do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
     do_check_eq(obs._result.errorDescription, null);
     do_check_eq(obs._result.matchCount, expectedResults[index].length);
 
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
@@ -18,17 +18,17 @@ const results = [ { email: "d <ema@test.
                   { email: "test <l>", dirName: kPABData.dirName } ];
 
 const firstNames = [ { search: "f",      expected: [4, 0, 1, 2, 3, 8] },
                      { search: "fi",     expected: [4, 0, 1, 3] },
                      { search: "fir",    expected: [4, 0, 1] },
                      { search: "firs",   expected: [0, 1] },
                      { search: "first",  expected: [1] } ];
 
-const lastNames = [ { search: "l",      expected: [4, 0, 1, 2, 3, 5, 6, 7, 8] },
+const lastNames = [ { search: "l",      expected: [5, 6, 7, 8, 4, 0, 1, 2, 3] },
                     { search: "la",     expected: [4, 0, 2, 3] },
                     { search: "las",    expected: [4, 0, 3] },
                     { search: "last",   expected: [4, 0] },
                     { search: "lastn",  expected: [0] } ];
 
 const inputs = [ firstNames, lastNames];
 
 function acObserver() {}
@@ -58,19 +58,28 @@ function run_test() {
 
   // Ensure we've got the comment column set up for extra checking.
   Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
 
   // Test - Matches
 
   // Now check multiple matches
   function checkInputItem(element, index, array) {
-    print("Checking " + element.search);
+    print("Search #" + index + ": search=" + element.search);
     acs.startSearch(element.search, JSON.stringify({ type: "addr_to" }), null, obs);
 
+    for (var i = 0; i < obs._result.matchCount; i++) {
+      print("... got " + i + ": " + obs._result.getValueAt(i));
+    }
+
+    for (var i = 0; i < element.expected.length; i++) {
+      print("... expected " + i + " (card " + element.expected[i] + "): " +
+            results[element.expected[i]].email);
+    }
+
     do_check_eq(obs._search, acs);
     do_check_eq(obs._result.searchString, element.search);
     do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
     do_check_eq(obs._result.errorDescription, null);
     do_check_eq(obs._result.matchCount, element.expected.length);
     do_check_eq(obs._result.defaultIndex, 0);
 
     for (let i = 0; i < element.expected.length; ++i) {
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Tests for for nsAbAutoCompleteSearch scoring.
+ */
+
+const ACR = Components.interfaces.nsIAutoCompleteResult;
+
+const cards = [
+  { // 0
+    email: "jd.who@example.com", displayName: "John Doe (:xx)",
+    popularityIndex: 0, firstName: "John", value: "John Doe (:xx) <jd.who@example.com>"
+  },
+
+  { // 1
+    email: "janey_who@example.com", displayName: "Jane Doe",
+    popularityIndex: 0, value: "Jane Doe <janey_who@example.com>"
+  },
+
+  { // 2
+    email: "pf@example.com", displayName: "Paul \"Shitbreak\" Finch",
+    popularityIndex: 0, value: "Paul \"Shitbreak\" Finch <pf@example.com>"
+  },
+
+  { // 3
+    email: "js@example.com", displayName: "Janine (Stifflers Mom)",
+    popularityIndex: 0, value: "Janine (Stifflers Mom) <js@example.com>"
+  },
+
+  { // 4
+    email: "ex0@example.com", displayName: "Ajden",
+    popularityIndex: 0, value: "Ajden <ex0@example.com>"
+  },
+
+  { // 5
+    email: "5@example.com", displayName: "Foxx",
+    popularityIndex: 0, value: "Foxx <5@example.com>"
+  },
+
+  { // 6
+    email: "6@example.com", displayName: "thewho",
+    popularityIndex: 0, value: "thewho <6@example.com>"
+  },
+
+  { // 7
+    email: "7@example.com", displayName: "fakeshit",
+    popularityIndex: 0, value: "fakeshit <7@example.com>"
+  },
+
+  { // 8
+    email: "8@example.com", displayName: "mastiff",
+    popularityIndex: 0, value: "mastiff <8@example.com>"
+  },
+
+  { // 9
+    email: "9@example.com", displayName: "anyjohn",
+    popularityIndex: 0, value: "anyjohn <9@example.com>"
+  },
+
+  { // 10
+    email: "10@example.com", displayName: "däsh l18n",
+    popularityIndex: 0, value: "däsh l18n <10@example.com>"
+  }
+];
+
+const inputs = [
+  { search: "john", expected: [0, 9] },
+  { search: "doe", expected: [1, 0] },
+  { search: "jd", expected: [0, 4] },
+  { search: "who", expected: [1, 0, 6] },
+  { search: "xx", expected: [0, 5] },
+  { search: "jan", expected: [1, 3] },
+  { search: "sh", expected: [2, 10, 7] },
+  { search: "st", expected: [3,8] }
+];
+
+function acObserver() {}
+
+acObserver.prototype = {
+  _search: null,
+  _result: null,
+
+  onSearchResult: function (aSearch, aResult) {
+    this._search = aSearch;
+    this._result = aResult;
+  }
+};
+
+function run_test()
+{
+  // We set up the cards for this test manually as it is easier to set the
+  // popularity index and we don't need many.
+
+  // Ensure all the directories are initialised.
+  MailServices.ab.directories;
+
+  let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+  function createAndAddCard(element) {
+    var card = Cc["@mozilla.org/addressbook/cardproperty;1"]
+                 .createInstance(Ci.nsIAbCard);
+
+    card.primaryEmail = element.email;
+    card.displayName = element.displayName;
+    card.setProperty("PopularityIndex", element.popularityIndex);
+    card.firstName = element.firstName;
+
+    ab.addCard(card);
+  }
+
+  cards.forEach(createAndAddCard);
+
+  // Test - duplicate elements
+
+  var acs = Components.classes["@mozilla.org/autocomplete/search;1?name=addrbook"]
+    .getService(Components.interfaces.nsIAutoCompleteSearch);
+
+  var obs = new acObserver();
+
+  function checkInputItem(element, index, array) {
+    print("Search #" + index + ": search=" + element.search);
+    acs.startSearch(element.search, JSON.stringify({ type: "addr_to"  }), null, obs);
+
+    for (var i = 0; i < obs._result.matchCount; i++) {
+      print("... got " + i + ": " + obs._result.getValueAt(i));
+    }
+
+    for (var i = 0; i < element.expected.length; i++) {
+      print("... expected " + i + " (card " + element.expected[i] + "): " +
+            cards[element.expected[i]].value);
+    }
+
+    do_check_eq(obs._search, acs);
+    do_check_eq(obs._result.searchString, element.search);
+    do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
+    do_check_eq(obs._result.errorDescription, null);
+    do_check_eq(obs._result.matchCount, element.expected.length);
+
+    for (var i = 0; i < element.expected.length; ++i) {
+      do_check_eq(obs._result.getValueAt(i), cards[element.expected[i]].value);
+      do_check_eq(obs._result.getLabelAt(i), cards[element.expected[i]].value);
+    }
+  }
+
+  inputs.forEach(checkInputItem);
+}
--- a/mailnews/addrbook/test/unit/xpcshell.ini
+++ b/mailnews/addrbook/test/unit/xpcshell.ini
@@ -18,12 +18,13 @@ support-files = data/*
 [test_mailList1.js]
 [test_notifications.js]
 [test_nsAbAutoCompleteMyDomain.js]
 [test_nsAbAutoCompleteSearch1.js]
 [test_nsAbAutoCompleteSearch2.js]
 [test_nsAbAutoCompleteSearch3.js]
 [test_nsAbAutoCompleteSearch4.js]
 [test_nsAbAutoCompleteSearch5.js]
+[test_nsAbAutoCompleteSearch6.js]
 [test_nsAbManager1.js]
 [test_nsAbManager2.js]
 [test_nsIAbCard.js]
 [test_uuid.js]