Bug 1464454 - Expire adaptive history after 90 days and limit the number of top adaptive history matches in the Address Bar. r=adw draft
authorMarco Bonardo <mbonardo@mozilla.com>
Mon, 28 May 2018 17:38:11 +0200
changeset 802783 490d6373b2001101b961e4d2d12b6c02272300a4
parent 802711 9900cebb1f9000bd05731ba67736b7c51f7eb812
push id111955
push usermak77@bonardo.net
push dateFri, 01 Jun 2018 12:34:44 +0000
reviewersadw
bugs1464454
milestone62.0a1
Bug 1464454 - Expire adaptive history after 90 days and limit the number of top adaptive history matches in the Address Bar. r=adw Reduce adaptive history domination of the Address Bar results by expiring unused entries sooner and limiting the number of top adaptive results. MozReview-Commit-ID: EGOs6rVYGj6
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
toolkit/components/places/tests/unifiedcomplete/test_adaptive.js
toolkit/components/places/tests/unifiedcomplete/test_adaptive_behaviors.js
toolkit/components/places/tests/unifiedcomplete/test_adaptive_limited.js
toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
toolkit/components/places/tests/unit/test_adaptive.js
toolkit/components/places/tests/unit/test_adaptive_bug527311.js
toolkit/components/places/tests/unit/xpcshell.ini
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -55,16 +55,17 @@ const PREF_OTHER_DEFAULTS = new Map([
   ["keyword.enabled", true],
 ]);
 
 // AutoComplete query type constants.
 // Describes the various types of queries that we can process rows for.
 const QUERYTYPE_FILTERED            = 0;
 const QUERYTYPE_AUTOFILL_ORIGIN     = 1;
 const QUERYTYPE_AUTOFILL_URL        = 2;
+const QUERYTYPE_ADAPTIVE            = 3;
 
 // This separator is used as an RTL-friendly way to split the title and tags.
 // It can also be used by an nsIAutoCompleteResult consumer to re-split the
 // "comment" back into the title and the tag.
 const TITLE_TAGS_SEPARATOR = " \u2013 ";
 
 // Telemetry probes.
 const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
@@ -925,16 +926,20 @@ function Search(searchString, searchPara
       this._previousSearchMatchTypes.push(MATCHTYPE.SUGGESTION);
     } else if (style.includes("extension")) {
       this._previousSearchMatchTypes.push(MATCHTYPE.EXTENSION);
     } else {
       this._previousSearchMatchTypes.push(MATCHTYPE.GENERAL);
     }
   }
 
+  // Used to limit the number of adaptive results.
+  this._adaptiveCount = 0;
+  this._extraAdaptiveRows = [];
+
   // This is a replacement for this._result.matchCount, to be used when you need
   // to check how many "current" matches have been inserted.
   // Indeed this._result.matchCount may include matches from the previous search.
   this._currentMatchCount = 0;
 
   // These are used to avoid adding duplicate entries to the results.
   this._usedURLs = [];
   this._usedPlaceIds = new Set();
