Bug 962490 - Add a search field to the new tab page (part 2: about:newtab changes). r=ttaubert
authorDrew Willcoxon <adw@mozilla.com>
Thu, 24 Apr 2014 19:09:23 -0700
changeset 180541 a36dd9f2573919bcbe04fafc802db6861273f419
parent 180540 eeafc69ebfb15c487e0b8cebedae42003c31fefb
child 180542 532886a149ab30aa92dc91a1da81dbf0a30530e6
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersttaubert
bugs962490
milestone31.0a1
Bug 962490 - Add a search field to the new tab page (part 2: about:newtab changes). r=ttaubert
browser/base/content/newtab/grid.js
browser/base/content/newtab/newTab.css
browser/base/content/newtab/newTab.js
browser/base/content/newtab/newTab.xul
browser/base/content/newtab/page.js
browser/base/content/newtab/search.js
browser/base/content/test/newtab/browser.ini
browser/base/content/test/newtab/browser_newtab_focus.js
browser/base/content/test/newtab/browser_newtab_search.js
browser/base/content/test/newtab/browser_newtab_sponsored_icon_click.js
browser/base/content/test/newtab/head.js
browser/base/content/test/newtab/searchEngineLogo.xml
browser/base/content/test/newtab/searchEngineNoLogo.xml
--- a/browser/base/content/newtab/grid.js
+++ b/browser/base/content/newtab/grid.js
@@ -192,21 +192,27 @@ let gGrid = {
     if (this._cellMargin === undefined) {
       let refCell = document.querySelector(".newtab-cell");
       this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) * 2;
       this._cellHeight = refCell.offsetHeight + this._cellMargin;
       this._cellWidth = refCell.offsetWidth + this._cellMargin;
     }
 
     let availSpace = document.documentElement.clientHeight - this._cellMargin -
-                     document.querySelector("#newtab-margin-undo-container").offsetHeight;
+                     document.querySelector("#newtab-margin-undo-container").offsetHeight -
+                     document.querySelector("#newtab-search-form").offsetHeight;
     let visibleRows = Math.floor(availSpace / this._cellHeight);
     this._node.style.height = this._computeHeight() + "px";
     this._node.style.maxHeight = this._computeHeight(visibleRows) + "px";
     this._node.style.maxWidth = gGridPrefs.gridColumns * this._cellWidth +
                                 GRID_WIDTH_EXTRA + "px";
+
+    // Resize the search bar.
+    let width = parseFloat(window.getComputedStyle(this._node).width);
+    let visibleCols = Math.floor(width / this._cellWidth);
+    gSearch.setWidth(visibleCols * this._cellWidth - this._cellMargin);
   },
 
   _shouldRenderGrid : function Grid_shouldRenderGrid() {
     let cellsLength = this._node.querySelectorAll(".newtab-cell").length;
     return cellsLength != (gGridPrefs.gridRows * gGridPrefs.gridColumns);
   }
 };
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -1,22 +1,28 @@
 /* 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/. */
 
+input {
+  font: message-box !important;
+  font-size: 16px !important;
+}
+
 input[type=button] {
   cursor: pointer;
 }
 
 /* SCROLLBOX */
 #newtab-scrollbox {
   display: -moz-box;
   position: relative;
   -moz-box-flex: 1;
   -moz-user-focus: normal;
+  -moz-box-orient: vertical;
 }
 
 #newtab-scrollbox:not([page-disabled]) {
   overflow: auto;
 }
 
 /* UNDO */
 #newtab-undo-container {
@@ -49,28 +55,36 @@ input[type=button] {
   position: relative;
   -moz-box-flex: 1;
   -moz-box-orient: vertical;
 }
 
 #newtab-margin-undo-container {
   display: -moz-box;
   -moz-box-pack: center;
+  margin-bottom: 26px; /* 32 - 6 search form top "padding" */
 }
 
 #newtab-horizontal-margin {
   display: -moz-box;
   -moz-box-flex: 1;
 }
 
 #newtab-margin-top,
 #newtab-margin-bottom {
   display: -moz-box;
+  position: relative;
+}
+
+#newtab-margin-top {
   -moz-box-flex: 1;
-  position: relative;
+}
+
+#newtab-margin-bottom {
+  -moz-box-flex: 2;
 }
 
 .newtab-side-margin {
   min-width: 16px;
   -moz-box-flex: 1;
 }
 
 /* GRID */
