Bug 1335905 - Add Preferences search feature, preffed off by default. r?jaws, r?mconley draft
authormanotejmeka <manotejmeka@gmail.com>
Mon, 03 Apr 2017 14:02:01 -0400
changeset 555139 5404eaca4911c548c8bb57bed42f64de17e45e99
parent 553822 8df9fabf2587b7020889755acb9e75b664fe13cf
child 555176 6aaeea3e55739d2ef4b2b983b8bca0cb129c02b7
push id52170
push userbmo:manotejmeka@gmail.com
push dateMon, 03 Apr 2017 18:04:01 +0000
reviewersjaws, mconley
bugs1335905
milestone55.0a1
Bug 1335905 - Add Preferences search feature, preffed off by default. r?jaws, r?mconley Code written by Manotej Meka <manotejmeka@gmail.com> and Ian Ferguson <fergu272@msu.edu> This is the initial landing of the search feature, and is preffed off behind browser.preferences.search. MozReview-Commit-ID: 7iaeRsIIV3Y
browser/app/profile/firefox.js
browser/components/preferences/in-content/findInPage.js
browser/components/preferences/in-content/jar.mn
browser/components/preferences/in-content/preferences.js
browser/components/preferences/in-content/preferences.xul
browser/components/preferences/in-content/searchResults.xul
browser/components/preferences/in-content/tests/browser.ini
browser/components/preferences/in-content/tests/browser_search_within_preferences.js
browser/locales/en-US/chrome/browser/preferences/preferences.dtd
browser/locales/en-US/chrome/browser/preferences/preferences.properties
browser/themes/shared/incontentprefs/icons.svg
browser/themes/shared/incontentprefs/preferences.inc.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -665,16 +665,20 @@ pref("plugin.defaultXpi.state", 2);
 pref("plugin.state.flash", 2);
 pref("plugin.state.java", 1);
 
 #ifdef XP_WIN
 pref("browser.preferences.instantApply", false);
 #else
 pref("browser.preferences.instantApply", true);
 #endif
