diff options
Diffstat (limited to 'browser/components/asrouter/tests/xpcshell')
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"] |