Bug 1028985 - Provide search suggestions on Firefox new tab page (about:newtab). r=MattN, a=sledru
authorDrew Willcoxon <adw@mozilla.com>
Fri, 01 Aug 2014 12:00:49 -0700
changeset 217660 62505420ae112a5df004f2ec8fb8df5e1fce659f
parent 217659 ea4892a75c909b65d42591c8ccf060064aa06653
child 217661 02feec56dfae4902089820146683d60d267a3c68
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, sledru
bugs1028985
milestone33.0a2
Bug 1028985 - Provide search suggestions on Firefox new tab page (about:newtab). r=MattN, a=sledru
browser/base/content/newtab/newTab.css
browser/base/content/newtab/newTab.xul
browser/base/content/newtab/search.js
browser/base/content/test/newtab/browser.ini
browser/base/content/test/newtab/browser_newtab_search.js
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -391,8 +391,13 @@ input[type=button] {
   -moz-padding-start: 0;
   -moz-margin-start: 0;
   color: rgb(130, 132, 133);
 }
 
 .newtab-search-panel-engine[selected] {
   background: url("chrome://global/skin/menu/shared-menu-check.png") center left 4px no-repeat transparent;
 }
+
+.searchSuggestionTable {
+  font: message-box;
+  font-size: 16px;
+}
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -1,15 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!-- 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/. -->
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/searchSuggestionUI.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/newtab/newTab.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/newtab/newTab.css" type="text/css"?>
 
 <!DOCTYPE window [
   <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
   %newTabDTD;
   <!ENTITY % searchBarDTD SYSTEM "chrome://browser/locale/searchbar.dtd">
   %searchBarDTD;
@@ -78,9 +79,11 @@
       <div id="newtab-margin-bottom"/>
 
     </div>
     <input id="newtab-toggle" type="button"/>
   </div>
 
   <xul:script type="text/javascript;version=1.8"
               src="chrome://browser/content/newtab/newTab.js"/>
+  <xul:script type="text/javascript;version=1.8"
+              src="chrome://browser/content/searchSuggestionUI.js"/>
 </xul:window>
--- a/browser/base/content/newtab/search.js
+++ b/browser/base/content/newtab/search.js
@@ -25,34 +25,40 @@ let gSearch = {
     logo.setAttribute("active", "true");
     panel.addEventListener("popuphidden", function onHidden() {
       panel.removeEventListener("popuphidden", onHidden);
       logo.removeAttribute("active");
     });
   },
 
   search: function (event) {
-    event.preventDefault();
+    if (event) {
+      event.preventDefault();
+    }
     let searchStr = this._nodes.text.value;
     if (this.currentEngineName && searchStr.length) {
       this._send("Search", {
         engineName: this.currentEngineName,
         searchString: searchStr,
         whence: "newtab",
       });
     }
+    this._suggestionController.addInputValueToFormHistory();
   },
 
   manageEngines: function () {
     this._nodes.panel.hidePopup();
     this._send("ManageEngines");
   },
 
   handleEvent: function (event) {
-    this["on" + event.detail.type](event.detail.data);
+    let methodName = "on" + event.detail.type;
+    if (this.hasOwnProperty(methodName)) {
+      this[methodName](event.detail.data);
+    }
   },
 
   onState: function (data) {
     this._newEngines = data.engines;
     this._setCurrentEngine(data.currentEngine);
     this._initWhenInitalStateReceived();
   },
 
@@ -178,10 +184,19 @@ let gSearch = {
       let uri = URL.createObjectURL(new Blob([logoBuf]));
       this._nodes.logo.style.backgroundImage = "url(" + uri + ")";
       this._nodes.text.placeholder = "";
     }
     else {
       this._nodes.logo.hidden = true;
       this._nodes.text.placeholder = engine.name;
     }
+
+    // Set up the suggestion controller.
+    if (!this._suggestionController) {
+      let parent = document.getElementById("newtab-scrollbox");
+      this._suggestionController =
+        new SearchSuggestionUIController(this._nodes.text, parent,
+                                         () => this.search());
+    }
+    this._suggestionController.engineName = engine.name;
   },
 };
--- a/browser/base/content/test/newtab/browser.ini
+++ b/browser/base/content/test/newtab/browser.ini
@@ -28,13 +28,15 @@ skip-if = os == "mac" # Intermittent fai
 [browser_newtab_reportLinkAction.js]
 [browser_newtab_reset.js]
 [browser_newtab_search.js]
 support-files =
   searchEngineNoLogo.xml
   searchEngine1xLogo.xml
   searchEngine2xLogo.xml
   searchEngine1x2xLogo.xml
+  ../general/searchSuggestionEngine.xml
+  ../general/searchSuggestionEngine.sjs
 [browser_newtab_sponsored_icon_click.js]
 [browser_newtab_tabsync.js]
 [browser_newtab_undo.js]
 [browser_newtab_unpin.js]
 [browser_newtab_update.js]