+
+// Toggling Search bar on and off in about:preferences
+pref("browser.preferences.search", false);
+
 // Once the Storage Management is completed.
 // (The Storage Management-related prefs are browser.storageManager.* )
 // The Offline(Appcache) Group section in about:preferences will be hidden.
 // And the task to clear appcache will be done by Storage Management.
 pref("browser.preferences.offlineGroup.enabled", true);
 
 pref("browser.download.show_plugins_in_list", true);
 pref("browser.download.hide_plugins_without_extensions", true);
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/findInPage.js
@@ -0,0 +1,308 @@
+/* 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/. */
+
+/* import-globals-from preferences.js */
+
+var gSearchResultsPane = {
+  findSelection: null,
+  searchResultsCategory: null,
+  searchInput: null,
+
+  init() {
+    let controller = this.getSelectionController();
+    this.findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
+    this.searchResultsCategory = document.getElementById("category-search-results");
+
+    this.searchInput = document.getElementById("searchInput");
+    this.searchInput.hidden = !Services.prefs.getBoolPref("browser.preferences.search");
+    if (!this.searchInput.hidden) {
+      this.searchInput.addEventListener("command", this);
+      this.searchInput.addEventListener("focus", this);
+    }
+  },
+
+  handleEvent(event) {
+    if (event.type === "command") {
+      this.searchFunction(event);
+    } else if (event.type === "focus") {
+      this.initializeCategories();
+    }
+  },
+
+  /**
+   * Check that the passed string matches the filter arguments.
+   *
+   * @param String str
+   *    to search for filter words in.
+   * @param String filter
+   *    is a string containing all of the words to filter on.
+   * @returns boolean
+   *    true when match in string else false
+   */
+  stringMatchesFilters(str, filter) {
+    if (!filter || !str) {
+      return true;
+    }
+    let searchStr = str.toLowerCase();
+    let filterStrings = filter.toLowerCase().split(/\s+/);
+    return !filterStrings.some(f => searchStr.indexOf(f) == -1);
+  },
+
+  categoriesInitialized: false,
+
+  /**
+   * Will attempt to initialize all uninitialized categories
+   */
+  initializeCategories() {
+    //  Initializing all the JS for all the tabs
+    if (!this.categoriesInitialized) {
+      this.categoriesInitialized = true;
+      // Each element of gCategoryInits is a name
+      for (let [/* name */, category] of gCategoryInits) {
+        if (!category.inited) {
+          category.init();
+        }
+      }
+    }
+  },
+
+  /**
+   * Finds and returns text nodes within node and all descendants
+   * Iterates through all the sibilings of the node object and adds the sibilings
+   * to an array if sibiling is a TEXT_NODE else checks the text nodes with in current node
+   * Source - http://stackoverflow.com/questions/10730309/find-all-text-nodes-in-html-page
+   *
+   * @param Node nodeObject
+   *    DOM element
+   * @returns array of text nodes
+   */
+  textNodeDescendants(node) {
+    if (!node) {
+      return [];
+    }
+    let all = [];
+    for (node = node.firstChild; node; node = node.nextSibling) {
+      if (node.nodeType === node.TEXT_NODE) {
+        all.push(node);
+      } else {
+        all = all.concat(this.textNodeDescendants(node));
+      }
+    }
+    return all;
+  },
+
+  /**
+   * This function is used to find words contained within the text nodes.
+   * We pass in the textNodes because they contain the text to be highlighted.
+   * We pass in the nodeSizes to tell exactly where highlighting need be done.
+   * When creating the range for highlighting, if the nodes are section is split
+   * by an access key, it is important to have the size of each of the nodes summed.
+   * @param Array textNodes
+   *    List of DOM elements
+   * @param Array nodeSizes
+   *    Running size of text nodes. This will contain the same number of elements as textNodes.
+   *    The first element is the size of first textNode element.
+   *    For any nodes after, they will contain the summation of the nodes thus far in the array.
+   *    Example:
+   *    textNodes = [[This is ], [a], [n example]]
+   *    nodeSizes = [[8], [9], [18]]
+   *    This is used to determine the offset when highlighting
+   * @param String textSearch
+   *    Concatination of textNodes's text content
+   *    Example:
+   *    textNodes = [[This is ], [a], [n example]]
+   *    nodeSizes = "This is an example"
+   *    This is used when executing the regular expression
+   * @param String searchPhrase
+   *    word or words to search for
+   * @returns boolean
+   *      Returns true when atleast one instance of search phrase is found, otherwise false
+   */
+  highlightMatches(textNodes, nodeSizes, textSearch, searchPhrase) {
+    let indices = [];
+    let i = -1;
+    while ((i = textSearch.indexOf(searchPhrase, i + 1)) >= 0) {
+      indices.push(i);
+    }
+
+    // Looping through each spot the searchPhrase is found in the concatenated string
+    for (let startValue of indices) {
+      let endValue = startValue + searchPhrase.length;
+      let startNode = null;
+      let endNode = null;
+      let nodeStartIndex = null;
+
+      // Determining the start and end node to highlight from
+      nodeSizes.forEach(function(lengthNodes, index) {
+        // Determining the start node
+        if (!startNode && lengthNodes >= startValue) {
+          startNode = textNodes[index];
+          nodeStartIndex = index;
+          // Calculating the offset when found query is not in the first node
+          if (index > 0) {
+            startValue -= nodeSizes[index - 1];
+          }
+        }
+        // Determining the end node
+        if (!endNode && lengthNodes >= endValue) {
+          endNode = textNodes[index];
+          // Calculating the offset when endNode is different from startNode
+          // or when endNode is not the first node
+          if (index != nodeStartIndex || index > 0 ) {
+            endValue -= nodeSizes[index - 1];
+          }
+        }
+      });
+      let range = document.createRange();
+      range.setStart(startNode, startValue);
+      range.setEnd(endNode, endValue);
+      this.findSelection.addRange(range);
+    }
+
+    return indices.length > 0;
+  },
+
+  getSelectionController() {
+    //  Yuck. See bug 138068.
+    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIWebNavigation)
+                         .QueryInterface(Ci.nsIDocShell);
+
+    let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsISelectionDisplay)
+                             .QueryInterface(Ci.nsISelectionController);
+
+    return controller;
+  },
+
+  get strings() {
+    delete this.strings;
+    return this.strings = document.getElementById("searchResultBundle");
+  },
+
+  /**
+   * Shows or hides content according to search input
+   *
+   * @param String event
+   *    to search for filted query in
+   */
+  searchFunction(event) {
+    let query = event.target.value.trim().toLowerCase();
+    this.findSelection.removeAllRanges();
+
+    let srHeader = document.getElementById("header-searchResults");
+
+    if (query) {
+      // Showing the Search Results Tag
+      gotoPref("paneSearchResults");
+
+      this.searchResultsCategory.hidden = false;
+
+      let resultsFound = false;
+
+      // Building the range for highlighted areas
+      let rootPreferences = document.getElementById("mainPrefPane")
+      let rootPreferencesChildren = rootPreferences.children;
+
+      // Showing all the children to bind JS, Access Keys, etc
+      for (let i = 0; i < rootPreferences.childElementCount; i++) {
+        rootPreferencesChildren[i].hidden = false;
+      }
+
+      // Showing or Hiding specific section depending on if words in query are found
+      for (let i = 0; i < rootPreferences.childElementCount; i++) {
+        if (rootPreferencesChildren[i].className != "header" &&
+            rootPreferencesChildren[i].className != "no-results-message" &&
+            this.searchWithinNode(rootPreferencesChildren[i], query)) {
+          rootPreferencesChildren[i].hidden = false;
+          resultsFound = true;
+        } else {
+          rootPreferencesChildren[i].hidden = true;
+        }
+      }
+      // It hides Search Results header so turning it on
+      srHeader.hidden = false;
+
+      if (!resultsFound) {
+        let noResultsEl = document.querySelector(".no-results-message");
+        noResultsEl.hidden = false;
+
+        let strings = this.strings;
+        document.getElementById("sorry-message").textContent =
+          strings.getFormattedString("searchResults.sorryMessage", [query]);
+
+        let brandName = document.getElementById("bundleBrand").getString("brandShortName");
+        document.getElementById("need-help").innerHTML =
+          strings.getFormattedString("searchResults.needHelp", [brandName]);
+
+        document.getElementById("need-help-link").setAttribute("href", getHelpLinkURL("search"));
+      }
+    } else {
+      this.searchResultsCategory.hidden = true;
+      document.getElementById("sorry-message").textContent = "";
+      // Going back to General when cleared
+      gotoPref("paneGeneral");
+    }
+  },
+
+  /**
+   * Finding leaf nodes and checking their content for words to search,
+   * It is a recursive function
+   *
+   * @param Node nodeObject
+   *    DOM Element
+   * @param String searchPhrase
+   * @returns boolean
+   *    Returns true when found in at least one childNode, false otherwise
+   */
+  searchWithinNode(nodeObject, searchPhrase) {
+    let matchesFound = false;
+    if (nodeObject.childElementCount == 0) {
+      let simpleTextNodes = this.textNodeDescendants(nodeObject);
+
+      for (let node of simpleTextNodes) {
+        let result = this.highlightMatches([node], [node.length], node.textContent.toLowerCase(), searchPhrase);
+        matchesFound = matchesFound || result;
+      }
+
+      //  Collecting data from boxObject
+      let nodeSizes = [];
+      let allNodeText = "";
+      let runningSize = 0;
+      let labelResult = false;
+      let valueResult = false;
+      let accessKeyTextNodes = this.textNodeDescendants(nodeObject.boxObject);
+
+      for (let node of accessKeyTextNodes) {
+        runningSize += node.textContent.length;
+        allNodeText += node.textContent;
+        nodeSizes.push(runningSize);
+      }
+
+      // Access key are presented
+      let complexTextNodesResult = this.highlightMatches(accessKeyTextNodes, nodeSizes, allNodeText.toLowerCase(), searchPhrase);
+
+      //  Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text.
+      if (nodeObject.getAttribute("label")) {
+        labelResult = this.stringMatchesFilters(nodeObject.getAttribute("label"), searchPhrase);
+      }
+
+      //  Searching some elements, such as xul:label, store their user-visible text in a "value" attribute.
+      if (nodeObject.getAttribute("value")) {
+        valueResult = this.stringMatchesFilters(nodeObject.getAttribute("value"), searchPhrase);
+      }
+
+      matchesFound = matchesFound || complexTextNodesResult || labelResult || valueResult;
+    }
+
+    for (let i = 0; i < nodeObject.childNodes.length; i++) {
+      // Search only if child node is not hidden
+      if (!nodeObject.childNodes[i].hidden) {
+        let result = this.searchWithinNode(nodeObject.childNodes[i], searchPhrase);
+        matchesFound = matchesFound || result;
+      }
+    }
+    return matchesFound;
+  }
+}
--- a/browser/components/preferences/in-content/jar.mn
+++ b/browser/components/preferences/in-content/jar.mn
@@ -8,8 +8,9 @@ browser.jar:
    content/browser/preferences/in-content/subdialogs.js
 
    content/browser/preferences/in-content/main.js
    content/browser/preferences/in-content/privacy.js
    content/browser/preferences/in-content/containers.js
    content/browser/preferences/in-content/advanced.js
    content/browser/preferences/in-content/applications.js
    content/browser/preferences/in-content/sync.js