@@ -208,20 +222,173 @@ input[type=button] {
  */
 .newtab-drag {
   width: 1px;
   height: 1px;
   background-color: #fff;
   opacity: 0.01;
 }
 
-/* PANEL */
+/* SPONSORED PANEL */
 #sponsored-panel {
   width: 330px;
 }
 
 #sponsored-panel description {
   margin: 0;
 }
 
 #sponsored-panel .text-link {
   margin: 12px 0 0;
 }
+
+/* SEARCH */
+#newtab-search-container {
+  display: -moz-box;
+  position: relative;
+  -moz-box-align: center;
+  -moz-box-pack: center;
+}
+
+#newtab-search-container[page-disabled] {
+  opacity: 0;
+  pointer-events: none;
+}
+
+#newtab-search-form {
+  display: -moz-box;
+  -moz-box-orient: horizontal;
+  -moz-box-align: center;
+  height: 44px; /* 32 + 6 logo top "padding" + 6 logo bottom "padding" */
+  margin-bottom: 10px; /* 32 - 16 tiles top margin - 6 logo bottom "padding" */
+}
+
+#newtab-search-logo {
+  display: -moz-box;
+  width: 77px; /* 65 image width + 6 left "padding" + 6 right "padding" */
+  height: 38px; /* 26 image height + 6 top "padding" + 6 bottom "padding" */
+  border: 1px solid transparent;
+  -moz-margin-end: 8px;
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: 65px 26px;
+}
+
+#newtab-search-logo[hidden] {
+  display: none;
+}
+
+#newtab-search-logo[active],
+#newtab-search-logo:hover {
+  background-color: #e9e9e9;
+  border: 1px solid rgb(226, 227, 229);
+  border-radius: 2.5px;
+}
+
+#newtab-search-text {
+  height: 32px;
+  -moz-box-flex: 1;
+
+  padding: 0 8px;
+  background: hsla(0,0%,100%,.9) padding-box;
+  border: 1px solid;
+  border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+  box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
+              0 0 2px hsla(210,65%,9%,.1) inset,
+              0 1px 0 hsla(0,0%,100%,.2);
+  border-radius: 2.5px 0 0 2.5px;
+}
+
+#newtab-search-text:-moz-dir(rtl) {
+  border-radius: 0 2.5px 2.5px 0;
+}
+
+#newtab-search-text:focus,
+#newtab-search-text[autofocus] {
+  border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
+}
+
+#newtab-search-submit {
+  height: 32px;
+
+  -moz-margin-start: -1px;
+  background: linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
+  padding: 0 9px;
+  border: 1px solid;
+  border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+  -moz-border-start: 1px solid transparent;
+  border-radius: 0 2.5px 2.5px 0;
+  box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
+              0 1px 0 hsla(0,0%,100%,.2);
+  cursor: pointer;
+  transition-property: background-color, border-color, box-shadow;
+  transition-duration: 150ms;
+}
+
+#newtab-search-submit:-moz-dir(rtl) {
+  border-radius: 2.5px 0 0 2.5px;
+}
+
+#newtab-search-text:focus + #newtab-search-submit,
+#newtab-search-text + #newtab-search-submit:hover,
+#newtab-search-text[autofocus] + #newtab-search-submit {
+  border-color: #59b5fc #45a3e7 #3294d5;
+  color: white;
+}
+
+#newtab-search-text:focus + #newtab-search-submit,
+#newtab-search-text[autofocus] + #newtab-search-submit {
+  background-image: linear-gradient(#4cb1ff, #1793e5);
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+              0 0 0 1px hsla(0,0%,100%,.1) inset,
+              0 1px 0 hsla(210,54%,20%,.03);
+}
+
+#newtab-search-text + #newtab-search-submit:hover {
+  background-image: linear-gradient(#66bdff, #0d9eff);
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+              0 0 0 1px hsla(0,0%,100%,.1) inset,
+              0 1px 0 hsla(210,54%,20%,.03),
+              0 0 4px hsla(206,100%,20%,.2);
+}
+
+#newtab-search-text + #newtab-search-submit:hover:active {
+  box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset,
+              0 0 1px hsla(211,79%,6%,.2) inset;
+  transition-duration: 0ms;
+}
+
+#newtab-search-panel .panel-arrowcontent {
+  -moz-padding-start: 0;
+  -moz-padding-end: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  background: rgb(248, 250, 251);
+}
+
+.newtab-search-panel-engine {
+  -moz-box-align: center;
+  padding-top: 4px;
+  padding-bottom: 4px;
+  -moz-padding-start: 24px;
+  -moz-padding-end: 24px;
+}
+
+.newtab-search-panel-engine:not(:last-child) {
+  border-bottom: 1px solid #ccc;
+}
+
+.newtab-search-panel-engine > image {
+  -moz-margin-end: 8px;
+  width: 16px;
+  height: 16px;
+  list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
+}
+
+.newtab-search-panel-engine > label {
+  -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;
+}
--- a/browser/base/content/newtab/newTab.js
+++ b/browser/base/content/newtab/newTab.js
@@ -36,24 +36,26 @@ XPCOMUtils.defineLazyGetter(this, "gStri
 
 function newTabString(name) gStringBundle.GetStringFromName('newtab.' + name);
 
 function inPrivateBrowsingMode() {
   return PrivateBrowsingUtils.isWindowPrivate(window);
 }
 
 const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+const XUL_NAMESPACE = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 #include transformations.js
 #include page.js
 #include grid.js
 #include cells.js
 #include sites.js
 #include drag.js
 #include dragDataHelper.js
 #include drop.js
 #include dropTargetShim.js
 #include dropPreview.js
 #include updater.js
 #include undo.js
+#include search.js
 
 // Everything is loaded. Initialize the New Tab Page.
 gPage.init();
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -6,29 +6,38 @@
 
 <?xml-stylesheet href="chrome://global/skin/" 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;
 ]>
 
 <xul:window id="newtab-window" xmlns="http://www.w3.org/1999/xhtml"
             xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
             title="&newtab.pageTitle;">
 
   <xul:panel id="sponsored-panel" orient="vertical" type="arrow">
     <xul:description>&newtab.panel.message;</xul:description>
     <xul:label class="text-link"
                href="https://support.mozilla.org/kb/how-do-sponsored-tiles-work"
                value="&newtab.panel.link.text;" />
   </xul:panel>
 
