Bug 1500516 - Search aliases: @engine text should remain in the urlbar when highlighting search suggestion results, and modified suggestions should search with the @ engine r=mak a=RyanVM
authorDrew Willcoxon <adw@mozilla.com>
Thu, 25 Oct 2018 21:53:24 +0000
changeset 500884 38e139673b1933a7b950943ed997eab813924e05
parent 500883 2058a60f13a7172dec390d9608fe35e2e7761e55
child 500885 a8a181f1095f6c8e6954fa8ed9a4c60ba6bbc755
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, RyanVM
bugs1500516
milestone64.0
Bug 1500516 - Search aliases: @engine text should remain in the urlbar when highlighting search suggestion results, and modified suggestions should search with the @ engine r=mak a=RyanVM * Slightly rework the logic that makes `searchSuggestionsCompletePromise` so that it checks `this.hasBehavior("searches")` and `this._inPrivateWindow` earlier so that it can avoid getting the query string and truncating it (along with the pref accesss) * Get rid of the `input` param to `_addSearchEngineMatch`. It's only used for forcing a trailing space for alias results that don't have a query, but `_addSearchEngineMatch` can detect that case on its own -- no need for an `input` param. * A slightly unrelated change: I noticed that when the spec shows a search for "@amazon telescopes", the first suggestion is not "telescopes", like it actually is in Firefox, but "telescopes for adults". That makes sense. There's no point in having the first suggestion echo back the heuristic result. It's better not to because (1) there's no visual dupe and (2) you don't have to press the down arrow key twice to get to non-dupe suggestions. So I added some logic to the suggestions fetching to ignore suggestions that are duplicates of the search string. I changed `_searchEngineAliasMatch` to `_searchEngineHeuristicMatch` because of course you can do searches without using an alias, and this new logic needs the query string in that case. * Slightly rework `_addSearchEngineMatch` to be a little more straightforward * Fix `head_autocomplete.js` so it intelligently compares moz-action results instead of a simple string comparison (and hope that the object is stringified the same way) Differential Revision: https://phabricator.services.mozilla.com/D9472
browser/base/content/test/urlbar/browser_action_searchengine.js
browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
--- a/browser/base/content/test/urlbar/browser_action_searchengine.js
+++ b/browser/base/content/test/urlbar/browser_action_searchengine.js
@@ -17,19 +17,28 @@ add_task(async function() {
       BrowserTestUtils.removeTab(tab);
     } catch (ex) { /* tab may have already been closed in case of failure */ }
     await PlacesUtils.history.clear();
   });
 
   await promiseAutocompleteResultPopup("open a search");
   let result = await waitForAutocompleteResultAt(0);
   isnot(result, null, "Should have a result");
-  is(result.getAttribute("url"),
-     `moz-action:searchengine,{"engineName":"MozSearch","input":"open%20a%20search","searchQuery":"open%20a%20search"}`,
-     "Result should be a moz-action: for the correct search engine");
+  Assert.deepEqual(
+    PlacesUtils.parseActionUrl(result.getAttribute("url")),
+    {
+      type: "searchengine",
+      params: {
+        engineName: "MozSearch",
+        input: "open a search",
+        searchQuery: "open a search",
+      },
+    },
+    "Result should be a moz-action: for the correct search engine"
+  );
   is(result.hasAttribute("image"), false, "Result shouldn't have an image attribute");
 
   let tabPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
   result.click();
   await tabPromise;
 
   is(gBrowser.selectedBrowser.currentURI.spec, "http://example.com/?q=open+a+search", "Correct URL should be loaded");
 
--- a/browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
+++ b/browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
@@ -101,19 +101,28 @@ add_task(async function() {
     promiseEvent("reset")[1],
     promiseEvent("result")[1],
     promiseAutocompleteResultPopup(value, window, true),
   ]);
 
   // Check the heuristic result.
   let result = promiseValues[2];
   let engineName = Services.search.currentEngine.name;
