Previously the observers for the `app.shield.optoutstudies.enabled` and `datareporting.healthreport.uploadEnabled` prefs lived on the global `ExperimentManager`, which complicated our unit tests because (a) importing `ExperimentManager.sys.mjs` has side effects of initializing the global ExperimentManager with those listeners and (b) we have the global and per-test ExperimentManagers in our unit tests, which causes notifications to fire twice. We've worked around this in the past by manually removing the pref listeners in some cases, but its simpler to have them live on the ExperimentAPI itself. That way it can be controlled by `ExperimentAPI.init()` (and `ExperimentAPI._resetForTests()` in tests). Differential Revision: https://phabricator.services.mozilla.com/D248068
224 lines
6.4 KiB
JavaScript
224 lines
6.4 KiB
JavaScript
/* 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 */
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
|
|
FirefoxLabs: "resource://nimbus/FirefoxLabs.sys.mjs",
|
|
});
|
|
|
|
const gExperimentalPane = {
|
|
inited: false,
|
|
_featureGatesContainer: null,
|
|
_firefoxLabs: null,
|
|
|
|
async init() {
|
|
if (this.inited) {
|
|
return;
|
|
}
|
|
|
|
this.inited = true;
|
|
this._featureGatesContainer = document.getElementById(
|
|
"pane-experimental-featureGates"
|
|
);
|
|
|
|
this._onCheckboxChanged = this._onCheckboxChanged.bind(this);
|
|
this._onNimbusUpdate = this._onNimbusUpdate.bind(this);
|
|
this._onStudiesEnabledChanged = this._onStudiesEnabledChanged.bind(this);
|
|
this._resetAllFeatures = this._resetAllFeatures.bind(this);
|
|
|
|
setEventListener(
|
|
"experimentalCategory-reset",
|
|
"click",
|
|
this._resetAllFeatures
|
|
);
|
|
|
|
Services.obs.addObserver(
|
|
this._onStudiesEnabledChanged,
|
|
ExperimentAPI.STUDIES_ENABLED_CHANGED
|
|
);
|
|
window.addEventListener("unload", () => this._removeObservers());
|
|
|
|
await this._maybeRenderLabsRecipes();
|
|
},
|
|
|
|
async _maybeRenderLabsRecipes() {
|
|
this._firefoxLabs = await FirefoxLabs.create();
|
|
|
|
const shouldHide = this._firefoxLabs.count === 0;
|
|
this._setCategoryVisibility(shouldHide);
|
|
|
|
if (shouldHide) {
|
|
return;
|
|
}
|
|
|
|
const frag = document.createDocumentFragment();
|
|
|
|
const groups = new Map();
|
|
for (const optIn of this._firefoxLabs.all()) {
|
|
if (!groups.has(optIn.firefoxLabsGroup)) {
|
|
groups.set(optIn.firefoxLabsGroup, []);
|
|
}
|
|
|
|
groups.get(optIn.firefoxLabsGroup).push(optIn);
|
|
}
|
|
|
|
for (const [group, optIns] of groups) {
|
|
const card = document.createElement("moz-card");
|
|
card.classList.add("featureGate");
|
|
|
|
const fieldset = document.createElement("moz-fieldset");
|
|
document.l10n.setAttributes(fieldset, group);
|
|
|
|
card.append(fieldset);
|
|
|
|
for (const optIn of optIns) {
|
|
const checkbox = document.createElement("moz-checkbox");
|
|
checkbox.dataset.nimbusSlug = optIn.slug;
|
|
checkbox.dataset.nimbusBranchSlug = optIn.branches[0].slug;
|
|
const description = document.createElement("div");
|
|
description.slot = "description";
|
|
description.id = `${optIn.slug}-description`;
|
|
description.classList.add("featureGateDescription");
|
|
|
|
for (const [key, value] of Object.entries(
|
|
optIn.firefoxLabsDescriptionLinks ?? {}
|
|
)) {
|
|
const link = document.createElement("a");
|
|
link.setAttribute("data-l10n-name", key);
|
|
link.setAttribute("href", value);
|
|
link.setAttribute("target", "_blank");
|
|
|
|
description.append(link);
|
|
}
|
|
|
|
document.l10n.setAttributes(description, optIn.firefoxLabsDescription);
|
|
checkbox.id = optIn.slug;
|
|
checkbox.setAttribute("aria-describedby", description.id);
|
|
document.l10n.setAttributes(checkbox, optIn.firefoxLabsTitle);
|
|
|
|
checkbox.checked =
|
|
ExperimentAPI._manager.store.get(optIn.slug)?.active ?? false;
|
|
checkbox.addEventListener("change", this._onCheckboxChanged);
|
|
|
|
checkbox.append(description);
|
|
fieldset.append(checkbox);
|
|
}
|
|
|
|
frag.append(card);
|
|
}
|
|
|
|
this._featureGatesContainer.appendChild(frag);
|
|
|
|
ExperimentAPI._manager.store.on("update", this._onNimbusUpdate);
|
|
|
|
Services.obs.notifyObservers(window, "experimental-pane-loaded");
|
|
},
|
|
|
|
_removeLabsRecipes() {
|
|
ExperimentAPI._manager.store.off("update", this._onNimbusUpdate);
|
|
|
|
this._featureGatesContainer
|
|
.querySelectorAll(".featureGate")
|
|
.forEach(el => el.remove());
|
|
},
|
|
|
|
async _onCheckboxChanged(event) {
|
|
const target = event.target;
|
|
|
|
const slug = target.dataset.nimbusSlug;
|
|
const branchSlug = target.dataset.nimbusBranchSlug;
|
|
|
|
const enrolling = !(
|
|
ExperimentAPI._manager.store.get(slug)?.active ?? false
|
|
);
|
|
|
|
let shouldRestart = false;
|
|
if (this._firefoxLabs.get(slug).requiresRestart) {
|
|
const buttonIndex = await confirmRestartPrompt(enrolling, 1, true, false);
|
|
shouldRestart = buttonIndex === CONFIRM_RESTART_PROMPT_RESTART_NOW;
|
|
|
|
if (!shouldRestart) {
|
|
// The user declined to restart, so we will not enroll in the opt-in.
|
|
target.checked = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Disable the checkbox so that the user cannot interact with it during enrollment.
|
|
target.disabled = true;
|
|
|
|
if (enrolling) {
|
|
await this._firefoxLabs.enroll(slug, branchSlug);
|
|
} else {
|
|
this._firefoxLabs.unenroll(slug);
|
|
}
|
|
|
|
target.disabled = false;
|
|
|
|
if (shouldRestart) {
|
|
Services.startup.quit(
|
|
Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
|
|
);
|
|
}
|
|
},
|
|
|
|
_onNimbusUpdate(_event, { slug, active }) {
|
|
if (this._firefoxLabs.get(slug)) {
|
|
document.getElementById(slug).checked = active;
|
|
}
|
|
},
|
|
|
|
async _onStudiesEnabledChanged() {
|
|
const studiesEnabled = ExperimentAPI._manager.studiesEnabled;
|
|
|
|
if (studiesEnabled) {
|
|
await this._maybeRenderLabsRecipes();
|
|
} else {
|
|
this._setCategoryVisibility(true);
|
|
this._removeLabsRecipes();
|
|
this._firefoxLabs = null;
|
|
}
|
|
},
|
|
|
|
_removeObservers() {
|
|
ExperimentAPI._manager.store.off("update", this._onNimbusUpdate);
|
|
Services.obs.removeObserver(
|
|
this._onStudiesEnabledChanged,
|
|
ExperimentAPI.STUDIES_ENABLED_CHANGED
|
|
);
|
|
},
|
|
|
|
// Reset the features to their default values
|
|
async _resetAllFeatures() {
|
|
for (const optIn of this._firefoxLabs.all()) {
|
|
const enrolled =
|
|
(await ExperimentAPI._manager.store.get(optIn.slug)?.active) ?? false;
|
|
if (enrolled) {
|
|
this._firefoxLabs.unenroll(optIn.slug);
|
|
}
|
|
}
|
|
},
|
|
|
|
_setCategoryVisibility(shouldHide) {
|
|
document.getElementById("category-experimental").hidden = shouldHide;
|
|
|
|
// Cache the visibility so we can show it quicker in subsequent loads.
|
|
Services.prefs.setBoolPref(
|
|
"browser.preferences.experimental.hidden",
|
|
shouldHide
|
|
);
|
|
|
|
if (
|
|
shouldHide &&
|
|
document.getElementById("categories").selectedItem?.id ==
|
|
"category-experimental"
|
|
) {
|
|
// Leave the 'experimental' category if there are no available features
|
|
gotoPref("general");
|
|
}
|
|
},
|
|
};
|