summaryrefslogtreecommitdiffstats
path: root/browser/components/asrouter/tests/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/asrouter/tests/xpcshell')
-rw-r--r--browser/components/asrouter/tests/xpcshell/head.js98
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_attribution.js94
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_snapshot.js172
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_ASRouter_getTargetingParameters.js73
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_CFRMessageProvider.js32
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_InflightAssetsMessageProvider.js41
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_NimbusRolloutMessageProvider.js41
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_OnboardingMessageProvider.js229
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js84
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_reach_experiments.js97
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_remoteExperiments.js37
-rw-r--r--browser/components/asrouter/tests/xpcshell/xpcshell.toml24
12 files changed, 1022 insertions, 0 deletions
diff --git a/browser/components/asrouter/tests/xpcshell/head.js b/browser/components/asrouter/tests/xpcshell/head.js
new file mode 100644
index 0000000000..0c6cec1ac8
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/head.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
+});
+
+function assertValidates(validator, obj, msg) {
+ const result = validator.validate(obj);
+ Assert.ok(
+ result.valid && result.errors.length === 0,
+ `${msg} - errors = ${JSON.stringify(result.errors, undefined, 2)}`
+ );
+}
+
+async function fetchSchema(uri) {
+ try {
+ dump(`URI: ${uri}\n`);
+ return fetch(uri, { credentials: "omit" }).then(rsp => rsp.json());
+ } catch (e) {
+ throw new Error(`Could not fetch ${uri}`);
+ }
+}
+
+async function schemaValidatorFor(uri, { common = false } = {}) {
+ const schema = await fetchSchema(uri);
+ const validator = new lazy.JsonSchema.Validator(schema);
+
+ if (common) {
+ const commonSchema = await fetchSchema(
+ "resource://testing-common/FxMSCommon.schema.json"
+ );
+ validator.addSchema(commonSchema);
+ }
+
+ return validator;
+}
+
+async function makeValidators() {
+ const experimentValidator = await schemaValidatorFor(
+ "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json"
+ );
+
+ const messageValidators = {
+ cfr_doorhanger: await schemaValidatorFor(
+ "resource://testing-common/ExtensionDoorhanger.schema.json",
+ { common: true }
+ ),
+ cfr_urlbar_chiclet: await schemaValidatorFor(
+ "resource://testing-common/CFRUrlbarChiclet.schema.json",
+ { common: true }
+ ),
+ infobar: await schemaValidatorFor(
+ "resource://testing-common/InfoBar.schema.json",
+ { common: true }
+ ),
+ pb_newtab: await schemaValidatorFor(
+ "resource://testing-common/NewtabPromoMessage.schema.json",
+ { common: true }
+ ),
+ spotlight: await schemaValidatorFor(
+ "resource://testing-common/Spotlight.schema.json",
+ { common: true }
+ ),
+ toast_notification: await schemaValidatorFor(
+ "resource://testing-common/ToastNotification.schema.json",
+ { common: true }
+ ),
+ toolbar_badge: await schemaValidatorFor(
+ "resource://testing-common/ToolbarBadgeMessage.schema.json",
+ { common: true }
+ ),
+ update_action: await schemaValidatorFor(
+ "resource://testing-common/UpdateAction.schema.json",
+ { common: true }
+ ),
+ whatsnew_panel_message: await schemaValidatorFor(
+ "resource://testing-common/WhatsNewMessage.schema.json",
+ { common: true }
+ ),
+ feature_callout: await schemaValidatorFor(
+ // For now, Feature Callout and Spotlight share a common schema
+ "resource://testing-common/Spotlight.schema.json",
+ { common: true }
+ ),
+ };
+
+ messageValidators.milestone_message = messageValidators.cfr_doorhanger;
+
+ return { experimentValidator, messageValidators };
+}
diff --git a/browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_attribution.js b/browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_attribution.js
new file mode 100644
index 0000000000..a37cb6c793
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_attribution.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AttributionCode } = ChromeUtils.importESModule(
+ "resource:///modules/AttributionCode.sys.mjs"
+);
+const { ASRouterTargeting } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ASRouterTargeting.sys.mjs"
+);
+const { MacAttribution } = ChromeUtils.importESModule(
+ "resource:///modules/MacAttribution.sys.mjs"
+);
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+add_task(async function check_attribution_data() {
+ // Some setup to fake the correct attribution data
+ const campaign = "non-fx-button";
+ const source = "addons.mozilla.org";
+ const attrStr = `campaign%3D${campaign}%26source%3D${source}`;
+ await MacAttribution.setAttributionString(attrStr);
+ AttributionCode._clearCache();
+ await AttributionCode.getAttrDataAsync();
+
+ const { campaign: attributionCampain, source: attributionSource } =
+ ASRouterTargeting.Environment.attributionData;
+ equal(
+ attributionCampain,
+ campaign,
+ "should get the correct campaign out of attributionData"
+ );
+ equal(
+ attributionSource,
+ source,
+ "should get the correct source out of attributionData"
+ );
+
+ const messages = [
+ {
+ id: "foo1",
+ targeting:
+ "attributionData.campaign == 'back_to_school' && attributionData.source == 'addons.mozilla.org'",
+ },
+ {
+ id: "foo2",
+ targeting:
+ "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
+ },
+ ];
+
+ equal(
+ await ASRouterTargeting.findMatchingMessage({ messages }),
+ messages[1],
+ "should select the message with the correct campaign and source"
+ );
+ AttributionCode._clearCache();
+});
+
+add_task(async function check_enterprise_targeting() {
+ const messages = [
+ {
+ id: "foo1",
+ targeting: "hasActiveEnterprisePolicies",
+ },
+ {
+ id: "foo2",
+ targeting: "!hasActiveEnterprisePolicies",
+ },
+ ];
+
+ equal(
+ await ASRouterTargeting.findMatchingMessage({ messages }),
+ messages[1],
+ "should select the message for policies turned off"
+ );
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ DisableFirefoxStudies: {
+ Value: true,
+ },
+ },
+ });
+
+ equal(
+ await ASRouterTargeting.findMatchingMessage({ messages }),
+ messages[0],
+ "should select the message for policies turned on"
+ );
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_snapshot.js b/browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_snapshot.js
new file mode 100644
index 0000000000..74171ba1b9
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_snapshot.js
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { ASRouterTargeting } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ASRouterTargeting.sys.mjs"
+);
+
+add_task(async function should_ignore_rejections() {
+ let target = {
+ get foo() {
+ return new Promise(resolve => resolve(1));
+ },
+
+ get bar() {
+ return new Promise((resolve, reject) => reject(new Error("unspecified")));
+ },
+ };
+
+ let snapshot = await ASRouterTargeting.getEnvironmentSnapshot({
+ targets: [target],
+ });
+ Assert.deepEqual(snapshot, { environment: { foo: 1 }, version: 1 });
+});
+
+add_task(async function nested_objects() {
+ const target = {
+ get foo() {
+ return Promise.resolve("foo");
+ },
+ get bar() {
+ return Promise.reject(new Error("bar"));
+ },
+ baz: {
+ get qux() {
+ return Promise.resolve("qux");
+ },
+ get quux() {
+ return Promise.reject(new Error("quux"));
+ },
+ get corge() {
+ return {
+ get grault() {
+ return Promise.resolve("grault");
+ },
+ get garply() {
+ return Promise.reject(new Error("garply"));
+ },
+ };
+ },
+ },
+ };
+
+ const snapshot = await ASRouterTargeting.getEnvironmentSnapshot({
+ targets: [target],
+ });
+ Assert.deepEqual(
+ snapshot,
+ {
+ environment: {
+ foo: "foo",
+ baz: {
+ qux: "qux",
+ corge: {
+ grault: "grault",
+ },
+ },
+ },
+ version: 1,
+ },
+ "getEnvironmentSnapshot should resolve nested promises"
+ );
+});
+
+add_task(async function arrays() {
+ const target = {
+ foo: [1, 2, 3],
+ bar: [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
+ baz: Promise.resolve([1, 2, 3]),
+ qux: Promise.resolve([
+ Promise.resolve(1),
+ Promise.resolve(2),
+ Promise.resolve(3),
+ ]),
+ quux: Promise.resolve({
+ corge: [Promise.resolve(1), 2, 3],
+ }),
+ };
+
+ const snapshot = await ASRouterTargeting.getEnvironmentSnapshot({
+ targets: [target],
+ });
+ Assert.deepEqual(
+ snapshot,
+ {
+ environment: {
+ foo: [1, 2, 3],
+ bar: [1, 2, 3],
+ baz: [1, 2, 3],
+ qux: [1, 2, 3],
+ quux: { corge: [1, 2, 3] },
+ },
+ version: 1,
+ },
+ "getEnvironmentSnapshot should resolve arrays correctly"
+ );
+});
+
+add_task(async function target_order() {
+ let target1 = {
+ foo: 1,
+ bar: 1,
+ baz: 1,
+ };
+
+ let target2 = {
+ foo: 2,
+ bar: 2,
+ };
+
+ let target3 = {
+ foo: 3,
+ };
+
+ // target3 supercedes target2; both supercede target1.
+ let snapshot = await ASRouterTargeting.getEnvironmentSnapshot({
+ targets: [target3, target2, target1],
+ });
+ Assert.deepEqual(snapshot, {
+ environment: { foo: 3, bar: 2, baz: 1 },
+ version: 1,
+ });
+});
+
+/*
+ * NB: This test is last because it manipulates shutdown phases.
+ *
+ * Adding tests after this one will result in failures.
+ */
+add_task(async function should_ignore_rejections() {
+ // The order that `ASRouterTargeting.getEnvironmentSnapshot`
+ // enumerates the target object matters here, but it's guaranteed to
+ // be consistent by the `for ... in` ordering: see
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#description.
+ let target = {
+ get foo() {
+ return new Promise(resolve => resolve(1));
+ },
+
+ get bar() {
+ return new Promise(resolve => {
+ // Pretend that we're about to shut down.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN
+ );
+ resolve(2);
+ });
+ },
+
+ get baz() {
+ return new Promise(resolve => resolve(3));
+ },
+ };
+
+ let snapshot = await ASRouterTargeting.getEnvironmentSnapshot({
+ targets: [target],
+ });
+ // `baz` is dropped since we're shutting down by the time it's processed.
+ Assert.deepEqual(snapshot, { environment: { foo: 1, bar: 2 }, version: 1 });
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_ASRouter_getTargetingParameters.js b/browser/components/asrouter/tests/xpcshell/test_ASRouter_getTargetingParameters.js
new file mode 100644
index 0000000000..bda6d0cd41
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_ASRouter_getTargetingParameters.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { ASRouter } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ASRouter.sys.mjs"
+);
+
+add_task(async function nested_objects() {
+ const target = {
+ get foo() {
+ return Promise.resolve("foo");
+ },
+ baz: {
+ get qux() {
+ return Promise.resolve("qux");
+ },
+ get corge() {
+ return {
+ get grault() {
+ return Promise.resolve("grault");
+ },
+ };
+ },
+ },
+ };
+
+ const params = await ASRouter.getTargetingParameters(target);
+ Assert.deepEqual(
+ params,
+ {
+ foo: "foo",
+ baz: {
+ qux: "qux",
+ corge: {
+ grault: "grault",
+ },
+ },
+ },
+ "getTargetingParameters should resolve nested promises"
+ );
+});
+
+add_task(async function arrays() {
+ const target = {
+ foo: [1, 2, 3],
+ bar: [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
+ baz: Promise.resolve([1, 2, 3]),
+ qux: Promise.resolve([
+ Promise.resolve(1),
+ Promise.resolve(2),
+ Promise.resolve(3),
+ ]),
+ quux: Promise.resolve({
+ corge: [Promise.resolve(1), 2, 3],
+ }),
+ };
+
+ const params = await ASRouter.getTargetingParameters(target);
+ Assert.deepEqual(
+ params,
+ {
+ foo: [1, 2, 3],
+ bar: [1, 2, 3],
+ baz: [1, 2, 3],
+ qux: [1, 2, 3],
+ quux: { corge: [1, 2, 3] },
+ },
+ "getEnvironmentSnapshot should resolve arrays correctly"
+ );
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_CFRMessageProvider.js b/browser/components/asrouter/tests/xpcshell/test_CFRMessageProvider.js
new file mode 100644
index 0000000000..3354013067
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_CFRMessageProvider.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CFRMessageProvider } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/CFRMessageProvider.sys.mjs"
+);
+
+add_task(async function test_cfrMessages() {
+ const { experimentValidator, messageValidators } = await makeValidators();
+
+ const messages = await CFRMessageProvider.getMessages();
+ for (const message of messages) {
+ const validator = messageValidators[message.template];
+ Assert.ok(
+ typeof validator !== "undefined",
+ typeof validator !== "undefined"
+ ? `Schema validator found for ${message.template}.`
+ : `No schema validator found for template ${message.template}. Please update this test to add one.`
+ );
+
+ assertValidates(
+ validator,
+ message,
+ `Message ${message.id} validates as template ${message.template}`
+ );
+ assertValidates(
+ experimentValidator,
+ message,
+ `Message ${message.id} validates as MessagingExperiment`
+ );
+ }
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_InflightAssetsMessageProvider.js b/browser/components/asrouter/tests/xpcshell/test_InflightAssetsMessageProvider.js
new file mode 100644
index 0000000000..fce99362c7
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_InflightAssetsMessageProvider.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { InflightAssetsMessageProvider } = ChromeUtils.importESModule(
+ "resource://testing-common/InflightAssetsMessageProvider.sys.mjs"
+);
+
+const MESSAGE_VALIDATORS = {};
+let EXPERIMENT_VALIDATOR;
+
+add_setup(async function setup() {
+ const validators = await makeValidators();
+
+ EXPERIMENT_VALIDATOR = validators.experimentValidator;
+ Object.assign(MESSAGE_VALIDATORS, validators.messageValidators);
+});
+
+add_task(function test_InflightAssetsMessageProvider() {
+ const messages = InflightAssetsMessageProvider.getMessages();
+
+ for (const message of messages) {
+ const validator = MESSAGE_VALIDATORS[message.template];
+ Assert.ok(
+ typeof validator !== "undefined",
+ typeof validator !== "undefined"
+ ? `Schema validator found for ${message.template}`
+ : `No schema validator found for template ${message.template}. Please update this test to add one.`
+ );
+
+ assertValidates(
+ validator,
+ message,
+ `Message ${message.id} validates as ${message.template} template`
+ );
+ assertValidates(
+ EXPERIMENT_VALIDATOR,
+ message,
+ `Message ${message.id} validates as a MessagingExperiment`
+ );
+ }
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_NimbusRolloutMessageProvider.js b/browser/components/asrouter/tests/xpcshell/test_NimbusRolloutMessageProvider.js
new file mode 100644
index 0000000000..2fe01e2fed
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_NimbusRolloutMessageProvider.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { NimbusRolloutMessageProvider } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusRolloutMessageProvider.sys.mjs"
+);
+
+const MESSAGE_VALIDATORS = {};
+let EXPERIMENT_VALIDATOR;
+
+add_setup(async function setup() {
+ const validators = await makeValidators();
+
+ EXPERIMENT_VALIDATOR = validators.experimentValidator;
+ Object.assign(MESSAGE_VALIDATORS, validators.messageValidators);
+});
+
+add_task(function test_NimbusRolloutMessageProvider() {
+ const messages = NimbusRolloutMessageProvider.getMessages();
+
+ for (const message of messages) {
+ const validator = MESSAGE_VALIDATORS[message.template];
+ Assert.ok(
+ typeof validator !== "undefined",
+ typeof validator !== "undefined"
+ ? `Schema validator found for ${message.template}`
+ : `No schema validator found for template ${message.template}. Please update this test to add one.`
+ );
+
+ assertValidates(
+ validator,
+ message,
+ `Message ${message.id} validates as ${message.template} template`
+ );
+ assertValidates(
+ EXPERIMENT_VALIDATOR,
+ message,
+ `Message ${message.id} validates as a MessagingExperiment`
+ );
+ }
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_OnboardingMessageProvider.js b/browser/components/asrouter/tests/xpcshell/test_OnboardingMessageProvider.js
new file mode 100644
index 0000000000..7ea7c97a03
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_OnboardingMessageProvider.js
@@ -0,0 +1,229 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { OnboardingMessageProvider } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+function getOnboardingScreenById(screens, screenId) {
+ return screens.find(screen => {
+ return screen?.id === screenId;
+ });
+}
+
+add_task(
+ async function test_OnboardingMessageProvider_getUpgradeMessage_no_pin() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin").resolves(true);
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+ // If Firefox is not pinned, the screen should have "pin" content
+ equal(
+ message.content.screens[0].id,
+ "UPGRADE_PIN_FIREFOX",
+ "Screen has pin screen id"
+ );
+ equal(
+ message.content.screens[0].content.primary_button.action.type,
+ "PIN_FIREFOX_TO_TASKBAR",
+ "Primary button has pin action type"
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_OnboardingMessageProvider_getUpgradeMessage_pin_no_default() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin").resolves(false);
+ sandbox
+ .stub(OnboardingMessageProvider, "_doesAppNeedDefault")
+ .resolves(true);
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+ // If Firefox is pinned, but not the default, the screen should have "make default" content
+ equal(
+ message.content.screens[0].id,
+ "UPGRADE_ONLY_DEFAULT",
+ "Screen has make default screen id"
+ );
+ equal(
+ message.content.screens[0].content.primary_button.action.type,
+ "SET_DEFAULT_BROWSER",
+ "Primary button has make default action"
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_OnboardingMessageProvider_getUpgradeMessage_pin_and_default() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin").resolves(false);
+ sandbox
+ .stub(OnboardingMessageProvider, "_doesAppNeedDefault")
+ .resolves(false);
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+ // If Firefox is pinned and the default, the screen should have "get started" content
+ equal(
+ message.content.screens[0].id,
+ "UPGRADE_GET_STARTED",
+ "Screen has get started screen id"
+ );
+ ok(
+ !message.content.screens[0].content.primary_button.action.type,
+ "Primary button has no action type"
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_OnboardingMessageProvider_getNoImport_default() {
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(OnboardingMessageProvider, "_doesAppNeedDefault")
+ .resolves(false);
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+
+ // No import screen is shown when user has Firefox both pinned and default
+ Assert.notEqual(
+ message.content.screens[1]?.id,
+ "UPGRADE_IMPORT_SETTINGS_EMBEDDED",
+ "Screen has no import screen id"
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_OnboardingMessageProvider_getImport_nodefault() {
+ Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser");
+ });
+
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(OnboardingMessageProvider, "_doesAppNeedDefault").resolves(true);
+ sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin").resolves(false);
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+
+ // Import screen is shown when user doesn't have Firefox pinned and default
+ Assert.equal(
+ message.content.screens[1]?.id,
+ "UPGRADE_IMPORT_SETTINGS_EMBEDDED",
+ "Screen has import screen id"
+ );
+ sandbox.restore();
+});
+
+add_task(
+ async function test_OnboardingMessageProvider_getPinPrivateWindow_noPrivatePin() {
+ Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser");
+ });
+ let sandbox = sinon.createSandbox();
+ // User needs default to ensure Pin Private window shows as third screen after import
+ sandbox
+ .stub(OnboardingMessageProvider, "_doesAppNeedDefault")
+ .resolves(true);
+
+ let pinStub = sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin");
+ pinStub.resolves(false);
+ pinStub.withArgs(true).resolves(true);
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+
+ // Pin Private screen is shown when user doesn't have Firefox private pinned but has Firefox pinned
+ Assert.ok(
+ getOnboardingScreenById(
+ message.content.screens,
+ "UPGRADE_PIN_PRIVATE_WINDOW"
+ )
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_OnboardingMessageProvider_getNoPinPrivateWindow_noPin() {
+ Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser");
+ });
+ let sandbox = sinon.createSandbox();
+ // User needs default to ensure Pin Private window shows as third screen after import
+ sandbox
+ .stub(OnboardingMessageProvider, "_doesAppNeedDefault")
+ .resolves(true);
+
+ let pinStub = sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin");
+ pinStub.resolves(true);
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+
+ // Pin Private screen is not shown when user doesn't have Firefox pinned
+ Assert.ok(
+ !getOnboardingScreenById(
+ message.content.screens,
+ "UPGRADE_PIN_PRIVATE_WINDOW"
+ )
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_schemaValidation() {
+ const { experimentValidator, messageValidators } = await makeValidators();
+
+ const messages = await OnboardingMessageProvider.getMessages();
+ for (const message of messages) {
+ const validator = messageValidators[message.template];
+
+ Assert.ok(
+ typeof validator !== "undefined",
+ typeof validator !== "undefined"
+ ? `Schema validator found for ${message.template}.`
+ : `No schema validator found for template ${message.template}. Please update this test to add one.`
+ );
+ assertValidates(
+ validator,
+ message,
+ `Message ${message.id} validates as template ${message.template}`
+ );
+ assertValidates(
+ experimentValidator,
+ message,
+ `Message ${message.id} validates as MessagingExperiment`
+ );
+ }
+});
+
+add_task(
+ async function test_OnboardingMessageProvider_getPinPrivateWindow_pinPBMPrefDisabled() {
+ Services.prefs.setBoolPref(
+ "browser.startup.upgradeDialog.pinPBM.disabled",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(
+ "browser.startup.upgradeDialog.pinPBM.disabled"
+ );
+ });
+ let sandbox = sinon.createSandbox();
+ // User needs default to ensure Pin Private window shows as third screen after import
+ sandbox
+ .stub(OnboardingMessageProvider, "_doesAppNeedDefault")
+ .resolves(true);
+
+ let pinStub = sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin");
+ pinStub.resolves(true);
+
+ const message = await OnboardingMessageProvider.getUpgradeMessage();
+ // Pin Private screen is not shown when pref is turned on
+ Assert.ok(
+ !getOnboardingScreenById(
+ message.content.screens,
+ "UPGRADE_PIN_PRIVATE_WINDOW"
+ )
+ );
+ sandbox.restore();
+ }
+);
diff --git a/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
new file mode 100644
index 0000000000..3523355659
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { PanelTestProvider } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/PanelTestProvider.sys.mjs"
+);
+
+const MESSAGE_VALIDATORS = {};
+let EXPERIMENT_VALIDATOR;
+
+add_setup(async function setup() {
+ const validators = await makeValidators();
+
+ EXPERIMENT_VALIDATOR = validators.experimentValidator;
+ Object.assign(MESSAGE_VALIDATORS, validators.messageValidators);
+});
+
+add_task(async function test_PanelTestProvider() {
+ const messages = await PanelTestProvider.getMessages();
+
+ const EXPECTED_MESSAGE_COUNTS = {
+ cfr_doorhanger: 1,
+ milestone_message: 0,
+ update_action: 1,
+ whatsnew_panel_message: 7,
+ spotlight: 3,
+ feature_callout: 1,
+ pb_newtab: 2,
+ toast_notification: 3,
+ };
+
+ const EXPECTED_TOTAL_MESSAGE_COUNT = Object.values(
+ EXPECTED_MESSAGE_COUNTS
+ ).reduce((a, b) => a + b, 0);
+
+ Assert.strictEqual(
+ messages.length,
+ EXPECTED_TOTAL_MESSAGE_COUNT,
+ "PanelTestProvider should have the correct number of messages"
+ );
+
+ const messageCounts = Object.assign(
+ {},
+ ...Object.keys(EXPECTED_MESSAGE_COUNTS).map(key => ({ [key]: 0 }))
+ );
+
+ for (const message of messages) {
+ const validator = MESSAGE_VALIDATORS[message.template];
+ Assert.ok(
+ typeof validator !== "undefined",
+ typeof validator !== "undefined"
+ ? `Schema validator found for ${message.template}`
+ : `No schema validator found for template ${message.template}. Please update this test to add one.`
+ );
+ assertValidates(
+ validator,
+ message,
+ `Message ${message.id} validates as ${message.template} template`
+ );
+ assertValidates(
+ EXPERIMENT_VALIDATOR,
+ message,
+ `Message ${message.id} validates as MessagingExperiment`
+ );
+
+ messageCounts[message.template]++;
+ }
+
+ for (const [template, count] of Object.entries(messageCounts)) {
+ Assert.equal(
+ count,
+ EXPECTED_MESSAGE_COUNTS[template],
+ `Expected ${EXPECTED_MESSAGE_COUNTS[template]} ${template} messages`
+ );
+ }
+});
+
+add_task(async function test_emptyMessage() {
+ info(
+ "Testing blank FxMS messages validate with the Messaging Experiment schema"
+ );
+
+ assertValidates(EXPERIMENT_VALIDATOR, {}, "Empty message should validate");
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_reach_experiments.js b/browser/components/asrouter/tests/xpcshell/test_reach_experiments.js
new file mode 100644
index 0000000000..e69ce98677
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_reach_experiments.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ObjectUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ObjectUtils.sys.mjs"
+);
+
+const MESSAGES = [
+ {
+ trigger: { id: "defaultBrowserCheck" },
+ targeting:
+ "source == 'startup' && !isMajorUpgrade && !activeNotifications && totalBookmarksCount == 5",
+ },
+ {
+ groups: ["eco"],
+ trigger: {
+ id: "defaultBrowserCheck",
+ },
+ targeting:
+ "source == 'startup' && !isMajorUpgrade && !activeNotifications && totalBookmarksCount == 5",
+ },
+];
+
+let EXPERIMENT_VALIDATOR;
+
+add_setup(async function setup() {
+ EXPERIMENT_VALIDATOR = await schemaValidatorFor(
+ "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json"
+ );
+});
+
+add_task(function test_reach_experiments_validation() {
+ for (const [index, message] of MESSAGES.entries()) {
+ assertValidates(
+ EXPERIMENT_VALIDATOR,
+ message,
+ `Message ${index} validates as a MessagingExperiment`
+ );
+ }
+});
+
+function depError(has, missing) {
+ return {
+ instanceLocation: "#",
+ keyword: "dependentRequired",
+ keywordLocation: "#/oneOf/1/allOf/0/$ref/dependantRequired",
+ error: `Instance has "${has}" but does not have "${missing}".`,
+ };
+}
+
+function assertContains(haystack, needle) {
+ Assert.ok(
+ haystack.find(item => ObjectUtils.deepEqual(item, needle)) !== null
+ );
+}
+
+add_task(function test_reach_experiment_dependentRequired() {
+ info(
+ "Testing that if id is present then content and template are not required"
+ );
+
+ {
+ const message = {
+ ...MESSAGES[0],
+ id: "message-id",
+ };
+
+ const result = EXPERIMENT_VALIDATOR.validate(message);
+ Assert.ok(result.valid, "message should validate");
+ }
+
+ info("Testing that if content is present then id and template are required");
+ {
+ const message = {
+ ...MESSAGES[0],
+ content: {},
+ };
+
+ const result = EXPERIMENT_VALIDATOR.validate(message);
+ Assert.ok(!result.valid, "message should not validate");
+ assertContains(result.errors, depError("content", "id"));
+ assertContains(result.errors, depError("content", "template"));
+ }
+
+ info("Testing that if template is present then id and content are required");
+ {
+ const message = {
+ ...MESSAGES[0],
+ template: "cfr",
+ };
+
+ const result = EXPERIMENT_VALIDATOR.validate(message);
+ Assert.ok(!result.valid, "message should not validate");
+ assertContains(result.errors, depError("template", "content"));
+ assertContains(result.errors, depError("template", "id"));
+ }
+});
diff --git a/browser/components/asrouter/tests/xpcshell/test_remoteExperiments.js b/browser/components/asrouter/tests/xpcshell/test_remoteExperiments.js
new file mode 100644
index 0000000000..40c0993b4f
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/test_remoteExperiments.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CFRMessageProvider } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/CFRMessageProvider.sys.mjs"
+);
+
+add_task(async function test_multiMessageTreatment() {
+ const { experimentValidator } = await makeValidators();
+ // Use the entire list of messages as if it was a single treatment branch's
+ // feature value.
+ let messages = await CFRMessageProvider.getMessages();
+ let featureValue = { template: "multi", messages };
+ assertValidates(
+ experimentValidator,
+ featureValue,
+ `Multi-message treatment validates as MessagingExperiment`
+ );
+ for (const message of messages) {
+ assertValidates(
+ experimentValidator,
+ message,
+ `Message ${message.id} validates as MessagingExperiment`
+ );
+ }
+
+ // Add an invalid message to the list and make sure it fails validation.
+ messages.push({
+ id: "INVALID_MESSAGE",
+ template: "cfr_doorhanger",
+ });
+ const result = experimentValidator.validate(featureValue);
+ Assert.ok(
+ !(result.valid && result.errors.length === 0),
+ "Multi-message treatment with invalid message fails validation"
+ );
+});
diff --git a/browser/components/asrouter/tests/xpcshell/xpcshell.toml b/browser/components/asrouter/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..db28042ad2
--- /dev/null
+++ b/browser/components/asrouter/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+head = "head.js"
+firefox-appdir = "browser"
+
+["test_ASRouterTargeting_attribution.js"]
+run-if = ["os == 'mac'"] # osx specific tests
+
+["test_ASRouterTargeting_snapshot.js"]
+
+["test_ASRouter_getTargetingParameters.js"]
+
+["test_CFRMessageProvider.js"]
+
+["test_InflightAssetsMessageProvider.js"]
+
+["test_NimbusRolloutMessageProvider.js"]
+
+["test_OnboardingMessageProvider.js"]
+
+["test_PanelTestProvider.js"]
+
+["test_reach_experiments.js"]
+
+["test_remoteExperiments.js"]