+  <xul:panel id="newtab-search-panel" orient="vertical" type="arrow"
+             noautohide="true">
+    <xul:hbox id="newtab-search-manage" class="newtab-search-panel-engine">
+      <xul:label>&cmd_engineManager.label;</xul:label>
+    </xul:hbox>
+  </xul:panel>
+
   <div id="newtab-scrollbox">
 
     <div id="newtab-vertical-margin">
 
       <div id="newtab-margin-top"/>
 
       <div id="newtab-margin-undo-container">
         <div id="newtab-undo-container" undo-disabled="true">
@@ -41,16 +50,26 @@
                       label="&newtab.undo.restoreButton;"
                       class="newtab-undo-button" />
           <xul:toolbarbutton id="newtab-undo-close-button" tabindex="-1"
                              class="close-icon tabbable"
                              tooltiptext="&newtab.undo.closeTooltip;" />
         </div>
       </div>
 
+      <div id="newtab-search-container">
+        <form id="newtab-search-form" name="searchForm">
+          <div id="newtab-search-logo"/>
+          <input type="text" name="q" value="" id="newtab-search-text"
+                 maxlength="256" dir="auto"/>
+          <input id="newtab-search-submit" type="submit"
+                 value="&searchEndCap.label;"/>
+        </form>
+      </div>
+
       <div id="newtab-horizontal-margin">
         <div class="newtab-side-margin"/>
 
         <div id="newtab-grid">
         </div>
 
         <div class="newtab-side-margin"/>
       </div>