@@ -1206,16 +1211,22 @@ Search.prototype = {
 
     // Finally run all the other queries.
     for (let [query, params] of queries) {
       await conn.executeCached(query, params, this._onResultRow.bind(this));
       if (!this.pending)
         return;
     }
 
+    // If we have some unused adaptive matches, add them now.
+    while (this._extraAdaptiveRows.length &&
+           this._currentMatchCount < Prefs.get("maxRichResults")) {
+      this._addFilteredQueryMatch(this._extraAdaptiveRows.shift());
+    }
+
     // Ideally we should wait until MATCH_BOUNDARY_ANYWHERE, but that query
     // may be really slow and we may end up showing old results for too long.
     this._cleanUpNonCurrentMatches(MATCHTYPE.GENERAL);
 
     // If we do not have enough results, and our match type is
     // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
     // results.
     let count = this._counts[MATCHTYPE.GENERAL] + this._counts[MATCHTYPE.HEURISTIC];
@@ -1801,16 +1812,19 @@ Search.prototype = {
       case QUERYTYPE_AUTOFILL_ORIGIN:
         this._result.setDefaultIndex(0);
         this._addOriginAutofillMatch(row);
         break;
       case QUERYTYPE_AUTOFILL_URL:
         this._result.setDefaultIndex(0);
         this._addURLAutofillMatch(row);
         break;
+      case QUERYTYPE_ADAPTIVE:
+        this._addAdaptiveQueryMatch(row);
+        break;
       case QUERYTYPE_FILTERED:
         this._addFilteredQueryMatch(row);
         break;
     }
     // If the search has been canceled by the user or by _addMatch, or we
     // fetched enough results, we can stop the underlying Sqlite query.
     let count = this._counts[MATCHTYPE.GENERAL] + this._counts[MATCHTYPE.HEURISTIC];
     if (!this.pending || count >= Prefs.get("maxRichResults")) {
@@ -2111,16 +2125,32 @@ Search.prototype = {
       finalCompleteValue,
       comment,
       frecency,
       style: ["autofill"].concat(extraStyles).join(" "),
       icon: iconHelper(finalCompleteValue),
     });
   },
 