-  Assert.equal(result.url,
-               `moz-action:searchengine,{"engineName":"${engineName}","input":"test","searchQuery":"test"}`,
-               "result.url");
+  Assert.deepEqual(
+    PlacesUtils.parseActionUrl(result.url),
+    {
+      type: "searchengine",
+      params: {
+        engineName,
+        input: "test",
+        searchQuery: "test",
+      },
+    },
+    "result.url"
+  );
   Assert.ok("action" in result, "result.action");
   Assert.equal(result.action.type, "searchengine", "result.action.type");
   Assert.ok("params" in result.action, "result.action.params");
   Assert.equal(result.action.params.engineName, engineName,
                "result.action.params.engineName");
   Assert.equal(typeof(result.image), "string", "result.image");
   Assert.equal(result.title, engineName, "result.title");
   Assert.equal(result.type, "action searchengine heuristic", "result.type");
--- a/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
@@ -46,18 +46,25 @@ async function promiseTestResult(test) {
      `Autocomplete result should have displayed title as expected for search '${test.search}'`);
 
   is(result._actionText.textContent, test.resultListActionText,
      `Autocomplete action text should be as expected for search '${test.search}'`);
 
   is(result.getAttribute("type"), test.resultListType,
      `Autocomplete result should have searchengine for the type for search '${test.search}'`);
 