--- a/browser/base/content/test/newtab/browser_newtab_search.js
+++ b/browser/base/content/test/newtab/browser_newtab_search.js
@@ -3,16 +3,17 @@
 
 // See browser/components/search/test/browser_*_behavior.js for tests of actual
 // searches.
 
 const ENGINE_NO_LOGO = "searchEngineNoLogo.xml";
 const ENGINE_1X_LOGO = "searchEngine1xLogo.xml";
 const ENGINE_2X_LOGO = "searchEngine2xLogo.xml";
 const ENGINE_1X_2X_LOGO = "searchEngine1x2xLogo.xml";
+const ENGINE_SUGGESTIONS = "searchSuggestionEngine.xml";
 
 const SERVICE_EVENT_NAME = "ContentSearchService";
 
 const LOGO_1X_DPI_SIZE = [65, 26];
 const LOGO_2X_DPI_SIZE = [130, 52];
 
 // The test has an expected search event queue and a search event listener.
 // Search events that are expected to happen are added to the queue, and the
@@ -136,16 +137,60 @@ function runTests() {
   // In the search panel, click the Manage Engines box.
   let manageBox = $("manage");
   ok(!!manageBox, "The Manage Engines box should be present in the document");
   yield Promise.all([
     promiseManagerOpen(),
     promiseClick(manageBox),
   ]).then(TestRunner.next);
 
+  // Add the engine that provides search suggestions and switch to it.
+  let suggestionEngine = null;
+  yield promiseNewSearchEngine(ENGINE_SUGGESTIONS, 0).then(engine => {
+    suggestionEngine = engine;
+    TestRunner.next();
+  });
+  Services.search.currentEngine = suggestionEngine;
+  yield promiseSearchEvents(["CurrentEngine"]).then(TestRunner.next);
+  yield checkCurrentEngine(ENGINE_SUGGESTIONS, false, false);
+
+  // Avoid intermittent failures.
+  gSearch()._suggestionController.remoteTimeout = 5000;
+
+  // Type an X in the search input.  This is only a smoke test.  See
+  // browser_searchSuggestionUI.js for comprehensive content search suggestion
+  // UI tests.
+  let input = $("text");
+  input.focus();
+  EventUtils.synthesizeKey("x", {});
+  let suggestionsPromise = promiseSearchEvents(["Suggestions"]);
+
+  // Wait for the search suggestions to become visible and for the Suggestions
+  // message.
+  let table = getContentDocument().getElementById("searchSuggestionTable");
+  info("Waiting for suggestions table to open");
+  let observer = new MutationObserver(() => {
+    if (input.getAttribute("aria-expanded") == "true") {
+      observer.disconnect();
+      ok(!table.hidden, "Search suggestion table unhidden");
+      TestRunner.next();
+    }
+  });
+  observer.observe(input, {
+    attributes: true,
+    attributeFilter: ["aria-expanded"],
+  });
+  yield undefined;
+  yield suggestionsPromise.then(TestRunner.next);
+
+  // Empty the search input, causing the suggestions to be hidden.
+  EventUtils.synthesizeKey("a", { accelKey: true });
+  EventUtils.synthesizeKey("VK_DELETE", {});
+  ok(table.hidden, "Search suggestion table hidden");
+
   // Done.  Revert the current engine and remove the new engines.
   Services.search.currentEngine = oldCurrentEngine;
   yield promiseSearchEvents(["CurrentEngine"]).then(TestRunner.next);
 
   let events = [];
   for (let engine of gNewEngines) {
     Services.search.removeEngine(engine);
     events.push("CurrentState");
@@ -259,16 +304,21 @@ function checkCurrentEngine(basename, ha
   is(logo.hidden, !logoURI,
      "Logo should be visible iff engine has a logo: " + engine.name);
   if (logoURI) {
     // The URLs of blobs created with the same ArrayBuffer are different, so
     // just check that the URI is a blob URI.
     ok(/^url\("blob:/.test(logo.style.backgroundImage), "Logo URI"); //"
   }
 
+  if (logo.hidden) {
+    executeSoon(TestRunner.next);
+    return;
+  }
+
   // "selected" attributes of engines in the panel
   let panel = searchPanel();
   promisePanelShown(panel).then(() => {
     panel.hidePopup();
     for (let engineBox of panel.childNodes) {
       let engineName = engineBox.getAttribute("engine");
       if (engineName == engine.name) {
         is(engineBox.getAttribute("selected"), "true",
@@ -278,17 +328,17 @@ function checkCurrentEngine(basename, ha
       else {
         ok(!engineBox.hasAttribute("selected"),
            "Engine box's selected attribute should be absent for " +
            "non-selected engine: " + engineName);
       }
     }
     TestRunner.next();
   });
-  panel.openPopup(logoImg());
+  panel.openPopup(logo);
 }
 
 function promisePanelShown(panel) {
   let deferred = Promise.defer();
   info("Waiting for popupshown");
   panel.addEventListener("popupshown", function onEvent() {
     panel.removeEventListener("popupshown", onEvent);
     is(panel.state, "open", "Panel state");