+  // This is the same as _addFilteredQueryMatch, but it only returns a few
+  // results, caching the others. If at the end we don't find other results, we
+  // can add these.
+  _addAdaptiveQueryMatch(row) {
+    // Allow one quarter of the results to be adaptive results.
+    // Note: ideally adaptive results should have their own provider and the
+    // results muxer should decide what to show.  But that's too complex to
+    // support in the current code, so that's left for a future refactoring.
+    if (this._adaptiveCount < Math.ceil(Prefs.get("maxRichResults") / 4)) {
+      this._addFilteredQueryMatch(row);
+    } else {
+      this._extraAdaptiveRows.push(row);
+    }
+    this._adaptiveCount++;
+  },
+
   _addFilteredQueryMatch(row) {
     let match = {};
     match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
     let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
     let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
     let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
     let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
     let bookmarkTitle = bookmarked ?
@@ -2273,17 +2303,17 @@ Search.prototype = {
    *         database with and an object containing the params to bound.
    */
   get _adaptiveQuery() {
     return [
       SQL_ADAPTIVE_QUERY,
       {
         parent: PlacesUtils.tagsFolderId,
         search_string: this._searchString,
-        query_type: QUERYTYPE_FILTERED,
+        query_type: QUERYTYPE_ADAPTIVE,
         matchBehavior: this._matchBehavior,
         searchBehavior: this._behavior,
         userContextId: this._userContextId,
         maxResults: Prefs.get("maxRichResults")
       }
     ];
   },
 
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -105,16 +105,18 @@ using namespace mozilla::places;
 #define PREF_FREC_UNVISITED_TYPED_BONUS         "places.frecency.unvisitedTypedBonus"
 #define PREF_FREC_UNVISITED_TYPED_BONUS_DEF     200
 #define PREF_FREC_RELOAD_VISIT_BONUS            "places.frecency.reloadVisitBonus"
 #define PREF_FREC_RELOAD_VISIT_BONUS_DEF        0
 
 // This is a 'hidden' pref for the purposes of unit tests.
 #define PREF_FREC_DECAY_RATE     "places.frecency.decayRate"
 #define PREF_FREC_DECAY_RATE_DEF 0.975f
+// An adaptive history entry is removed if unused for these many days.
+#define ADAPTIVE_HISTORY_EXPIRE_DAYS 90
 
 // In order to avoid calling PR_now() too often we use a cached "now" value
 // for repeating stuff.  These are milliseconds between "now" cache refreshes.
 #define RENEW_CACHED_NOW_TIMEOUT ((int32_t)3 * PR_MSEC_PER_SEC)
 
 // character-set annotation
 #define CHARSET_ANNO NS_LITERAL_CSTRING("URIProperties/characterSet")
 
@@ -2516,60 +2518,70 @@ public:
 };
 
 nsresult
 nsNavHistory::DecayFrecency()
 {
   nsresult rv = FixInvalidFrecencies();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  float decayRate = Preferences::GetFloat(PREF_FREC_DECAY_RATE, PREF_FREC_DECAY_RATE_DEF);
+  float decayRate = Preferences::GetFloat(PREF_FREC_DECAY_RATE,
+                                          PREF_FREC_DECAY_RATE_DEF);
+  if (decayRate > 1.0f) {
+    MOZ_ASSERT(false, "The frecency decay rate should not be greater than 1.0");
+    decayRate = PREF_FREC_DECAY_RATE_DEF;
+  }
 
   // Globally decay places frecency rankings to estimate reduced frecency
   // values of pages that haven't been visited for a while, i.e., they do
   // not get an updated frecency.  A scaling factor of .975 results in .5 the
   // original value after 28 days.
   // When changing the scaling factor, ensure that the barrier in
   // moz_places_afterupdate_frecency_trigger still ignores these changes.
   nsCOMPtr<mozIStorageAsyncStatement> decayFrecency = mDB->GetAsyncStatement(
     "UPDATE moz_places SET frecency = ROUND(frecency * :decay_rate) "
     "WHERE frecency > 0"
   );
   NS_ENSURE_STATE(decayFrecency);
-
   rv = decayFrecency->BindDoubleByName(NS_LITERAL_CSTRING("decay_rate"),
                                        static_cast<double>(decayRate));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Decay potentially unused adaptive entries (e.g. those that are at 1)
   // to allow better chances for new entries that will start at 1.
   nsCOMPtr<mozIStorageAsyncStatement> decayAdaptive = mDB->GetAsyncStatement(
-    "UPDATE moz_inputhistory SET use_count = use_count * .975"
+    "UPDATE moz_inputhistory SET use_count = use_count * :decay_rate"
   );
   NS_ENSURE_STATE(decayAdaptive);
+  rv = decayAdaptive->BindDoubleByName(NS_LITERAL_CSTRING("decay_rate"),
+                                       static_cast<double>(decayRate));
+  NS_ENSURE_SUCCESS(rv, rv);
 
   // Delete any adaptive entries that won't help in ordering anymore.
   nsCOMPtr<mozIStorageAsyncStatement> deleteAdaptive = mDB->GetAsyncStatement(
-    "DELETE FROM moz_inputhistory WHERE use_count < .01"
+    "DELETE FROM moz_inputhistory WHERE use_count < :use_count"
   );
   NS_ENSURE_STATE(deleteAdaptive);
+  rv = deleteAdaptive->BindDoubleByName(NS_LITERAL_CSTRING("use_count"),
+                                        std::pow(static_cast<double>(decayRate),
+                                                 ADAPTIVE_HISTORY_EXPIRE_DAYS));
+  NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
   if (!conn) {
     return NS_ERROR_UNEXPECTED;
   }
   mozIStorageBaseStatement *stmts[] = {
     decayFrecency.get(),
     decayAdaptive.get(),
     deleteAdaptive.get()
   };
   nsCOMPtr<mozIStoragePendingStatement> ps;
   RefPtr<PlacesDecayFrecencyCallback> cb = new PlacesDecayFrecencyCallback();
-  rv = conn->ExecuteAsync(stmts, ArrayLength(stmts), cb,
-                                     getter_AddRefs(ps));
+  rv = conn->ExecuteAsync(stmts, ArrayLength(stmts), cb, getter_AddRefs(ps));
   NS_ENSURE_SUCCESS(rv, rv);
 
   mDecayFrecencyPendingCount++;
 
   return NS_OK;
 }
 
 void
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -487,8 +487,33 @@ add_task(async function ensure_search_en
   for (let engine of Services.search.getEngines()) {
     Services.search.removeEngine(engine);
   }
   Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
                                        "http://s.example.com/search");
   let engine = Services.search.getEngineByName("MozSearch");
   Services.search.currentEngine = engine;
 });
