Bug 1134986 - Address autocomplete sorting wrong - appears to ignore recent use (popularityindex) information in 31.4.0+. r=neil, a=rkent
authorMagnus Melin <mkmelin+mozilla@iki.fi>
Sun, 19 Apr 2015 21:53:15 +0300
changeset 25959 85bcf1bc0b53c167f46d21e810ecf9863ac056e9
parent 25958 e149d5f4bcdbe457f53fc1e16b28f23ff6a4f856
child 25960 e4457a5dd5f718fbfc369607d8e624cbba52aa8e
push id1850
push userclokep@gmail.com
push dateWed, 08 Mar 2017 19:29:12 +0000
treeherdercomm-esr52@028df196b2d9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersneil, rkent
bugs1134986
Bug 1134986 - Address autocomplete sorting wrong - appears to ignore recent use (popularityindex) information in 31.4.0+. r=neil, a=rkent
mailnews/addrbook/src/nsAbAutoCompleteSearch.js
mailnews/addrbook/test/unit/data/autocomplete.mab
mailnews/addrbook/test/unit/data/autocomplete2.mab
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
mailnews/addrbook/test/unit/xpcshell.ini
--- a/mailnews/addrbook/src/nsAbAutoCompleteSearch.js
+++ b/mailnews/addrbook/src/nsAbAutoCompleteSearch.js
@@ -153,38 +153,36 @@ nsAbAutoCompleteSearch.prototype = {
     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".
+    // all of these should score 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;
-    }
+    if (/[ :."'(\-_<&]/.test(prevCh))
+      return BEST;
 
     // 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.
    */
-  _searchCards: function _searchCards(searchQuery, directory, result) {
+  _searchCards: function(searchQuery, directory, result) {
     let childCards;
     try {
       childCards = this._abManager.getDirectory(directory.URI + searchQuery).childCards;
     } catch (e) {
       Components.utils.reportError("Error running addressbook query '" + searchQuery + "': " + e);
       return;
     }
 
@@ -354,28 +352,32 @@ nsAbAutoCompleteSearch.prototype = {
     } catch(e) { }
 
     if (aPreviousResult instanceof nsIAbAutoCompleteResult &&
         aSearchString.startsWith(aPreviousResult.searchString) &&
         aPreviousResult.searchResult == ACR.RESULT_SUCCESS) {
       // We have successful previous matches, therefore iterate through the
       // list and reduce as appropriate
       for (let i = 0; i < aPreviousResult.matchCount; ++i) {
-        if (this._checkEntry(aPreviousResult.getCardAt(i),
-                             aPreviousResult.getEmailToUse(i), searchWords))
-          // If it matches, just add it straight onto the array, these will
-          // already be in order because the previous search returned them
-          // in the correct order.
+        let card = aPreviousResult.getCardAt(i);
+        let email = aPreviousResult.getEmailToUse(i);
+        if (this._checkEntry(card, email, searchWords)) {
+          // Add matches into the results array. We re-sort as needed later.
           result._searchResults.push({
             value: aPreviousResult.getValueAt(i),
             comment: aPreviousResult.getCommentAt(i),
-            card: aPreviousResult.getCardAt(i),
-            emailToUse: aPreviousResult.getEmailToUse(i),
-            popularity: parseInt(aPreviousResult.getCardAt(i).getProperty("PopularityIndex", "0"))
+            card: card,
+            isPrimaryEmail: (card.primaryEmail == email),
+            emailToUse: email,
+            popularity: parseInt(card.getProperty("PopularityIndex", "0")),
+            score: this._getScore(card,
+              aPreviousResult.getValueAt(i).toLocaleLowerCase(),
+              fullString)
           });
+        }
       }
     }
     else
     {
       // Construct the search query; using a query means we can optimise
       // on running the search through c++ which is better for string
       // comparisons (_checkEntry is relatively slow).
       // When user's fullstring search expression is a multiword query, search
@@ -401,28 +403,30 @@ 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()];
-      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) .
-      });
     }
 
+    // Sort the results. Scoring may have changed so do it even if this is
+    // just filtered previous results.
+    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 emails sorted alphabetically.
+      return (b.score - a.score) ||
+             (b.popularity - a.popularity) ||
+             ((a.card == b.card && a.isPrimaryEmail) ? -1 : 0) ||
+             a.value.localeCompare(b.value);
+    });
+
     if (result.matchCount) {
       result.searchResult = ACR.RESULT_SUCCESS;
       result.defaultIndex = 0;
     }
 
     aListener.onSearchResult(this, result);
   },
 
