Bug 1420761 - Handle new localization API in Preferences Search. r=jaws
authorZibi Braniecki <zbraniecki@mozilla.com>
Mon, 19 Mar 2018 21:17:45 -0700
changeset 409089 a831178ed112be4db3c867876b99e61d22bdbc7a
parent 409088 e62d73ef396c32ca930897eb95d328e2ac15a901
child 409090 ad457a385c5bbaf3f7e3ca9305323795a25a5236
push id33675
push usertoros@mozilla.com
push dateWed, 21 Mar 2018 09:40:24 +0000
treeherdermozilla-central@f4ddf30ecf57 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1420761
milestone61.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 1420761 - Handle new localization API in Preferences Search. r=jaws MozReview-Commit-ID: 8J1siQtFn1t
browser/components/preferences/in-content/findInPage.js
browser/components/preferences/in-content/tests/browser_search_within_preferences_1.js
browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
--- a/browser/components/preferences/in-content/findInPage.js
+++ b/browser/components/preferences/in-content/findInPage.js
@@ -4,16 +4,19 @@
 
 /* import-globals-from extensionControlled.js */
 /* import-globals-from preferences.js */
 
 var gSearchResultsPane = {
   listSearchTooltips: new Set(),
   listSearchMenuitemIndicators: new Set(),
   searchInput: null,
+  // A map of DOM Elements to a string of keywords used in search
+  // XXX: We should invalidate this cache on `intl:app-locales-changed`
+  searchKeywords: new WeakMap(),
   inited: false,
 
   init() {
     if (this.inited) {
       return;
     }
     this.inited = true;
     this.searchInput = document.getElementById("searchInput");
@@ -259,17 +262,17 @@ var gSearchResultsPane = {
           ts = await new Promise(resolve => window.requestAnimationFrame(resolve));
           if (query !== this.query) {
             return;
           }
         }
 
         if (!child.classList.contains("header") &&
             !child.classList.contains("subcategory") &&
-            this.searchWithinNode(child, this.query)) {
+            await this.searchWithinNode(child, this.query)) {
           child.hidden = false;
           child.classList.remove("visually-hidden");
 
           // Show the preceding search-header if one exists.
           let groupbox = child.closest("groupbox");
           let groupHeader = groupbox && groupbox.querySelector(".search-header");
           if (groupHeader) {
             groupHeader.hidden = false;
@@ -322,17 +325,17 @@ var gSearchResultsPane = {
    * 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) {
+  async searchWithinNode(nodeObject, searchPhrase) {
     let matchesFound = false;
     if (nodeObject.childElementCount == 0 ||
         nodeObject.tagName == "label" ||
         nodeObject.tagName == "description" ||
         nodeObject.tagName == "menulist") {
       let simpleTextNodes = this.textNodeDescendants(nodeObject);
       for (let node of simpleTextNodes) {
         let result = this.highlightMatches([node], [node.length], node.textContent.toLowerCase(), searchPhrase);
@@ -361,18 +364,30 @@ var gSearchResultsPane = {
       // Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text.
       let labelResult = this.queryMatchesContent(nodeObject.getAttribute("label"), searchPhrase);
 
       // Searching some elements, such as xul:label, store their user-visible text in a "value" attribute.
       // Value will be skipped for menuitem since value in menuitem could represent index number to distinct each item.
       let valueResult = nodeObject.tagName !== "menuitem" ?
         this.queryMatchesContent(nodeObject.getAttribute("value"), searchPhrase) : false;
 
-      // Searching some elements, such as xul:button, buttons to open subdialogs.
-      let keywordsResult = this.queryMatchesContent(nodeObject.getAttribute("searchkeywords"), searchPhrase);
+      // Searching some elements, such as xul:button, buttons to open subdialogs
+      // using l10n ids.
+      let keywordsResult =
+        nodeObject.hasAttribute("search-l10n-ids") &&
+        await this.matchesSearchL10nIDs(nodeObject, searchPhrase);
+
+      if (!keywordsResult) {
+        // Searching some elements, such as xul:button, buttons to open subdialogs
+        // using searchkeywords attribute.
+        keywordsResult =
+          !keywordsResult &&
+          nodeObject.hasAttribute("searchkeywords") &&
+          this.queryMatchesContent(nodeObject.getAttribute("searchkeywords"), searchPhrase);
+      }
 
       // Creating tooltips for buttons
       if (keywordsResult && (nodeObject.tagName === "button" || nodeObject.tagName == "menulist")) {
         this.listSearchTooltips.add(nodeObject);
       }
 
       if (keywordsResult && nodeObject.tagName === "menuitem") {
         nodeObject.setAttribute("indicator", "true");
@@ -393,22 +408,22 @@ var gSearchResultsPane = {
       matchesFound = matchesFound || complexTextNodesResult || labelResult || valueResult || keywordsResult;
     }
 
     // Should not search unselected child nodes of a <xul:deck> element
     // except the "historyPane" <xul:deck> element.
     if (nodeObject.tagName == "deck" && nodeObject.id != "historyPane") {
       let index = nodeObject.selectedIndex;
       if (index != -1) {
-        let result = this.searchChildNodeIfVisible(nodeObject, index, searchPhrase);
+        let result = await this.searchChildNodeIfVisible(nodeObject, index, searchPhrase);
         matchesFound = matchesFound || result;
       }
     } else {
       for (let i = 0; i < nodeObject.childNodes.length; i++) {
-        let result = this.searchChildNodeIfVisible(nodeObject, i, searchPhrase);
+        let result = await this.searchChildNodeIfVisible(nodeObject, i, searchPhrase);
         matchesFound = matchesFound || result;
       }
     }
     return matchesFound;
   },
 
   /**
    * Search for a phrase within a child node if it is visible.
@@ -416,29 +431,79 @@ var gSearchResultsPane = {
    * @param Node nodeObject
    *    The parent DOM Element
    * @param Number index
    *    The index for the childNode
    * @param String searchPhrase
    * @returns boolean
    *    Returns true when found the specific childNode, false otherwise
    */
-  searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
+  async searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
     let result = false;
     if (!nodeObject.childNodes[index].hidden && nodeObject.getAttribute("data-hidden-from-search") !== "true") {
-      result = this.searchWithinNode(nodeObject.childNodes[index], searchPhrase);
+      result = await this.searchWithinNode(nodeObject.childNodes[index], searchPhrase);
       // Creating tooltips for menulist element
       if (result && nodeObject.tagName === "menulist") {
         this.listSearchTooltips.add(nodeObject);
       }
     }
     return result;
   },
 
   /**
+   * Search for a phrase in l10n messages associated with the element.
+   *
+   * @param Node nodeObject
+   *    The parent DOM Element
+   * @param String searchPhrase
+   * @returns boolean
+   *    true when the text content contains the query string else false
+   */
+  async matchesSearchL10nIDs(nodeObject, searchPhrase) {
+    if (!this.searchKeywords.has(nodeObject)) {
+      // The `search-l10n-ids` attribute is a comma-separated list of
+      // l10n ids. It may also uses a dot notation to specify an attribute
+      // of the message to be used.
+      //
+      // Example: "containers-add-button.label, user-context-personal"
+      //
+      // The result is an array of arrays of l10n ids and optionally attribute names.
+      //
+      // Example: [["containers-add-button", "label"], ["user-context-personal"]]
+      const refs = nodeObject.getAttribute("search-l10n-ids")
+        .split(",")
+        .map(s => s.trim().split(".")).filter(s => s[0].length > 0);
+
+      const messages = await document.l10n.formatMessages(refs.map(ref => [ref[0]]));
+
+      // Map the localized messages taking value or a selected attribute and
+      // building a string of concatenated translated strings out of it.
+      let keywords = messages.map((msg, i) => {
+        if (msg === null) {
+          console.warn(`Missing search l10n id "${refs[i][0]}"`);
+          return null;
+        }
+        if (refs[i][1]) {
+          let attr = msg.attrs.find(a => a.name === refs[i][1]);
+          if (attr) {
+            return attr.value;
+          }
+          return null;
+        }
+        return msg.value;
+      }).filter(keyword => keyword !== null).join(" ");
+
+      this.searchKeywords.set(nodeObject, keywords);
+      return this.queryMatchesContent(keywords, searchPhrase);
+    }
+
+    return this.queryMatchesContent(this.searchKeywords.get(nodeObject), searchPhrase);
+  },
+
+  /**
    * Inserting a div structure infront of the DOM element matched textContent.
    * Then calculation the offsets to position the tooltip in the correct place.
    *
    * @param Node anchorNode
    *    DOM Element
    * @param String query
    *    Word or words that are being searched for
    */
--- a/browser/components/preferences/in-content/tests/browser_search_within_preferences_1.js
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences_1.js
@@ -137,43 +137,46 @@ add_task(async function search_for_passw
 
 /**
  * Test for if nothing is found
  */
 add_task(async function search_with_nothing_found() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", { leaveOpen: true });
 
   let noResultsEl = gBrowser.contentDocument.querySelector("#no-results-message");
+  let sorryMsgQueryEl = gBrowser.contentDocument.getElementById("sorry-message-query");
 
   is_element_hidden(noResultsEl, "Should not be in search results yet");
 
   // Performs search
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
 
   is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
     "Search input should be focused when visiting preferences");
 
   let query = "coach";
   let searchCompletedPromise = BrowserTestUtils.waitForEvent(
     gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
   EventUtils.sendString(query);
   await searchCompletedPromise;
 
   is_element_visible(noResultsEl, "Should be in search results");
+  is(sorryMsgQueryEl.textContent, query, "sorry-message-query should contain the query");
 
   // Takes search off
   searchCompletedPromise = BrowserTestUtils.waitForEvent(
     gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == "");
   let count = query.length;
   while (count--) {
     EventUtils.sendKey("BACK_SPACE");
   }
   await searchCompletedPromise;
 
   is_element_hidden(noResultsEl, "Should not be in search results");
+  is(sorryMsgQueryEl.textContent.length, 0, "sorry-message-query should be empty");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for if we go back to general tab after search case
  */
 add_task(async function exiting_search_reverts_to_general_pane() {
--- a/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
@@ -60,8 +60,69 @@ add_task(async function() {
   EventUtils.sendString(query);
   await searchCompletedPromise;
 
   let noResultsEl = gBrowser.contentDocument.querySelector("#no-results-message");
   is_element_visible(noResultsEl, "Should be reporting no results");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
+
+
+/**
+ * Test that we search using `search-l10n-ids`.
+ *
+ * The test uses element `browserContainersSettings` and
+ * l10n id `language-and-appearance-header` and expects the element
+ * to be matched on the first word from the l10n id value ("Language" in en-US).
+ */
+add_task(async function() {
+  let l10nId = "language-and-appearance-header";
+
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+  // First, lets make sure that the element is not matched without
+  // `search-l10n-ids`.
+  {
+    let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+    let bcsElem = gBrowser.contentDocument.getElementById("browserContainersSettings");
+
+    is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
+      "Search input should be focused when visiting preferences");
+
+    ok(!bcsElem.getAttribute("search-l10n-ids").includes(l10nId),
+      "browserContainersSettings element should not contain the l10n id here.");
+
+    let query = "Language";
+    let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+        gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
+    EventUtils.sendString(query);
+    await searchCompletedPromise;
+
+    is_element_hidden(bcsElem, "browserContainersSettings should not be in search results");
+  }
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  // Now, let's add the l10n id to the element and perform the same search again.
+
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+  {
+    let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+    is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
+      "Search input should be focused when visiting preferences");
+
+    let bcsElem = gBrowser.contentDocument.getElementById("browserContainersSettings");
+    bcsElem.setAttribute("search-l10n-ids", l10nId);
+
+    let query = "Language";
+    let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+        gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
+    EventUtils.sendString(query);
+    await searchCompletedPromise;
+
+    is_element_visible(bcsElem, "browserContainersSettings should be in search results");
+  }
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});