+
+/**
+ * Add a adaptive result for a given (url, string) tuple.
+ * @param {string} aUrl
+ *        The url to add an adaptive result for.
+ * @param {string} aSearch
+ *        The string to add an adaptive result for.
+ * @resolves When the operation is complete.
+ */
+function addAdaptiveFeedback(aUrl, aSearch) {
+  let promise = TestUtils.topicObserved("places-autocomplete-feedback-updated");
+  let thing = {
+    QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompleteInput,
+                                            Ci.nsIAutoCompletePopup,
+                                            Ci.nsIAutoCompleteController]),
+    get popup() { return thing; },
+    get controller() { return thing; },
+    popupOpen: true,
+    selectedIndex: 0,
+    getValueAt: () => aUrl,
+    searchString: aSearch
+  };
+  Services.obs.notifyObservers(thing, "autocomplete-will-enter-text");
+  return promise;
+}
rename from toolkit/components/places/tests/unit/test_adaptive.js
rename to toolkit/components/places/tests/unifiedcomplete/test_adaptive.js
--- a/toolkit/components/places/tests/unit/test_adaptive.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_adaptive.js
@@ -13,80 +13,35 @@
  *
  * This also tests bug 395735 for the instrumentation feedback mechanism.
  *
  * Bug 411293 is tested to make sure the drop down strongly prefers previously
  * typed pages that have been selected and are moved to the top with adaptive
  * learning.
  */
 
-function AutoCompleteInput(aSearches) {
-  this.searches = aSearches;
-}
-AutoCompleteInput.prototype = {
-  constructor: AutoCompleteInput,
-
-  get minResultsForPopup() {
-    return 0;
-  },
-  get timeout() {
-    return 10;
-  },
-  get searchParam() {
-    return "";
-  },
-  get textValue() {
-    return "";
-  },
-  get disableAutoComplete() {
-    return false;
-  },
-  get completeDefaultIndex() {
-    return false;
-  },
-
-  get searchCount() {
-    return this.searches.length;
-  },
-  getSearchAt(aIndex) {
-    return this.searches[aIndex];
-  },
-
-  onSearchBegin() {},
-  onSearchComplete() {},
-
-  get popupOpen() {
-    return false;
-  },
-  popup: {
-    set selectedIndex(aIndex) {},
-    invalidate() {},
-    QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompletePopup])
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompleteInput])
-};
-
 /**
  * Checks that autocomplete results are ordered correctly.
  */
 function ensure_results(expected, searchTerm, callback) {
   let controller = Cc["@mozilla.org/autocomplete/controller;1"].
                    getService(Ci.nsIAutoCompleteController);
 
   // Make an AutoCompleteInput that uses our searches
   // and confirms results on search complete.
   let input = new AutoCompleteInput(["unifiedcomplete"]);
 
   controller.input = input;
 
   input.onSearchComplete = function() {
     Assert.equal(controller.searchStatus,
-                 Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
-    Assert.equal(controller.matchCount, expected.length);
+                 Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH,
+                 "The search should be complete");
+    Assert.equal(controller.matchCount, expected.length,
+                 "All the expected results should have been found");
     for (let i = 0; i < controller.matchCount; i++) {
       print("Testing for '" + expected[i].uri.spec + "' got '" + controller.getValueAt(i) + "'");
       Assert.equal(controller.getValueAt(i), expected[i].uri.spec);
       Assert.equal(controller.getStyleAt(i), expected[i].style);
     }
 
     callback();
   };
@@ -363,25 +318,45 @@ var tests = [
       makeResult(uri1, "tag"),
       makeResult(uri2),
     ];
     observer.search = s0;
     observer.runCount = c1 + c2;
     await task_setCountRank(uri1, c1, c1, s2, "tag");
     await task_setCountRank(uri2, c1, c2, s2);
   },