deleted file mode 100644
--- a/mailnews/addrbook/test/unit/data/autocomplete.mab
+++ /dev/null
@@ -1,74 +0,0 @@
-// <!-- <mdb:mork:z v="1.4"/> -->
-< <(a=c)> // (f=iso-8859-1)
-  (B8=Custom4)(B9=Notes)(BA=LastModifiedDate)(BB=RecordKey)
-  (BC=AddrCharSet)(BD=LastRecordKey)(BE=ns:addrbk:db:table:kind:pab)
-  (BF=ListName)(C0=ListNickName)(C1=ListDescription)
-  (C2=ListTotalAddresses)(C3=LowercaseListName)
-  (C4=ns:addrbk:db:table:kind:deleted)
-  (80=ns:addrbk:db:row:scope:card:all)
-  (81=ns:addrbk:db:row:scope:list:all)
-  (82=ns:addrbk:db:row:scope:data:all)(83=FirstName)(84=LastName)
-  (85=PhoneticFirstName)(86=PhoneticLastName)(87=DisplayName)
-  (88=NickName)(89=PrimaryEmail)(8A=LowercasePrimaryEmail)
-  (8B=SecondEmail)(8C=PreferMailFormat)(8D=PopularityIndex)
-  (8E=AllowRemoteContent)(8F=WorkPhone)(90=HomePhone)(91=FaxNumber)
-  (92=PagerNumber)(93=CellularNumber)(94=WorkPhoneType)(95=HomePhoneType)
-  (96=FaxNumberType)(97=PagerNumberType)(98=CellularNumberType)
-  (99=HomeAddress)(9A=HomeAddress2)(9B=HomeCity)(9C=HomeState)
-  (9D=HomeZipCode)(9E=HomeCountry)(9F=WorkAddress)(A0=WorkAddress2)
-  (A1=WorkCity)(A2=WorkState)(A3=WorkZipCode)(A4=WorkCountry)
-  (A5=JobTitle)(A6=Department)(A7=Company)(A8=_AimScreenName)
-  (A9=AnniversaryYear)(AA=AnniversaryMonth)(AB=AnniversaryDay)
-  (AC=SpouseName)(AD=FamilyName)(AE=DefaultAddress)(AF=Category)
-  (B0=WebPage1)(B1=WebPage2)(B2=BirthYear)(B3=BirthMonth)(B4=BirthDay)
-  (B5=Custom1)(B6=Custom2)(B7=Custom3)>
-
-<(A8=8)(81=firs)(82=lastn)(83=)(84=d)(85=ni)(86=ema@foo.invalid)(80=0)
-  (87=1)(88=first)(89=l)(8A=di)(8B=nic)(8C=emai@foo.invalid)(8D=2)(8E=f)
-  (8F=la)(90=dis)(91=nick)(92=email@foo.invalid)(93=3)(94=fi)(95=las)
-  (96=disp)(97=nickn)(98=e@foo.invalid)(99=4)(9A=fir)(9B=last)(9C=displ)
-  (9D=n)(9E=em@foo.invalid)(9F=5)(A0=t)(A1=list)(A2=6)(A3=te)(A4=lis)
-  (A5=7)(A6=tes)(A7=li)>
-{1:^80 {(k^BE:c)(s=9)} 
-  [1:^82(^BD=8)]
-  [1(^83^81)(^84^82)(^85=)(^86=)(^87=d)(^88=ni)(^89^86)(^8A^86)(^8B=)
-    (^8C=0)(^8D=0)(^8E=0)(^8F=)(^90=)(^91=)(^92=)(^93=)(^94=)(^95=)
-    (^96=)(^97=)(^98=)(^99=)(^9A=)(^9B=)(^9C=)(^9D=)(^9E=)(^9F=)(^A0=)
-    (^A1=)(^A2=)(^A3=)(^A4=)(^A5=)(^A6=)(^A7=)(^A8=)(^A9=)(^AA=)(^AB=)
-    (^AC=)(^AD=)(^AE=)(^AF=)(^B0=)(^B1=)(^B2=)(^B3=)(^B4=)(^B5=)(^B6=)
-    (^B7=)(^B8=)(^B9=)(^BA=0)(^BB=1)]
-  [2(^83^88)(^84=l)(^85=)(^86=)(^87=di)(^88^8B)(^89^8C)(^8A^8C)(^8B=)
-    (^8C=0)(^8D=0)(^8E=0)(^8F=)(^90=)(^91=)(^92=)(^93=)(^94=)(^95=)
-    (^96=)(^97=)(^98=)(^99=)(^9A=)(^9B=)(^9C=)(^9D=)(^9E=)(^9F=)(^A0=)
-    (^A1=)(^A2=)(^A3=)(^A4=)(^A5=)(^A6=)(^A7=)(^A8=)(^A9=)(^AA=)(^AB=)
-    (^AC=)(^AD=)(^AE=)(^AF=)(^B0=)(^B1=)(^B2=)(^B3=)(^B4=)(^B5=)(^B6=)
-    (^B7=)(^B8=)(^B9=)(^BA=0)(^BB=2)]
-  [3(^83=f)(^84=la)(^85=)(^86=)(^87^90)(^88^91)(^89^92)(^8A^92)(^8B=)
-    (^8C=0)(^8D=0)(^8E=0)(^8F=)(^90=)(^91=)(^92=)(^93=)(^94=)(^95=)
-    (^96=)(^97=)(^98=)(^99=)(^9A=)(^9B=)(^9C=)(^9D=)(^9E=)(^9F=)(^A0=)
-    (^A1=)(^A2=)(^A3=)(^A4=)(^A5=)(^A6=)(^A7=)(^A8=)(^A9=)(^AA=)(^AB=)
-    (^AC=)(^AD=)(^AE=)(^AF=)(^B0=)(^B1=)(^B2=)(^B3=)(^B4=)(^B5=)(^B6=)
-    (^B7=)(^B8=)(^B9=)(^BA=0)(^BB=3)]
-  [4(^83=fi)(^84^95)(^85=)(^86=)(^87^96)(^88^97)(^89^98)(^8A^98)(^8B=)
-    (^8C=0)(^8D=0)(^8E=0)(^8F=)(^90=)(^91=)(^92=)(^93=)(^94=)(^95=)
-    (^96=)(^97=)(^98=)(^99=)(^9A=)(^9B=)(^9C=)(^9D=)(^9E=)(^9F=)(^A0=)
-    (^A1=)(^A2=)(^A3=)(^A4=)(^A5=)(^A6=)(^A7=)(^A8=)(^A9=)(^AA=)(^AB=)
-    (^AC=)(^AD=)(^AE=)(^AF=)(^B0=)(^B1=)(^B2=)(^B3=)(^B4=)(^B5=)(^B6=)
-    (^B7=)(^B8=)(^B9=)(^BA=0)(^BB=4)]
-  [5(^83^9A)(^84^9B)(^85=)(^86=)(^87^9C)(^88=n)(^89^9E)(^8A^9E)(^8B=)
-    (^8C=0)(^8D=0)(^8E=0)(^8F=)(^90=)(^91=)(^92=)(^93=)(^94=)(^95=)
-    (^96=)(^97=)(^98=)(^99=)(^9A=)(^9B=)(^9C=)(^9D=)(^9E=)(^9F=)(^A0=)
-    (^A1=)(^A2=)(^A3=)(^A4=)(^A5=)(^A6=)(^A7=)(^A8=)(^A9=)(^AA=)(^AB=)
-    (^AC=)(^AD=)(^AE=)(^AF=)(^B0=)(^B1=)(^B2=)(^B3=)(^B4=)(^B5=)(^B6=)
-    (^B7=)(^B8=)(^B9=)(^BA=0)(^BB=5)]
-  [1:^81(^BF=t)(^C3=t)(^C0=)(^C1^A1)(^C2=0)(^BB=6)]
-  [2:^81(^BF=te)(^C3=te)(^C0=)(^C1^A4)(^C2=0)(^BB=7)]
-  [3:^81(^BF^A6)(^C3^A6)(^C0=)(^C1=li)(^C2=0)(^BB=8)]}
-
-@$${7{@
-
-<(AB=9)(A9=test)(AA=abcdef)>
-{1:^80 {(k^BE:c)(s=9)} 
-  [-4:^81(^BF^A9)(^C3^A9)(^C0^AA)(^C1=l)(^C2=0)(^BB=9)]}
-[1:^82(^BD=9)]
-@$$}7}@
deleted file mode 100755
--- a/mailnews/addrbook/test/unit/data/autocomplete2.mab
+++ /dev/null
@@ -1,58 +0,0 @@
-// <!-- <mdb:mork:z v="1.4"/> -->
-< <(a=c)> // (f=iso-8859-1)
-  (B8=LastModifiedDate)(B9=RecordKey)(BA=AddrCharSet)(BB=LastRecordKey)
-  (BC=ns:addrbk:db:table:kind:pab)(BD=ListName)(BE=ListNickName)
-  (BF=ListDescription)(C0=ListTotalAddresses)(C1=LowercaseListName)
-  (C2=ns:addrbk:db:table:kind:deleted)(C3=DbRowID)
-  (80=ns:addrbk:db:row:scope:card:all)
-  (81=ns:addrbk:db:row:scope:list:all)
-  (82=ns:addrbk:db:row:scope:data:all)(83=FirstName)(84=LastName)
-  (85=PhoneticFirstName)(86=PhoneticLastName)(87=DisplayName)
-  (88=NickName)(89=PrimaryEmail)(8A=LowercasePrimaryEmail)
-  (8B=SecondEmail)(8C=PreferMailFormat)(8D=PopularityIndex)
-  (8E=AllowRemoteContent)(8F=WorkPhone)(90=HomePhone)(91=FaxNumber)
-  (92=PagerNumber)(93=CellularNumber)(94=WorkPhoneType)(95=HomePhoneType)
-  (96=FaxNumberType)(97=PagerNumberType)(98=CellularNumberType)
-  (99=HomeAddress)(9A=HomeAddress2)(9B=HomeCity)(9C=HomeState)
-  (9D=HomeZipCode)(9E=HomeCountry)(9F=WorkAddress)(A0=WorkAddress2)
-  (A1=WorkCity)(A2=WorkState)(A3=WorkZipCode)(A4=WorkCountry)
-  (A5=JobTitle)(A6=Department)(A7=Company)(A8=_AimScreenName)
-  (A9=AnniversaryYear)(AA=AnniversaryMonth)(AB=AnniversaryDay)
-  (AC=SpouseName)(AD=FamilyName)(AE=WebPage1)(AF=WebPage2)(B0=BirthYear)
-  (B1=BirthMonth)(B2=BirthDay)(B3=Custom1)(B4=Custom2)(B5=Custom3)
-  (B6=Custom4)(B7=Notes)>
-
-<(89=2)(81=Email)(82=1)(83=Empty Email)(80=0)(84=Empty)(85=)(86=LastName1)
-  (87=Custom41)(88=1237281794)(8A=http://WebPage11)(8B=NickName1)(8C
-    =DisplayName1)(8D=WorkZipCode1)(8E=ScreenName1)(8F=WorkAddress1)
-  (90=HomeCountry1)(91=WorkPhone1)(92=PrimaryEmail1@test.invalid)(93
-    =HomeAddress11)(94=primaryemail1@test.invalid)(95=WorkCity1)(96
-    =SecondEmail1$C3$90@test.invalid)(97=HomeZipCode1)(98=Custom31)
-  (99=FaxNumber1)(9A=Custom11)(9B=HomePhone1)(9C=FirstName1)(9D=HomeCity1)
-  (9E=PagerNumber1)(9F=CellularNumber1)(A0=WorkAddress21)(A1=WorkState1)
-  (A2=HomeAddress21)(A3=http://WebPage21)(A4=Notes1)(A5=Custom21)(A6
-    =Department1)(A7=WorkCountry1)(A8=HomeState1)(A9=JobTitle1)(AA
-    =Organization1)>
-{1:^80 {(k^BC:c)(s=9)} 
-  [1:^82(^BB=2)]
-  [1(^84^81)(^C3=1)(^87^83)(^8E=0)(^8D=0)(^83^84)(^8C=0)(^B8=0)(^B9=1)
-    (^89=)(^8A=)]
-  [2(^B0=)(^84^86)(^B6^87)(^8D=0)(^B8^88)(^B9=2)(^AF^8A)(^88^8B)(^87^8C)
-    (^A3^8D)(^A8^8E)(^85=)(^9F^8F)(^9E^90)(^8F^91)(^89^92)(^99^93)(^8A^94)
-    (^A1^95)(^8E=1)(^8B^96)(^9D^97)(^B5^98)(^91^99)(^B3^9A)(^90^9B)
-    (^83^9C)(^9B^9D)(^92^9E)(^93^9F)(^8C=0)(^A0^A0)(^A2^A1)(^B2=)(^9A^A2)
-    (^AE^A3)(^B7^A4)(^B4^A5)(^A6^A6)(^A4^A7)(^86=)(^9C^A8)(^C3=2)(^A5^A9)
-    (^A7^AA)(^B1=)]}
-
-@$${2{@
-< <(a=c)> // (f=iso-8859-1)
-  (C4=PhotoType)(C5=PhotoURI)>
-<(AB=1254739870)(AC=generic)(AD
-    =chrome://messenger/skin/addressbook/icons/contact-generic.png)>
-[-2:^80(^B0=)(^84^86)(^B6^87)(^8D=0)(^B8^AB)(^B9=2)(^AF^8A)(^88^8B)
-    (^87^8C)(^A3^8D)(^A8^8E)(^85=)(^9F^8F)(^9E^90)(^8F^91)(^89^92)(^99^93)
-    (^8A^94)(^A1^95)(^8E=1)(^8B=)(^9D^97)(^B5^98)(^91^99)(^B3^9A)(^90^9B)
-    (^83^9C)(^9B^9D)(^92^9E)(^93^9F)(^8C=0)(^A0^A0)(^A2^A1)(^B2=)(^9A^A2)
-    (^AE^A3)(^B7^A4)(^B4^A5)(^A6^A6)(^A4^A7)(^86=)(^9C^A8)(^C3=2)(^A5^A9)
-    (^A7^AA)(^B1=)(^C4^AC)(^C5^AD)]
-@$$}2}@
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
@@ -20,63 +20,63 @@ const results = [ { email: "d <ema@foo.i
                   { email: "DisplayName1 <PrimaryEmail1@test.invalid>", // 5
                     dirName: kCABData.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 } // 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] },
+const firstNames = [ { search: "f",      expected: [0, 1, 2, 3, 4, 5, 9] },
+                     { search: "fi",     expected: [0, 1, 3, 4, 5] },
+                     { search: "fir",    expected: [0, 1, 4, 5] },
+                     { search: "firs",   expected: [0, 1, 5] },
+                     { search: "first",  expected: [1, 5] },
                      { search: "firstn", expected: [5] } ];
 
-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] },
+const lastNames = [ { search: "l",      expected: [6, 7, 8, 9, 0, 1, 2, 3, 4, 5] },
+                    { search: "la",     expected: [0, 2, 3, 4, 5] },
+                    { search: "las",    expected: [0, 3, 4, 5] },
+                    { search: "last",   expected: [0, 4, 5] },
+                    { search: "lastn",  expected: [0, 5] },
                     { search: "lastna", expected: [5]} ];
 
-const displayNames = [ { search: "d",      expected: [5, 0, 1, 2, 3, 4, 9] },
-                       { search: "di",     expected: [5, 1, 2, 3, 4] },
-                       { search: "dis",    expected: [5, 2, 3, 4] },
-                       { search: "disp",   expected: [5, 3, 4]},
-                       { search: "displ",  expected: [5, 4]},
+const displayNames = [ { search: "d",      expected: [0, 1, 2, 3, 4, 5, 9] },
+                       { search: "di",     expected: [1, 2, 3, 4, 5] },
+                       { search: "dis",    expected: [2, 3, 4, 5] },
+                       { search: "disp",   expected: [3, 4, 5]},
+                       { search: "displ",  expected: [4, 5]},
                        { search: "displa", expected: [5]} ];
 
-const nickNames = [ { search: "n",      expected: [4, 5, 0, 1, 2, 3] },
-                    { search: "ni",     expected: [0, 5, 1, 2, 3] },
-                    { search: "nic",    expected: [1, 5, 2, 3] },
-                    { search: "nick",   expected: [2, 5, 3] },
+const nickNames = [ { search: "n",      expected: [4, 0, 1, 2, 3, 5] },
+                    { search: "ni",     expected: [0, 1, 2, 3, 5] },
+                    { search: "nic",    expected: [1, 2, 3, 5] },
+                    { search: "nick",   expected: [2, 3, 5] },
                     { search: "nickn",  expected: [3, 5] },
                     { search: "nickna", expected: [5] } ];
 
 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: [6, 7, 8, 5, 0, 1, 2, 3, 4] },
+const lists = [ { search: "li", expected: [6, 7, 8, 0, 1, 2, 3, 4, 5] },
                 { search: "lis", expected: [6, 7] },
                 { search: "list", expected: [6] },
-                { search: "t", expected: [6, 7, 8, 9, 5, 0, 1, 4] },
+                { search: "t", expected: [6, 7, 8, 9, 0, 1, 4, 5] },
                 { 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] },
+const bothNames = [ { search: "f l",            expected: [0, 1, 2, 3, 4, 5, 9] },
+                    { search: "l f",            expected: [0, 1, 2, 3, 4, 5, 9] },
                     { search: "firstn lastna",  expected: [5] },
                     { search: "lastna firstna", expected: [5] } ];
 
 const inputs = [ firstNames, lastNames, displayNames, nickNames, emails,
                  lists, bothNames ];
 
 function acObserver() {}
 
@@ -85,25 +85,153 @@ acObserver.prototype = {
   _result: null,
 
   onSearchResult: function (aSearch, aResult) {
     this._search = aSearch;
     this._result = aResult;
   }
 };
 
-function run_test() {
-  // Copy the data files into place
-  var testAB = do_get_file("data/autocomplete.mab");
+const PAB_CARD_DATA = [
+  {
+    "FirstName": "firs",
+    "LastName": "lastn",
+    "DisplayName": "d",
+    "NickName": "ni",
+    "PrimaryEmail": "ema@foo.invalid",
+    "PreferDisplayName": true,
+    "PopularityIndex": 0
+  },
+  {
+    "FirstName": "first",
+    "LastName": "l",
+    "DisplayName": "di",
+    "NickName": "nic",
+    "PrimaryEmail": "emai@foo.invalid",
+    "PreferDisplayName": true,
+    "PopularityIndex": 0
+  },
+  {
+    "FirstName": "f",
+    "LastName": "la",
+    "DisplayName": "dis",
+    "NickName": "nick",
+    "PrimaryEmail": "email@foo.invalid",
+    "PreferDisplayName": true,
+    "PopularityIndex": 0
+  },
+  {
+    "FirstName": "fi",
+    "LastName": "las",
+    "DisplayName": "disp",
+    "NickName": "nickn",
+    "PrimaryEmail": "e@foo.invalid",
+    "PreferDisplayName": true,
+    "PopularityIndex": 0
+  },
+  {
+    "FirstName": "fir",
+    "LastName": "last",
+    "DisplayName": "displ",
+    "NickName": "n",
+    "PrimaryEmail": "em@foo.invalid",
+    "PreferDisplayName": true,
+    "PopularityIndex": 0
+  }
+];
 
-  testAB.copyTo(do_get_profile(), kPABData.fileName);
+const PAB_LIST_DATA =  [
+  {
+    "dirName" : "t",
+    "listNickName": null,
+    "description": "list"
+  },
+  {
+    "dirName" : "te",
+    "listNickName": null,
+    "description": "lis"
+  },
+  {
+    "dirName" : "tes",
+    "listNickName": null,
+    "description": "li"
+  },
+  {
+    "dirName" : "test",
+    "listNickName": "abcdef",
+    "description": "l"
+  }
+];
+
+const CAB_CARD_DATA = [
+  {
+    "FirstName": "FirstName1",
+    "LastName": "LastName1",
+    "DisplayName": "DisplayName1",
+    "NickName": "NickName1",
+    "PrimaryEmail": "PrimaryEmail1@test.invalid",
+    "PreferDisplayName": true,
+    "PopularityIndex": 0
+  },
+  {
+    "FirstName": "Empty",
+    "LastName": "Email",
+    "DisplayName": "Empty Email",
+    "PreferDisplayName": true,
+    "PopularityIndex": 0
+  }
+];
+
+const CAB_LIST_DATA = [];
 
-  testAB = do_get_file("data/autocomplete2.mab");
+const ABMDB_PREFIX = "moz-abmdbdirectory://";
+
+function setupAddressBookData(aDirURI, aCardData, aMailListData) {
+  let ab = MailServices.ab.getDirectory(aDirURI);
+
+  // Getting all directories ensures we create all ABs because mailing
+  // lists need help initialising themselves
+  MailServices.ab.directories;
+
+  let childCards0 = ab.childCards;
+  while (childCards0.hasMoreElements()) {
+    let c = childCards0.getNext().QueryInterface(Components.interfaces.nsIAbCard);
+    ab.dropCard(c, false);
+  }
 
-  testAB.copyTo(do_get_profile(), kCABData.fileName);
+  aCardData.forEach(function(cd) {
+    let card = Components.classes["@mozilla.org/addressbook/cardproperty;1"]
+      .createInstance(Components.interfaces.nsIAbCard);
+    for (var prop in cd) {
+      card.setProperty(prop, cd[prop]);
+    }
+    ab.addCard(card);
+  });
+
+  aMailListData.forEach(function(ld) {
+    let list = Components.classes["@mozilla.org/addressbook/directoryproperty;1"]
+      .createInstance(Components.interfaces.nsIAbDirectory);
+    list.isMailList = true;
+    for (var prop in ld) {
+      list[prop] = ld[prop];
+    }
+    ab.addMailList(list);
+  });
+
+  let childCards = ab.childCards;
+  while (childCards.hasMoreElements()) {
+    let c = childCards.getNext().QueryInterface(Components.interfaces.nsIAbCard);
+  }
+}
+
+function run_test() {
+  // Set up addresses for in the personal address book.
+  setupAddressBookData(kPABData.URI, PAB_CARD_DATA, PAB_LIST_DATA);
+  // ... and collected addresses address book.
+  setupAddressBookData(kCABData.URI, CAB_CARD_DATA, CAB_LIST_DATA);
 
   // Test - Create a new search component
 
   var acs = Components.classes["@mozilla.org/autocomplete/search;1?name=addrbook"]
     .getService(Components.interfaces.nsIAutoCompleteSearch);
 
   var obs = new acObserver();
   let obsNews = new acObserver();
@@ -215,21 +343,21 @@ function run_test() {
   do_check_eq(obs._result.defaultIndex, 0);
 
   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) {
+    let prevRes = obs._result;
     print("Search #" + index + ": search=" + element.search);
-    acs.startSearch(element.search, param, null, obs);
+    acs.startSearch(element.search, param, prevRes, 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);
@@ -251,41 +379,40 @@ function run_test() {
     }
   }
   function checkInputSet(element, index, array) {
     element.forEach(checkInputItem);
   }
 
   inputs.forEach(checkInputSet);
 
-
   // Test - Popularity Index
   print("Checking by popularity index:");
   let pab = MailServices.ab.getDirectory(kPABData.URI);
 
   var childCards = pab.childCards;
 
   while (childCards.hasMoreElements()) {
     var card = childCards.getNext().QueryInterface(Ci.nsIAbCard);
 
     if (card.isMailList)
       continue;
 
     switch (card.displayName) {
-    case "dis":
-    case "disp":
+    case "dis": // 2
+    case "disp": // 3
       card.setProperty("PopularityIndex", 4);
       break;
-    case "displ":
+    case "displ": // 4
       card.setProperty("PopularityIndex", 5);
       break;
-    case "d":
+    case "d": // 0
       card.setProperty("PopularityIndex", 1);
       break;
-    case "di":
+    case "di": // 1
       card.setProperty("PopularityIndex", 20);
       break;
     default:
       break;
     }
 
     pab.modifyCard(card);
   }
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
@@ -107,17 +107,18 @@ function run_test()
 
   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.setProperty("SecondEmail", element.secondEmail);
+    if ("secondEmail" in element)
+      card.setProperty("SecondEmail", element.secondEmail);
     card.displayName = element.displayName;
     if ("popularityIndex" in element)
       card.setProperty("PopularityIndex", element.popularityIndex);
     card.firstName = element.firstName;
 
     ab.addCard(card);
   }
 
@@ -129,16 +130,20 @@ function run_test()
   var obs = new acObserver();
 
   print("Checking Initial Searches");
 
   function checkSearch(element, index, array) {
     print("Search #" + index + ": search=" + element);
     acs.startSearch(element, JSON.stringify({ type: "addr_to"  }), null, obs);
 
+    for (var i = 0; i < obs._result.matchCount; i++) {
+      print("... got " + i + ": " + obs._result.getValueAt(i));
+    }
+
     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);
 
     for (var i = 0; i < expectedResults[index].length; ++i) {
       do_check_eq(obs._result.getValueAt(i), expectedResults[index][i]);
--- a/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
@@ -100,17 +100,17 @@ const cards = [
 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] },
   // expecting nickname to score highest.
-  { search: "sh", expected: [15, 14, 16, 2, 10, 7] },
+  { search: "sh", expected: [15, 14, 2, 16, 10, 7] },
   { search: "st", expected: [3,8] },
   { search: "paul mary", expected: [11, 12] },
   { search: "\"paul mary\"", expected: [11] },
   { search: "\"iron man\" mr \"exp dev\"", expected: [13] },
   { search: "short", expected: [14] }
 ];
 
 function acObserver() {}
@@ -139,17 +139,18 @@ function run_test()
     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;
     card.lastName = element.lastName;
-    card.setProperty("NickName", element.nickName);
+    if ("nickName" in element)
+      card.setProperty("NickName", element.nickName);
 
     ab.addCard(card);
   }
 
   cards.forEach(createAndAddCard);
 
   // Test - duplicate elements
 
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests for nsAbAutoCompleteSearch - tests searching in address
+ * books for autocomplete matches, and checks sort order is correct
+ * according to scores.
+ */
+
+const ACR = Components.interfaces.nsIAutoCompleteResult;
+
+// Input and results arrays for the autocomplete tests.
+
+// Note the expected arrays are in expected sort order as well.
+
+const results =  [
+  { email: "Tomas Doe <tomez.doe@foo.invalid>" }, // 0
+  { email: "Tomas Doe <tomez.doe@foo2.invalid>" }, // 1
+  { email: "Tomas Doe <tomez.doe@b.example.com>" }, // 2
+  { email: "Tomas Doe <tomez.doe@a.example.com>" }, // 3
+  { email: "Tomek Smith <tomek@example.com>" } // 4
+]
+
+const inputs = [
+  [
+    { search: "t",            expected: [2, 3, 0, 1, 4] },
+    { search: "tom",          expected: [0, 1, 2, 3, 4] },
+    { search: "tomek",        expected: [4] }
+  ]
+];
+
+function acObserver() {}
+
+acObserver.prototype = {
+  _search: null,
+  _result: null,
+
+  onSearchResult: function (aSearch, aResult) {
+    this._search = aSearch;
+    this._result = aResult;
+  }
+};
+
+const PAB_CARD_DATA = [
+  {
+    "FirstName": "Tomas",
+    "LastName": "Doe",
+    "DisplayName": "Tomas Doe",
+    "NickName": "tom",
+    "PrimaryEmail": "tomez.doe@foo.invalid",
+    "SecondEmail": "tomez.doe@foo2.invalid",
+    "PreferDisplayName": true,
+    "PopularityIndex": 10
+  },
+  {
+    "FirstName": "Tomas",
+    "LastName": "Doe",
+    "DisplayName": "Tomas Doe",
+    "PrimaryEmail": "tomez.doe@b.example.com",
+    "SecondEmail": "tomez.doe@a.example.com",
+    "PreferDisplayName": true,
+    "PopularityIndex": 200
+  },
+  {
+    "FirstName": "Tomek",
+    "LastName": "Smith",
+    "DisplayName": "Tomek Smith",
+    "PrimaryEmail": "tomek@example.com",
+    "PreferDisplayName": true,
+    "PopularityIndex": 3
+  }
+];
+
+const ABMDB_PREFIX = "moz-abmdbdirectory://";
+
+function setupAddressBookData(aDirURI, aCardData, aMailListData) {
+  let ab = MailServices.ab.getDirectory(aDirURI);
+
+  // Getting all directories ensures we create all ABs because mailing
+  // lists need help initialising themselves
+  MailServices.ab.directories;
+
+  let childCards0 = ab.childCards;
+  while (childCards0.hasMoreElements()) {
+    let c = childCards0.getNext().QueryInterface(Components.interfaces.nsIAbCard);
+    ab.dropCard(c, false);
+  }
+
+  aCardData.forEach(function(cd) {
+    let card = Components.classes["@mozilla.org/addressbook/cardproperty;1"]
+      .createInstance(Components.interfaces.nsIAbCard);
+    for (var prop in cd) {
+      card.setProperty(prop, cd[prop]);
+    }
+    ab.addCard(card);
+  });
+
+  aMailListData.forEach(function(ld) {
+    let list = Components.classes["@mozilla.org/addressbook/directoryproperty;1"]
+      .createInstance(Components.interfaces.nsIAbDirectory);
+    list.isMailList = true;
+    for (var prop in ld) {
+      list[prop] = ld[prop];
+    }
+    ab.addMailList(list);
+  });
+
+  let childCards = ab.childCards;
+  while (childCards.hasMoreElements()) {
+    let c = childCards.getNext().QueryInterface(Components.interfaces.nsIAbCard);
+  }
+}
+
+function run_test() {
+  // Set up addresses for in the personal address book.
+  setupAddressBookData(kPABData.URI, PAB_CARD_DATA, []);
+
+  // Test - Create a new search component
+
+  var acs = Components.classes["@mozilla.org/autocomplete/search;1?name=addrbook"]
+    .getService(Components.interfaces.nsIAutoCompleteSearch);
+
+  var obs = new acObserver();
+  let obsNews = new acObserver();
+  let obsFollowup = new acObserver();
+
+  let param = JSON.stringify({ type: "addr_to" });
+
+  // Now check multiple matches
+  function checkInputItem(element, index, array) {
+    let prevRes = obs._result;
+    print("Search #" + index + ": search=" + element.search);
+    acs.startSearch(element.search, param, prevRes, 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) {
+      do_check_eq(obs._result.getValueAt(i), results[element.expected[i]].email);
+      do_check_eq(obs._result.getLabelAt(i), results[element.expected[i]].email);
+      do_check_eq(obs._result.getCommentAt(i), "");
+      do_check_eq(obs._result.getStyleAt(i), "local-abook");
+      do_check_eq(obs._result.getImageAt(i), "");
+    }
+  }
+  function checkInputSet(element, index, array) {
+    element.forEach(checkInputItem);
+  }
+
+  inputs.forEach(checkInputSet);
+};
--- a/mailnews/addrbook/test/unit/xpcshell.ini
+++ b/mailnews/addrbook/test/unit/xpcshell.ini
@@ -19,12 +19,13 @@ support-files = data/*
 [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_nsAbAutoCompleteSearch7.js]
 [test_nsAbManager1.js]
 [test_nsAbManager2.js]
 [test_nsIAbCard.js]
 [test_uuid.js]