Bug 1335905 - Add Preferences search feature, preffed off by default. r=jaws,mconley
authormanotejmeka <manotejmeka@gmail.com>
Tue, 04 Apr 2017 16:33:34 -0400
changeset 351389 05ba929d3d1b5d678960a89210ead7d8d6dd4541
parent 351388 ccb8a0d883c889e95bc3aa59179562cf3fe45dfb
child 351390 cb2a92a861321c8c5c630ce1964d55d4a869f6fd
push id31610
push usercbook@mozilla.com
push dateThu, 06 Apr 2017 09:36:41 +0000
treeherdermozilla-central@3c68d659c2b7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws, mconley
bugs1335905
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1335905 - Add Preferences search feature, preffed off by default. r=jaws,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/base/content/test/static/browser_misused_characters_in_strings.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);
--- a/browser/base/content/test/static/browser_misused_characters_in_strings.js
+++ b/browser/base/content/test/static/browser_misused_characters_in_strings.js
@@ -99,16 +99,20 @@ let gWhitelist = [{
   }, {
     file: "pocket.properties",
     key: "tos",
     type: "double-quote"
   }, {
     file: "aboutNetworking.dtd",
     key: "aboutNetworking.logTutorial",
     type: "single-quote"
+  }, {
+    file: "preferences.properties",
+    key: "searchResults.needHelp",
+    type: "double-quote"
   }
 ];
 
 /**
  * Check if an error should be ignored due to matching one of the whitelist
  * objects defined in gWhitelist.
  *
  * @param filepath The URI spec of the locale file
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
@@ -6,16 +6,17 @@ support-files =
 
 [browser_applications_selection.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 {