зеркало из https://github.com/mozilla/gecko-dev.git
Backed out 3 changesets (bug 1447499) for browser chrome failures on browser_all_files_referenced. CLOSED TREE
Backed out changeset 8eeaf62be0dc (bug 1447499) Backed out changeset 3579f8912b70 (bug 1447499) Backed out changeset 675b77e1c236 (bug 1447499)
This commit is contained in:
Родитель
66aa4d36c4
Коммит
20460bde38
|
@ -6,10 +6,9 @@
|
|||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "AddonStudies", "resource://normandy/lib/AddonStudies.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "AddonStudyAction", "resource://normandy/actions/AddonStudyAction.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "CleanupManager", "resource://normandy/lib/CleanupManager.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "PreferenceExperiments", "resource://normandy/lib/PreferenceExperiments.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "AddonStudies", "resource://normandy/lib/AddonStudies.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "RecipeRunner", "resource://normandy/lib/RecipeRunner.jsm");
|
||||
|
||||
var EXPORTED_SYMBOLS = ["AboutPages"];
|
||||
|
@ -62,7 +61,7 @@ var AboutPages = {
|
|||
this.aboutStudies.registerParentListeners();
|
||||
|
||||
CleanupManager.addCleanupHandler(() => {
|
||||
// Stop loading process scripts and notify existing scripts to clean up.
|
||||
// Stop loading processs scripts and notify existing scripts to clean up.
|
||||
Services.ppmm.broadcastAsyncMessage("Shield:ShuttingDown");
|
||||
Services.mm.broadcastAsyncMessage("Shield:ShuttingDown");
|
||||
|
||||
|
@ -96,10 +95,8 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
|
|||
* Register listeners for messages from the content processes.
|
||||
*/
|
||||
registerParentListeners() {
|
||||
Services.mm.addMessageListener("Shield:GetAddonStudyList", this);
|
||||
Services.mm.addMessageListener("Shield:GetPreferenceStudyList", this);
|
||||
Services.mm.addMessageListener("Shield:RemoveAddonStudy", this);
|
||||
Services.mm.addMessageListener("Shield:RemovePreferenceStudy", this);
|
||||
Services.mm.addMessageListener("Shield:GetStudyList", this);
|
||||
Services.mm.addMessageListener("Shield:RemoveStudy", this);
|
||||
Services.mm.addMessageListener("Shield:OpenDataPreferences", this);
|
||||
Services.mm.addMessageListener("Shield:GetStudiesEnabled", this);
|
||||
},
|
||||
|
@ -108,10 +105,8 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
|
|||
* Unregister listeners for messages from the content process.
|
||||
*/
|
||||
unregisterParentListeners() {
|
||||
Services.mm.removeMessageListener("Shield:GetAddonStudyList", this);
|
||||
Services.mm.removeMessageListener("Shield:GetPreferenceStudyList", this);
|
||||
Services.mm.removeMessageListener("Shield:RemoveAddonStudy", this);
|
||||
Services.mm.removeMessageListener("Shield:RemovePreferenceStudy", this);
|
||||
Services.mm.removeMessageListener("Shield:GetStudyList", this);
|
||||
Services.mm.removeMessageListener("Shield:RemoveStudy", this);
|
||||
Services.mm.removeMessageListener("Shield:OpenDataPreferences", this);
|
||||
Services.mm.removeMessageListener("Shield:GetStudiesEnabled", this);
|
||||
},
|
||||
|
@ -123,17 +118,11 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
|
|||
*/
|
||||
receiveMessage(message) {
|
||||
switch (message.name) {
|
||||
case "Shield:GetAddonStudyList":
|
||||
this.sendAddonStudyList(message.target);
|
||||
case "Shield:GetStudyList":
|
||||
this.sendStudyList(message.target);
|
||||
break;
|
||||
case "Shield:GetPreferenceStudyList":
|
||||
this.sendPreferenceStudyList(message.target);
|
||||
break;
|
||||
case "Shield:RemoveAddonStudy":
|
||||
this.removeAddonStudy(message.data.recipeId, message.data.reason);
|
||||
break;
|
||||
case "Shield:RemovePreferenceStudy":
|
||||
this.removePreferenceStudy(message.data.experimentName, message.data.reason);
|
||||
case "Shield:RemoveStudy":
|
||||
this.removeStudy(message.data.recipeId, message.data.reason);
|
||||
break;
|
||||
case "Shield:OpenDataPreferences":
|
||||
this.openDataPreferences();
|
||||
|
@ -145,15 +134,15 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
|
|||
},
|
||||
|
||||
/**
|
||||
* Fetch a list of add-on studies from storage and send it to the process
|
||||
* that requested them.
|
||||
* Fetch a list of studies from storage and send it to the process that
|
||||
* requested them.
|
||||
* @param {<browser>} target
|
||||
* XUL <browser> element for the tab containing the about:studies page
|
||||
* that requested a study list.
|
||||
*/
|
||||
async sendAddonStudyList(target) {
|
||||
async sendStudyList(target) {
|
||||
try {
|
||||
target.messageManager.sendAsyncMessage("Shield:ReceiveAddonStudyList", {
|
||||
target.messageManager.sendAsyncMessage("Shield:ReceiveStudyList", {
|
||||
studies: await AddonStudies.getAll(),
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -162,24 +151,6 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a list of preference studies from storage and send it to the
|
||||
* process that requested them.
|
||||
* @param {<browser>} target
|
||||
* XUL <browser> element for the tab containing the about:studies page
|
||||
* that requested a study list.
|
||||
*/
|
||||
async sendPreferenceStudyList(target) {
|
||||
try {
|
||||
target.messageManager.sendAsyncMessage("Shield:ReceivePreferenceStudyList", {
|
||||
studies: await PreferenceExperiments.getAll(),
|
||||
});
|
||||
} catch (err) {
|
||||
// The child process might be gone, so no need to throw here.
|
||||
Cu.reportError(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get if studies are enabled and send it to the process that
|
||||
* requested them. This has to be in the parent process, since
|
||||
|
@ -203,32 +174,19 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
|
|||
},
|
||||
|
||||
/**
|
||||
* Disable an active add-on study and remove its add-on.
|
||||
* Disable an active study and remove its add-on.
|
||||
* @param {String} studyName
|
||||
*/
|
||||
async removeAddonStudy(recipeId, reason) {
|
||||
async removeStudy(recipeId, reason) {
|
||||
const action = new AddonStudyAction();
|
||||
await action.unenroll(recipeId, reason);
|
||||
|
||||
// Update any open tabs with the new study list now that it has changed.
|
||||
Services.mm.broadcastAsyncMessage("Shield:ReceiveAddonStudyList", {
|
||||
Services.mm.broadcastAsyncMessage("Shield:ReceiveStudyList", {
|
||||
studies: await AddonStudies.getAll(),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable an active preference study
|
||||
* @param {String} studyName
|
||||
*/
|
||||
async removePreferenceStudy(experimentName, reason) {
|
||||
PreferenceExperiments.stop(experimentName, { reason });
|
||||
|
||||
// Update any open tabs with the new study list now that it has changed.
|
||||
Services.mm.broadcastAsyncMessage("Shield:ReceivePreferenceStudyList", {
|
||||
studies: await PreferenceExperiments.getAll(),
|
||||
});
|
||||
},
|
||||
|
||||
openDataPreferences() {
|
||||
const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
browserWindow.openPreferences("privacy-reports", {origin: "aboutStudies"});
|
||||
|
|
|
@ -42,23 +42,16 @@ class ShieldFrameChild extends ActorChild {
|
|||
// We waited until after we received an event to register message listeners
|
||||
// in order to save resources for tabs that don't ever load about:studies.
|
||||
this.mm.addMessageListener("Shield:ShuttingDown", this);
|
||||
this.mm.addMessageListener("Shield:ReceiveAddonStudyList", this);
|
||||
this.mm.addMessageListener("Shield:ReceivePreferenceStudyList", this);
|
||||
this.mm.addMessageListener("Shield:ReceiveStudyList", this);
|
||||
this.mm.addMessageListener("Shield:ReceiveStudiesEnabled", this);
|
||||
|
||||
switch (event.detail.action) {
|
||||
// Actions that require the parent process
|
||||
case "GetRemoteValue:AddonStudyList":
|
||||
this.mm.sendAsyncMessage("Shield:GetAddonStudyList");
|
||||
case "GetRemoteValue:StudyList":
|
||||
this.mm.sendAsyncMessage("Shield:GetStudyList");
|
||||
break;
|
||||
case "GetRemoteValue:PreferenceStudyList":
|
||||
this.mm.sendAsyncMessage("Shield:GetPreferenceStudyList");
|
||||
break;
|
||||
case "RemoveAddonStudy":
|
||||
this.mm.sendAsyncMessage("Shield:RemoveAddonStudy", event.detail.data);
|
||||
break;
|
||||
case "RemovePreferenceStudy":
|
||||
this.mm.sendAsyncMessage("Shield:RemovePreferenceStudy", event.detail.data);
|
||||
case "RemoveStudy":
|
||||
this.mm.sendAsyncMessage("Shield:RemoveStudy", event.detail.data);
|
||||
break;
|
||||
case "GetRemoteValue:StudiesEnabled":
|
||||
this.mm.sendAsyncMessage("Shield:GetStudiesEnabled");
|
||||
|
@ -96,11 +89,8 @@ class ShieldFrameChild extends ActorChild {
|
|||
*/
|
||||
receiveMessage(message) {
|
||||
switch (message.name) {
|
||||
case "Shield:ReceiveAddonStudyList":
|
||||
this.triggerPageCallback("ReceiveRemoteValue:AddonStudyList", message.data.studies);
|
||||
break;
|
||||
case "Shield:ReceivePreferenceStudyList":
|
||||
this.triggerPageCallback("ReceiveRemoteValue:PreferenceStudyList", message.data.studies);
|
||||
case "Shield:ReceiveStudyList":
|
||||
this.triggerPageCallback("ReceiveRemoteValue:StudyList", message.data.studies);
|
||||
break;
|
||||
case "Shield:ReceiveStudiesEnabled":
|
||||
this.triggerPageCallback("ReceiveRemoteValue:StudiesEnabled", message.data.studiesEnabled);
|
||||
|
|
|
@ -30,9 +30,28 @@ button > .button-box {
|
|||
}
|
||||
|
||||
.about-studies-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 1.25rem;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#categories {
|
||||
flex: 0 0;
|
||||
margin: 0;
|
||||
min-width: 200px;
|
||||
padding: 40px 0 0;
|
||||
}
|
||||
|
||||
#categories .category {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
|
@ -141,21 +160,23 @@ button > .button-box {
|
|||
.study-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3em;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.study-header > * {
|
||||
.study-description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.study-description > * {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.study-header > *:last-child {
|
||||
.study-description > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.study-description code {
|
||||
font: italic 1.0rem 'Fira Mono', 'mono', 'monospace';
|
||||
}
|
||||
|
||||
.study-actions {
|
||||
flex: 0 0;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
<script src="resource://normandy-vendor/ReactDOM.js"></script>
|
||||
<script src="resource://normandy-vendor/PropTypes.js"></script>
|
||||
<script src="resource://normandy-vendor/classnames.js"></script>
|
||||
<script src="resource://normandy-content/about-studies/common.js"></script>
|
||||
<script src="resource://normandy-content/about-studies/shield-studies.js"></script>
|
||||
<script src="resource://normandy-content/about-studies/about-studies.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
/* global classnames PropTypes React ReactDOM */
|
||||
/* global classnames PropTypes r React ReactDOM remoteValues ShieldStudies */
|
||||
|
||||
/**
|
||||
* Shorthand for creating elements (to avoid using a JSX preprocessor)
|
||||
* Mapping of pages displayed on the sidebar. Keys are the value used in the
|
||||
* URL hash to identify the current page.
|
||||
*
|
||||
* Pages will appear in the sidebar in the order they are defined here. If the
|
||||
* URL doesn't contain a hash, the first page will be displayed in the content area.
|
||||
*/
|
||||
const r = React.createElement;
|
||||
|
||||
/**
|
||||
* Dispatches a page event to the privileged frame script for this tab.
|
||||
* @param {String} action
|
||||
* @param {Object} data
|
||||
*/
|
||||
function sendPageEvent(action, data) {
|
||||
const event = new CustomEvent("ShieldPageEvent", { bubbles: true, detail: { action, data } });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
const PAGES = new Map([
|
||||
["shieldStudies", {
|
||||
name: "title",
|
||||
component: ShieldStudies,
|
||||
icon: "resource://normandy-content/about-studies/img/shield-logo.png",
|
||||
}],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handle basic layout and routing within about:studies.
|
||||
|
@ -27,279 +26,133 @@ class AboutStudies extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.remoteValueNameMap = {
|
||||
AddonStudyList: "addonStudies",
|
||||
PreferenceStudyList: "prefStudies",
|
||||
ShieldLearnMoreHref: "learnMoreHref",
|
||||
StudiesEnabled: "studiesEnabled",
|
||||
ShieldTranslations: "translations",
|
||||
let hash = new URL(window.location).hash.slice(1);
|
||||
if (!PAGES.has(hash)) {
|
||||
hash = "shieldStudies";
|
||||
}
|
||||
|
||||
this.state = {
|
||||
currentPageId: hash,
|
||||
};
|
||||
|
||||
this.state = {};
|
||||
for (const stateName of Object.values(this.remoteValueNameMap)) {
|
||||
this.state[stateName] = null;
|
||||
}
|
||||
this.handleEvent = this.handleEvent.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
for (const remoteName of Object.keys(this.remoteValueNameMap)) {
|
||||
document.addEventListener(`ReceiveRemoteValue:${remoteName}`, this);
|
||||
sendPageEvent(`GetRemoteValue:${remoteName}`);
|
||||
}
|
||||
componentDidMount() {
|
||||
remoteValues.shieldTranslations.subscribe(this);
|
||||
window.addEventListener("hashchange", this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
for (const remoteName of Object.keys(this.remoteValueNameMap)) {
|
||||
document.removeEventListener(`ReceiveRemoteValue:${remoteName}`, this);
|
||||
}
|
||||
remoteValues.shieldTranslations.unsubscribe(this);
|
||||
window.removeEventListener("hashchange", this);
|
||||
}
|
||||
|
||||
/** Event handle to receive remote values from documentAddEventListener */
|
||||
handleEvent({ type, detail: value }) {
|
||||
const prefix = "ReceiveRemoteValue:";
|
||||
if (type.startsWith(prefix)) {
|
||||
const name = type.substring(prefix.length);
|
||||
this.setState({ [this.remoteValueNameMap[name]]: value });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { translations, learnMoreHref, studiesEnabled, addonStudies, prefStudies } = this.state;
|
||||
|
||||
// Wait for all values to be loaded before rendering. Some of the values may
|
||||
// be falsey, so an explicit null check is needed.
|
||||
if (Object.values(this.state).some(v => v === null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
r("div", { className: "about-studies-container main-content" },
|
||||
r(WhatsThisBox, { translations, learnMoreHref, studiesEnabled }),
|
||||
r(StudyList, { translations, addonStudies, prefStudies }),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explains the contents of the page, and offers a way to learn more and update preferences.
|
||||
*/
|
||||
class WhatsThisBox extends React.Component {
|
||||
handleUpdateClick() {
|
||||
sendPageEvent("NavigateToDataPreferences");
|
||||
}
|
||||
|
||||
render() {
|
||||
const { learnMoreHref, studiesEnabled, translations } = this.props;
|
||||
|
||||
return (
|
||||
r("div", { className: "info-box" },
|
||||
r("div", { className: "info-box-content" },
|
||||
r("span", {},
|
||||
studiesEnabled ? translations.enabledList : translations.disabledList,
|
||||
),
|
||||
r("a", { id: "shield-studies-learn-more", href: learnMoreHref }, translations.learnMore),
|
||||
|
||||
r("button", { id: "shield-studies-update-preferences", onClick: this.handleUpdateClick },
|
||||
r("div", { className: "button-box" },
|
||||
navigator.platform.includes("Win") ? translations.updateButtonWin : translations.updateButtonUnix
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a list of studies, with an option to end in-progress ones.
|
||||
*/
|
||||
class StudyList extends React.Component {
|
||||
render() {
|
||||
const { addonStudies, prefStudies, translations } = this.props;
|
||||
|
||||
if (!addonStudies.length && !prefStudies.length) {
|
||||
return r("p", { className: "study-list-info" }, translations.noStudies);
|
||||
}
|
||||
|
||||
const activeStudies = [];
|
||||
const inactiveStudies = [];
|
||||
|
||||
// Since we are modifying the study objects, it is polite to make copies
|
||||
for (const study of addonStudies) {
|
||||
const clonedStudy = Object.assign({}, study, {type: "addon", sortDate: study.studyStartDate});
|
||||
if (study.active) {
|
||||
activeStudies.push(clonedStudy);
|
||||
} else {
|
||||
inactiveStudies.push(clonedStudy);
|
||||
receiveRemoteValue(name, value) {
|
||||
switch (name) {
|
||||
case "ShieldTranslations": {
|
||||
this.setState({ translations: value });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.error(`Unknown remote value ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const study of prefStudies) {
|
||||
const clonedStudy = Object.assign({}, study, {type: "pref", sortDate: new Date(study.lastSeen)});
|
||||
if (study.expired) {
|
||||
inactiveStudies.push(clonedStudy);
|
||||
} else {
|
||||
activeStudies.push(clonedStudy);
|
||||
}
|
||||
handleEvent(event) {
|
||||
const newHash = new URL(event.newURL).hash.slice(1);
|
||||
if (PAGES.has(newHash)) {
|
||||
this.setState({currentPageId: newHash});
|
||||
}
|
||||
|
||||
activeStudies.sort((a, b) => b.sortDate - a.sortDate);
|
||||
inactiveStudies.sort((a, b) => b.sortDate - a.sortDate);
|
||||
|
||||
return (
|
||||
r("div", {},
|
||||
r("h2", {}, translations.activeStudiesList),
|
||||
r("ul", { className: "study-list" },
|
||||
activeStudies.map(study => (
|
||||
study.type === "addon"
|
||||
? r(AddonStudyListItem, { key: study.name, study, translations })
|
||||
: r(PreferenceStudyListItem, { key: study.name, study, translations })
|
||||
)),
|
||||
),
|
||||
r("h2", {}, translations.completedStudiesList),
|
||||
r("ul", { className: "study-list" },
|
||||
inactiveStudies.map(study => (
|
||||
study.type === "addon"
|
||||
? r(AddonStudyListItem, { key: study.name, study, translations })
|
||||
: r(PreferenceStudyListItem, { key: study.name, study, translations })
|
||||
)),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
StudyList.propTypes = {
|
||||
addonStudies: PropTypes.array.isRequired,
|
||||
translations: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Details about an individual add-on study, with an option to end it if it is active.
|
||||
*/
|
||||
class AddonStudyListItem extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickRemove = this.handleClickRemove.bind(this);
|
||||
}
|
||||
|
||||
handleClickRemove() {
|
||||
sendPageEvent("RemoveAddonStudy", {
|
||||
recipeId: this.props.study.recipeId,
|
||||
reason: "individual-opt-out",
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { study, translations } = this.props;
|
||||
const currentPageId = this.state.currentPageId;
|
||||
const pageEntries = Array.from(PAGES.entries());
|
||||
const currentPage = PAGES.get(currentPageId);
|
||||
const { translations } = this.state;
|
||||
|
||||
return (
|
||||
r("li", {
|
||||
className: classnames("study addon-study", { disabled: !study.active }),
|
||||
"data-study-name": study.name,
|
||||
},
|
||||
r("div", { className: "study-icon" },
|
||||
study.name.replace(/-?add-?on-?/, "").replace(/-?study-?/, "").slice(0, 1)
|
||||
r("div", {className: "about-studies-container"},
|
||||
translations && r(Sidebar, {},
|
||||
pageEntries.map(([id, page]) => (
|
||||
r(SidebarItem, {
|
||||
key: id,
|
||||
pageId: id,
|
||||
selected: id === currentPageId,
|
||||
page,
|
||||
translations,
|
||||
})
|
||||
)),
|
||||
),
|
||||
r("div", { className: "study-details" },
|
||||
r("div", { className: "study-header" },
|
||||
r("span", { className: "study-name" }, study.name),
|
||||
r("span", {}, "\u2022"), // •
|
||||
r("span", { className: "study-status" }, study.active ? translations.activeStatus : translations.completeStatus),
|
||||
),
|
||||
r("div", { className: "study-description" },
|
||||
study.description
|
||||
),
|
||||
),
|
||||
r("div", { className: "study-actions" },
|
||||
study.active &&
|
||||
r("button", { className: "remove-button", onClick: this.handleClickRemove },
|
||||
r("div", { className: "button-box" },
|
||||
translations.removeButton
|
||||
),
|
||||
)
|
||||
r(Content, {},
|
||||
translations && currentPage && r(currentPage.component, {translations})
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
AddonStudyListItem.propTypes = {
|
||||
study: PropTypes.shape({
|
||||
recipeId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
class Sidebar extends React.Component {
|
||||
render() {
|
||||
return r("ul", {id: "categories"}, this.props.children);
|
||||
}
|
||||
}
|
||||
Sidebar.propTypes = {
|
||||
children: PropTypes.node,
|
||||
translations: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Details about an individual preference study, with an option to end it if it is active.
|
||||
*/
|
||||
class PreferenceStudyListItem extends React.Component {
|
||||
class SidebarItem extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickRemove = this.handleClickRemove.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClickRemove() {
|
||||
sendPageEvent("RemovePreferenceStudy", {
|
||||
experimentName: this.props.study.name,
|
||||
reason: "individual-opt-out",
|
||||
});
|
||||
handleClick() {
|
||||
window.location = `#${this.props.pageId}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { study, translations } = this.props;
|
||||
|
||||
// Sanitize the values by setting them as the text content of an element,
|
||||
// and then getting the HTML representation of that text. This will have the
|
||||
// browser safely sanitize them. Use outerHTML to also include the <code>
|
||||
// element in the string.
|
||||
const sanitizer = document.createElement("code");
|
||||
sanitizer.textContent = study.preferenceName;
|
||||
const sanitizedPreferenceName = sanitizer.outerHTML;
|
||||
sanitizer.textContent = study.preferenceValue;
|
||||
const sanitizedPreferenceValue = sanitizer.outerHTML;
|
||||
const description = translations.preferenceStudyDescription
|
||||
.replace(/%(?:1\$)?S/, sanitizedPreferenceName)
|
||||
.replace(/%(?:2\$)?S/, sanitizedPreferenceValue);
|
||||
|
||||
const { page, selected, translations } = this.props;
|
||||
return (
|
||||
r("li", {
|
||||
className: classnames("study pref-study", { disabled: study.expired }),
|
||||
"data-study-name": study.name,
|
||||
className: classnames("category", {selected}),
|
||||
onClick: this.handleClick,
|
||||
},
|
||||
r("div", { className: "study-icon" },
|
||||
study.name.replace(/-?pref-?(flip|study)-?/, "").replace(/-?study-?/, "").slice(0, 1)
|
||||
),
|
||||
r("div", { className: "study-details" },
|
||||
r("div", { className: "study-header" },
|
||||
r("span", { className: "study-name" }, study.name),
|
||||
r("span", {}, "\u2022"), // •
|
||||
r("span", { className: "study-status" }, study.expired ? translations.completeStatus : translations.activeStatus),
|
||||
),
|
||||
r("div", { className: "study-description", dangerouslySetInnerHTML: { __html: description }}),
|
||||
),
|
||||
r("div", { className: "study-actions" },
|
||||
!study.expired &&
|
||||
r("button", { className: "remove-button", onClick: this.handleClickRemove },
|
||||
r("div", { className: "button-box" },
|
||||
translations.removeButton
|
||||
),
|
||||
)
|
||||
page.icon && r("img", {className: "category-icon", src: page.icon}),
|
||||
r("span", {className: "category-name"}, translations[page.name]),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
SidebarItem.propTypes = {
|
||||
pageId: PropTypes.string.isRequired,
|
||||
page: PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
selected: PropTypes.bool,
|
||||
translations: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class Content extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
r("div", {className: "main-content"},
|
||||
r("div", {className: "content-box"},
|
||||
this.props.children,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
PreferenceStudyListItem.propTypes = {
|
||||
study: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
expired: PropTypes.bool.isRequired,
|
||||
preferenceName: PropTypes.string.isRequired,
|
||||
preferenceValue: PropTypes.oneOf(PropTypes.string, PropTypes.bool, PropTypes.number).isRequired,
|
||||
}).isRequired,
|
||||
translations: PropTypes.object.isRequired,
|
||||
Content.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
ReactDOM.render(r(AboutStudies), document.getElementById("app"));
|
||||
ReactDOM.render(
|
||||
r(AboutStudies),
|
||||
document.getElementById("app"),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/* 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/. */
|
||||
"use strict";
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* global PropTypes React */
|
||||
|
||||
/**
|
||||
* Shorthand for creating elements (to avoid using a JSX preprocessor)
|
||||
*/
|
||||
const r = React.createElement;
|
||||
|
||||
/**
|
||||
* Information box used at the top of listings.
|
||||
*/
|
||||
window.InfoBox = class InfoBox extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
r("div", {className: "info-box"},
|
||||
r("div", {className: "info-box-content"},
|
||||
this.props.children,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
window.InfoBox.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
/**
|
||||
* Button using in-product styling.
|
||||
*/
|
||||
window.FxButton = class FxButton extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
r("button", Object.assign({}, this.props, {children: undefined}),
|
||||
r("div", {className: "button-box"},
|
||||
this.props.children,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
window.FxButton.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper class for a value that is provided by the frame script.
|
||||
*
|
||||
* Emits a "GetRemoteValue:{name}" page event on load to fetch the initial
|
||||
* value, and listens for "ReceiveRemoteValue:{name}" page callbacks to receive
|
||||
* the value when it updates.
|
||||
*
|
||||
* @example
|
||||
* const myRemoteValue = new RemoteValue("MyValue", 5);
|
||||
* class MyComponent extends React.Component {
|
||||
* constructor(props) {
|
||||
* super(props);
|
||||
* this.state = {
|
||||
* myValue: null,
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* componentWillMount() {
|
||||
* myRemoteValue.subscribe(this);
|
||||
* }
|
||||
*
|
||||
* componentWillUnmount() {
|
||||
* myRemoteValue.unsubscribe(this);
|
||||
* }
|
||||
*
|
||||
* receiveRemoteValue(name, value) {
|
||||
* this.setState({myValue: value});
|
||||
* }
|
||||
*
|
||||
* render() {
|
||||
* return r("div", {}, this.state.myValue);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
class RemoteValue {
|
||||
constructor(name, defaultValue = null) {
|
||||
this.name = name;
|
||||
this.handlers = [];
|
||||
this.value = defaultValue;
|
||||
|
||||
document.addEventListener(`ReceiveRemoteValue:${this.name}`, this);
|
||||
sendPageEvent(`GetRemoteValue:${this.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to this value as it updates. Handlers are called with the current
|
||||
* value immediately after subscribing.
|
||||
* @param {Object} handler
|
||||
* Object with a receiveRemoteValue(name, value) method that is called with
|
||||
* the name and value of this RemoteValue when it is updated.
|
||||
*/
|
||||
subscribe(handler) {
|
||||
this.handlers.push(handler);
|
||||
handler.receiveRemoteValue(this.name, this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a previously-registered handler.
|
||||
* @param {Object} handler
|
||||
*/
|
||||
unsubscribe(handler) {
|
||||
this.handlers = this.handlers.filter(h => h !== handler);
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
this.value = event.detail;
|
||||
for (const handler of this.handlers) {
|
||||
handler.receiveRemoteValue(this.name, this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of RemoteValue instances used within the page.
|
||||
*/
|
||||
const remoteValues = {
|
||||
studyList: new RemoteValue("StudyList"),
|
||||
shieldLearnMoreHref: new RemoteValue("ShieldLearnMoreHref"),
|
||||
studiesEnabled: new RemoteValue("StudiesEnabled"),
|
||||
shieldTranslations: new RemoteValue("ShieldTranslations"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches a page event to the privileged frame script for this tab.
|
||||
* @param {String} action
|
||||
* @param {Object} data
|
||||
*/
|
||||
function sendPageEvent(action, data) {
|
||||
const event = new CustomEvent("ShieldPageEvent", {bubbles: true, detail: {action, data}});
|
||||
document.dispatchEvent(event);
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/* 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/. */
|
||||
"use strict";
|
||||
/* global classnames FxButton InfoBox PropTypes r React remoteValues sendPageEvent */
|
||||
|
||||
window.ShieldStudies = class ShieldStudies extends React.Component {
|
||||
|
||||
render() {
|
||||
const { translations } = this.props;
|
||||
|
||||
return (
|
||||
r("div", {},
|
||||
r(WhatsThisBox, {translations}),
|
||||
r(StudyList, {translations}),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
class UpdatePreferencesButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
sendPageEvent("NavigateToDataPreferences");
|
||||
}
|
||||
|
||||
render() {
|
||||
return r(
|
||||
FxButton,
|
||||
Object.assign({
|
||||
id: "shield-studies-update-preferences",
|
||||
onClick: this.handleClick,
|
||||
}, this.props),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StudyList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
studies: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
remoteValues.studyList.subscribe(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
remoteValues.studyList.unsubscribe(this);
|
||||
}
|
||||
|
||||
receiveRemoteValue(name, value) {
|
||||
if (value) {
|
||||
const studies = value.slice();
|
||||
|
||||
// Sort by active status, then by start date descending.
|
||||
studies.sort((a, b) => {
|
||||
if (a.active !== b.active) {
|
||||
return a.active ? -1 : 1;
|
||||
}
|
||||
return b.studyStartDate - a.studyStartDate;
|
||||
});
|
||||
|
||||
this.setState({studies});
|
||||
} else {
|
||||
this.setState({studies: value});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { studies } = this.state;
|
||||
const { translations } = this.props;
|
||||
|
||||
if (studies === null) {
|
||||
// loading
|
||||
return null;
|
||||
}
|
||||
|
||||
let info = null;
|
||||
if (studies.length === 0) {
|
||||
info = r("p", {className: "study-list-info"}, translations.noStudies);
|
||||
}
|
||||
|
||||
return (
|
||||
r("div", {},
|
||||
info,
|
||||
r("ul", {className: "study-list"},
|
||||
this.state.studies.map(study => (
|
||||
r(StudyListItem, {key: study.name, study, translations})
|
||||
))
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StudyListItem extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickRemove = this.handleClickRemove.bind(this);
|
||||
}
|
||||
|
||||
handleClickRemove() {
|
||||
sendPageEvent("RemoveStudy", {recipeId: this.props.study.recipeId, reason: "individual-opt-out"});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {study, translations} = this.props;
|
||||
return (
|
||||
r("li", {
|
||||
className: classnames("study", {disabled: !study.active}),
|
||||
"data-study-name": study.name,
|
||||
},
|
||||
r("div", {className: "study-icon"},
|
||||
study.name.slice(0, 1)
|
||||
),
|
||||
r("div", {className: "study-details"},
|
||||
r("div", {className: "study-name"}, study.name),
|
||||
r("div", {className: "study-description", title: study.description},
|
||||
r("span", {className: "study-status"}, study.active ? translations.activeStatus : translations.completeStatus),
|
||||
r("span", {}, "\u2022"), // •
|
||||
r("span", {}, study.description),
|
||||
),
|
||||
),
|
||||
r("div", {className: "study-actions"},
|
||||
study.active &&
|
||||
r(FxButton, {className: "remove-button", onClick: this.handleClickRemove}, translations.removeButton),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
StudyListItem.propTypes = {
|
||||
study: PropTypes.shape({
|
||||
recipeId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
active: PropTypes.boolean,
|
||||
description: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
translations: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class WhatsThisBox extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
learnMoreHref: null,
|
||||
studiesEnabled: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
remoteValues.shieldLearnMoreHref.subscribe(this);
|
||||
remoteValues.studiesEnabled.subscribe(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
remoteValues.shieldLearnMoreHref.unsubscribe(this);
|
||||
remoteValues.studiesEnabled.unsubscribe(this);
|
||||
}
|
||||
|
||||
receiveRemoteValue(name, value) {
|
||||
switch (name) {
|
||||
case "ShieldLearnMoreHref": {
|
||||
this.setState({ learnMoreHref: value });
|
||||
break;
|
||||
}
|
||||
case "StudiesEnabled": {
|
||||
this.setState({ studiesEnabled: value });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.error(`Unknown remote value ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { learnMoreHref, studiesEnabled } = this.state;
|
||||
const { translations } = this.props;
|
||||
|
||||
let message = null;
|
||||
|
||||
// studiesEnabled can be null, in which case do nothing
|
||||
if (studiesEnabled === false) {
|
||||
message = r("span", {}, translations.disabledList);
|
||||
} else if (studiesEnabled === true) {
|
||||
message = r("span", {}, translations.enabledList);
|
||||
}
|
||||
|
||||
const updateButtonKey = navigator.platform.includes("Win") ? "updateButtonWin" : "updateButtonUnix";
|
||||
|
||||
return (
|
||||
r(InfoBox, {},
|
||||
message,
|
||||
r("a", {id: "shield-studies-learn-more", href: learnMoreHref}, translations.learnMore),
|
||||
r(UpdatePreferencesButton, {}, translations[updateButtonKey]),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -90,14 +90,14 @@ const PreferenceBranchType = {
|
|||
/**
|
||||
* Asynchronously load the JSON file that stores experiment status in the profile.
|
||||
*/
|
||||
let gStorePromise;
|
||||
let storePromise;
|
||||
function ensureStorage() {
|
||||
if (gStorePromise === undefined) {
|
||||
if (storePromise === undefined) {
|
||||
const path = OS.Path.join(OS.Constants.Path.profileDir, EXPERIMENT_FILE);
|
||||
const storage = new JSONFile({path});
|
||||
gStorePromise = storage.load().then(() => storage);
|
||||
storePromise = storage.load().then(() => storage);
|
||||
}
|
||||
return gStorePromise;
|
||||
return storePromise;
|
||||
}
|
||||
|
||||
const log = LogManager.getLogger("preference-experiments");
|
||||
|
@ -248,30 +248,23 @@ var PreferenceExperiments = {
|
|||
* Test wrapper that temporarily replaces the stored experiment data with fake
|
||||
* data for testing.
|
||||
*/
|
||||
withMockExperiments(mockExperiments = []) {
|
||||
return function wrapper(testFunction) {
|
||||
return async function wrappedTestFunction(...args) {
|
||||
const data = {};
|
||||
|
||||
for (const exp of mockExperiments) {
|
||||
data[exp.name] = exp;
|
||||
}
|
||||
|
||||
const oldPromise = gStorePromise;
|
||||
gStorePromise = Promise.resolve({
|
||||
data,
|
||||
saveSoon() { },
|
||||
});
|
||||
const oldObservers = experimentObservers;
|
||||
experimentObservers = new Map();
|
||||
try {
|
||||
await testFunction(...args, mockExperiments);
|
||||
} finally {
|
||||
gStorePromise = oldPromise;
|
||||
PreferenceExperiments.stopAllObservers();
|
||||
experimentObservers = oldObservers;
|
||||
}
|
||||
};
|
||||
withMockExperiments(testFunction) {
|
||||
return async function inner(...args) {
|
||||
const oldPromise = storePromise;
|
||||
const mockExperiments = {};
|
||||
storePromise = Promise.resolve({
|
||||
data: mockExperiments,
|
||||
saveSoon() { },
|
||||
});
|
||||
const oldObservers = experimentObservers;
|
||||
experimentObservers = new Map();
|
||||
try {
|
||||
await testFunction(...args, mockExperiments);
|
||||
} finally {
|
||||
storePromise = oldPromise;
|
||||
PreferenceExperiments.stopAllObservers();
|
||||
experimentObservers = oldObservers;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ decorate_task(
|
|||
|
||||
decorate_task(
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({name: "test-study"}),
|
||||
studyFactory({name: "test-study"}),
|
||||
]),
|
||||
async function testGet([study]) {
|
||||
const storedStudy = await AddonStudies.get(study.recipeId);
|
||||
|
@ -43,8 +43,8 @@ decorate_task(
|
|||
|
||||
decorate_task(
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory(),
|
||||
addonStudyFactory(),
|
||||
studyFactory(),
|
||||
studyFactory(),
|
||||
]),
|
||||
async function testGetAll(studies) {
|
||||
const storedStudies = await AddonStudies.getAll();
|
||||
|
@ -58,7 +58,7 @@ decorate_task(
|
|||
|
||||
decorate_task(
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({name: "test-study"}),
|
||||
studyFactory({name: "test-study"}),
|
||||
]),
|
||||
async function testHas([study]) {
|
||||
let hasStudy = await AddonStudies.has(study.recipeId);
|
||||
|
@ -95,8 +95,8 @@ decorate_task(
|
|||
|
||||
decorate_task(
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({name: "test-study1"}),
|
||||
addonStudyFactory({name: "test-study2"}),
|
||||
studyFactory({name: "test-study1"}),
|
||||
studyFactory({name: "test-study2"}),
|
||||
]),
|
||||
async function testClear([study1, study2]) {
|
||||
const hasAll = (
|
||||
|
@ -116,9 +116,9 @@ decorate_task(
|
|||
|
||||
decorate_task(
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({active: true, addonId: "does.not.exist@example.com", studyEndDate: null}),
|
||||
addonStudyFactory({active: true, addonId: "installed@example.com"}),
|
||||
addonStudyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}),
|
||||
studyFactory({active: true, addonId: "does.not.exist@example.com", studyEndDate: null}),
|
||||
studyFactory({active: true, addonId: "installed@example.com"}),
|
||||
studyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}),
|
||||
]),
|
||||
withSendEventStub,
|
||||
withInstalledWebExtension({id: "installed@example.com"}),
|
||||
|
@ -162,7 +162,7 @@ decorate_task(
|
|||
|
||||
decorate_task(
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({active: true, addonId: "installed@example.com", studyEndDate: null}),
|
||||
studyFactory({active: true, addonId: "installed@example.com", studyEndDate: null}),
|
||||
]),
|
||||
withInstalledWebExtension({id: "installed@example.com"}, /* expectUninstall: */ true),
|
||||
async function testInit([study], [id, addonFile]) {
|
||||
|
|
|
@ -173,7 +173,7 @@ decorate_task(
|
|||
decorate_task(
|
||||
withSandboxManager(Assert),
|
||||
withMockPreferences,
|
||||
PreferenceExperiments.withMockExperiments(),
|
||||
PreferenceExperiments.withMockExperiments,
|
||||
async function testPreferenceStudies(sandboxManager) {
|
||||
const driver = new NormandyDriver(sandboxManager);
|
||||
sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true});
|
||||
|
|
|
@ -28,8 +28,9 @@ function experimentFactory(attrs) {
|
|||
|
||||
// clearAllExperimentStorage
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test" })]),
|
||||
withMockExperiments,
|
||||
async function(experiments) {
|
||||
experiments.test = experimentFactory({name: "test"});
|
||||
ok(await PreferenceExperiments.has("test"), "Mock experiment is detected.");
|
||||
await PreferenceExperiments.clearAllExperimentStorage();
|
||||
ok(
|
||||
|
@ -41,9 +42,10 @@ decorate_task(
|
|||
|
||||
// start should throw if an experiment with the given name already exists
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test" })]),
|
||||
withMockExperiments,
|
||||
withSendEventStub,
|
||||
async function(experiments, sendEventStub) {
|
||||
experiments.test = experimentFactory({name: "test"});
|
||||
await Assert.rejects(
|
||||
PreferenceExperiments.start({
|
||||
name: "test",
|
||||
|
@ -67,9 +69,10 @@ decorate_task(
|
|||
|
||||
// start should throw if an experiment for the given preference is active
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test", preferenceName: "fake.preference" })]),
|
||||
withMockExperiments,
|
||||
withSendEventStub,
|
||||
async function(experiments, sendEventStub) {
|
||||
experiments.test = experimentFactory({name: "test", preferenceName: "fake.preference"});
|
||||
await Assert.rejects(
|
||||
PreferenceExperiments.start({
|
||||
name: "different",
|
||||
|
@ -93,7 +96,7 @@ decorate_task(
|
|||
|
||||
// start should throw if an invalid preferenceBranchType is given
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withSendEventStub,
|
||||
async function(experiments, sendEventStub) {
|
||||
await Assert.rejects(
|
||||
|
@ -120,7 +123,7 @@ decorate_task(
|
|||
// start should save experiment data, modify the preference, and register a
|
||||
// watcher.
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "startObserver"),
|
||||
withSendEventStub,
|
||||
|
@ -136,7 +139,7 @@ decorate_task(
|
|||
preferenceBranchType: "default",
|
||||
preferenceType: "string",
|
||||
});
|
||||
ok(await PreferenceExperiments.get("test"), "start saved the experiment");
|
||||
ok("test" in experiments, "start saved the experiment");
|
||||
ok(
|
||||
startObserverStub.calledWith("test", "fake.preference", "string", "newvalue"),
|
||||
"start registered an observer",
|
||||
|
@ -153,8 +156,7 @@ decorate_task(
|
|||
preferenceBranchType: "default",
|
||||
};
|
||||
const experiment = {};
|
||||
const actualExperiment = await PreferenceExperiments.get("test");
|
||||
Object.keys(expectedExperiment).forEach(key => experiment[key] = actualExperiment[key]);
|
||||
Object.keys(expectedExperiment).forEach(key => experiment[key] = experiments.test[key]);
|
||||
Assert.deepEqual(experiment, expectedExperiment, "start saved the experiment");
|
||||
|
||||
is(
|
||||
|
@ -177,7 +179,7 @@ decorate_task(
|
|||
|
||||
// start should modify the user preference for the user branch type
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "startObserver"),
|
||||
async function(experiments, mockPreferences, startObserver) {
|
||||
|
@ -209,8 +211,7 @@ decorate_task(
|
|||
};
|
||||
|
||||
const experiment = {};
|
||||
const actualExperiment = await PreferenceExperiments.get("test");
|
||||
Object.keys(expectedExperiment).forEach(key => experiment[key] = actualExperiment[key]);
|
||||
Object.keys(expectedExperiment).forEach(key => experiment[key] = experiments.test[key]);
|
||||
Assert.deepEqual(experiment, expectedExperiment, "start saved the experiment");
|
||||
|
||||
Assert.notEqual(
|
||||
|
@ -253,7 +254,7 @@ decorate_task(
|
|||
// startObserver should throw if an observer for the experiment is already
|
||||
// active.
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
async function() {
|
||||
PreferenceExperiments.startObserver("test", "fake.preference", "string", "newvalue");
|
||||
Assert.throws(
|
||||
|
@ -268,7 +269,7 @@ decorate_task(
|
|||
// startObserver should register an observer that calls stop when a preference
|
||||
// changes from its experimental value.
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
async function(mockExperiments, mockPreferences) {
|
||||
const tests = [
|
||||
|
@ -299,7 +300,7 @@ decorate_task(
|
|||
);
|
||||
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
async function testHasObserver() {
|
||||
PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentValue");
|
||||
|
||||
|
@ -315,7 +316,7 @@ decorate_task(
|
|||
|
||||
// stopObserver should throw if there is no observer active for it to stop.
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
async function() {
|
||||
Assert.throws(
|
||||
() => PreferenceExperiments.stopObserver("neveractive", "another.fake", "othervalue"),
|
||||
|
@ -327,7 +328,7 @@ decorate_task(
|
|||
|
||||
// stopObserver should cancel an active observer.
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
async function(mockExperiments, mockPreferences) {
|
||||
const stop = sinon.stub(PreferenceExperiments, "stop");
|
||||
|
@ -356,7 +357,7 @@ decorate_task(
|
|||
|
||||
// stopAllObservers
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
async function(mockExperiments, mockPreferences) {
|
||||
const stop = sinon.stub(PreferenceExperiments, "stop");
|
||||
|
@ -389,7 +390,7 @@ decorate_task(
|
|||
|
||||
// markLastSeen should throw if it can't find a matching experiment
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
async function() {
|
||||
await Assert.rejects(
|
||||
PreferenceExperiments.markLastSeen("neveractive"),
|
||||
|
@ -400,13 +401,14 @@ decorate_task(
|
|||
);
|
||||
|
||||
// markLastSeen should update the lastSeen date
|
||||
const oldDate = new Date(1988, 10, 1).toJSON();
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test", lastSeen: oldDate })]),
|
||||
async function([experiment]) {
|
||||
withMockExperiments,
|
||||
async function(experiments) {
|
||||
const oldDate = new Date(1988, 10, 1).toJSON();
|
||||
experiments.test = experimentFactory({name: "test", lastSeen: oldDate});
|
||||
await PreferenceExperiments.markLastSeen("test");
|
||||
Assert.notEqual(
|
||||
experiment.lastSeen,
|
||||
experiments.test.lastSeen,
|
||||
oldDate,
|
||||
"markLastSeen updated the experiment lastSeen date",
|
||||
);
|
||||
|
@ -415,7 +417,7 @@ decorate_task(
|
|||
|
||||
// stop should throw if an experiment with the given name doesn't exist
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withSendEventStub,
|
||||
async function(experiments, sendEventStub) {
|
||||
await Assert.rejects(
|
||||
|
@ -434,9 +436,10 @@ decorate_task(
|
|||
|
||||
// stop should throw if the experiment is already expired
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test", expired: true })]),
|
||||
withMockExperiments,
|
||||
withSendEventStub,
|
||||
async function(experiments, sendEventStub) {
|
||||
experiments.test = experimentFactory({name: "test", expired: true});
|
||||
await Assert.rejects(
|
||||
PreferenceExperiments.stop("test"),
|
||||
/already expired/,
|
||||
|
@ -454,18 +457,7 @@ decorate_task(
|
|||
// stop should mark the experiment as expired, stop its observer, and revert the
|
||||
// preference value.
|
||||
decorate_task(
|
||||
withMockExperiments([
|
||||
experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
branch: "fakebranch",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "default",
|
||||
}),
|
||||
]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withSpy(PreferenceExperiments, "stopObserver"),
|
||||
withSendEventStub,
|
||||
|
@ -476,12 +468,21 @@ decorate_task(
|
|||
|
||||
mockPreferences.set(`${startupPrefs}.fake.preference`, "experimentvalue", "user");
|
||||
mockPreferences.set("fake.preference", "experimentvalue", "default");
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
branch: "fakebranch",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "default",
|
||||
});
|
||||
PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentvalue");
|
||||
|
||||
await PreferenceExperiments.stop("test", {reason: "test-reason"});
|
||||
ok(stopObserverSpy.calledWith("test"), "stop removed an observer");
|
||||
const experiment = await PreferenceExperiments.get("test");
|
||||
is(experiment.expired, true, "stop marked the experiment as expired");
|
||||
is(experiments.test.expired, true, "stop marked the experiment as expired");
|
||||
is(
|
||||
DefaultPreferences.get("fake.preference"),
|
||||
"oldvalue",
|
||||
|
@ -494,7 +495,7 @@ decorate_task(
|
|||
|
||||
Assert.deepEqual(
|
||||
sendEventStub.args,
|
||||
[["unenroll", "preference_study", "test", {
|
||||
[["unenroll", "preference_study", experiments.test.name, {
|
||||
didResetValue: "true",
|
||||
reason: "test-reason",
|
||||
branch: "fakebranch",
|
||||
|
@ -508,15 +509,7 @@ decorate_task(
|
|||
|
||||
// stop should also support user pref experiments
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "user",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "stopObserver"),
|
||||
withStub(PreferenceExperiments, "hasObserver"),
|
||||
|
@ -524,12 +517,20 @@ decorate_task(
|
|||
hasObserver.returns(true);
|
||||
|
||||
mockPreferences.set("fake.preference", "experimentvalue", "user");
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "user",
|
||||
});
|
||||
PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentvalue");
|
||||
|
||||
await PreferenceExperiments.stop("test");
|
||||
ok(stopObserver.calledWith("test"), "stop removed an observer");
|
||||
const experiment = await PreferenceExperiments.get("test");
|
||||
is(experiment.expired, true, "stop marked the experiment as expired");
|
||||
is(experiments.test.expired, true, "stop marked the experiment as expired");
|
||||
is(
|
||||
Preferences.get("fake.preference"),
|
||||
"oldvalue",
|
||||
|
@ -542,19 +543,20 @@ decorate_task(
|
|||
|
||||
// stop should remove a preference that had no value prior to an experiment for user prefs
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: null,
|
||||
preferenceBranchType: "user",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
async function(experiments, mockPreferences) {
|
||||
const stopObserver = sinon.stub(PreferenceExperiments, "stopObserver");
|
||||
mockPreferences.set("fake.preference", "experimentvalue", "user");
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: null,
|
||||
preferenceBranchType: "user",
|
||||
});
|
||||
|
||||
await PreferenceExperiments.stop("test");
|
||||
ok(
|
||||
|
@ -568,21 +570,22 @@ decorate_task(
|
|||
|
||||
// stop should not modify a preference if resetValue is false
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
branch: "fakebranch",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "default",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "stopObserver"),
|
||||
withSendEventStub,
|
||||
async function testStopReset(experiments, mockPreferences, stopObserverStub, sendEventStub) {
|
||||
mockPreferences.set("fake.preference", "customvalue", "default");
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
branch: "fakebranch",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "default",
|
||||
});
|
||||
|
||||
await PreferenceExperiments.stop("test", {reason: "test-reason", resetValue: false});
|
||||
is(
|
||||
|
@ -592,7 +595,7 @@ decorate_task(
|
|||
);
|
||||
Assert.deepEqual(
|
||||
sendEventStub.args,
|
||||
[["unenroll", "preference_study", "test", {
|
||||
[["unenroll", "preference_study", experiments.test.name, {
|
||||
didResetValue: "false",
|
||||
reason: "test-reason",
|
||||
branch: "fakebranch",
|
||||
|
@ -604,7 +607,7 @@ decorate_task(
|
|||
|
||||
// get should throw if no experiment exists with the given name
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
async function() {
|
||||
await Assert.rejects(
|
||||
PreferenceExperiments.get("neverexisted"),
|
||||
|
@ -616,25 +619,29 @@ decorate_task(
|
|||
|
||||
// get
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test" })]),
|
||||
withMockExperiments,
|
||||
async function(experiments) {
|
||||
const experiment = await PreferenceExperiments.get("test");
|
||||
is(experiment.name, "test", "get fetches the correct experiment");
|
||||
const experiment = experimentFactory({name: "test"});
|
||||
experiments.test = experiment;
|
||||
|
||||
const fetchedExperiment = await PreferenceExperiments.get("test");
|
||||
Assert.deepEqual(fetchedExperiment, experiment, "get fetches the correct experiment");
|
||||
|
||||
// Modifying the fetched experiment must not edit the data source.
|
||||
experiment.name = "othername";
|
||||
const refetched = await PreferenceExperiments.get("test");
|
||||
is(refetched.name, "test", "get returns a copy of the experiment");
|
||||
fetchedExperiment.name = "othername";
|
||||
is(experiments.test.name, "test", "get returns a copy of the experiment");
|
||||
}
|
||||
);
|
||||
|
||||
// get all
|
||||
decorate_task(
|
||||
withMockExperiments([
|
||||
experimentFactory({ name: "experiment1", disabled: false }),
|
||||
experimentFactory({ name: "experiment2", disabled: true }),
|
||||
]),
|
||||
async function testGetAll([experiment1, experiment2]) {
|
||||
withMockExperiments,
|
||||
async function testGetAll(experiments) {
|
||||
const experiment1 = experimentFactory({name: "experiment1"});
|
||||
const experiment2 = experimentFactory({name: "experiment2", disabled: true});
|
||||
experiments.experiment1 = experiment1;
|
||||
experiments.experiment2 = experiment2;
|
||||
|
||||
const fetchedExperiments = await PreferenceExperiments.getAll();
|
||||
is(fetchedExperiments.length, 2, "getAll returns a list of all stored experiments");
|
||||
Assert.deepEqual(
|
||||
|
@ -656,29 +663,28 @@ decorate_task(
|
|||
|
||||
// get all active
|
||||
decorate_task(
|
||||
withMockExperiments([
|
||||
experimentFactory({
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
async function testGetAllActive(experiments) {
|
||||
experiments.active = experimentFactory({
|
||||
name: "active",
|
||||
expired: false,
|
||||
}),
|
||||
experimentFactory({
|
||||
});
|
||||
experiments.inactive = experimentFactory({
|
||||
name: "inactive",
|
||||
expired: true,
|
||||
}),
|
||||
]),
|
||||
withMockPreferences,
|
||||
async function testGetAllActive([activeExperiment, inactiveExperiment]) {
|
||||
let allActiveExperiments = await PreferenceExperiments.getAllActive();
|
||||
});
|
||||
|
||||
const activeExperiments = await PreferenceExperiments.getAllActive();
|
||||
Assert.deepEqual(
|
||||
allActiveExperiments,
|
||||
[activeExperiment],
|
||||
activeExperiments,
|
||||
[experiments.active],
|
||||
"getAllActive only returns active experiments",
|
||||
);
|
||||
|
||||
allActiveExperiments[0].name = "newfakename";
|
||||
allActiveExperiments = await PreferenceExperiments.getAllActive();
|
||||
activeExperiments[0].name = "newfakename";
|
||||
Assert.notEqual(
|
||||
allActiveExperiments,
|
||||
experiments.active.name,
|
||||
"newfakename",
|
||||
"getAllActive returns copies of stored experiments",
|
||||
);
|
||||
|
@ -687,8 +693,9 @@ decorate_task(
|
|||
|
||||
// has
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test" })]),
|
||||
withMockExperiments,
|
||||
async function(experiments) {
|
||||
experiments.test = experimentFactory({name: "test"});
|
||||
ok(await PreferenceExperiments.has("test"), "has returned true for a stored experiment");
|
||||
ok(!(await PreferenceExperiments.has("missing")), "has returned false for a missing experiment");
|
||||
}
|
||||
|
@ -696,22 +703,25 @@ decorate_task(
|
|||
|
||||
// init should register telemetry experiments
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
branch: "branch",
|
||||
preferenceName: "fake.pref",
|
||||
preferenceValue: "experiment value",
|
||||
expired: false,
|
||||
preferenceBranchType: "default",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(TelemetryEnvironment, "setExperimentActive"),
|
||||
withStub(PreferenceExperiments, "startObserver"),
|
||||
async function testInit(experiments, mockPreferences, setActiveStub, startObserverStub) {
|
||||
mockPreferences.set("fake.pref", "experiment value");
|
||||
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
branch: "branch",
|
||||
preferenceName: "fake.pref",
|
||||
preferenceValue: "experiment value",
|
||||
expired: false,
|
||||
preferenceBranchType: "default",
|
||||
});
|
||||
|
||||
await PreferenceExperiments.init();
|
||||
ok(
|
||||
setActiveStub.calledWith("test", "branch", { type: "normandy-exp" }),
|
||||
setActiveStub.calledWith("test", "branch", {type: "normandy-exp"}),
|
||||
"Experiment is registered by init",
|
||||
);
|
||||
},
|
||||
|
@ -719,21 +729,24 @@ decorate_task(
|
|||
|
||||
// init should use the provided experiment type
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
branch: "branch",
|
||||
preferenceName: "fake.pref",
|
||||
preferenceValue: "experiment value",
|
||||
experimentType: "pref-test",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(TelemetryEnvironment, "setExperimentActive"),
|
||||
withStub(PreferenceExperiments, "startObserver"),
|
||||
async function testInit(experiments, mockPreferences, setActiveStub, startObserverStub) {
|
||||
mockPreferences.set("fake.pref", "experiment value");
|
||||
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
branch: "branch",
|
||||
preferenceName: "fake.pref",
|
||||
preferenceValue: "experiment value",
|
||||
experimentType: "pref-test",
|
||||
});
|
||||
|
||||
await PreferenceExperiments.init();
|
||||
ok(
|
||||
setActiveStub.calledWith("test", "branch", { type: "normandy-pref-test" }),
|
||||
setActiveStub.calledWith("test", "branch", {type: "normandy-pref-test"}),
|
||||
"init should use the provided experiment type",
|
||||
);
|
||||
},
|
||||
|
@ -741,7 +754,7 @@ decorate_task(
|
|||
|
||||
// starting and stopping experiments should register in telemetry
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withStub(TelemetryEnvironment, "setExperimentActive"),
|
||||
withStub(TelemetryEnvironment, "setExperimentInactive"),
|
||||
withSendEventStub,
|
||||
|
@ -757,10 +770,10 @@ decorate_task(
|
|||
|
||||
Assert.deepEqual(
|
||||
setActiveStub.getCall(0).args,
|
||||
["test", "branch", { type: "normandy-exp" }],
|
||||
["test", "branch", {type: "normandy-exp"}],
|
||||
"Experiment is registered by start()",
|
||||
);
|
||||
await PreferenceExperiments.stop("test", { reason: "test-reason" });
|
||||
await PreferenceExperiments.stop("test", {reason: "test-reason"});
|
||||
Assert.deepEqual(setInactiveStub.args, [["test"]], "Experiment is unregistered by stop()");
|
||||
|
||||
Assert.deepEqual(
|
||||
|
@ -783,7 +796,7 @@ decorate_task(
|
|||
|
||||
// starting experiments should use the provided experiment type
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withStub(TelemetryEnvironment, "setExperimentActive"),
|
||||
withStub(TelemetryEnvironment, "setExperimentInactive"),
|
||||
withSendEventStub,
|
||||
|
@ -800,7 +813,7 @@ decorate_task(
|
|||
|
||||
Assert.deepEqual(
|
||||
setActiveStub.getCall(0).args,
|
||||
["test", "branch", { type: "normandy-pref-test" }],
|
||||
["test", "branch", {type: "normandy-pref-test"}],
|
||||
"start() should register the experiment with the provided type",
|
||||
);
|
||||
|
||||
|
@ -821,9 +834,10 @@ decorate_task(
|
|||
|
||||
// Experiments shouldn't be recorded by init() in telemetry if they are expired
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "expired", branch: "branch", expired: true })]),
|
||||
withMockExperiments,
|
||||
withStub(TelemetryEnvironment, "setExperimentActive"),
|
||||
async function testInitTelemetryExpired(experiments, setActiveStub) {
|
||||
experiments.experiment1 = experimentFactory({name: "expired", branch: "branch", expired: true});
|
||||
await PreferenceExperiments.init();
|
||||
ok(!setActiveStub.called, "Expired experiment is not registered by init");
|
||||
},
|
||||
|
@ -831,16 +845,17 @@ decorate_task(
|
|||
|
||||
// Experiments should end if the preference has been changed when init() is called
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experiment value",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "stop"),
|
||||
async function testInitChanges(experiments, mockPreferences, stopStub) {
|
||||
mockPreferences.set("fake.preference", "experiment value", "default");
|
||||
mockPreferences.set("fake.preference", "changed value", "user");
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experiment value",
|
||||
});
|
||||
mockPreferences.set("fake.preference", "changed value");
|
||||
await PreferenceExperiments.init();
|
||||
|
||||
is(Preferences.get("fake.preference"), "changed value", "Preference value was not changed");
|
||||
|
@ -858,11 +873,7 @@ decorate_task(
|
|||
|
||||
// init should register an observer for experiments
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experiment value",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "startObserver"),
|
||||
withStub(PreferenceExperiments, "stop"),
|
||||
|
@ -871,6 +882,11 @@ decorate_task(
|
|||
stop.throws("Stop should not be called");
|
||||
mockPreferences.set("fake.preference", "experiment value", "default");
|
||||
is(Preferences.get("fake.preference"), "experiment value", "pref shouldn't have a user value");
|
||||
experiments.test = experimentFactory({
|
||||
name: "test",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experiment value",
|
||||
});
|
||||
await PreferenceExperiments.init();
|
||||
|
||||
ok(startObserver.calledOnce, "init should register an observer");
|
||||
|
@ -884,24 +900,21 @@ decorate_task(
|
|||
|
||||
// saveStartupPrefs
|
||||
decorate_task(
|
||||
withMockExperiments([
|
||||
experimentFactory({
|
||||
name: "char",
|
||||
preferenceName: `fake.char`,
|
||||
preferenceValue: "string",
|
||||
}),
|
||||
experimentFactory({
|
||||
name: "int",
|
||||
preferenceName: `fake.int`,
|
||||
preferenceValue: 2,
|
||||
}),
|
||||
experimentFactory({
|
||||
name: "bool",
|
||||
preferenceName: `fake.bool`,
|
||||
preferenceValue: true,
|
||||
}),
|
||||
]),
|
||||
withMockExperiments,
|
||||
async function testSaveStartupPrefs(experiments) {
|
||||
const experimentPrefs = {
|
||||
char: "string",
|
||||
int: 2,
|
||||
bool: true,
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(experimentPrefs)) {
|
||||
experiments[key] = experimentFactory({
|
||||
preferenceName: `fake.${key}`,
|
||||
preferenceValue: value,
|
||||
});
|
||||
}
|
||||
|
||||
Services.prefs.deleteBranch(startupPrefs);
|
||||
Services.prefs.setBoolPref(`${startupPrefs}.fake.old`, true);
|
||||
await PreferenceExperiments.saveStartupPrefs();
|
||||
|
@ -929,12 +942,13 @@ decorate_task(
|
|||
|
||||
// saveStartupPrefs errors for invalid pref type
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
preferenceName: "fake.invalidValue",
|
||||
preferenceValue: new Date(),
|
||||
})]),
|
||||
withMockExperiments,
|
||||
async function testSaveStartupPrefsError(experiments) {
|
||||
experiments.test = experimentFactory({
|
||||
preferenceName: "fake.invalidValue",
|
||||
preferenceValue: new Date(),
|
||||
});
|
||||
|
||||
await Assert.rejects(
|
||||
PreferenceExperiments.saveStartupPrefs(),
|
||||
/invalid preference type/i,
|
||||
|
@ -945,21 +959,19 @@ decorate_task(
|
|||
|
||||
// saveStartupPrefs should not store values for user-branch recipes
|
||||
decorate_task(
|
||||
withMockExperiments([
|
||||
experimentFactory({
|
||||
name: "defaultBranchRecipe",
|
||||
withMockExperiments,
|
||||
async function testSaveStartupPrefsUserBranch(experiments) {
|
||||
experiments.defaultBranchRecipe = experimentFactory({
|
||||
preferenceName: "fake.default",
|
||||
preferenceValue: "experiment value",
|
||||
branch: "default",
|
||||
}),
|
||||
experimentFactory({
|
||||
name: "userBranchRecipe",
|
||||
});
|
||||
experiments.userBranchRecipe = experimentFactory({
|
||||
preferenceName: "fake.user",
|
||||
preferenceValue: "experiment value",
|
||||
branch: "user",
|
||||
}),
|
||||
]),
|
||||
async function testSaveStartupPrefsUserBranch(experiments) {
|
||||
});
|
||||
|
||||
await PreferenceExperiments.saveStartupPrefs();
|
||||
|
||||
is(
|
||||
|
@ -979,7 +991,7 @@ decorate_task(
|
|||
|
||||
// test that default branch prefs restore to the right value if the default pref changes
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "startObserver"),
|
||||
withStub(PreferenceExperiments, "stopObserver"),
|
||||
|
@ -1027,7 +1039,7 @@ decorate_task(
|
|||
|
||||
// test that default branch prefs restore to the right value if the preference is removed
|
||||
decorate_task(
|
||||
withMockExperiments(),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "startObserver"),
|
||||
withStub(PreferenceExperiments, "stopObserver"),
|
||||
|
@ -1074,12 +1086,13 @@ decorate_task(
|
|||
|
||||
// stop should pass "unknown" to telemetry event for `reason` if none is specified
|
||||
decorate_task(
|
||||
withMockExperiments([experimentFactory({ name: "test", preferenceName: "fake.preference" })]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "stopObserver"),
|
||||
withSendEventStub,
|
||||
async function testStopUnknownReason(experiments, mockPreferences, stopObserverStub, sendEventStub) {
|
||||
mockPreferences.set("fake.preference", "default value", "default");
|
||||
experiments.test = experimentFactory({ name: "test", preferenceName: "fake.preference" });
|
||||
await PreferenceExperiments.stop("test");
|
||||
is(
|
||||
sendEventStub.getCall(0).args[3].reason,
|
||||
|
@ -1091,16 +1104,14 @@ decorate_task(
|
|||
|
||||
// stop should pass along the value for resetValue to Telemetry Events as didResetValue
|
||||
decorate_task(
|
||||
withMockExperiments([
|
||||
experimentFactory({ name: "test1", preferenceName: "fake.preference1" }),
|
||||
experimentFactory({ name: "test2", preferenceName: "fake.preference2" }),
|
||||
]),
|
||||
withMockExperiments,
|
||||
withMockPreferences,
|
||||
withStub(PreferenceExperiments, "stopObserver"),
|
||||
withSendEventStub,
|
||||
async function testStopResetValue(experiments, mockPreferences, stopObserverStub, sendEventStub) {
|
||||
mockPreferences.set("fake.preference1", "default value", "default");
|
||||
await PreferenceExperiments.stop("test1", { resetValue: true });
|
||||
experiments.test1 = experimentFactory({ name: "test1", preferenceName: "fake.preference1" });
|
||||
await PreferenceExperiments.stop("test1", {resetValue: true});
|
||||
is(sendEventStub.callCount, 1);
|
||||
is(
|
||||
sendEventStub.getCall(0).args[3].didResetValue,
|
||||
|
@ -1109,7 +1120,8 @@ decorate_task(
|
|||
);
|
||||
|
||||
mockPreferences.set("fake.preference2", "default value", "default");
|
||||
await PreferenceExperiments.stop("test2", { resetValue: false });
|
||||
experiments.test2 = experimentFactory({ name: "test2", preferenceName: "fake.preference2" });
|
||||
await PreferenceExperiments.stop("test2", {resetValue: false});
|
||||
is(sendEventStub.callCount, 2);
|
||||
is(
|
||||
sendEventStub.getCall(1).args[3].didResetValue,
|
||||
|
@ -1124,19 +1136,20 @@ decorate_task(
|
|||
decorate_task(
|
||||
withMockPreferences,
|
||||
withSendEventStub,
|
||||
withMockExperiments([experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
branch: "fakebranch",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "default",
|
||||
})]),
|
||||
withMockExperiments,
|
||||
async function testPrefChangeEventTelemetry(mockPreferences, sendEventStub, mockExperiments) {
|
||||
is(Preferences.get("fake.preference"), null, "preference should start unset");
|
||||
mockPreferences.set("fake.preference", "oldvalue", "default");
|
||||
mockExperiments.test = experimentFactory({
|
||||
name: "test",
|
||||
expired: false,
|
||||
branch: "fakebranch",
|
||||
preferenceName: "fake.preference",
|
||||
preferenceValue: "experimentvalue",
|
||||
preferenceType: "string",
|
||||
previousPreferenceValue: "oldvalue",
|
||||
preferenceBranchType: "default",
|
||||
});
|
||||
PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentvalue");
|
||||
|
||||
// setting the preference on the user branch should trigger the observer to stop the experiment
|
||||
|
|
|
@ -11,8 +11,8 @@ ShieldPreferences.init();
|
|||
decorate_task(
|
||||
withMockPreferences,
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({active: true}),
|
||||
addonStudyFactory({active: true}),
|
||||
studyFactory({active: true}),
|
||||
studyFactory({active: true}),
|
||||
]),
|
||||
async function testDisableStudiesWhenOptOutDisabled(mockPreferences, [study1, study2]) {
|
||||
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
"use strict";
|
||||
|
||||
ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
|
||||
ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
|
||||
ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
|
||||
ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
|
||||
|
||||
function withAboutStudies(testFunc) {
|
||||
return async (...args) => (
|
||||
return async (...args) => (
|
||||
BrowserTestUtils.withNewTab("about:studies", async browser => (
|
||||
testFunc(...args, browser)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// Test that the code renders at all
|
||||
decorate_task(
|
||||
withAboutStudies,
|
||||
async function testAboutStudiesWorks(browser) {
|
||||
|
@ -22,7 +20,6 @@ decorate_task(
|
|||
}
|
||||
);
|
||||
|
||||
// Test that the learn more element is displayed correctly
|
||||
decorate_task(
|
||||
withPrefEnv({
|
||||
set: [["app.normandy.shieldLearnMoreUrl", "http://test/%OS%/"]],
|
||||
|
@ -44,10 +41,9 @@ decorate_task(
|
|||
}
|
||||
);
|
||||
|
||||
// Test that jumping to preferences worked as expected
|
||||
decorate_task(
|
||||
withAboutStudies,
|
||||
async function testUpdatePreferences(browser) {
|
||||
async function testUpdatePreferencesNewOrganization(browser) {
|
||||
let loadPromise = BrowserTestUtils.firstBrowserLoaded(window);
|
||||
|
||||
// We have to use gBrowser instead of browser in most spots since we're
|
||||
|
@ -73,159 +69,90 @@ decorate_task(
|
|||
|
||||
decorate_task(
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({
|
||||
name: "A Fake Add-on Study",
|
||||
// Sort order should be study3, study1, study2 (order by enabled, then most recent).
|
||||
studyFactory({
|
||||
name: "A Fake Study",
|
||||
active: true,
|
||||
description: "A fake description",
|
||||
studyStartDate: new Date(2018, 0, 4),
|
||||
studyStartDate: new Date(2017),
|
||||
}),
|
||||
addonStudyFactory({
|
||||
name: "B Fake Add-on Study",
|
||||
studyFactory({
|
||||
name: "B Fake Study",
|
||||
active: false,
|
||||
description: "B fake description",
|
||||
studyStartDate: new Date(2018, 0, 2),
|
||||
description: "A fake description",
|
||||
studyStartDate: new Date(2019),
|
||||
}),
|
||||
addonStudyFactory({
|
||||
name: "C Fake Add-on Study",
|
||||
studyFactory({
|
||||
name: "C Fake Study",
|
||||
active: true,
|
||||
description: "C fake description",
|
||||
studyStartDate: new Date(2018, 0, 1),
|
||||
}),
|
||||
]),
|
||||
PreferenceExperiments.withMockExperiments([
|
||||
preferenceStudyFactory({
|
||||
name: "D Fake Preference Study",
|
||||
lastSeen: new Date(2018, 0, 3),
|
||||
expired: false,
|
||||
}),
|
||||
preferenceStudyFactory({
|
||||
name: "E Fake Preference Study",
|
||||
lastSeen: new Date(2018, 0, 5),
|
||||
expired: true,
|
||||
}),
|
||||
preferenceStudyFactory({
|
||||
name: "F Fake Preference Study",
|
||||
lastSeen: new Date(2018, 0, 6),
|
||||
expired: false,
|
||||
description: "A fake description",
|
||||
studyStartDate: new Date(2018),
|
||||
}),
|
||||
]),
|
||||
withAboutStudies,
|
||||
async function testStudyListing(addonStudies, prefStudies, browser) {
|
||||
await ContentTask.spawn(browser, { addonStudies, prefStudies }, async ({ addonStudies, prefStudies }) => {
|
||||
async function testStudyListing([study1, study2, study3], browser) {
|
||||
await ContentTask.spawn(browser, [study1, study2, study3], async ([cStudy1, cStudy2, cStudy3]) => {
|
||||
const doc = content.document;
|
||||
|
||||
function getStudyRow(docElem, studyName) {
|
||||
return docElem.querySelector(`.study[data-study-name="${studyName}"]`);
|
||||
}
|
||||
|
||||
await ContentTaskUtils.waitForCondition(() => doc.querySelectorAll(".active-study-list .study").length);
|
||||
const activeNames = Array.from(doc.querySelectorAll(".active-study-list .study"))
|
||||
.map(row => row.dataset.studyName);
|
||||
const inactiveNames = Array.from(doc.querySelectorAll(".inactive-study-list .study"))
|
||||
.map(row => row.dataset.studyName);
|
||||
await ContentTaskUtils.waitForCondition(() => doc.querySelectorAll(".study-list .study").length);
|
||||
const studyRows = doc.querySelectorAll(".study-list .study");
|
||||
|
||||
const names = Array.from(studyRows).map(row => row.querySelector(".study-name").textContent);
|
||||
Assert.deepEqual(
|
||||
activeNames,
|
||||
[prefStudies[2].name, addonStudies[0].name, prefStudies[0].name, addonStudies[2].name],
|
||||
"Active studies are grouped by enabled status, and sorted by date",
|
||||
);
|
||||
Assert.deepEqual(
|
||||
inactiveNames,
|
||||
[prefStudies[1].name, addonStudies[1].name],
|
||||
"Inactive studies are grouped by enabled status, and sorted by date",
|
||||
names,
|
||||
[cStudy3.name, cStudy1.name, cStudy2.name],
|
||||
"Studies are sorted first by enabled status, and then by descending start date."
|
||||
);
|
||||
|
||||
const activeAddonStudy = getStudyRow(doc, addonStudies[0].name);
|
||||
const study1Row = getStudyRow(doc, cStudy1.name);
|
||||
ok(
|
||||
activeAddonStudy.querySelector(".study-description").textContent.includes(addonStudies[0].description),
|
||||
study1Row.querySelector(".study-description").textContent.includes(cStudy1.description),
|
||||
"Study descriptions are shown in about:studies."
|
||||
);
|
||||
is(
|
||||
activeAddonStudy.querySelector(".study-status").textContent,
|
||||
study1Row.querySelector(".study-status").textContent,
|
||||
"Active",
|
||||
"Active studies show an 'Active' indicator."
|
||||
);
|
||||
ok(
|
||||
activeAddonStudy.querySelector(".remove-button"),
|
||||
study1Row.querySelector(".remove-button"),
|
||||
"Active studies show a remove button"
|
||||
);
|
||||
is(
|
||||
activeAddonStudy.querySelector(".study-icon").textContent.toLowerCase(),
|
||||
study1Row.querySelector(".study-icon").textContent.toLowerCase(),
|
||||
"a",
|
||||
"Study icons use the first letter of the study name."
|
||||
);
|
||||
|
||||
const inactiveAddonStudy = getStudyRow(doc, addonStudies[1].name);
|
||||
const study2Row = getStudyRow(doc, cStudy2.name);
|
||||
is(
|
||||
inactiveAddonStudy.querySelector(".study-status").textContent,
|
||||
study2Row.querySelector(".study-status").textContent,
|
||||
"Complete",
|
||||
"Inactive studies are marked as complete."
|
||||
);
|
||||
ok(
|
||||
!inactiveAddonStudy.querySelector(".remove-button"),
|
||||
!study2Row.querySelector(".remove-button"),
|
||||
"Inactive studies do not show a remove button"
|
||||
);
|
||||
|
||||
const activePrefStudy = getStudyRow(doc, prefStudies[0].name);
|
||||
ok(
|
||||
activePrefStudy.querySelector(".study-description").textContent.includes(prefStudies[0].preferenceName),
|
||||
"Preference studies show the preference they are changing"
|
||||
);
|
||||
is(
|
||||
activePrefStudy.querySelector(".study-status").textContent,
|
||||
"Active",
|
||||
"Active studies show an 'Active' indicator."
|
||||
);
|
||||
ok(
|
||||
activePrefStudy.querySelector(".remove-button"),
|
||||
"Active studies show a remove button"
|
||||
);
|
||||
is(
|
||||
activePrefStudy.querySelector(".study-icon").textContent.toLowerCase(),
|
||||
"d",
|
||||
"Study icons use the first letter of the study name."
|
||||
);
|
||||
|
||||
const inactivePrefStudy = getStudyRow(doc, prefStudies[1].name);
|
||||
is(
|
||||
inactivePrefStudy.querySelector(".study-status").textContent,
|
||||
"Complete",
|
||||
"Inactive studies are marked as complete."
|
||||
);
|
||||
ok(
|
||||
!inactivePrefStudy.querySelector(".remove-button"),
|
||||
"Inactive studies do not show a remove button"
|
||||
);
|
||||
|
||||
activeAddonStudy.querySelector(".remove-button").click();
|
||||
study1Row.querySelector(".remove-button").click();
|
||||
await ContentTaskUtils.waitForCondition(() => (
|
||||
getStudyRow(doc, addonStudies[0].name).matches(".study--disabled")
|
||||
getStudyRow(doc, cStudy1.name).matches(".disabled")
|
||||
));
|
||||
ok(
|
||||
getStudyRow(doc, addonStudies[0].name).matches(".study--disabled"),
|
||||
"Clicking the remove button updates the UI to show that the study has been disabled."
|
||||
);
|
||||
|
||||
activePrefStudy.querySelector(".remove-button").click();
|
||||
await ContentTaskUtils.waitForCondition(() => (
|
||||
getStudyRow(doc, prefStudies[0].name).matches(".study--disabled")
|
||||
));
|
||||
ok(
|
||||
getStudyRow(doc, prefStudies[0].name).matches(".study--disabled"),
|
||||
getStudyRow(doc, cStudy1.name).matches(".disabled"),
|
||||
"Clicking the remove button updates the UI to show that the study has been disabled."
|
||||
);
|
||||
});
|
||||
|
||||
const updatedAddonStudy = await AddonStudies.get(addonStudies[0].recipeId);
|
||||
const updatedStudy1 = await AddonStudies.get(study1.recipeId);
|
||||
ok(
|
||||
!updatedAddonStudy.active,
|
||||
"Clicking the remove button marks addon studies as inactive in storage."
|
||||
);
|
||||
|
||||
const updatedPrefStudy = await PreferenceExperiments.get(prefStudies[0].name);
|
||||
ok(
|
||||
updatedPrefStudy.expired,
|
||||
"Clicking the remove button marks preference studies as expired in storage."
|
||||
!updatedStudy1.active,
|
||||
"Clicking the remove button marks the study as inactive in storage."
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -236,7 +163,7 @@ decorate_task(
|
|||
async function testStudyListing(studies, browser) {
|
||||
await ContentTask.spawn(browser, null, async () => {
|
||||
const doc = content.document;
|
||||
await ContentTaskUtils.waitForCondition(() => doc.querySelectorAll(".study-list-info").length);
|
||||
await ContentTaskUtils.waitForCondition(() => doc.querySelectorAll(".study-list").length);
|
||||
const studyRows = doc.querySelectorAll(".study-list .study");
|
||||
is(studyRows.length, 0, "There should be no studies");
|
||||
is(
|
||||
|
@ -256,7 +183,7 @@ decorate_task(
|
|||
|
||||
await ContentTask.spawn(browser, null, async () => {
|
||||
const doc = content.document;
|
||||
await ContentTaskUtils.waitForCondition(() => doc.querySelector(".info-box-content > span"));
|
||||
await ContentTaskUtils.waitForCondition(() => !!doc.querySelector(".info-box-content > span"));
|
||||
|
||||
is(
|
||||
doc.querySelector(".info-box-content > span").textContent,
|
||||
|
@ -269,4 +196,4 @@ decorate_task(
|
|||
RecipeRunner.checkPrefs();
|
||||
}
|
||||
}
|
||||
).only();
|
||||
);
|
||||
|
|
|
@ -41,7 +41,7 @@ function ensureAddonCleanup(testFunction) {
|
|||
// Test that enroll is not called if recipe is already enrolled
|
||||
decorate_task(
|
||||
ensureAddonCleanup,
|
||||
AddonStudies.withStudies([addonStudyFactory()]),
|
||||
AddonStudies.withStudies([studyFactory()]),
|
||||
withSendEventStub,
|
||||
async function enrollTwiceFail([study], sendEventStub) {
|
||||
const recipe = recipeFactory({
|
||||
|
@ -176,7 +176,7 @@ decorate_task(
|
|||
decorate_task(
|
||||
ensureAddonCleanup,
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({active: false}),
|
||||
studyFactory({active: false}),
|
||||
]),
|
||||
withSendEventStub,
|
||||
async ([study], sendEventStub) => {
|
||||
|
@ -194,7 +194,7 @@ const testStopId = "testStop@example.com";
|
|||
decorate_task(
|
||||
ensureAddonCleanup,
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({active: true, addonId: testStopId, studyEndDate: null}),
|
||||
studyFactory({active: true, addonId: testStopId, studyEndDate: null}),
|
||||
]),
|
||||
withInstalledWebExtension({id: testStopId}, /* expectUninstall: */ true),
|
||||
withSendEventStub,
|
||||
|
@ -228,7 +228,7 @@ decorate_task(
|
|||
decorate_task(
|
||||
ensureAddonCleanup,
|
||||
AddonStudies.withStudies([
|
||||
addonStudyFactory({active: true, addonId: "missingAddon@example.com", studyEndDate: null}),
|
||||
studyFactory({active: true, addonId: "missingAddon@example.com", studyEndDate: null}),
|
||||
]),
|
||||
withSendEventStub,
|
||||
async function unenrollTest([study], sendEventStub) {
|
||||
|
@ -291,7 +291,7 @@ decorate_task(
|
|||
// Test that enroll is not called if recipe is already enrolled
|
||||
decorate_task(
|
||||
ensureAddonCleanup,
|
||||
AddonStudies.withStudies([addonStudyFactory()]),
|
||||
AddonStudies.withStudies([studyFactory()]),
|
||||
async function enrollTwiceFail([study]) {
|
||||
const action = new AddonStudyAction();
|
||||
const unenrollSpy = sinon.stub(action, "unenroll");
|
||||
|
|
|
@ -280,10 +280,10 @@ this.decorate_task = function(...args) {
|
|||
return add_task(decorate(...args));
|
||||
};
|
||||
|
||||
let _addonStudyFactoryId = 0;
|
||||
this.addonStudyFactory = function(attrs) {
|
||||
let _studyFactoryId = 0;
|
||||
this.studyFactory = function(attrs) {
|
||||
return Object.assign({
|
||||
recipeId: _addonStudyFactoryId++,
|
||||
recipeId: _studyFactoryId++,
|
||||
name: "Test study",
|
||||
description: "fake",
|
||||
active: true,
|
||||
|
@ -294,22 +294,6 @@ this.addonStudyFactory = function(attrs) {
|
|||
}, attrs);
|
||||
};
|
||||
|
||||
let _preferenceStudyFactoryId = 0;
|
||||
this.preferenceStudyFactory = function(attrs) {
|
||||
return Object.assign({
|
||||
name: "Test study",
|
||||
branch: "control",
|
||||
expired: false,
|
||||
lastSeen: new Date().toJSON(),
|
||||
preferenceName: "test.study",
|
||||
preferenceValue: false,
|
||||
preferenceType: "boolean",
|
||||
previousPreferenceValue: undefined,
|
||||
preferenceBranchType: "default",
|
||||
experimentType: "exp",
|
||||
}, attrs);
|
||||
};
|
||||
|
||||
this.withStub = function(...stubArgs) {
|
||||
return function wrapper(testFunction) {
|
||||
return async function wrappedTestFunction(...args) {
|
||||
|
|
|
@ -8,12 +8,9 @@
|
|||
title = Shield Studies
|
||||
removeButton = Remove
|
||||
|
||||
# LOCALIZATION NOTE (activeStudiesList): Title above a list of active studies
|
||||
activeStudiesList = Active studies
|
||||
# LOCALIZATION NOTE (activeStudiesList): Title above a list of completed studies
|
||||
completedStudiesList = Completed studies
|
||||
# LOCALIZATION NOTE (activeStatus): Displayed for an active study
|
||||
activeStatus = Active
|
||||
|
||||
# LOCALIZATION NOTE (completeStatus): Displayed for a study that is already complete
|
||||
completeStatus = Complete
|
||||
|
||||
|
@ -24,9 +21,3 @@ noStudies = You have not participated in any studies.
|
|||
disabledList = This is a list of studies that you have participated in. No new studies will run.
|
||||
# LOCALIZATION NOTE (enabledList): %S is brandShortName (e.g. Firefox)
|
||||
enabledList = What’s this? %S may install and run studies from time to time.
|
||||
|
||||
# LOCALIZATION NOTE (preferenceStudyDescription) $1%S will be replaced with the
|
||||
# name of a preference (such as "stream.improvesearch.topSiteSearchShortcuts")
|
||||
# and $2%S will be replaced with the value of that preference. Both values will
|
||||
# be formatted differently than the surrounding text.
|
||||
preferenceStudyDescription = This study sets %1$S to %2$S.
|
Загрузка…
Ссылка в новой задаче