+  // Test that many results are all shown if no other results are available.
+  async function() {
+    print("Test 14 -  many results");
+    let n = 10;
+    observer.results = Array(n).fill(0).map(
+      (e, i) => makeResult(Services.io.newURI("http://site.tld/" + i))
+    );
+    observer.search = s2;
+    observer.runCount = n * (n + 1) / 2;
+    let c = n;
+    for (let result of observer.results) {
+      task_setCountRank(result.uri, c, c, s2);
+      c--;
+    }
+  },
 ];
 
 /**
  * Test adaptive autocomplete.
  */
 add_task(async function test_adaptive() {
   // Disable autoFill for this test.
   Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
-  registerCleanupFunction(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+
+  registerCleanupFunction(async function() {
+    await PlacesUtils.bookmarks.eraseEverything();
+    await PlacesUtils.history.clear();
+  });
+
   for (let test of tests) {
     // Cleanup.
     await PlacesUtils.bookmarks.eraseEverything();
 
     let types = ["history", "bookmark", "openpage"];
     for (let type of types) {
       Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
     }
rename from toolkit/components/places/tests/unit/test_adaptive_bug527311.js
rename to toolkit/components/places/tests/unifiedcomplete/test_adaptive_behaviors.js
--- a/toolkit/components/places/tests/unit/test_adaptive_bug527311.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_adaptive_behaviors.js
@@ -1,137 +1,40 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
+// Test for Bug 527311
+// Addressbar suggests adaptive results regardless of the requested behavior.
+
 const TEST_URL = "http://adapt.mozilla.org/";
 const SEARCH_STRING = "adapt";
 const SUGGEST_TYPES = ["history", "bookmark", "openpage"];
 
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-const PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC =
-  "places-autocomplete-feedback-updated";
-
-function cleanup() {
-  for (let type of SUGGEST_TYPES) {
-    Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
-  }
-}
-
-function AutoCompleteInput(aSearches) {
-  this.searches = aSearches;
-}
-AutoCompleteInput.prototype = {
-  constructor: AutoCompleteInput,
-  searches: null,
-  minResultsForPopup: 0,
-  timeout: 10,
-  searchParam: "",
-  textValue: "",
-  disableAutoComplete: false,
-  completeDefaultIndex: false,
-
-  get searchCount() {
-    return this.searches.length;
-  },
-
-  getSearchAt: function ACI_getSearchAt(aIndex) {
-    return this.searches[aIndex];
-  },
-
-  onSearchComplete: function ACI_onSearchComplete() {},
-
-  popupOpen: false,
-
-  popup: {
-    setSelectedIndex() {},
-    invalidate() {},
-
-    QueryInterface: ChromeUtils.generateQI(["nsIAutoCompletePopup"])
-  },
-
-  onSearchBegin() {},
-
-  QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteInput"])
-};
-
+add_task(async function test_adaptive_search_specific() {
+  Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
 
-function check_results() {
-  return new Promise(resolve => {
-    let controller = Cc["@mozilla.org/autocomplete/controller;1"].
-                     getService(Ci.nsIAutoCompleteController);
-    let input = new AutoCompleteInput(["unifiedcomplete"]);
-    controller.input = input;
-
-    input.onSearchComplete = function() {
-      Assert.equal(controller.searchStatus,
-                   Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
-      Assert.equal(controller.matchCount, 0);
-
-      PlacesUtils.bookmarks.eraseEverything().then(() => {
-        cleanup();
-        resolve();
-      });
-    };
-
-    controller.startSearch(SEARCH_STRING);
-  });
-}
-
-
-function addAdaptiveFeedback(aUrl, aSearch) {
-  return new Promise(resolve => {
-    let observer = {
-      observe(aSubject, aTopic, aData) {
-        Services.obs.removeObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC);
-        do_timeout(0, resolve);
-      }
-    };
-    Services.obs.addObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC);
-
-    let thing = {
-      QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompleteInput,
-                                              Ci.nsIAutoCompletePopup,
-                                              Ci.nsIAutoCompleteController]),
-      get popup() { return thing; },
-      get controller() { return thing; },
-      popupOpen: true,
-      selectedIndex: 0,
-      getValueAt: () => aUrl,
-      searchString: aSearch
-    };
-
-    Services.obs.notifyObservers(thing, "autocomplete-will-enter-text");
-  });
-}
-
-
-add_task(function init() {
-  Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
-  registerCleanupFunction(() => {
-    Services.prefs.clearUserPref("browser.urlbar.autoFill");
-  });
-});
-
-add_task(async function test_adaptive_search_specific() {
   // Add a bookmark to our url.
   await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     title: "test_book",
     url: TEST_URL,
   });
+  registerCleanupFunction(async function() {
+    await PlacesUtils.bookmarks.eraseEverything();
+  });
 
   // We want to search only history.
   for (let type of SUGGEST_TYPES) {
     type == "history" ? Services.prefs.setBoolPref("browser.urlbar.suggest." + type, true)
                       : Services.prefs.setBoolPref("browser.urlbar.suggest." + type, false);
   }
 
   // Add an adaptive entry.
   await addAdaptiveFeedback(TEST_URL, SEARCH_STRING);
 
-  await check_results();
-
-  await PlacesTestUtils.promiseAsyncUpdates();
+  await check_autocomplete({
+    search: SEARCH_STRING,
+    matches: [],
+  });
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_adaptive_limited.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that top adaptive results are limited, remaining ones are enqueued.
+
+add_task(async function() {
+  Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+  let n = 10;
+  let uris = Array(n).fill(0).map((e, i) => "http://site.tld/" + i);
+
+  // Add a bookmark to one url.
+  let bm = await PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+    title: "test_book",
+    url: uris.shift(),
+  });
+
+  // Make remaining ones adaptive results.
+  for (let uri of uris) {
+    await PlacesTestUtils.addVisits(uri);
+    await addAdaptiveFeedback(uri, "book");
+  }
+
+  registerCleanupFunction(async function() {
+    await PlacesUtils.bookmarks.eraseEverything();
+    await PlacesUtils.history.clear();
+  });
+
+  let matches = uris.map(uri => ({ uri: Services.io.newURI(uri),
+                                   title: "test visit for " + uri }));
+  let book_index = Math.ceil(Services.prefs.getIntPref("browser.urlbar.maxRichResults") / 4);
+  matches.splice(book_index, 0, { uri: Services.io.newURI(bm.url.href),
+                                  title: "test_book", "style": ["bookmark"] });
+
+  await check_autocomplete({
+    search: "book",
+    matches,
+    checkSorting: true,
+  });
+});
--- a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
+++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
@@ -8,16 +8,19 @@ support-files =
   !/toolkit/components/places/tests/favicons/favicon-normal16.png
   autofill_tasks.js
 
 [test_416211.js]
 [test_416214.js]
 [test_417798.js]
 [test_418257.js]
 [test_422277.js]
+[test_adaptive.js]
+[test_adaptive_behaviors.js]
+[test_adaptive_limited.js]
 [test_autocomplete_functional.js]
 [test_autocomplete_stopSearch_no_throw.js]
 [test_autofill_origins.js]
 [test_autofill_search_engines.js]
 [test_autofill_urls.js]
 [test_avoid_middle_complete.js]
 [test_avoid_stripping_to_empty_tokens.js]
 [test_casing.js]
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -37,18 +37,16 @@ skip-if = os == "linux"
 [test_454977.js]
 [test_463863.js]
 [test_485442_crash_bug_nsNavHistoryQuery_GetUri.js]
 [test_486978_sort_by_date_queries.js]
 [test_536081.js]
 [test_1085291.js]
 [test_1105208.js]
 [test_1105866.js]
-[test_adaptive.js]
-[test_adaptive_bug527311.js]
 [test_annotations.js]
 [test_asyncExecuteLegacyQueries.js]
 [test_async_transactions.js]
 [test_bookmarks_json.js]
 [test_bookmarks_json_corrupt.js]
 [test_bookmarks_html.js]
 [test_bookmarks_html_corrupt.js]
 [test_bookmarks_html_escape_entities.js]