summaryrefslogtreecommitdiffstats
path: root/toolkit/components/normandy/test/browser/browser_AddonStudies.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/normandy/test/browser/browser_AddonStudies.js')
-rw-r--r--toolkit/components/normandy/test/browser/browser_AddonStudies.js293
1 files changed, 293 insertions, 0 deletions
diff --git a/toolkit/components/normandy/test/browser/browser_AddonStudies.js b/toolkit/components/normandy/test/browser/browser_AddonStudies.js
new file mode 100644
index 0000000000..090ed92d59
--- /dev/null
+++ b/toolkit/components/normandy/test/browser/browser_AddonStudies.js
@@ -0,0 +1,293 @@
+"use strict";
+
+const { IndexedDB } = ChromeUtils.importESModule(
+ "resource://gre/modules/IndexedDB.sys.mjs"
+);
+
+const { NormandyTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/NormandyTestUtils.sys.mjs"
+);
+const { addonStudyFactory, branchedAddonStudyFactory } =
+ NormandyTestUtils.factories;
+
+// Initialize test utils
+AddonTestUtils.initMochitest(this);
+
+decorate_task(AddonStudies.withStudies(), async function testGetMissing() {
+ ok(
+ !(await AddonStudies.get("does-not-exist")),
+ "get returns null when the requested study does not exist"
+ );
+});
+
+decorate_task(
+ AddonStudies.withStudies([addonStudyFactory({ slug: "test-study" })]),
+ async function testGet({ addonStudies: [study] }) {
+ const storedStudy = await AddonStudies.get(study.recipeId);
+ Assert.deepEqual(study, storedStudy, "get retrieved a study from storage.");
+ }
+);
+
+decorate_task(
+ AddonStudies.withStudies([addonStudyFactory(), addonStudyFactory()]),
+ async function testGetAll({ addonStudies }) {
+ const storedStudies = await AddonStudies.getAll();
+ Assert.deepEqual(
+ new Set(storedStudies),
+ new Set(addonStudies),
+ "getAll returns every stored study."
+ );
+ }
+);
+
+decorate_task(
+ AddonStudies.withStudies([addonStudyFactory({ slug: "test-study" })]),
+ async function testHas({ addonStudies: [study] }) {
+ let hasStudy = await AddonStudies.has(study.recipeId);
+ ok(hasStudy, "has returns true for a study that exists in storage.");
+
+ hasStudy = await AddonStudies.has("does-not-exist");
+ ok(
+ !hasStudy,
+ "has returns false for a study that doesn't exist in storage."
+ );
+ }
+);
+
+decorate_task(
+ AddonStudies.withStudies([
+ addonStudyFactory({ slug: "test-study1" }),
+ addonStudyFactory({ slug: "test-study2" }),
+ ]),
+ async function testClear({ addonStudies: [study1, study2] }) {
+ const hasAll =
+ (await AddonStudies.has(study1.recipeId)) &&
+ (await AddonStudies.has(study2.recipeId));
+ ok(hasAll, "Before calling clear, both studies are in storage.");
+
+ await AddonStudies.clear();
+ const hasAny =
+ (await AddonStudies.has(study1.recipeId)) ||
+ (await AddonStudies.has(study2.recipeId));
+ ok(!hasAny, "After calling clear, all studies are removed from storage.");
+ }
+);
+
+decorate_task(
+ AddonStudies.withStudies([addonStudyFactory({ slug: "foo" })]),
+ async function testUpdate({ addonStudies: [study] }) {
+ Assert.deepEqual(await AddonStudies.get(study.recipeId), study);
+
+ const updatedStudy = {
+ ...study,
+ slug: "bar",
+ };
+ await AddonStudies.update(updatedStudy);
+
+ Assert.deepEqual(await AddonStudies.get(study.recipeId), updatedStudy);
+ }
+);
+
+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),
+ }),
+ ]),
+ withSendEventSpy(),
+ withInstalledWebExtension(
+ { id: "installed@example.com" },
+ { expectUninstall: true }
+ ),
+ async function testInit({
+ addonStudies: [activeUninstalledStudy, activeInstalledStudy, inactiveStudy],
+ sendEventSpy,
+ installedWebExtension: { addonId },
+ }) {
+ await AddonStudies.init();
+
+ const newActiveStudy = await AddonStudies.get(
+ activeUninstalledStudy.recipeId
+ );
+ ok(
+ !newActiveStudy.active,
+ "init marks studies as inactive if their add-on is not installed."
+ );
+ ok(
+ newActiveStudy.studyEndDate,
+ "init sets the study end date if a study's add-on is not installed."
+ );
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(e => e[1] == "normandy");
+ Assert.deepEqual(
+ events[0].slice(2), // strip timestamp and "normandy"
+ [
+ "unenroll",
+ "addon_study",
+ activeUninstalledStudy.slug,
+ {
+ addonId: activeUninstalledStudy.addonId,
+ addonVersion: activeUninstalledStudy.addonVersion,
+ reason: "uninstalled-sideload",
+ branch: AddonStudies.NO_BRANCHES_MARKER,
+ },
+ ],
+ "AddonStudies.init() should send the correct telemetry event"
+ );
+
+ const newInactiveStudy = await AddonStudies.get(inactiveStudy.recipeId);
+ is(
+ newInactiveStudy.studyEndDate.getFullYear(),
+ 2012,
+ "init does not modify inactive studies."
+ );
+
+ const newActiveInstalledStudy = await AddonStudies.get(
+ activeInstalledStudy.recipeId
+ );
+ Assert.deepEqual(
+ activeInstalledStudy,
+ newActiveInstalledStudy,
+ "init does not modify studies whose add-on is still installed."
+ );
+
+ // Only activeUninstalledStudy should have generated any events
+ ok(sendEventSpy.calledOnce, "no extra events should be generated");
+
+ // Clean up
+ const addon = await AddonManager.getAddonByID(addonId);
+ await addon.uninstall();
+ await TestUtils.topicObserved("shield-study-ended", (subject, message) => {
+ return message === `${activeInstalledStudy.recipeId}`;
+ });
+ }
+);
+
+// init should register telemetry experiments
+decorate_task(
+ AddonStudies.withStudies([
+ branchedAddonStudyFactory({
+ active: true,
+ addonId: "installed1@example.com",
+ }),
+ branchedAddonStudyFactory({
+ active: true,
+ addonId: "installed2@example.com",
+ }),
+ ]),
+ withInstalledWebExtensionSafe({ id: "installed1@example.com" }),
+ withInstalledWebExtension({ id: "installed2@example.com" }),
+ withStub(TelemetryEnvironment, "setExperimentActive"),
+ async function testInit({ addonStudies, setExperimentActiveStub }) {
+ await AddonStudies.init();
+ Assert.deepEqual(
+ setExperimentActiveStub.args,
+ [
+ [
+ addonStudies[0].slug,
+ addonStudies[0].branch,
+ {
+ type: "normandy-addonstudy",
+ },
+ ],
+ [
+ addonStudies[1].slug,
+ addonStudies[1].branch,
+ {
+ type: "normandy-addonstudy",
+ },
+ ],
+ ],
+ "Add-on studies are registered in Telemetry by AddonStudies.init"
+ );
+ }
+);
+
+// Test that AddonStudies.init() ends studies that have been uninstalled
+decorate_task(
+ AddonStudies.withStudies([
+ addonStudyFactory({
+ active: true,
+ addonId: "installed@example.com",
+ studyEndDate: null,
+ }),
+ ]),
+ withInstalledWebExtension(
+ { id: "installed@example.com" },
+ { expectUninstall: true }
+ ),
+ async function testInit({
+ addonStudies: [study],
+ installedWebExtension: { addonId },
+ }) {
+ const addon = await AddonManager.getAddonByID(addonId);
+ await addon.uninstall();
+ await TestUtils.topicObserved("shield-study-ended", (subject, message) => {
+ return message === `${study.recipeId}`;
+ });
+
+ const newStudy = await AddonStudies.get(study.recipeId);
+ ok(
+ !newStudy.active,
+ "Studies are marked as inactive when their add-on is uninstalled."
+ );
+ ok(
+ newStudy.studyEndDate,
+ "The study end date is set when the add-on for the study is uninstalled."
+ );
+ }
+);
+
+decorate_task(
+ AddonStudies.withStudies([
+ NormandyTestUtils.factories.addonStudyFactory({ active: true }),
+ NormandyTestUtils.factories.branchedAddonStudyFactory(),
+ ]),
+ async function testRemoveOldAddonStudies({
+ addonStudies: [noBranchStudy, branchedStudy],
+ }) {
+ // pre check, both studies are active
+ const preActiveIds = (await AddonStudies.getAllActive()).map(
+ addon => addon.recipeId
+ );
+ Assert.deepEqual(
+ preActiveIds,
+ [noBranchStudy.recipeId, branchedStudy.recipeId],
+ "Both studies should be active"
+ );
+
+ // run the migration
+ await AddonStudies.migrations.migration02RemoveOldAddonStudyAction();
+
+ // The unbrached study should end
+ const postActiveIds = (await AddonStudies.getAllActive()).map(
+ addon => addon.recipeId
+ );
+ Assert.deepEqual(
+ postActiveIds,
+ [branchedStudy.recipeId],
+ "The unbranched study should end"
+ );
+
+ // But both studies should still be present
+ const postAllIds = (await AddonStudies.getAll()).map(
+ addon => addon.recipeId
+ );
+ Assert.deepEqual(
+ postAllIds,
+ [noBranchStudy.recipeId, branchedStudy.recipeId],
+ "Both studies should still be present"
+ );
+ }
+);