Bug 1644537 - Use FeatureGates for templating the Experimental Features section. r=Gijs
authorJared Wein <jwein@mozilla.com>
Wed, 17 Jun 2020 03:39:48 +0000
changeset 535980 3491c53c891da8ec163d4531691ea3ce41c8cc0b
parent 535979 ffe294ce39df09942d332b2c9291f53a19b6f4f3
child 535981 aa23badb0824457636bde2db432e1637a9d3b382
push id37515
push usernerli@mozilla.com
push dateWed, 17 Jun 2020 14:49:45 +0000
treeherdermozilla-central@1e3e996bb9a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1644537
milestone79.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 1644537 - Use FeatureGates for templating the Experimental Features section. r=Gijs Differential Revision: https://phabricator.services.mozilla.com/D79025
browser/components/preferences/experimental.inc.xhtml
browser/components/preferences/experimental.js
browser/components/preferences/preferences.js
browser/components/preferences/tests/browser_sync_disabled.js
browser/themes/shared/preferences/preferences.inc.css
toolkit/components/featuregates/FeatureGate.jsm
toolkit/components/featuregates/test/unit/test_FeatureGate.js
--- a/browser/components/preferences/experimental.inc.xhtml
+++ b/browser/components/preferences/experimental.inc.xhtml
@@ -11,9 +11,19 @@
           hidden="true"
           data-category="paneExperimental">
   <html:h1 style="-moz-box-flex: 1;"
            data-l10n-id="pane-experimental-title"/>
   <html:h2 id="pane-experimental-subtitle"
            data-l10n-id="pane-experimental-subtitle"/>
   <html:p data-l10n-id="pane-experimental-description"/>
 </html:div>
+<html:div id="pane-experimental-featureGates"
+          data-category="paneExperimental"/>
 </html:template>