-  is(gURLBar.mController.getFinalCompleteValueAt(0), test.finalCompleteValue,
-     `Autocomplete item should go to the expected final value for search '${test.search}'`);
+  let actualValue = gURLBar.mController.getFinalCompleteValueAt(0);
+  let actualAction = PlacesUtils.parseActionUrl(actualValue);
+  let expectedAction = PlacesUtils.parseActionUrl(test.finalCompleteValue);
+  Assert.equal(!!actualAction, !!expectedAction);
+  if (actualAction) {
+    Assert.deepEqual(actualAction, expectedAction);
+  } else {
+    Assert.equal(actualValue, test.finalCompleteValue);
+  }
 }
 
 const tests = [{
     search: "http://",
     autofilledValue: "http://",
     resultListDisplayTitle: "http://",
     resultListActionText: "Search with Google",
     resultListType: "searchengine",
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -339,16 +339,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AboutPagesUtils: "resource://gre/modules/AboutPagesUtils.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
   ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm",
+  ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   PlacesRemoteTabsAutocompleteProvider: "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm",
   PlacesSearchAutocompleteProvider: "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   ProfileAge: "resource://gre/modules/ProfileAge.jsm",
   Sqlite: "resource://gre/modules/Sqlite.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
@@ -473,33 +474,57 @@ function stripHttpAndTrim(spec, trimSlas
   if (trimSlash && spec.endsWith("/")) {
     spec = spec.slice(0, -1);
   }
   return spec;
 }
 
 /**
  * Returns the key to be used for a match in a map for the purposes of removing
- * duplicate entries - any 2 URLs that should be considered the same should
- * return the same key. For some moz-action URLs this will unwrap the params
- * and return a key based on the wrapped URL.
+ * duplicate entries - any 2 matches that should be considered the same should
+ * return the same key.  The type of the returned key depends on the type of the
+ * match, so don't assume you can compare keys using ==.  Instead, use
+ * ObjectUtils.deepEqual().
+ *
+ * @param   {object} match
+ *          The match object.
+ * @returns {value} Some opaque key object.  Use ObjectUtils.deepEqual() to
+ *          compare keys.
  */
-function makeKeyForURL(match) {
-  let url = match.value;
-  let action = PlacesUtils.parseActionUrl(url);
-  // At this stage we only consider moz-action URLs.
-  if (!action || !("url" in action.params)) {
-    // For autofill entries, we need to have a key based on the comment rather
-    // than the value field, because the latter may have been trimmed.
-    if (match.hasOwnProperty("style") && match.style.includes("autofill")) {
-      url = match.comment;
-    }
-    return [stripHttpAndTrim(url), null];
+function makeKeyForMatch(match) {
+  // For autofill entries, we need to have a key based on the comment rather
+  // than the value field, because the latter may have been trimmed.
+  if (match.hasOwnProperty("style") && match.style.includes("autofill")) {
+    return [stripHttpAndTrim(match.comment), null];
+  }
+
+  let action = PlacesUtils.parseActionUrl(match.value);
+  if (!action) {
+    return [stripHttpAndTrim(match.value), null];
   }
-  return [stripHttpAndTrim(action.params.url), action];
+
+  let key;
+  switch (action.type) {
+    case "searchengine":
+      // We want to exclude search suggestion matches that simply echo back the
+      // query string in the heuristic result.  For example, if the user types
+      // "@engine test", we want to exclude a "test" suggestion match.
+      key = [
+        action.type,
+        action.params.engineName,
+        (action.params.searchSuggestion || action.params.searchQuery)
+          .toLocaleLowerCase(),
+      ];
+      break;
+    default:
+      key = stripHttpAndTrim(action.params.url || match.value);
+      break;
+  }
+
+  return [key, action];
 }
 
 /**
  * Returns whether the passed in string looks like a url.
  */
 function looksLikeUrl(str, ignoreAlphanumericHosts = false) {
   // Single word including special chars.
   return !REGEXP_SPACES.test(str) &&
@@ -810,17 +835,17 @@ Search.prototype = {
     // are not enabled).
 
     // Check for Preloaded Sites Expiry before Autofill
     await this._checkPreloadedSitesExpiry();
 
     // If the query is simply "@", then the results should be a list of all the
     // search engines with "@" aliases, without a hueristic result.
     if (this._trimmedOriginalSearchString == "@") {
-      let added = await this._addSearchEngineTokenAliasResults();
+      let added = await this._addSearchEngineTokenAliasMatches();
       if (added) {
         this._cleanUpNonCurrentMatches(null);
         this._autocompleteSearch.finishSearch(true);
         return;
       }
     }
 
     // Add the first heuristic result, if any.  Set _addingHeuristicFirstMatch
@@ -862,44 +887,53 @@ Search.prototype = {
         !this._searchEngineAliasMatch) {
       // Do not await on this, since extensions cannot notify when they are done
       // adding results, it may take too long.
       extensionsCompletePromise = this._matchExtensionSuggestions();
     } else if (ExtensionSearchHandler.hasActiveInputSession()) {
       ExtensionSearchHandler.handleInputCancelled();
     }
 
+    // Start adding search suggestions, unless they aren't required or the
+    // window is private.
     let searchSuggestionsCompletePromise = Promise.resolve();
-    if (this._enableActions) {
+    if (this._enableActions &&
+        this.hasBehavior("searches") &&
+        !this._inPrivateWindow) {
+      // Get the query string stripped of any engine alias or restriction token.
+      // In the former case, _searchTokens[0] will be the alias, so use
+      // this._searchEngineHeuristicMatch.query.  In the latter case, the token
+      // has been removed from _searchTokens, so use _searchTokens.join().
       let query =
         this._searchEngineAliasMatch ? this._searchEngineAliasMatch.query :
         this._searchTokens.join(" ");
       if (query) {
         // Limit the string sent for search suggestions to a maximum length.
         query = query.substr(0, UrlbarPrefs.get("maxCharsForSearchSuggestions"));
-        // Avoid fetching suggestions if they are not required, private browsing
-        // mode is enabled, or the query may expose sensitive information.
-        if (this.hasBehavior("searches") &&
-            !this._inPrivateWindow &&
-            !this._prohibitSearchSuggestionsFor(query)) {
+        // Don't add suggestions if the query may expose sensitive information.
+        if (!this._prohibitSearchSuggestionsFor(query)) {
           let engine;
           if (this._searchEngineAliasMatch) {
             engine = this._searchEngineAliasMatch.engine;
           } else {
             engine = await PlacesSearchAutocompleteProvider.currentEngine();
             if (!this.pending) {
               return;
             }
           }
+          let alias =
+            this._searchEngineAliasMatch &&
+            this._searchEngineAliasMatch.alias ||
+            "";
           searchSuggestionsCompletePromise =
-            this._matchSearchSuggestions(engine, query);
+            this._matchSearchSuggestions(engine, query, alias);
           // If the user has used a search engine alias, then the only results
           // we want to show are suggestions from that engine, so we're done.
           // We're also done if we're restricting results to suggestions.
-          if (this._searchEngineAliasMatch || this.hasBehavior("restrict")) {
+          if (alias || this.hasBehavior("restrict")) {
             // Wait for the suggestions to be added.
             await searchSuggestionsCompletePromise;
             this._cleanUpNonCurrentMatches(null);
             this._autocompleteSearch.finishSearch(true);
             return;
           }
         }
       }
@@ -1083,30 +1117,25 @@ Search.prototype = {
     return true;
   },
 
   /**
    * Adds matches for all the engines with "@" aliases, if any.
    *
    * @returns {bool} True if any results were added, false if not.
    */
-  async _addSearchEngineTokenAliasResults() {
+  async _addSearchEngineTokenAliasMatches() {
     let engines = await PlacesSearchAutocompleteProvider.tokenAliasEngines();
     if (!engines || !engines.length) {
       return false;
     }
     for (let { engine, tokenAliases } of engines) {
-      let alias = tokenAliases[0];
-      // `input` should have a trailing space so that when the user selects the
-      // result, they can start typing their query without first having to enter
-      // a space between the alias and query.
       this._addSearchEngineMatch({
         engine,
-        alias,
-        input: alias + " ",
+        alias: tokenAliases[0],
       });
     }
     return true;
   },
 
   async _matchSearchEngineTokenAlias() {
     // We need a single "@engine" search token.
     if (this._searchTokens.length != 1) {
@@ -1257,17 +1286,17 @@ Search.prototype = {
       if (matched) {
         return true;
       }
     }
 
     return false;
   },
 
-  _matchSearchSuggestions(engine, searchString) {
+  _matchSearchSuggestions(engine, searchString, alias) {
     this._suggestionsFetch =
       PlacesSearchAutocompleteProvider.newSuggestionsFetch(
         engine,
         searchString,
         this._inPrivateWindow,
         UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
         UrlbarPrefs.get("maxRichResults") - UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
         this._userContextId
@@ -1285,16 +1314,17 @@ Search.prototype = {
       while (this.pending) {
         let result = this._suggestionsFetch.consume();
         if (!result)
           break;
         let { suggestion, historical } = result;
         if (!looksLikeUrl(suggestion)) {
           this._addSearchEngineMatch({
             engine,
+            alias,
             query: searchString,
             suggestion,
             historical,
           });
         }
       }
     }).catch(Cu.reportError);
   },
@@ -1540,50 +1570,57 @@ Search.prototype = {
    * @param {string} [alias]
    *        The search engine alias associated with the match, if any.
    * @param {string} [suggestion]
    *        The suggestion from the search engine, if you're adding a suggestion
    *        match.
    * @param {bool} [historical]
    *        True if you're adding a suggestion match and the suggestion is from
    *        the user's local history (and not the search engine).
-   * @param {string} [input]
-   *        Use this value to override the action.params.input that this method
-   *        would otherwise compute.
    */
   _addSearchEngineMatch({engine,
                          query = "",
                          alias = undefined,
                          suggestion = undefined,
-                         historical = false,
-                         input = undefined}) {
+                         historical = false}) {
     let actionURLParams = {
       engineName: engine.name,
-      input: input || suggestion || this._originalSearchString,
       searchQuery: query,
     };
+
+    if (suggestion) {
+      // `input` should include the alias.
+      actionURLParams.input = (alias ? `${alias} ` : "") + suggestion;
+    } else if (alias && !query) {
+      // `input` should have a trailing space so that when the user selects the
+      // result, they can start typing their query without first having to enter
+      // a space between the alias and query.
+      actionURLParams.input = `${alias} `;
+    } else {
+      actionURLParams.input = this._originalSearchString;
+    }
+
+    let match = {
+      comment: engine.name,
+      icon: engine.iconURI && !suggestion ? engine.iconURI.spec : null,
+      style: "action searchengine",
+      frecency: FRECENCY_DEFAULT,
+    };
+
     if (alias) {
       actionURLParams.alias = alias;
+      match.style += " alias";
     }
     if (suggestion) {
       actionURLParams.searchSuggestion = suggestion;
-    }
-    let value = PlacesUtils.mozActionURI("searchengine", actionURLParams);
-    let match = {
-      value,
-      comment: engine.name,
-      icon: engine.iconURI ? engine.iconURI.spec : null,
-      style: "action searchengine",
-      frecency: FRECENCY_DEFAULT,
-    };
-    if (suggestion) {
       match.style += " suggestion";
       match.type = UrlbarUtils.MATCH_GROUP.SUGGESTION;
     }
 
+    match.value = PlacesUtils.mozActionURI("searchengine", actionURLParams);
     this._addMatch(match);
   },
 
   _matchExtensionSuggestions() {
     let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
       suggestions => {
         for (let suggestion of suggestions) {
           let content = `${this._searchTokens[0]} ${suggestion.content}`;
@@ -1831,26 +1868,26 @@ Search.prototype = {
     // Check for duplicates and either discard (by returning -1) the duplicate
     // or suggest to replace the original match, in case the new one is more
     // specific (for example a Remote Tab wins over History, and a Switch to Tab
     // wins over a Remote Tab).
     // Must check both id and url, cause keywords dynamically modify the url.
     // Note: this partially fixes Bug 1222435,  but not if the urls differ more
     // than just by "http://". We should still evaluate www and other schemes
     // equivalences.
-    let [urlMapKey, action] = makeKeyForURL(match);
+    let [urlMapKey, action] = makeKeyForMatch(match);
     if ((match.placeId && this._usedPlaceIds.has(match.placeId)) ||
-        this._usedURLs.map(e => e.key).includes(urlMapKey)) {
+        this._usedURLs.some(e => ObjectUtils.deepEqual(e.key, urlMapKey))) {
       let isDupe = true;
       if (action && ["switchtab", "remotetab"].includes(action.type)) {
         // The new entry is a switch/remote tab entry, look for the duplicate
         // among current matches.
         for (let i = 0; i < this._usedURLs.length; ++i) {
           let {key: matchKey, action: matchAction, type: matchType} = this._usedURLs[i];
-          if (matchKey == urlMapKey) {
+          if (ObjectUtils.deepEqual(matchKey, urlMapKey)) {
             isDupe = true;
             // Don't replace the match if the existing one is heuristic and the
             // new one is a switchtab, instead also add the switchtab match.
             if (matchType == UrlbarUtils.MATCH_GROUP.HEURISTIC &&
                 action.type == "switchtab") {
               isDupe = false;
               // Since we allow to insert a dupe in this case, we must continue
               // checking the next matches to be sure we won't insert more than
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -1,14 +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/. */
 
 const FRECENCY_DEFAULT = 10000;
 
+ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://testing-common/httpd.js");
 
 // Import common head.
 {
   /* import-globals-from ../head_common.js */
   let commonFile = do_get_file("../head_common.js", false);
   let uri = Services.io.newFileURI(commonFile);
@@ -138,17 +139,28 @@ async function _check_autocomplete_match
   else
     style = ["favicon"];
 
   let actual = { value: result.value, comment: result.comment };
   let expected = { value: match.value || uri, comment: title };
   info(`Checking match: ` +
        `actual=${JSON.stringify(actual)} ... ` +
        `expected=${JSON.stringify(expected)}`);
-  if (actual.value != expected.value || actual.comment != expected.comment) {
+
+  let actualAction = PlacesUtils.parseActionUrl(actual.value);
+  let expectedAction = PlacesUtils.parseActionUrl(expected.value);
+  if (actualAction && expectedAction) {
+    if (!ObjectUtils.deepEqual(actualAction, expectedAction)) {
+      return false;
+    }
+  } else if (actual.value != expected.value) {
+    return false;
+  }
+
+  if (actual.comment != expected.comment) {
     return false;
   }
 
   let actualStyle = result.style.split(/\s+/).sort();
   if (style)
     Assert.equal(actualStyle.toString(), style.toString(), "Match should have expected style");
   if (uri && uri.startsWith("moz-action:")) {
     Assert.ok(actualStyle.includes("action"), "moz-action results should always have 'action' in their style");
@@ -358,36 +370,32 @@ function makeActionURI(action, params) {
   }
   let url = "moz-action:" + action + "," + JSON.stringify(encodedParams);
   return NetUtil.newURI(url);
 }
 
 // Creates a full "match" entry for a search result, suitable for passing as
 // an entry to check_autocomplete.
 function makeSearchMatch(input, extra = {}) {
-  // Note that counter-intuitively, the order the object properties are defined
-  // in the object passed to makeActionURI is important for check_autocomplete
-  // to match them :(
   let params = {
     engineName: extra.engineName || "MozSearch",
     input,
     searchQuery: "searchQuery" in extra ? extra.searchQuery : input,
   };
-  if ("alias" in extra) {
-    // May be undefined, which is expected, but in that case make sure it's not
-    // included in the params of the moz-action URL.
-    params.alias = extra.alias;
-  }
   let style = [ "action", "searchengine" ];
   if ("style" in extra && Array.isArray(extra.style)) {
     style.push(...extra.style);
   }
   if (extra.heuristic) {
     style.push("heuristic");
   }
+  if ("alias" in extra) {
+    params.alias = extra.alias;
+    style.push("alias");
+  }
   if ("searchSuggestion" in extra) {
     params.searchSuggestion = extra.searchSuggestion;
     style.push("suggestion");
   }
   return {
     uri: makeActionURI("searchengine", params),
     title: params.engineName,
     style,
@@ -484,17 +492,17 @@ async function addTestSuggestionsEngine(
   // This port number should match the number in engine-suggestions.xml.
   let server = makeTestServer(9000);
   server.registerPathHandler("/suggest", (req, resp) => {
     // URL query params are x-www-form-urlencoded, which converts spaces into
     // plus signs, so un-convert any plus signs back to spaces.
     let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
     let suggestions =
       suggestionsFn ? suggestionsFn(searchStr) :
-      ["foo", "bar"].map(s => searchStr + " " + s);
+      [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s));
     let data = [searchStr, suggestions];
     resp.setHeader("Content-Type", "application/json", false);
     resp.write(JSON.stringify(data));
   });
   let engine = await addTestEngine("engine-suggestions.xml", server);
   return engine;
 }
 
--- a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
@@ -21,17 +21,17 @@ add_task(async function getPost() {
   Services.search.addEngineWithDetails("AliasedPOSTMozSearch", "", "post", "",
                                        "POST", "http://s.example.com/search");
 
   for (let alias of ["get", "post"]) {
     await check_autocomplete({
       search: alias,
       searchParam: "enable-actions",
       matches: [
-        makeSearchMatch(alias, {
+        makeSearchMatch(`${alias} `, {
           engineName: `Aliased${alias.toUpperCase()}MozSearch`,
           searchQuery: "",
           alias,
           heuristic: true,
         }),
       ],
     });
 
@@ -110,20 +110,20 @@ add_task(async function engineWithSugges
   let engine = await addTestSuggestionsEngine();
 
   // Use a normal alias and then one with an "@", the latter to simulate the
   // built-in "@" engine aliases (e.g., "@google").
   for (let alias of ["moz", "@moz"]) {
     engine.alias = alias;
 
     await check_autocomplete({
-      search: `${alias}`,
+      search: alias,
       searchParam: "enable-actions",
       matches: [
-        makeSearchMatch(alias, {
+        makeSearchMatch(`${alias} `, {
           engineName: SUGGESTIONS_ENGINE_NAME,
           alias,
           searchQuery: "",
           heuristic: true,
         }),
       ],
     });
 
@@ -145,23 +145,25 @@ add_task(async function engineWithSugges
       searchParam: "enable-actions",
       matches: [
         makeSearchMatch(`${alias} fire`, {
           engineName: SUGGESTIONS_ENGINE_NAME,
           alias,
           searchQuery: "fire",
           heuristic: true,
         }),
-        makeSearchMatch(`fire foo`, {
+        makeSearchMatch(`${alias} fire foo`, {
           engineName: SUGGESTIONS_ENGINE_NAME,
+          alias,
           searchQuery: "fire",
           searchSuggestion: "fire foo",
         }),
-        makeSearchMatch(`fire bar`, {
+        makeSearchMatch(`${alias} fire bar`, {
           engineName: SUGGESTIONS_ENGINE_NAME,
+          alias,
           searchQuery: "fire",
           searchSuggestion: "fire bar",
         }),
       ],
     });
   }
 
   engine.alias = "";
--- a/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
@@ -27,17 +27,17 @@ add_task(async function setup() {
   Services.prefs.setCharPref("browser.urlbar.matchBuckets", "general:5,suggestion:Infinity");
   Services.prefs.setBoolPref("browser.urlbar.geoSpecificDefaults", false);
 
   let engine = await addTestSuggestionsEngine(searchStr => {
     return suggestionsFn(searchStr);
   });
   setSuggestionsFn(searchStr => {
     let suffixes = ["foo", "bar"];
-    return suffixes.map(s => searchStr + " " + s);
+    return [searchStr].concat(suffixes.map(s => searchStr + " " + s));
   });
 
   // Install the test engine.
   let oldCurrentEngine = Services.search.currentEngine;
   registerCleanupFunction(() => Services.search.currentEngine = oldCurrentEngine);
   Services.search.currentEngine = engine;
 
   // We must make sure the FormHistoryStartup component is initialized.
@@ -91,16 +91,19 @@ add_task(async function singleWordQuery(
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
 
   await check_autocomplete({
     search: "hello",
     searchParam: "enable-actions",
     matches: [
       makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+      // The test engine echoes back the search string as the first suggestion,
+      // so it would appear here (as "hello"), but we remove suggestions that
+      // duplicate the search string, so it should not actually appear.
       { uri: makeActionURI(("searchengine"), {
         engineName: ENGINE_NAME,
         input: "hello foo",
         searchQuery: "hello",
         searchSuggestion: "hello foo",
       }),
       title: ENGINE_NAME,
       style: ["action", "searchengine", "suggestion"],
@@ -300,16 +303,27 @@ add_task(async function restrictToken() 
     search: SUGGEST_RESTRICT_TOKEN + " hello",
     searchParam: "enable-actions",
     matches: [
       // TODO (bug 1177895) This is wrong.
       makeSearchMatch(SUGGEST_RESTRICT_TOKEN + " hello", { engineName: ENGINE_NAME, heuristic: true }),
       {
         uri: makeActionURI(("searchengine"), {
           engineName: ENGINE_NAME,
+          input: "hello",
+          searchQuery: "hello",
+          searchSuggestion: "hello",
+        }),
+        title: ENGINE_NAME,
+        style: ["action", "searchengine", "suggestion"],
+        icon: "",
+      },
+      {
+        uri: makeActionURI(("searchengine"), {
+          engineName: ENGINE_NAME,
           input: "hello foo",
           searchQuery: "hello",
           searchSuggestion: "hello foo",
         }),
         title: ENGINE_NAME,
         style: ["action", "searchengine", "suggestion"],
         icon: "",
       },