+   content/browser/preferences/in-content/findInPage.js
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -5,16 +5,17 @@
 // Import globals from the files imported by the .xul files.
 /* import-globals-from subdialogs.js */
 /* import-globals-from advanced.js */
 /* import-globals-from main.js */
 /* import-globals-from containers.js */
 /* import-globals-from privacy.js */
 /* import-globals-from applications.js */
 /* import-globals-from sync.js */
+/* import-globals-from findInPage.js */
 /* import-globals-from ../../../base/content/utilityOverlay.js */
 
 "use strict";
 
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cr = Components.results;
@@ -54,16 +55,18 @@ function init_all() {
 
   gSubDialog.init();
   register_module("paneGeneral", gMainPane);
   register_module("panePrivacy", gPrivacyPane);
   register_module("paneContainers", gContainersPane);
   register_module("paneAdvanced", gAdvancedPane);
   register_module("paneApplications", gApplicationsPane);
   register_module("paneSync", gSyncPane);
+  register_module("paneSearchResults", gSearchResultsPane);
+  gSearchResultsPane.init();
 
   let categories = document.getElementById("categories");
   categories.addEventListener("select", event => gotoPref(event.target.value));
 
   document.documentElement.addEventListener("keydown", function(event) {
     if (event.keyCode == KeyEvent.DOM_VK_TAB) {
       categories.setAttribute("keyboard-navigation", "true");
     }
@@ -130,17 +133,17 @@ function telemetryBucketForCategory(cate
 }
 
 function onHashChange() {
   gotoPref();
 }
 
 function gotoPref(aCategory) {
   let categories = document.getElementById("categories");
-  const kDefaultCategoryInternalName = categories.firstElementChild.value;
+  const kDefaultCategoryInternalName = "paneGeneral";
   let hash = document.location.hash;
   let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
   category = friendlyPrefCategoryNameToInternalName(category);
 
   // Updating the hash (below) or changing the selected category
   // will re-enter gotoPref.
   if (gLastHash == category)
     return;
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -70,16 +70,17 @@
 
   <html:link rel="shortcut icon"
               href="chrome://browser/skin/preferences/in-content/favicon.ico"/>
 
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/preferences/in-content/preferences.js"/>
+  <script src="chrome://browser/content/preferences/in-content/findInPage.js"/>
   <script src="chrome://browser/content/preferences/in-content/subdialogs.js"/>
 
   <stringbundle id="bundleBrand"
                 src="chrome://branding/locale/brand.properties"/>
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
 
   <stringbundleset id="appManagerBundleset">
@@ -87,16 +88,27 @@
                   src="chrome://browser/locale/preferences/applicationManager.properties"/>
   </stringbundleset>
 
   <stack flex="1">
   <hbox flex="1">
 
     <!-- category list -->
     <richlistbox id="categories">
+      <richlistitem id="category-search-results"
+                    class="category"
+                    value="paneSearchResults"
+                    helpTopic="prefs-main"
+                    tooltiptext="&paneSearchResults.title;"
+                    align="center"
+                    hidden="true">
+        <image class="category-icon"/>
+        <label class="category-name" flex="1">&paneSearchResults.title;</label>
+      </richlistitem>
+
       <richlistitem id="category-general"
                     class="category"
                     value="paneGeneral"
                     helpTopic="prefs-main"
                     tooltiptext="&paneGeneral.title;"
                     align="center">
         <image class="category-icon"/>
         <label class="category-name" flex="1">&paneGeneral.title;</label>
@@ -152,26 +164,29 @@
     <keyset>
       <!-- Disable the findbar because it doesn't work properly.
            Remove this keyset once bug 1094240 ("disablefastfind" attribute
            broken in e10s mode) is fixed. -->
       <key key="&focusSearch1.key;" modifiers="accel" id="focusSearch1" oncommand=";"/>
     </keyset>
 
     <vbox class="main-content" flex="1">
+      <hbox pack="end">
+        <textbox type="search" id="searchInput" placeholder="&searchInput.label;" hidden="true"/>
+      </hbox>
       <prefpane id="mainPrefPane">
+#include searchResults.xul
 #include main.xul
 #include privacy.xul
 #include containers.xul
 #include advanced.xul
 #include applications.xul
 #include sync.xul
       </prefpane>
     </vbox>
-
   </hbox>
 
     <vbox id="dialogOverlay" align="center" pack="center">
       <groupbox id="dialogBox"
                 orient="vertical"
                 pack="end"
                 role="dialog"
                 aria-labelledby="dialogTitle">
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/searchResults.xul
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<stringbundle id="searchResultBundle" src="chrome://browser/locale/preferences/preferences.properties"/>
+
+<hbox id="header-searchResults"
+      class="header"
+      hidden="true"
+      data-category="paneSearchResults">
+  <label class="header-name" flex="1">&paneSearchResults.title;</label>
+  <html:a class="help-button" target="_blank" aria-label="&helpButton.label;"></html:a>
+</hbox>
+
+<groupbox class="no-results-message" align="start" data-category="paneSearchResults" hidden="true">
+  <label id="sorry-message"></label>
+  <label id="need-help"></label>
+</groupbox>
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -7,16 +7,17 @@ support-files =
 [browser_applications_selection.js]
 [browser_advanced_siteData.js]
 [browser_advanced_update.js]
 skip-if = !updater
 [browser_basic_rebuild_fonts_test.js]
 [browser_bug410900.js]
 [browser_bug705422.js]
 [browser_bug731866.js]
+[browser_search_within_preferences.js]
 [browser_bug795764_cachedisabled.js]
 [browser_bug1018066_resetScrollPosition.js]
 [browser_bug1020245_openPreferences_to_paneContent.js]
 [browser_bug1184989_prevent_scrolling_when_preferences_flipped.js]
 support-files =
   browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul
 [browser_change_app_handler.js]
 skip-if = os != "win" # This test tests the windows-specific app selection dialog, so can't run on non-Windows
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences.js
@@ -0,0 +1,172 @@
+/*
+* This file contains tests for the Preferences search bar.
+*/
+
+/**
+ * Tests to see if search bar is being hidden when pref is turned off
+ */
+add_task(function*() {
+  yield SpecialPowers.pushPrefEnv({"set": [["browser.preferences.search", false]]});
+  yield openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  let searchInput = gBrowser.contentDocument.querySelectorAll("#searchInput");
+  is(searchInput.length, 1, "There should only be one element name searchInput querySelectorAll");
+  is_element_hidden(searchInput[0], "Search box should be hidden");
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+  yield SpecialPowers.popPrefEnv();
+});
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(function*() {
+  yield SpecialPowers.pushPrefEnv({"set": [["browser.preferences.search", true]]});
+});
+
+/**
+ * Tests to see if search bar is being shown when pref is turned on
+ */
+add_task(function*() {
+  yield openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+  is_element_visible(searchInput, "Search box should be shown");
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for "Search Result" panel.
+ * After it runs a search, it tests if the "Search Results" panel is the only selected category.
+ * The search is then cleared, it then tests if the "General" panel is the only selected category.
+ */
+add_task(function*() {
+  yield openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+  // Performs search
+  let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+  searchInput.doCommand()
+  searchInput.value = "password";
+  searchInput.doCommand()
+
+  let categoriesList = gBrowser.contentDocument.getElementById("categories");
+
+  for (let i = 0; i < categoriesList.childElementCount; i++) {
+    let child = categoriesList.children[i]
+    if (child.id == "category-search-results") {
+      is(child.selected, true, "Search results panel should be selected");
+    } else if (child.id) {
+      is(child.selected, false, "No other panel should be selected");
+    }
+  }
+  // Takes search off
+  searchInput.value = "";
+  searchInput.doCommand()
+
+  // Checks if back to generalPane
+  for (let i = 0; i < categoriesList.childElementCount; i++) {
+    let child = categoriesList.children[i]
+    if (child.id == "category-general") {
+      is(child.selected, true, "General panel should be selected");
+    } else if (child.id) {
+      is(child.selected, false, "No other panel should be selected");
+    }
+  }
+
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for "password" case. When we search "password", it should show the "passwordGroup"
+ */
+add_task(function*() {
+  yield openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+  // Performs search
+  let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+  searchInput.doCommand()
+  searchInput.value = "password";
+  searchInput.doCommand()
+
+  let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane");
+
+  for (let i = 0; i < mainPrefTag.childElementCount; i++) {
+    let child = mainPrefTag.children[i]
+    if (child.id == "passwordsGroup" || child.id == "weavePrefsDeck" || child.id == "header-searchResults") {
+      is_element_visible(child, "Should be in search results");
+    } else if (child.id) {
+      is_element_hidden(child, "Should not be in search results");
+    }
+  }
+
+  // Takes search off
+  searchInput.value = "";
+  searchInput.doCommand()
+
+  // Checks if back to generalPane
+  for (let i = 0; i < mainPrefTag.childElementCount; i++) {
+    let child = mainPrefTag.children[i]
+    if (child.id == "startupGroup"
+    || child.id == "defaultEngineGroup"
+    || child.id == "oneClickSearchProvidersGroup"
+    || child.id == "paneGeneral"
+    || child.id == "accessibilityGroup"
+    || child.id == "languagesGroup"
+    || child.id == "fontsGroup"
+    || child.id == "browsingGroup"
+    || child.id == "header-general") {
+      is_element_visible(child, "Should be in general tab");
+    } else if (child.id) {
+      is_element_hidden(child, "Should not be in general tab");
+    }
+  }
+
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for if nothing is found
+ */
+add_task(function*() {
+  yield openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+  let noResultsEl = gBrowser.contentDocument.querySelector(".no-results-message");
+
+  is_element_hidden(noResultsEl, "Should not be in search results yet");
+
+  // Performs search
+  let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+  searchInput.doCommand()
+  searchInput.value = "coach";
+  searchInput.doCommand()
+
+  is_element_visible(noResultsEl, "Should be in search results");
+
+  // Takes search off
+  searchInput.value = "";
+  searchInput.doCommand()
+
+  is_element_hidden(noResultsEl, "Should not be in search results");
+
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Test for if we go back to general tab after search case
+ */
+add_task(function*() {
+  yield openPreferencesViaOpenPreferencesAPI("privacy", {leaveOpen: true});
+  let generalPane = gBrowser.contentDocument.getElementById("header-general");
+
+  is_element_hidden(generalPane, "Should not be in general");
+
+  // Performs search
+  let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+  searchInput.doCommand()
+  searchInput.value = "password";
+  searchInput.doCommand()
+
+  // Takes search off
+  searchInput.value = "";
+  searchInput.doCommand()
+
+  // Checks if back to normal
+  is_element_visible(generalPane, "Should be in generalPane");
+
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.dtd
@@ -8,18 +8,21 @@
 <!-- LOCALIZATION NOTE (prefWindow.titleGNOME): This is not used for in-content preferences -->
 <!ENTITY  prefWindow.titleGNOME   "&brandShortName; Preferences">
 <!-- When making changes to prefWindow.styleWin test both Windows Classic and
      Luna since widget heights are different based on the OS theme -->
 <!ENTITY  prefWinMinSize.styleWin2      "width: 42em; min-height: 37.5em;">
 <!ENTITY  prefWinMinSize.styleMac       "width: 47em; min-height: 40em;">
 <!ENTITY  prefWinMinSize.styleGNOME     "width: 45.5em; min-height: 40.5em;">
 
+<!ENTITY  paneSearchResults.title       "Search Results">
 <!ENTITY  paneGeneral.title             "General">
 <!ENTITY  paneDownloadLinks.title       "Downloads &amp; Links">
 <!ENTITY  panePrivacySecurity.title     "Privacy &amp; Security">
 <!ENTITY  paneContainers.title          "Container Tabs">
 <!ENTITY  paneUpdates.title             "Updates">
 
 <!-- LOCALIZATION NOTE (paneSync1.title): This should match syncBrand.fxAccount.label in ../syncBrand.dtd -->
 <!ENTITY  paneSync1.title          "Firefox Account">
 
 <!ENTITY  helpButton.label        "Help">
+
+<!ENTITY searchInput.label        "Search">
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -246,8 +246,14 @@ removeContainerAlertTitle=Remove This Co
 
 # LOCALIZATION NOTE (removeContainerMsg): Semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #S is the number of container tabs
 removeContainerMsg=If you remove this Container now, #S container tab will be closed. Are you sure you want to remove this Container?;If you remove this Container now, #S container tabs will be closed. Are you sure you want to remove this Container?
 
 removeContainerOkButton=Remove this Container
 removeContainerButton2=Don’t remove this Container
+
+# Search Results Pane
+# LOCALIZATION NOTE %S will be replaced by the word being searched
+searchResults.sorryMessage=Sorry! No results were found for “%S”
+# LOCALIZATION NOTE %S gets replaced with the browser name
+searchResults.needHelp=Need help? Visit <html:a id="need-help-link">%S Support</html:a>
--- a/browser/themes/shared/incontentprefs/icons.svg
+++ b/browser/themes/shared/incontentprefs/icons.svg
@@ -63,16 +63,24 @@
       c0.8,0.5,1.7,0.7,2.8,0.5c0.4-0.1,0.7-0.2,1-0.4c-0.1,0.1-0.1,0.3-0.2,0.4c0.1,0.1,0.4-0.1,0.6-0.5c0.1-0.1,0.3-0.3,0.6-0.4
       c0,0,0,0.1,0,0.2c0,0.1,0,0.1,0,0.2s0,0.1,0.1,0.1c0.4,0,0.7-0.4,1.1-1.3c0.3-0.6,0.5-1.3,0.6-2.2c0.1,0.2,0.2,0.5,0.2,0.8
       c0.2-0.5,0.3-1,0.3-1.3c0-0.3,0-1.3-0.1-2.9c0.3,0.4,0.5,0.7,0.7,1.1c0.1-1.2-0.1-2.2-0.5-3.1c-0.4-0.8-0.9-1.5-1.4-1.9
       c0.5,0.1,1,0.3,1.4,0.6l-0.3-0.2c-1.4-1.4-3.1-2.2-5.1-2.4c-2-0.2-3.8,0.3-5.4,1.4c-0.6,0-1.1,0-1.5,0.1C3.6,2.7,3.5,2.6,3.5,2.5
       C5.3,0.8,7.4,0,9.9,0s4.6,0.8,6.5,2.4L16.2,2c0.6,0.3,1,0.7,1.4,1.2l-0.1-0.3l0,0.1l0-0.1c0.9,0.7,1.4,1.6,1.7,2.7
       c0.2,0.9,0.2,1.6,0.1,2.3c0.1,0.1,0.1,0.1,0.1,0.3l0.3-1.1c0.1,0.3,0.2,0.7,0.2,1C20,8.5,20,8.9,20,9.2c0,0.4-0.1,0.7-0.1,1
       c-0.1,0.3-0.1,0.7-0.2,1s-0.2,0.6-0.3,0.8c-0.1,0.2-0.2,0.5-0.3,0.7C19.1,12.8,19.2,13,19.3,13.4L19.3,13.4z"/>
     </g>
+    <g id="searchResults-shape">
+      <path d="M8,16.3c1.5,0,3-0.4,4.3-1.3l4.6,4.6c0.3,0.3,0.8,0.4,1.2,0.3s0.8-0.5,0.9-0.9s0-0.9-0.3-1.2l-4.5-4.5
+			c2.4-2.9,2.5-7.2,0.2-10.2S8-0.8,4.6,0.8s-5.2,5.4-4.4,9.1S4.2,16.3,8,16.3z M8,1.9c3.4,0,6.1,2.8,6.1,6.2s-2.7,6.2-6.1,6.2
+			S1.9,11.5,1.9,8C1.9,4.6,4.6,1.9,8,1.9L8,1.9z"/>
+			<path d="M8,12.9c2.6,0,4.7-2.1,4.7-4.8S10.6,3.4,8,3.4c-2.6,0-4.7,2.1-4.7,4.7c0,1.3,0.5,2.5,1.4,3.4
+			C5.6,12.4,6.7,12.9,8,12.9L8,12.9z M7.8,4.5c0.4,0,0.8,0.4,0.8,0.8S8.3,6.1,7.8,6.1C6.8,6.1,6,6.9,6,7.9c0,0.4-0.4,0.8-0.8,0.8
+			S4.5,8.3,4.5,7.9C4.5,6,6,4.5,7.8,4.5L7.8,4.5z"/>
+    </g>
   </defs>
   <use id="general" xlink:href="#general-shape"/>
   <use id="general-native" xlink:href="#general-shape"/>
   <use id="search" xlink:href="#search-shape"/>
   <use id="search-native" xlink:href="#search-shape"/>
   <use id="content" xlink:href="#content-shape"/>
   <use id="content-native" xlink:href="#content-shape"/>
   <use id="applications" xlink:href="#applications-shape"/>
@@ -80,9 +88,11 @@
   <use id="privacy" xlink:href="#privacy-shape"/>
   <use id="privacy-native" xlink:href="#privacy-shape"/>
   <use id="security" xlink:href="#security-shape"/>
   <use id="security-native" xlink:href="#security-shape"/>
   <use id="sync" xlink:href="#sync-shape"/>
   <use id="sync-native" xlink:href="#sync-shape"/>
   <use id="advanced" xlink:href="#advanced-shape"/>
   <use id="advanced-native" xlink:href="#advanced-shape"/>
+  <use id="searchResults" xlink:href="#searchResults-shape"/>
+  <use id="searchResults-native" xlink:href="#searchResults-shape"/>
 </svg>
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -96,16 +96,20 @@ treecol {
 #category-sync > .category-icon {
   list-style-image: url("chrome://browser/skin/preferences/in-content/icons.svg#sync");
 }
 
 #category-advanced > .category-icon {
   list-style-image: url("chrome://browser/skin/preferences/in-content/icons.svg#advanced");
 }
 
+#category-search-results > .category-icon {
+  list-style-image: url("chrome://browser/skin/preferences/in-content/icons.svg#searchResults");
+}
+
 @media (max-width: 800px) {
   .category-name {
     display: none;
   }
 }
 
 /* header */
 .header {