+
+<html:template id="template-featureGate">
+    <html:div class="featureGate">
+        <checkbox class="featureGateCheckbox"/>
+        <label class="featureGateTitle"/>
+        <label class="featureGateDescription"/>
+    </html:div>
+</html:template>
--- a/browser/components/preferences/experimental.js
+++ b/browser/components/preferences/experimental.js
@@ -1,7 +1,67 @@
 /* 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 { FeatureGate } = ChromeUtils.import(
+  "resource://featuregates/FeatureGate.jsm"
+);
+
 var gExperimentalPane = {
-  init() {},
+  inited: false,
+  _template: null,
+  _featureGatesContainer: null,
+
+  _featureGatePrefTypeToPrefServiceType(featureGatePrefType) {
+    if (featureGatePrefType != "boolean") {
+      throw new Error("Only boolean FeatureGates are supported");
+    }
+    return "bool";
+  },
+
+  async init() {
+    if (this.inited) {
+      return;
+    }
+    this._template = document.getElementById("template-featureGate");
+    this._featureGatesContainer = document.getElementById(
+      "pane-experimental-featureGates"
+    );
+    this.inited = true;
+    let features = await FeatureGate.all();
+    let frag = document.createDocumentFragment();
+    for (let feature of features) {
+      if (Preferences.get(feature.preference)) {
+        console.error(
+          "Preference control already exists for experimental feature '" +
+            feature.id +
+            "' with preference '" +
+            feature.preference +
+            "'"
+        );
+        continue;
+      }
+      let template = this._template.content.cloneNode(true);
+      let checkbox = template.querySelector(".featureGateCheckbox");
+      checkbox.setAttribute("preference", feature.preference);
+      checkbox.id = feature.id;
+      let title = template.querySelector(".featureGateTitle");
+      title.textContent = feature.title;
+      title.setAttribute("control", feature.id);
+      let description = template.querySelector(".featureGateDescription");
+      description.textContent = feature.description;
+      description.setAttribute("control", feature.id);
+      frag.appendChild(template);
+      let preference = Preferences.add({
+        id: feature.preference,
+        type: gExperimentalPane._featureGatePrefTypeToPrefServiceType(
+          feature.type
+        ),
+      });
+      preference.setElementValue(checkbox);
+    }
+    this._featureGatesContainer.appendChild(frag);
+    Preferences.updateAllElements();
+  },
 };
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -82,19 +82,16 @@ function init_all() {
   register_module("paneGeneral", gMainPane);
   register_module("paneHome", gHomePane);
   register_module("paneSearch", gSearchPane);
   register_module("panePrivacy", gPrivacyPane);
   register_module("paneContainers", gContainersPane);
   if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
     document.getElementById("category-sync").hidden = false;
     register_module("paneSync", gSyncPane);
-  } else {
-    // Remove the pane from the DOM so it doesn't get incorrectly included in search results.
-    document.getElementById("template-paneSync").remove();
   }
   if (Services.prefs.getBoolPref("browser.preferences.experimental")) {
     document.getElementById("category-experimental").hidden = false;
     register_module("paneExperimental", gExperimentalPane);
   }
   register_module("paneSearchResults", gSearchResultsPane);
   gSearchResultsPane.init();
   gMainPane.preInit();
--- a/browser/components/preferences/tests/browser_sync_disabled.js
+++ b/browser/components/preferences/tests/browser_sync_disabled.js
@@ -10,20 +10,16 @@
 add_task(async function() {
   await SpecialPowers.pushPrefEnv({
     set: [["identity.fxaccounts.enabled", false]],
   });
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
     leaveOpen: true,
   });
   ok(
-    !gBrowser.contentDocument.getElementById("template-paneSync"),
-    "sync pane removed"
-  );
-  ok(
     gBrowser.contentDocument.getElementById("category-sync").hidden,
     "sync category hidden"
   );
 
   // Check that we don't get any results in sync when searching:
   await evaluateSearchResults("sync", "no-results-message");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
--- a/browser/themes/shared/preferences/preferences.inc.css
+++ b/browser/themes/shared/preferences/preferences.inc.css
@@ -1114,8 +1114,30 @@ richlistitem .text-link:hover {
   background-repeat: no-repeat;
   background-position: 0 center;
   background-size: contain;
   min-height: 30px;
   padding-inline-start: 38px;
   display: flex;
   align-items: center;
 }
+
+#pane-experimental-featureGates {
+  margin-top: 16px;
+}
+
+.featureGate {
+  display: grid;
+  grid-template-columns: min-content 1fr;
+  margin-bottom: 16px;
+}
+
+.featureGateCheckbox {
+  align-self: center;
+}
+
+.featureGateTitle {
+  font-size: 1.14em;
+}
+
+.featureGateDescription {
+  grid-column: 2;
+}
--- a/toolkit/components/featuregates/FeatureGate.jsm
+++ b/toolkit/components/featuregates/FeatureGate.jsm
@@ -71,16 +71,24 @@ function evaluateTargetedValue(targetedV
     if (key.split(",").every(part => targetingFacts.get(part))) {
       return value;
     }
   }
 
   return targetedValue.default;
 }
 
+function buildFeatureGateImplementation(definition) {
+  const targetValueKeys = ["defaultValue", "isPublic"];
+  for (const key of targetValueKeys) {
+    definition[key] = evaluateTargetedValue(definition[key], kTargetFacts);
+  }
+  return new FeatureGateImplementation(definition);
+}
+
 const kFeatureGateCache = new Map();
 
 /** A high level control for turning features on and off. */
 class FeatureGate {
   /*
    * This is structured as a class with static methods to that sphinx-js can
    * easily document it. This constructor is required for sphinx-js to detect
    * this class for documentation.
@@ -106,22 +114,39 @@ class FeatureGate {
 
     if (!featureDefinitions.has(id)) {
       throw new Error(
         `Unknown feature id ${id}. Features must be defined in toolkit/components/featuregates/Features.toml`
       );
     }
 
     // Make a copy of the definition, since we are about to modify it
-    const definition = { ...featureDefinitions.get(id) };
-    const targetValueKeys = ["defaultValue", "isPublic"];
-    for (const key of targetValueKeys) {
-      definition[key] = evaluateTargetedValue(definition[key], kTargetFacts);
+    return buildFeatureGateImplementation({ ...featureDefinitions.get(id) });
+  }
+
+  /**
+   * Constructs feature gate objects for each of the definitions in ``Features.toml``.
+   * @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests.
+   */
+  static async all(testDefinitionsUrl = undefined) {
+    let featureDefinitions;
+    if (testDefinitionsUrl) {
+      featureDefinitions = await fetchFeatureDefinitions(testDefinitionsUrl);
+    } else {
+      featureDefinitions = await gFeatureDefinitionsPromise;
     }
-    return new FeatureGateImplementation(definition);
+
+    let definitions = [];
+    for (let definition of featureDefinitions.values()) {
+      // Make a copy of the definition, since we are about to modify it
+      definitions[definitions.length] = buildFeatureGateImplementation(
+        Object.assign({}, definition)
+      );
+    }
+    return definitions;
   }
 
   /**
    * Add an observer for a feature gate by ID. If the feature is of type
    * boolean and currently enabled, `onEnable` will be called.
    *
    * The underlying feature gate instance will be shared with all other callers
    * of this function, and share an observer.
--- a/toolkit/components/featuregates/test/unit/test_FeatureGate.js
+++ b/toolkit/components/featuregates/test/unit/test_FeatureGate.js
@@ -59,16 +59,33 @@ class DefinitionServer {
     definition.isPublic = { default: definition.isPublic };
     definition.defaultValue = { default: definition.defaultValue };
     this.definitions[definition.id] = definition;
     return definition;
   }
 }
 
 // ============================================================================
+add_task(async function testReadAll() {
+  const server = new DefinitionServer();
+  let ids = ["test-featureA", "test-featureB", "test-featureC"];
+  for (let id of ids) {
+    server.addDefinition({ id });
+  }
+  let sortedIds = ids.sort();
+  const features = await FeatureGate.all(server.definitionsUrl);
+  for (let feature of features) {
+    equal(
+      feature.id,
+      sortedIds.shift(),
+      "Features are returned in order of definition"
+    );
+  }
+  equal(sortedIds.length, 0, "All features are returned when calling all()");
+});
 
 // The getters and setters should read correctly from the definition
 add_task(async function testReadFromDefinition() {
   const server = new DefinitionServer();
   const definition = server.addDefinition({ id: "test-feature" });
   const feature = await FeatureGate.fromId(
     "test-feature",
     server.definitionsUrl