--- a/browser/base/content/newtab/page.js
+++ b/browser/base/content/newtab/page.js
@@ -106,16 +106,18 @@ let gPage = {
    * is/gets enabled.
    */
   _init: function Page_init() {
     if (this._initialized)
       return;
 
     this._initialized = true;
 
+    gSearch.init();
+
     this._mutationObserver = new MutationObserver(() => {
       if (this.allowBackgroundCaptures) {
         Services.telemetry.getHistogramById("NEWTAB_PAGE_SHOWN").add(true);
 
         // Initialize type counting with the types we want to count
         let directoryCount = {};
         for (let type of DirectoryLinksProvider.linkTypes) {
           directoryCount[type] = 0;
@@ -133,16 +135,20 @@ let gPage = {
 
         // Record how many directory sites were shown, but place counts over the
         // default 9 in the same bucket
         for (let [type, count] of Iterator(directoryCount)) {
           let shownId = "NEWTAB_PAGE_DIRECTORY_" + type.toUpperCase() + "_SHOWN";
           let shownCount = Math.min(10, count);
           Services.telemetry.getHistogramById(shownId).add(shownCount);
         }
+
+        // content.js isn't loaded for the page while it's in the preloader,
+        // which is why this is necessary.
+        gSearch.setUpInitialState();
       }
     });
     this._mutationObserver.observe(document.documentElement, {
       attributes: true,
       attributeFilter: ["allow-background-captures"],
     });
 
     // Initialize and render the grid.
@@ -159,17 +165,17 @@ let gPage = {
   },
 
   /**
    * Updates the 'page-disabled' attributes of the respective DOM nodes.
    * @param aValue Whether the New Tab Page is enabled or not.
    */
   _updateAttributes: function Page_updateAttributes(aValue) {
     // Set the nodes' states.
-    let nodeSelector = "#newtab-scrollbox, #newtab-toggle, #newtab-grid";
+    let nodeSelector = "#newtab-scrollbox, #newtab-toggle, #newtab-grid, #newtab-search-container";
     for (let node of document.querySelectorAll(nodeSelector)) {
       if (aValue)
         node.removeAttribute("page-disabled");
       else
         node.setAttribute("page-disabled", "true");
     }
 
     // Enables/disables the control and link elements.
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/search.js
@@ -0,0 +1,170 @@
+#ifdef 0
+/* 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/. */
+#endif
+
+let gSearch = {
+
+  currentEngineName: null,
+
+  init: function () {
+    for (let idSuffix of this._nodeIDSuffixes) {
+      this._nodes[idSuffix] =
+        document.getElementById("newtab-search-" + idSuffix);
+    }
+
+    window.addEventListener("ContentSearchService", this);
+    this.setUpInitialState();
+  },
+
+  setUpInitialState: function () {
+    this._send("GetState");
+  },
+
+  showPanel: function () {
+    let panel = this._nodes.panel;
+    let logo = this._nodes.logo;
+    panel.openPopup(logo);
+    logo.setAttribute("active", "true");
+    panel.addEventListener("popuphidden", function onHidden() {
+      panel.removeEventListener("popuphidden", onHidden);
+      logo.removeAttribute("active");
+    });
+  },
+
+  search: function (event) {
+    event.preventDefault();
+    let searchStr = this._nodes.text.value;
+    if (this.currentEngineName && searchStr.length) {
+      this._send("Search", {
+        engineName: this.currentEngineName,
+        searchString: searchStr,
+        whence: "newtab",
+      });
+    }
+  },
+
+  manageEngines: function () {
+    this._nodes.panel.hidePopup();
+    this._send("ManageEngines");
+  },
+
+  setWidth: function (width) {
+    this._nodes.form.style.width = width + "px";
+    this._nodes.form.style.maxWidth = width + "px";
+  },
+
+  handleEvent: function (event) {
+    this["on" + event.detail.type](event.detail.data);
+  },
+
+  onState: function (data) {
+    this._makePanel(data.engines);
+    this._setCurrentEngine(data.currentEngine);
+    this._initWhenInitalStateReceived();
+  },
+
+  onCurrentEngine: function (engineName) {
+    this._setCurrentEngine(engineName);
+  },
+
+  _nodeIDSuffixes: [
+    "form",
+    "logo",
+    "manage",
+    "panel",
+    "text",
+  ],
+
+  _nodes: {},
+
+  _initWhenInitalStateReceived: function () {
+    this._nodes.form.addEventListener("submit", e => this.search(e));
+    this._nodes.logo.addEventListener("click", e => this.showPanel());
+    this._nodes.manage.addEventListener("click", e => this.manageEngines());
+    this._initWhenInitalStateReceived = function () {};
+  },
+
+  _send: function (type, data=null) {
+    window.dispatchEvent(new CustomEvent("ContentSearchClient", {
+      detail: {
+        type: type,
+        data: data,
+      },
+    }));
+  },
+
+  _makePanel: function (engines) {
+    let panel = this._nodes.panel;
+
+    // Empty the panel except for the Manage Engines row.
+    let i = 0;
+    while (i < panel.childNodes.length) {
+      let node = panel.childNodes[i];
+      if (node != this._nodes.manage) {
+        panel.removeChild(node);
+      }
+      else {
+        i++;
+      }
+    }
+
+    // Add all the engines.
+    for (let engine of engines) {
+      panel.insertBefore(this._makePanelEngine(panel, engine),
+                         this._nodes.manage);
+    }
+  },
+
+  _makePanelEngine: function (panel, engine) {
+    let box = document.createElementNS(XUL_NAMESPACE, "hbox");
+    box.className = "newtab-search-panel-engine";
+    box.setAttribute("engine", engine.name);
+
+    box.addEventListener("click", () => {
+      this._send("SetCurrentEngine", engine.name);
+      panel.hidePopup();
+      this._nodes.text.focus();
+    });
+
+    let image = document.createElementNS(XUL_NAMESPACE, "image");
+    if (engine.iconURI) {
+      image.setAttribute("src", engine.iconURI);
+    }
+    box.appendChild(image);
+
+    let label = document.createElementNS(XUL_NAMESPACE, "label");
+    label.setAttribute("value", engine.name);
+    box.appendChild(label);
+
+    return box;
+  },
+
+  _setCurrentEngine: function (engine) {
+    this.currentEngineName = engine.name;
+
+    // Set the logo.
+    let logoURI = window.devicePixelRatio == 2 ? engine.logo2xURI :
+                  engine.logoURI;
+    if (logoURI) {
+      this._nodes.logo.hidden = false;
+      this._nodes.logo.style.backgroundImage = "url(" + logoURI + ")";
+      this._nodes.text.placeholder = "";
+    }
+    else {
+      this._nodes.logo.hidden = true;
+      this._nodes.text.placeholder = engine.name;
+    }
+
+    // Set the selected state of all the engines in the panel.
+    for (let box of this._nodes.panel.childNodes) {
+      if (box.getAttribute("engine") == engine.name) {
+        box.setAttribute("selected", "true");
+      }
+      else {
+        box.removeAttribute("selected");
+      }
+    }
+  },
+};
--- a/browser/base/content/test/newtab/browser.ini
+++ b/browser/base/content/test/newtab/browser.ini
@@ -1,11 +1,14 @@
 [DEFAULT]
-support-files = head.js
 skip-if = e10s # Bug ?????? - about:newtab tests don't work in e10s
+support-files =
+  head.js
+  searchEngineLogo.xml
+  searchEngineNoLogo.xml
 
 [browser_newtab_background_captures.js]
 [browser_newtab_block.js]
 [browser_newtab_bug721442.js]
 [browser_newtab_bug722273.js]
 [browser_newtab_bug723102.js]
 [browser_newtab_bug723121.js]
 [browser_newtab_bug725996.js]
@@ -20,13 +23,14 @@ skip-if = os == "mac" # Intermittent fai
 [browser_newtab_bug998387.js]
 [browser_newtab_disable.js]
 [browser_newtab_drag_drop.js]
 [browser_newtab_drag_drop_ext.js]
 [browser_newtab_drop_preview.js]
 [browser_newtab_focus.js]
 [browser_newtab_perwindow_private_browsing.js]
 [browser_newtab_reset.js]
+[browser_newtab_search.js]
 [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_focus.js
+++ b/browser/base/content/test/newtab/browser_newtab_focus.js
@@ -4,19 +4,19 @@
 /*
  * These tests make sure that focusing the 'New Tage Page' works as expected.
  */
 function runTests() {
   // Handle the OSX full keyboard access setting
   Services.prefs.setIntPref("accessibility.tabfocus", 7);
 
   // Focus count in new tab page.
-  // 28 = 9 * 3 + 1 = 9 sites and 1 toggle button, each site has a link, a pin
-  // and a remove button.
-  let FOCUS_COUNT = 28;
+  // 30 = 9 * 3 + 3 = 9 sites, each with link, pin and remove buttons; search
+  // bar; search button; and toggle button.
+  let FOCUS_COUNT = 30;
 
   // Create a new tab page.
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("");
 
   yield addNewTabPageTab();
   gURLBar.focus();
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_search.js
@@ -0,0 +1,295 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// See browser/components/search/test/browser_*_behavior.js for tests of actual
+// searches.
+
+const ENGINE_LOGO = "searchEngineLogo.xml";
+const ENGINE_NO_LOGO = "searchEngineNoLogo.xml";
+
+const SERVICE_EVENT_NAME = "ContentSearchService";
+
+const LOGO_LOW_DPI_SIZE = [65, 26];
+const LOGO_HIGH_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
+// listener consumes the queue and ensures that each event it receives is at
+// the head of the queue.
+//
+// Each item in the queue is an object { type, deferred }.  type is the
+// expected search event type.  deferred is a Promise.defer() value that is
+// resolved when the event is consumed.
+var gExpectedSearchEventQueue = [];
+
+var gNewEngines = [];
+
+function runTests() {
+  let oldCurrentEngine = Services.search.currentEngine;
+
+  yield addNewTabPageTab();
+  yield whenSearchInitDone();
+
+  // The tab is removed at the end of the test, so there's no need to remove
+  // this listener at the end of the test.
+  info("Adding search event listener");
+  getContentWindow().addEventListener(SERVICE_EVENT_NAME, searchEventListener);
+
+  let panel = searchPanel();
+  is(panel.state, "closed", "Search panel should be closed initially");
+
+  // The panel's animation often is not finished when the test clicks on panel
+  // children, which makes the test click the wrong children, so disable it.
+  panel.setAttribute("animate", "false");
+
+  // Add the two test engines.
+  let logoEngine = null;
+  yield promiseNewSearchEngine(true).then(engine => {
+    logoEngine = engine;
+    TestRunner.next();
+  });
+  ok(!!logoEngine.getIconURLBySize(...LOGO_LOW_DPI_SIZE),
+     "Sanity check: engine should have 1x logo");
+  ok(!!logoEngine.getIconURLBySize(...LOGO_HIGH_DPI_SIZE),
+     "Sanity check: engine should have 2x logo");
+
+  let noLogoEngine = null;
+  yield promiseNewSearchEngine(false).then(engine => {
+    noLogoEngine = engine;
+    TestRunner.next();
+  });
+  ok(!noLogoEngine.getIconURLBySize(...LOGO_LOW_DPI_SIZE),
+     "Sanity check: engine should not have 1x logo");
+  ok(!noLogoEngine.getIconURLBySize(...LOGO_HIGH_DPI_SIZE),
+     "Sanity check: engine should not have 2x logo");
+
+  // Use the search service to change the current engine to the logo engine.
+  Services.search.currentEngine = logoEngine;
+  yield promiseSearchEvents(["CurrentEngine"]).then(TestRunner.next);
+  checkCurrentEngine(ENGINE_LOGO);
+
+  // Click the logo to open the search panel.
+  yield Promise.all([
+    promisePanelShown(panel),
+    promiseClick(logoImg()),
+  ]).then(TestRunner.next);
+
+  // In the search panel, click the no-logo engine.  It should become the
+  // current engine.
+  let noLogoBox = null;
+  for (let box of panel.childNodes) {
+    if (box.getAttribute("engine") == noLogoEngine.name) {
+      noLogoBox = box;
+      break;
+    }
+  }
+  ok(noLogoBox, "Search panel should contain the no-logo engine");
+  yield Promise.all([
+    promiseSearchEvents(["CurrentEngine"]),
+    promiseClick(noLogoBox),
+  ]).then(TestRunner.next);
+
+  checkCurrentEngine(ENGINE_NO_LOGO);
+
+  // Switch back to the logo engine.
+  Services.search.currentEngine = logoEngine;
+  yield promiseSearchEvents(["CurrentEngine"]).then(TestRunner.next);
+  checkCurrentEngine(ENGINE_LOGO);
+
+  // Open the panel again.
+  yield Promise.all([
+    promisePanelShown(panel),
+    promiseClick(logoImg()),
+  ]).then(TestRunner.next);
+
+  // 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);
+
+  // 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("State");
+  }
+  yield promiseSearchEvents(events).then(TestRunner.next);
+}
+
+function searchEventListener(event) {
+  info("Got search event " + event.detail.type);
+  let passed = false;
+  let nonempty = gExpectedSearchEventQueue.length > 0;
+  ok(nonempty, "Expected search event queue should be nonempty");
+  if (nonempty) {
+    let { type, deferred } = gExpectedSearchEventQueue.shift();
+    is(event.detail.type, type, "Got expected search event " + type);
+    if (event.detail.type == type) {
+      passed = true;
+      // Let gSearch respond to the event before continuing.
+      executeSoon(() => deferred.resolve());
+    }
+  }
+  if (!passed) {
+    info("Didn't get expected event, stopping the test");
+    getContentWindow().removeEventListener(SERVICE_EVENT_NAME,
+                                           searchEventListener);
+    // Set next() to a no-op so the test really does stop.
+    TestRunner.next = function () {};
+    TestRunner.finish();
+  }
+}
+
+function $(idSuffix) {
+  return getContentDocument().getElementById("newtab-search-" + idSuffix);
+}
+
+function promiseSearchEvents(events) {
+  info("Expecting search events: " + events);
+  events = events.map(e => ({ type: e, deferred: Promise.defer() }));
+  gExpectedSearchEventQueue.push(...events);
+  return Promise.all(events.map(e => e.deferred.promise));
+}
+
+function promiseNewSearchEngine(withLogo) {
+  let basename = withLogo ? ENGINE_LOGO : ENGINE_NO_LOGO;
+  info("Waiting for engine to be added: " + basename);
+
+  // Wait for the search events triggered by adding the new engine.
+  // engine-added engine-loaded
+  let expectedSearchEvents = ["State", "State"];
+  if (withLogo) {
+    // an engine-changed for each of the two logos
+    expectedSearchEvents.push("State", "State");
+  }
+  let eventPromise = promiseSearchEvents(expectedSearchEvents);
+
+  // Wait for addEngine().
+  let addDeferred = Promise.defer();
+  let url = getRootDirectory(gTestPath) + basename;
+  Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, {
+    onSuccess: function (engine) {
+      info("Search engine added: " + basename);
+      gNewEngines.push(engine);
+      addDeferred.resolve(engine);
+    },
+    onError: function (errCode) {
+      ok(false, "addEngine failed with error code " + errCode);
+      addDeferred.reject();
+    },
+  });
+
+  // Make a new promise that wraps the previous promises.  The only point of
+  // this is to pass the new engine to the yielder via deferred.resolve(),
+  // which is a little nicer than passing an array whose first element is the
+  // new engine.
+  let deferred = Promise.defer();
+  Promise.all([addDeferred.promise, eventPromise]).then(values => {
+    let newEngine = values[0];
+    deferred.resolve(newEngine);
+  }, () => deferred.reject());
+  return deferred.promise;
+}
+
+function checkCurrentEngine(basename) {
+  let engine = Services.search.currentEngine;
+  ok(engine.name.contains(basename),
+     "Sanity check: current engine: engine.name=" + engine.name +
+     " basename=" + basename);
+
+  // gSearch.currentEngineName
+  is(gSearch().currentEngineName, engine.name,
+     "currentEngineName: " + engine.name);
+
+  // search bar logo
+  let logoSize = [px * window.devicePixelRatio for (px of LOGO_LOW_DPI_SIZE)];
+  let logoURI = engine.getIconURLBySize(...logoSize);
+  let logo = logoImg();
+  is(logo.hidden, !logoURI,
+     "Logo should be visible iff engine has a logo: " + engine.name);
+  if (logoURI) {
+    is(logo.style.backgroundImage, 'url("' + logoURI + '")', "Logo URI");
+  }
+
+  // "selected" attributes of engines in the panel
+  let panel = searchPanel();
+  for (let engineBox of panel.childNodes) {
+    let engineName = engineBox.getAttribute("engine");
+    if (engineName == engine.name) {
+      is(engineBox.getAttribute("selected"), "true",
+         "Engine box's selected attribute should be true for " +
+         "selected engine: " + engineName);
+    }
+    else {
+      ok(!engineBox.hasAttribute("selected"),
+         "Engine box's selected attribute should be absent for " +
+         "non-selected engine: " + engineName);
+    }
+  }
+}
+
+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");
+    executeSoon(() => deferred.resolve());
+  });
+  return deferred.promise;
+}
+
+function promiseClick(node) {
+  let deferred = Promise.defer();
+  let win = getContentWindow();
+  SimpleTest.waitForFocus(() => {
+    EventUtils.synthesizeMouseAtCenter(node, {}, win);
+    deferred.resolve();
+  }, win);
+  return deferred.promise;
+}
+
+function promiseManagerOpen() {
+  info("Waiting for the search manager window to open...");
+  let deferred = Promise.defer();
+  let winWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+                   getService(Ci.nsIWindowWatcher);
+  winWatcher.registerNotification(function onWin(subj, topic, data) {
+    if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+      subj.addEventListener("load", function onLoad() {
+        subj.removeEventListener("load", onLoad);
+        if (subj.document.documentURI ==
+            "chrome://browser/content/search/engineManager.xul") {
+          winWatcher.unregisterNotification(onWin);
+          ok(true, "Observed search manager window opened");
+          is(subj.opener, gWindow,
+             "Search engine manager opener should be the chrome browser " +
+             "window containing the newtab page");
+          executeSoon(() => {
+            subj.close();
+            deferred.resolve();
+          });
+        }
+      });
+    }
+  });
+  return deferred.promise;
+}
+
+function searchPanel() {
+  return $("panel");
+}
+
+function logoImg() {
+  return $("logo");
+}
+
+function gSearch() {
+  return getContentWindow().gSearch;
+}
--- a/browser/base/content/test/newtab/browser_newtab_sponsored_icon_click.js
+++ b/browser/base/content/test/newtab/browser_newtab_sponsored_icon_click.js
@@ -1,15 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function runTests() {
   yield setLinks("0");
   yield addNewTabPageTab();
 
+  // When gSearch modifies the DOM as it sets itself up, it can prevent the
+  // popup from opening, depending on the timing.  Wait until that's done.
+  yield whenSearchInitDone();
+
   let site = getCell(0).node.querySelector(".newtab-site");
   site.setAttribute("type", "sponsored");
 
   let sponsoredPanel = getContentDocument().getElementById("sponsored-panel");
   is(sponsoredPanel.state, "closed", "Sponsored panel must be closed");
 
   function continueOnceOn(event) {
     sponsoredPanel.addEventListener(event, function listener() {
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -20,20 +20,51 @@ let {Promise, NewTabUtils, Sanitizer, cl
 let uri = Services.io.newURI("about:newtab", null, null);
 let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
 
 let isMac = ("nsILocalFileMac" in Ci);
 let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc);
 let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
 let gWindow = window;
 
+// The tests assume all three rows of sites are shown, but the window may be too
+// short to actually show three rows.  Resize it if necessary.
+let requiredInnerHeight =
+  40 + 32 + // undo container + bottom margin
+  44 + 32 + // search bar + bottom margin
+  (3 * (150 + 32)) + // 3 rows * (tile height + title and bottom margin)
+  100; // breathing room
+
+let oldInnerHeight = null;
+if (gBrowser.contentWindow.innerHeight < requiredInnerHeight) {
+  oldInnerHeight = gBrowser.contentWindow.innerHeight;
+  info("Changing browser inner height from " + oldInnerHeight + " to " +
+       requiredInnerHeight);
+  gBrowser.contentWindow.innerHeight = requiredInnerHeight;
+  let screenHeight = {};
+  Cc["@mozilla.org/gfx/screenmanager;1"].
+    getService(Ci.nsIScreenManager).
+    primaryScreen.
+    GetAvailRectDisplayPix({}, {}, {}, screenHeight);
+  screenHeight = screenHeight.value;
+  if (screenHeight < gBrowser.contentWindow.outerHeight) {
+    info("Warning: Browser outer height is now " +
+         gBrowser.contentWindow.outerHeight + ", which is larger than the " +
+         "available screen height, " + screenHeight +
+         ". That may cause problems.");
+  }
+}
+
 registerCleanupFunction(function () {
   while (gWindow.gBrowser.tabs.length > 1)
     gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]);
 
+  if (oldInnerHeight)
+    gBrowser.contentWindow.innerHeight = oldInnerHeight;
+
   Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
   Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
 
   // Stop any update timers to prevent unexpected updates in later tests
   let timer = NewTabUtils.allPages._scheduleUpdateTimeout;
   if (timer) {
     clearTimeout(timer);
     delete NewTabUtils.allPages._scheduleUpdateTimeout;
@@ -544,8 +575,40 @@ function whenPagesUpdated(aCallback, aOn
     }
   };
 
   NewTabUtils.allPages.register(page);
   registerCleanupFunction(function () {
     NewTabUtils.allPages.unregister(page);
   });
 }
+
+/**
+ * Waits a small amount of time for search events to stop occurring in the
+ * newtab page.
+ *
+ * newtab pages receive some search events around load time that are difficult
+ * to predict.  There are two categories of such events: (1) "State" events
+ * triggered by engine notifications like engine-changed, due to the search
+ * service initializing itself on app startup.  This can happen when a test is
+ * the first test to run.  (2) "State" events triggered by the newtab page
+ * itself when gSearch first sets itself up.  newtab preloading makes these a
+ * pain to predict.
+ */
+function whenSearchInitDone() {
+  info("Waiting for initial search events...");
+  let numTicks = 0;
+  function reset(event) {
+    info("Got initial search event " + event.detail.type +
+         ", waiting for more...");
+    numTicks = 0;
+  }
+  let eventName = "ContentSearchService";
+  getContentWindow().addEventListener(eventName, reset);
+  let interval = window.setInterval(() => {
+    if (++numTicks >= 100) {
+      info("Done waiting for initial search events");
+      window.clearInterval(interval);
+      getContentWindow().removeEventListener(eventName, reset);
+      TestRunner.next();
+    }
+  }, 0);
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/searchEngineLogo.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_newtab_search searchEngineLogo.xml</ShortName>
+<Url type="text/html" method="GET" template="http://browser-newtab-search.com/logo" rel="searchform"/>
+<Image width="65" height="26">data:image/png;base64,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</Image>
+<Image width="130" height="52">data:image/png;base64,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</Image>
+</SearchPlugin>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/searchEngineNoLogo.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_newtab_search searchEngineNoLogo.xml</ShortName>
+<Url type="text/html" method="GET" template="http://browser-newtab-search.com/nologo" rel="searchform"/>
+</SearchPlugin>