зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1624728 - Messaging Experiments should show up in about:studies r=mythmon
Differential Revision: https://phabricator.services.mozilla.com/D81711
This commit is contained in:
Родитель
402b0a5c46
Коммит
85c0304595
|
@ -28,4 +28,8 @@ export interface Enrollment {
|
|||
active: boolean;
|
||||
experimentType: string;
|
||||
source: string;
|
||||
// Shown in about:studies
|
||||
userFacingName: string;
|
||||
userFacingDescription: string;
|
||||
lastSeen: string;
|
||||
}
|
||||
|
|
|
@ -152,7 +152,13 @@ class _ExperimentManager {
|
|||
* @memberof _ExperimentManager
|
||||
*/
|
||||
async enroll(
|
||||
{ slug, branches, experimentType = DEFAULT_EXPERIMENT_TYPE },
|
||||
{
|
||||
slug,
|
||||
branches,
|
||||
experimentType = DEFAULT_EXPERIMENT_TYPE,
|
||||
userFacingName,
|
||||
userFacingDescription,
|
||||
},
|
||||
source
|
||||
) {
|
||||
if (this.store.has(slug)) {
|
||||
|
@ -179,6 +185,9 @@ class _ExperimentManager {
|
|||
enrollmentId,
|
||||
experimentType,
|
||||
source,
|
||||
userFacingName,
|
||||
userFacingDescription,
|
||||
lastSeen: new Date().toJSON(),
|
||||
};
|
||||
|
||||
this.store.addExperiment(experiment);
|
||||
|
@ -246,7 +255,7 @@ class _ExperimentManager {
|
|||
experiment.enrollmentId || TelemetryEvents.NO_ENROLLMENT_ID_MARKER,
|
||||
});
|
||||
|
||||
log.debug(`Experiment unenrolled: ${slug}}`);
|
||||
log.debug(`Experiment unenrolled: ${slug}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -95,6 +95,21 @@ class SharedDataMap extends EventEmitter {
|
|||
this._notifyUpdate();
|
||||
}
|
||||
|
||||
// Only used in tests
|
||||
_deleteForTests(key) {
|
||||
if (!this.isParent) {
|
||||
throw new Error(
|
||||
"Setting values from within a content process is not allowed"
|
||||
);
|
||||
}
|
||||
if (this.has(key)) {
|
||||
delete this._store.data[key];
|
||||
this._store.saveSoon();
|
||||
this._syncToChildren();
|
||||
this._notifyUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return Boolean(this._data[key]);
|
||||
}
|
||||
|
|
|
@ -63,6 +63,8 @@ const ExperimentFakes = {
|
|||
{ slug: "control", value: null },
|
||||
{ slug: "treatment", value: { title: "hello" } },
|
||||
],
|
||||
userFacingName: "Messaging System recipe",
|
||||
userFacingDescription: "Messaging System MSTestUtils recipe",
|
||||
...props,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -28,6 +28,11 @@ ChromeUtils.defineModuleGetter(
|
|||
"RecipeRunner",
|
||||
"resource://normandy/lib/RecipeRunner.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"ExperimentManager",
|
||||
"resource://messaging-system/experiments/ExperimentManager.jsm"
|
||||
);
|
||||
|
||||
var EXPORTED_SYMBOLS = ["AboutPages"];
|
||||
|
||||
|
@ -113,6 +118,10 @@ XPCOMUtils.defineLazyGetter(AboutPages, "aboutStudies", () => {
|
|||
return PreferenceExperiments.getAll();
|
||||
},
|
||||
|
||||
getMessagingSystemList() {
|
||||
return ExperimentManager.store.getAll();
|
||||
},
|
||||
|
||||
/** Add a browsing context to the weak set;
|
||||
* this weak set keeps track of all contexts
|
||||
* that are housing an about:studies page.
|
||||
|
@ -200,6 +209,14 @@ XPCOMUtils.defineLazyGetter(AboutPages, "aboutStudies", () => {
|
|||
}
|
||||
},
|
||||
|
||||
async removeMessagingSystemExperiment(slug, reason) {
|
||||
ExperimentManager.unenroll(slug, reason);
|
||||
this._sendToAll(
|
||||
"Shield:UpdateMessagingSystemExperimentList",
|
||||
ExperimentManager.store.getAll()
|
||||
);
|
||||
},
|
||||
|
||||
openDataPreferences() {
|
||||
const browserWindow = Services.wm.getMostRecentWindow(
|
||||
"navigator:browser"
|
||||
|
|
|
@ -71,6 +71,13 @@ class ShieldFrameChild extends JSWindowActorChild {
|
|||
prefStudies
|
||||
);
|
||||
break;
|
||||
case "GetRemoteValue:MessagingSystemList":
|
||||
let experiments = await this.sendQuery("Shield:GetMessagingSystemList");
|
||||
this.triggerPageCallback(
|
||||
"ReceiveRemoteValue:MessagingSystemList",
|
||||
experiments
|
||||
);
|
||||
break;
|
||||
case "RemoveAddonStudy":
|
||||
this.sendAsyncMessage("Shield:RemoveAddonStudy", event.detail.data);
|
||||
break;
|
||||
|
@ -80,6 +87,12 @@ class ShieldFrameChild extends JSWindowActorChild {
|
|||
event.detail.data
|
||||
);
|
||||
break;
|
||||
case "RemoveMessagingSystemExperiment":
|
||||
this.sendAsyncMessage(
|
||||
"Shield:RemoveMessagingSystemExperiment",
|
||||
event.detail.data
|
||||
);
|
||||
break;
|
||||
case "GetRemoteValue:StudiesEnabled":
|
||||
let studiesEnabled = await this.sendQuery("Shield:GetStudiesEnabled");
|
||||
this.triggerPageCallback(
|
||||
|
@ -127,6 +140,12 @@ class ShieldFrameChild extends JSWindowActorChild {
|
|||
msg.data
|
||||
);
|
||||
break;
|
||||
case "Shield:UpdateMessagingSystemExperimentList":
|
||||
this.triggerPageCallback(
|
||||
"ReceiveRemoteValue:MessagingSystemList",
|
||||
msg.data
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,8 @@ class ShieldFrameParent extends JSWindowActorParent {
|
|||
return aboutStudies.getAddonStudyList();
|
||||
case "Shield:GetPreferenceStudyList":
|
||||
return aboutStudies.getPreferenceStudyList();
|
||||
case "Shield:GetMessagingSystemList":
|
||||
return aboutStudies.getMessagingSystemList();
|
||||
case "Shield:RemoveAddonStudy":
|
||||
aboutStudies.removeAddonStudy(msg.data.recipeId, msg.data.reason);
|
||||
break;
|
||||
|
@ -34,6 +36,12 @@ class ShieldFrameParent extends JSWindowActorParent {
|
|||
msg.data.reason
|
||||
);
|
||||
break;
|
||||
case "Shield:RemoveMessagingSystemExperiment":
|
||||
aboutStudies.removeMessagingSystemExperiment(
|
||||
msg.data.slug,
|
||||
msg.data.reason
|
||||
);
|
||||
break;
|
||||
case "Shield:OpenDataPreferences":
|
||||
aboutStudies.openDataPreferences();
|
||||
break;
|
||||
|
|
|
@ -33,6 +33,7 @@ class AboutStudies extends React.Component {
|
|||
this.remoteValueNameMap = {
|
||||
AddonStudyList: "addonStudies",
|
||||
PreferenceStudyList: "prefStudies",
|
||||
MessagingSystemList: "experiments",
|
||||
ShieldLearnMoreHref: "learnMoreHref",
|
||||
StudiesEnabled: "studiesEnabled",
|
||||
ShieldTranslations: "translations",
|
||||
|
@ -73,6 +74,7 @@ class AboutStudies extends React.Component {
|
|||
studiesEnabled,
|
||||
addonStudies,
|
||||
prefStudies,
|
||||
experiments,
|
||||
} = this.state;
|
||||
|
||||
// Wait for all values to be loaded before rendering. Some of the values may
|
||||
|
@ -85,7 +87,7 @@ class AboutStudies extends React.Component {
|
|||
"div",
|
||||
{ className: "about-studies-container main-content" },
|
||||
r(WhatsThisBox, { translations, learnMoreHref, studiesEnabled }),
|
||||
r(StudyList, { translations, addonStudies, prefStudies })
|
||||
r(StudyList, { translations, addonStudies, prefStudies, experiments })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -142,9 +144,9 @@ class WhatsThisBox extends React.Component {
|
|||
*/
|
||||
class StudyList extends React.Component {
|
||||
render() {
|
||||
const { addonStudies, prefStudies, translations } = this.props;
|
||||
const { addonStudies, prefStudies, translations, experiments } = this.props;
|
||||
|
||||
if (!addonStudies.length && !prefStudies.length) {
|
||||
if (!addonStudies.length && !prefStudies.length && !experiments.length) {
|
||||
return r("p", { className: "study-list-info" }, translations.noStudies);
|
||||
}
|
||||
|
||||
|
@ -176,6 +178,18 @@ class StudyList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
for (const study of experiments) {
|
||||
const clonedStudy = Object.assign({}, study, {
|
||||
type: study.experimentType,
|
||||
sortDate: new Date(study.lastSeen),
|
||||
});
|
||||
if (!study.active) {
|
||||
inactiveStudies.push(clonedStudy);
|
||||
} else {
|
||||
activeStudies.push(clonedStudy);
|
||||
}
|
||||
}
|
||||
|
||||
activeStudies.sort((a, b) => b.sortDate - a.sortDate);
|
||||
inactiveStudies.sort((a, b) => b.sortDate - a.sortDate);
|
||||
|
||||
|
@ -186,29 +200,53 @@ class StudyList extends React.Component {
|
|||
r(
|
||||
"ul",
|
||||
{ className: "study-list active-study-list" },
|
||||
activeStudies.map(study =>
|
||||
study.type === "addon"
|
||||
? r(AddonStudyListItem, { key: study.slug, study, translations })
|
||||
: r(PreferenceStudyListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
})
|
||||
)
|
||||
activeStudies.map(study => {
|
||||
if (study.type === "addon") {
|
||||
return r(AddonStudyListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
});
|
||||
}
|
||||
if (study.type === "messaging_experiment") {
|
||||
return r(MessagingSystemListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
});
|
||||
}
|
||||
return r(PreferenceStudyListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
});
|
||||
})
|
||||
),
|
||||
r("h2", {}, translations.completedStudiesList),
|
||||
r(
|
||||
"ul",
|
||||
{ className: "study-list inactive-study-list" },
|
||||
inactiveStudies.map(study =>
|
||||
study.type === "addon"
|
||||
? r(AddonStudyListItem, { key: study.slug, study, translations })
|
||||
: r(PreferenceStudyListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
})
|
||||
)
|
||||
inactiveStudies.map(study => {
|
||||
if (study.type === "addon") {
|
||||
return r(AddonStudyListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
});
|
||||
}
|
||||
if (study.experimentType === "messaging_experiment") {
|
||||
return r(MessagingSystemListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
});
|
||||
}
|
||||
return r(PreferenceStudyListItem, {
|
||||
key: study.slug,
|
||||
study,
|
||||
translations,
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -218,6 +256,66 @@ StudyList.propTypes = {
|
|||
translations: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class MessagingSystemListItem extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickRemove = this.handleClickRemove.bind(this);
|
||||
}
|
||||
|
||||
handleClickRemove() {
|
||||
sendPageEvent("RemoveMessagingSystemExperiment", {
|
||||
slug: this.props.study.slug,
|
||||
reason: "individual-opt-out",
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { study, translations } = this.props;
|
||||
return r(
|
||||
"li",
|
||||
{
|
||||
className: classnames("study messaging-system", {
|
||||
disabled: !study.active,
|
||||
}),
|
||||
"data-study-slug": study.slug, // used to identify this row in tests
|
||||
},
|
||||
r("div", { className: "study-icon" }, study.userFacingName.slice(0, 1)),
|
||||
r(
|
||||
"div",
|
||||
{ className: "study-details" },
|
||||
r(
|
||||
"div",
|
||||
{ className: "study-header" },
|
||||
r("span", { className: "study-name" }, study.userFacingName),
|
||||
r("span", {}, "\u2022"), // •
|
||||
r(
|
||||
"span",
|
||||
{ className: "study-status" },
|
||||
study.active
|
||||
? translations.activeStatus
|
||||
: translations.completeStatus
|
||||
)
|
||||
),
|
||||
r(
|
||||
"div",
|
||||
{ className: "study-description" },
|
||||
study.userFacingDescription
|
||||
)
|
||||
),
|
||||
r(
|
||||
"div",
|
||||
{ className: "study-actions" },
|
||||
study.active &&
|
||||
r(
|
||||
"button",
|
||||
{ className: "remove-button", onClick: this.handleClickRemove },
|
||||
r("div", { className: "button-box" }, translations.removeButton)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Details about an individual add-on study, with an option to end it if it is active.
|
||||
*/
|
||||
|
|
|
@ -4,6 +4,12 @@ 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);
|
||||
const { ExperimentFakes } = ChromeUtils.import(
|
||||
"resource://testing-common/MSTestUtils.jsm"
|
||||
);
|
||||
const { ExperimentManager } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentManager.jsm"
|
||||
);
|
||||
|
||||
const { NormandyTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/NormandyTestUtils.jsm"
|
||||
|
@ -620,3 +626,49 @@ decorate_task(
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_messaging_system_about_studies() {
|
||||
const recipe = ExperimentFakes.recipe("about-studies-foo");
|
||||
await ExperimentManager.enroll(recipe);
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{ gBrowser, url: "about:studies" },
|
||||
async browser => {
|
||||
const name = await SpecialPowers.spawn(browser, [], async () => {
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() =>
|
||||
content.document.querySelector(".messaging-system .remove-button"),
|
||||
"waiting for page/experiment to load"
|
||||
);
|
||||
return content.document.querySelector(".study-name").innerText;
|
||||
});
|
||||
// Make sure strings are properly shown
|
||||
Assert.equal(
|
||||
name,
|
||||
recipe.userFacingName,
|
||||
"Correct active experiment name"
|
||||
);
|
||||
}
|
||||
);
|
||||
ExperimentManager.unenroll(recipe.slug);
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{ gBrowser, url: "about:studies" },
|
||||
async browser => {
|
||||
const name = await SpecialPowers.spawn(browser, [], async () => {
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => content.document.querySelector(".messaging-system.disabled"),
|
||||
"waiting for experiment to become disabled"
|
||||
);
|
||||
return content.document.querySelector(".study-name").innerText;
|
||||
});
|
||||
// Make sure strings are properly shown
|
||||
Assert.equal(
|
||||
name,
|
||||
recipe.userFacingName,
|
||||
"Correct disabled experiment name"
|
||||
);
|
||||
}
|
||||
);
|
||||
// Cleanup for multiple test runs
|
||||
ExperimentManager.store._deleteForTests(recipe.slug);
|
||||
Assert.equal(ExperimentManager.store.getAll().length, 0, "Cleanup done");
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче