summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/xpcshell')
-rw-r--r--browser/components/newtab/test/xpcshell/ds_layout.json89
-rw-r--r--browser/components/newtab/test/xpcshell/head.js105
-rw-r--r--browser/components/newtab/test/xpcshell/test_ASRouterTargeting_attribution.js98
-rw-r--r--browser/components/newtab/test/xpcshell/test_ASRouterTargeting_snapshot.js138
-rw-r--r--browser/components/newtab/test/xpcshell/test_ASRouter_getTargetingParameters.js73
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheChild.js33
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js251
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutNewTab.js359
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js69
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js101
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js143
-rw-r--r--browser/components/newtab/test/xpcshell/test_CFRMessageProvider.js32
-rw-r--r--browser/components/newtab/test/xpcshell/test_InflightAssetsMessageProvider.js41
-rw-r--r--browser/components/newtab/test/xpcshell/test_OnboardingMessageProvider.js229
-rw-r--r--browser/components/newtab/test/xpcshell/test_PanelTestProvider.js83
-rw-r--r--browser/components/newtab/test/xpcshell/test_reach_experiments.js97
-rw-r--r--browser/components/newtab/test/xpcshell/test_remoteExperiments.js37
-rw-r--r--browser/components/newtab/test/xpcshell/topstories.json53
-rw-r--r--browser/components/newtab/test/xpcshell/xpcshell.ini32
19 files changed, 2063 insertions, 0 deletions
diff --git a/browser/components/newtab/test/xpcshell/ds_layout.json b/browser/components/newtab/test/xpcshell/ds_layout.json
new file mode 100644
index 0000000000..4193fa635d
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/ds_layout.json
@@ -0,0 +1,89 @@
+{
+ "spocs": {
+ "url": ""
+ },
+ "layout": [
+ {
+ "width": 12,
+ "components": [
+ {
+ "type": "TopSites",
+ "header": {
+ "title": "Top Sites"
+ },
+ "properties": null
+ },
+ {
+ "type": "Message",
+ "header": {
+ "title": "Recommended by Pocket",
+ "subtitle": "",
+ "link_text": "How it works",
+ "link_url": "https://getpocket.com/firefox/new_tab_learn_more",
+ "icon": "chrome://global/skin/icons/pocket.svg"
+ },
+ "properties": null,
+ "styles": {
+ ".ds-message": "margin-bottom: -20px"
+ }
+ },
+ {
+ "type": "CardGrid",
+ "properties": {
+ "items": 3
+ },
+ "header": {
+ "title": ""
+ },
+ "feed": {
+ "embed_reference": null,
+ "url": "http://example.com/topstories.json"
+ },
+ "spocs": {
+ "probability": 1,
+ "positions": [
+ {
+ "index": 2
+ }
+ ]
+ }
+ },
+ {
+ "type": "Navigation",
+ "properties": {
+ "alignment": "left-align",
+ "links": [
+ {
+ "name": "Must Reads",
+ "url": "https://getpocket.com/explore/must-reads?src=fx_new_tab"
+ },
+ {
+ "name": "Productivity",
+ "url": "https://getpocket.com/explore/productivity?src=fx_new_tab"
+ },
+ {
+ "name": "Health",
+ "url": "https://getpocket.com/explore/health?src=fx_new_tab"
+ },
+ {
+ "name": "Finance",
+ "url": "https://getpocket.com/explore/finance?src=fx_new_tab"
+ },
+ {
+ "name": "Technology",
+ "url": "https://getpocket.com/explore/technology?src=fx_new_tab"
+ },
+ {
+ "name": "More Recommendations ›",
+ "url": "https://getpocket.com/explore/trending?src=fx_new_tab"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "feeds": {},
+ "error": 0,
+ "status": 1
+}
diff --git a/browser/components/newtab/test/xpcshell/head.js b/browser/components/newtab/test/xpcshell/head.js
new file mode 100644
index 0000000000..49463fe0a8
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/head.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-disable no-unused-vars */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
+
+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 {
+ 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(
+ "resource://activity-stream/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 }
+ ),
+ protections_panel: await schemaValidatorFor(
+ "resource://testing-common/ProtectionsPanelMessage.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/newtab/test/xpcshell/test_ASRouterTargeting_attribution.js b/browser/components/newtab/test/xpcshell/test_ASRouterTargeting_attribution.js
new file mode 100644
index 0000000000..f2b473144b
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_ASRouterTargeting_attribution.js
@@ -0,0 +1,98 @@
+/* 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.import(
+ "resource://activity-stream/lib/ASRouterTargeting.jsm"
+);
+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 appPath = MacAttribution.applicationPath;
+ const attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
+ Ci.nsIMacAttributionService
+ );
+ const campaign = "non-fx-button";
+ const source = "addons.mozilla.org";
+ const referrer = `https://allizom.org/anything/?utm_campaign=${campaign}&utm_source=${source}`;
+ attributionSvc.setReferrerUrl(appPath, referrer, true);
+ 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/newtab/test/xpcshell/test_ASRouterTargeting_snapshot.js b/browser/components/newtab/test/xpcshell/test_ASRouterTargeting_snapshot.js
new file mode 100644
index 0000000000..cb5a13baf5
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_ASRouterTargeting_snapshot.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { ASRouterTargeting } = ChromeUtils.import(
+ "resource://activity-stream/lib/ASRouterTargeting.jsm"
+);
+
+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(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(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(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"
+ );
+});
+
+/*
+ * 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(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/newtab/test/xpcshell/test_ASRouter_getTargetingParameters.js b/browser/components/newtab/test/xpcshell/test_ASRouter_getTargetingParameters.js
new file mode 100644
index 0000000000..fb3b037660
--- /dev/null
+++ b/browser/components/newtab/test/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.import(
+ "resource://activity-stream/lib/ASRouter.jsm"
+);
+
+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/newtab/test/xpcshell/test_AboutHomeStartupCacheChild.js b/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheChild.js
new file mode 100644
index 0000000000..a0cb2cf324
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheChild.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AboutHomeStartupCacheChild } = ChromeUtils.import(
+ "resource:///modules/AboutNewTabService.jsm"
+);
+
+/**
+ * Tests that AboutHomeStartupCacheChild will terminate its PromiseWorker
+ * on memory-pressure, and that a new PromiseWorker can then be generated on
+ * demand.
+ */
+add_task(async function test_memory_pressure() {
+ AboutHomeStartupCacheChild.init();
+
+ let worker = AboutHomeStartupCacheChild.getOrCreateWorker();
+ Assert.ok(worker, "Should have been able to get the worker.");
+
+ Assert.equal(
+ worker,
+ AboutHomeStartupCacheChild.getOrCreateWorker(),
+ "The worker is cached and re-usable."
+ );
+
+ Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
+
+ let newWorker = AboutHomeStartupCacheChild.getOrCreateWorker();
+ Assert.notEqual(worker, newWorker, "Old worker should have been replaced.");
+
+ AboutHomeStartupCacheChild.uninit();
+});
diff --git a/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js b/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js
new file mode 100644
index 0000000000..0cbb81351b
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that the about:home startup cache worker
+ * script can correctly convert a state object from the Activity
+ * Stream Redux store into an HTML document and script.
+ */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const { AboutNewTab } = ChromeUtils.import(
+ "resource:///modules/AboutNewTab.jsm"
+);
+const { PREFS_CONFIG } = ChromeUtils.import(
+ "resource://activity-stream/lib/ActivityStream.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
+});
+
+const CACHE_WORKER_URL = "resource://activity-stream/lib/cache-worker.js";
+const NEWTAB_RENDER_URL =
+ "resource://activity-stream/data/content/newtab-render.js";
+
+/**
+ * In order to make this test less brittle, much of Activity Stream is
+ * initialized here in order to generate a state object at runtime, rather
+ * than hard-coding one in. This requires quite a bit of machinery in order
+ * to work properly. Specifically, we need to launch an HTTP server to serve
+ * a dynamic layout, and then have that layout point to a local feed rather
+ * than one from the Pocket CDN.
+ */
+add_setup(async function () {
+ do_get_profile();
+ // The SearchService is also needed in order to construct the initial state,
+ // which means that the AddonManager needs to be available.
+ await AddonTestUtils.promiseStartupManager();
+
+ // The example.com domain will be used to host the dynamic layout JSON and
+ // the top stories JSON.
+ let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+ server.registerDirectory("/", do_get_cwd());
+
+ // Top Stories are disabled by default in our testing profiles.
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.section.topstories",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.system.topstories",
+ true
+ );
+
+ let defaultDSConfig = JSON.parse(
+ PREFS_CONFIG.get("discoverystream.config").getValue({
+ geo: "US",
+ locale: "en-US",
+ })
+ );
+
+ let newConfig = Object.assign(defaultDSConfig, {
+ show_spocs: false,
+ hardcoded_layout: false,
+ layout_endpoint: "http://example.com/ds_layout.json",
+ });
+
+ // Configure Activity Stream to query for the layout JSON file that points
+ // at the local top stories feed.
+ Services.prefs.setCharPref(
+ "browser.newtabpage.activity-stream.discoverystream.config",
+ JSON.stringify(newConfig)
+ );
+
+ // We need to allow example.com as a place to get both the layout and the
+ // top stories from.
+ Services.prefs.setCharPref(
+ "browser.newtabpage.activity-stream.discoverystream.endpoints",
+ `http://example.com`
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.telemetry.structuredIngestion",
+ false
+ );
+ Services.prefs.setBoolPref("browser.ping-centre.telemetry", false);
+
+ // We need a default search engine set up for rendering the search input.
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "Test engine",
+ keyword: "@testengine",
+ search_url_get_params: "s={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+
+ // Initialize Activity Stream, and pretend that a new window has been loaded
+ // to kick off initializing all of the feeds.
+ AboutNewTab.init();
+ AboutNewTab.onBrowserReady();
+
+ // Much of Activity Stream initializes asynchronously. This is the easiest way
+ // I could find to ensure that enough of the feeds had initialized to produce
+ // a meaningful cached document.
+ await TestUtils.waitForCondition(() => {
+ let feed = AboutNewTab.activityStream.store.feeds.get(
+ "feeds.discoverystreamfeed"
+ );
+ return feed?.loaded;
+ });
+});
+
+/**
+ * Gets the Activity Stream Redux state from Activity Stream and sends it
+ * into an instance of the cache worker to ensure that the resulting markup
+ * and script makes sense.
+ */
+add_task(async function test_cache_worker() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ });
+
+ let state = AboutNewTab.activityStream.store.getState();
+
+ let cacheWorker = new BasePromiseWorker(CACHE_WORKER_URL);
+ let { page, script } = await cacheWorker.post("construct", [state]);
+ ok(!!page.length, "Got page content");
+ ok(!!script.length, "Got script content");
+
+ // The template strings should have been replaced.
+ equal(
+ page.indexOf("{{ MARKUP }}"),
+ -1,
+ "Page template should have {{ MARKUP }} replaced"
+ );
+ equal(
+ page.indexOf("{{ CACHE_TIME }}"),
+ -1,
+ "Page template should have {{ CACHE_TIME }} replaced"
+ );
+ equal(
+ script.indexOf("{{ STATE }}"),
+ -1,
+ "Script template should have {{ STATE }} replaced"
+ );
+
+ // Now let's make sure that the generated script makes sense. We'll
+ // evaluate it in a sandbox to make sure broken JS doesn't break the
+ // test.
+ let sandbox = Cu.Sandbox(Cu.getGlobalForObject({}));
+ let passedState = null;
+
+ // window.NewtabRenderUtils.renderCache is the exposed API from
+ // activity-stream.jsx that the script is expected to call to hydrate
+ // the pre-rendered markup. We'll implement that, and use that to ensure
+ // that the passed in state object matches the state we sent into the
+ // worker.
+ sandbox.window = {
+ NewtabRenderUtils: {
+ renderCache(aState) {
+ passedState = aState;
+ },
+ },
+ };
+ Cu.evalInSandbox(script, sandbox);
+
+ // The NEWTAB_RENDER_URL script is what ultimately causes the state
+ // to be passed into the renderCache function.
+ Services.scriptloader.loadSubScript(NEWTAB_RENDER_URL, sandbox);
+
+ equal(
+ sandbox.window.__FROM_STARTUP_CACHE__,
+ true,
+ "Should have set __FROM_STARTUP_CACHE__ to true"
+ );
+
+ // The worker is expected to modify the state slightly before running
+ // it through ReactDOMServer by setting App.isForStartupCache to true.
+ // This allows React components to change their behaviour if the cache
+ // is being generated.
+ state.App.isForStartupCache = true;
+
+ // Some of the properties on the state might have values set to undefined.
+ // There is no way to express a named undefined property on an object in
+ // JSON, so we filter those out by stringifying and re-parsing.
+ state = JSON.parse(JSON.stringify(state));
+
+ Assert.deepEqual(
+ passedState,
+ state,
+ "Should have called renderCache with the expected state"
+ );
+
+ // Now let's do a quick smoke-test on the markup to ensure that the
+ // one Top Story from topstories.json is there.
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(page, "text/html");
+ let root = doc.getElementById("root");
+ ok(root.childElementCount, "There are children on the root node");
+
+ // There should be the 1 top story, and 2 placeholders.
+ equal(
+ Array.from(root.querySelectorAll(".ds-card")).length,
+ 3,
+ "There are 3 DSCards"
+ );
+ let cardHostname = doc.querySelector(
+ "[data-section-id='topstories'] .source"
+ ).innerText;
+ equal(cardHostname, "bbc.com", "Card hostname is bbc.com");
+
+ let placeholders = doc.querySelectorAll(".ds-card.placeholder");
+ equal(placeholders.length, 2, "There should be 2 placeholders");
+});
+
+/**
+ * Tests that if the cache-worker construct method throws an exception
+ * that the construct Promise still resolves. Passing a null state should
+ * be enough to get it to throw.
+ */
+add_task(async function test_cache_worker_exception() {
+ let cacheWorker = new BasePromiseWorker(CACHE_WORKER_URL);
+ let { page, script } = await cacheWorker.post("construct", [null]);
+ equal(page, null, "Should have gotten a null page nsIInputStream");
+ equal(script, null, "Should have gotten a null script nsIInputStream");
+});
diff --git a/browser/components/newtab/test/xpcshell/test_AboutNewTab.js b/browser/components/newtab/test/xpcshell/test_AboutNewTab.js
new file mode 100644
index 0000000000..9b31a2add1
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutNewTab.js
@@ -0,0 +1,359 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * This file tests both the AboutNewTab and nsIAboutNewTabService
+ * for its default URL values, as well as its behaviour when overriding
+ * the default URL values.
+ */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { AboutNewTab } = ChromeUtils.import(
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService"
+);
+
+AboutNewTab.init();
+
+const IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;
+
+const DOWNLOADS_URL =
+ "chrome://browser/content/downloads/contentAreaDownloadsView.xhtml";
+const SEPARATE_PRIVILEGED_CONTENT_PROCESS_PREF =
+ "browser.tabs.remote.separatePrivilegedContentProcess";
+const ACTIVITY_STREAM_DEBUG_PREF = "browser.newtabpage.activity-stream.debug";
+const SIMPLIFIED_WELCOME_ENABLED_PREF = "browser.aboutwelcome.enabled";
+
+function cleanup() {
+ Services.prefs.clearUserPref(SEPARATE_PRIVILEGED_CONTENT_PROCESS_PREF);
+ Services.prefs.clearUserPref(ACTIVITY_STREAM_DEBUG_PREF);
+ Services.prefs.clearUserPref(SIMPLIFIED_WELCOME_ENABLED_PREF);
+ AboutNewTab.resetNewTabURL();
+}
+
+registerCleanupFunction(cleanup);
+
+let ACTIVITY_STREAM_URL;
+let ACTIVITY_STREAM_DEBUG_URL;
+
+function setExpectedUrlsWithScripts() {
+ ACTIVITY_STREAM_URL =
+ "resource://activity-stream/prerendered/activity-stream.html";
+ ACTIVITY_STREAM_DEBUG_URL =
+ "resource://activity-stream/prerendered/activity-stream-debug.html";
+}
+
+function setExpectedUrlsWithoutScripts() {
+ ACTIVITY_STREAM_URL =
+ "resource://activity-stream/prerendered/activity-stream-noscripts.html";
+
+ // Debug urls are the same as non-debug because debug scripts load dynamically
+ ACTIVITY_STREAM_DEBUG_URL = ACTIVITY_STREAM_URL;
+}
+
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+ Assert.equal(aData, aNewURL, testMessage);
+ resolve();
+ }, "newtab-url-changed");
+ });
+}
+
+function setPrivilegedContentProcessPref(usePrivilegedContentProcess) {
+ if (
+ usePrivilegedContentProcess === AboutNewTab.privilegedAboutProcessEnabled
+ ) {
+ return Promise.resolve();
+ }
+
+ let notificationPromise = nextChangeNotificationPromise("about:newtab");
+
+ Services.prefs.setBoolPref(
+ SEPARATE_PRIVILEGED_CONTENT_PROCESS_PREF,
+ usePrivilegedContentProcess
+ );
+ return notificationPromise;
+}
+
+// Default expected URLs to files with scripts in them.
+setExpectedUrlsWithScripts();
+
+function addTestsWithPrivilegedContentProcessPref(test) {
+ add_task(async () => {
+ await setPrivilegedContentProcessPref(true);
+ setExpectedUrlsWithoutScripts();
+ await test();
+ });
+ add_task(async () => {
+ await setPrivilegedContentProcessPref(false);
+ setExpectedUrlsWithScripts();
+ await test();
+ });
+}
+
+function setBoolPrefAndWaitForChange(pref, value, testMessage) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+ Assert.equal(aData, AboutNewTab.newTabURL, testMessage);
+ resolve();
+ }, "newtab-url-changed");
+
+ Services.prefs.setBoolPref(pref, value);
+ });
+}
+
+add_task(async function test_as_initial_values() {
+ Assert.ok(
+ AboutNewTab.activityStreamEnabled,
+ ".activityStreamEnabled should be set to the correct initial value"
+ );
+ // This pref isn't defined on release or beta, so we fall back to false
+ Assert.equal(
+ AboutNewTab.activityStreamDebug,
+ Services.prefs.getBoolPref(ACTIVITY_STREAM_DEBUG_PREF, false),
+ ".activityStreamDebug should be set to the correct initial value"
+ );
+});
+
+/**
+ * Test the overriding of the default URL
+ */
+add_task(async function test_override_activity_stream_disabled() {
+ let notificationPromise;
+
+ Assert.ok(
+ !AboutNewTab.newTabURLOverridden,
+ "Newtab URL should not be overridden"
+ );
+ const ORIGINAL_URL = aboutNewTabService.defaultURL;
+
+ // override with some remote URL
+ let url = "http://example.com/";
+ notificationPromise = nextChangeNotificationPromise(url);
+ AboutNewTab.newTabURL = url;
+ await notificationPromise;
+ Assert.ok(AboutNewTab.newTabURLOverridden, "Newtab URL should be overridden");
+ Assert.ok(
+ !AboutNewTab.activityStreamEnabled,
+ "Newtab activity stream should not be enabled"
+ );
+ Assert.equal(
+ AboutNewTab.newTabURL,
+ url,
+ "Newtab URL should be the custom URL"
+ );
+ Assert.equal(
+ aboutNewTabService.defaultURL,
+ ORIGINAL_URL,
+ "AboutNewTabService defaultURL is unchanged"
+ );
+
+ // test reset with activity stream disabled
+ notificationPromise = nextChangeNotificationPromise("about:newtab");
+ AboutNewTab.resetNewTabURL();
+ await notificationPromise;
+ Assert.ok(
+ !AboutNewTab.newTabURLOverridden,
+ "Newtab URL should not be overridden"
+ );
+ Assert.equal(
+ AboutNewTab.newTabURL,
+ "about:newtab",
+ "Newtab URL should be the default"
+ );
+
+ // test override to a chrome URL
+ notificationPromise = nextChangeNotificationPromise(DOWNLOADS_URL);
+ AboutNewTab.newTabURL = DOWNLOADS_URL;
+ await notificationPromise;
+ Assert.ok(AboutNewTab.newTabURLOverridden, "Newtab URL should be overridden");
+ Assert.equal(
+ AboutNewTab.newTabURL,
+ DOWNLOADS_URL,
+ "Newtab URL should be the custom URL"
+ );
+
+ cleanup();
+});
+
+addTestsWithPrivilegedContentProcessPref(
+ async function test_override_activity_stream_enabled() {
+ Assert.equal(
+ aboutNewTabService.defaultURL,
+ ACTIVITY_STREAM_URL,
+ "Newtab URL should be the default activity stream URL"
+ );
+ Assert.ok(
+ !AboutNewTab.newTabURLOverridden,
+ "Newtab URL should not be overridden"
+ );
+ Assert.ok(
+ AboutNewTab.activityStreamEnabled,
+ "Activity Stream should be enabled"
+ );
+
+ // change to a chrome URL while activity stream is enabled
+ let notificationPromise = nextChangeNotificationPromise(DOWNLOADS_URL);
+ AboutNewTab.newTabURL = DOWNLOADS_URL;
+ await notificationPromise;
+ Assert.equal(
+ AboutNewTab.newTabURL,
+ DOWNLOADS_URL,
+ "Newtab URL set to chrome url"
+ );
+ Assert.equal(
+ aboutNewTabService.defaultURL,
+ ACTIVITY_STREAM_URL,
+ "Newtab URL defaultURL still set to the default activity stream URL"
+ );
+ Assert.ok(
+ AboutNewTab.newTabURLOverridden,
+ "Newtab URL should be overridden"
+ );
+ Assert.ok(
+ !AboutNewTab.activityStreamEnabled,
+ "Activity Stream should not be enabled"
+ );
+
+ cleanup();
+ }
+);
+
+addTestsWithPrivilegedContentProcessPref(async function test_default_url() {
+ Assert.equal(
+ aboutNewTabService.defaultURL,
+ ACTIVITY_STREAM_URL,
+ "Newtab defaultURL initially set to AS url"
+ );
+
+ // Only debug variants aren't available on release/beta
+ if (!IS_RELEASE_OR_BETA) {
+ await setBoolPrefAndWaitForChange(
+ ACTIVITY_STREAM_DEBUG_PREF,
+ true,
+ "A notification occurs after changing the debug pref to true"
+ );
+ Assert.equal(
+ AboutNewTab.activityStreamDebug,
+ true,
+ "the .activityStreamDebug property is set to true"
+ );
+ Assert.equal(
+ aboutNewTabService.defaultURL,
+ ACTIVITY_STREAM_DEBUG_URL,
+ "Newtab defaultURL set to debug AS url after the pref has been changed"
+ );
+ await setBoolPrefAndWaitForChange(
+ ACTIVITY_STREAM_DEBUG_PREF,
+ false,
+ "A notification occurs after changing the debug pref to false"
+ );
+ } else {
+ Services.prefs.setBoolPref(ACTIVITY_STREAM_DEBUG_PREF, true);
+
+ Assert.equal(
+ AboutNewTab.activityStreamDebug,
+ false,
+ "the .activityStreamDebug property is remains false"
+ );
+ }
+
+ Assert.equal(
+ aboutNewTabService.defaultURL,
+ ACTIVITY_STREAM_URL,
+ "Newtab defaultURL set to un-prerendered AS if prerender is false and debug is false"
+ );
+
+ cleanup();
+});
+
+addTestsWithPrivilegedContentProcessPref(async function test_welcome_url() {
+ // Disable about:welcome to load newtab
+ Services.prefs.setBoolPref(SIMPLIFIED_WELCOME_ENABLED_PREF, false);
+ Assert.equal(
+ aboutNewTabService.welcomeURL,
+ ACTIVITY_STREAM_URL,
+ "Newtab welcomeURL set to un-prerendered AS when debug disabled."
+ );
+ Assert.equal(
+ aboutNewTabService.welcomeURL,
+ aboutNewTabService.defaultURL,
+ "Newtab welcomeURL is equal to defaultURL when prerendering disabled and debug disabled."
+ );
+
+ // Only debug variants aren't available on release/beta
+ if (!IS_RELEASE_OR_BETA) {
+ await setBoolPrefAndWaitForChange(
+ ACTIVITY_STREAM_DEBUG_PREF,
+ true,
+ "A notification occurs after changing the debug pref to true."
+ );
+ Assert.equal(
+ aboutNewTabService.welcomeURL,
+ ACTIVITY_STREAM_DEBUG_URL,
+ "Newtab welcomeURL set to un-prerendered debug AS when debug enabled"
+ );
+ }
+
+ cleanup();
+});
+
+/**
+ * Tests response to updates to prefs
+ */
+addTestsWithPrivilegedContentProcessPref(async function test_updates() {
+ // Simulates a "cold-boot" situation, with some pref already set before testing a series
+ // of changes.
+ AboutNewTab.resetNewTabURL(); // need to set manually because pref notifs are off
+ let notificationPromise;
+
+ // test update fires on override and reset
+ let testURL = "https://example.com/";
+ notificationPromise = nextChangeNotificationPromise(
+ testURL,
+ "a notification occurs on override"
+ );
+ AboutNewTab.newTabURL = testURL;
+ await notificationPromise;
+
+ // from overridden to default
+ notificationPromise = nextChangeNotificationPromise(
+ "about:newtab",
+ "a notification occurs on reset"
+ );
+ AboutNewTab.resetNewTabURL();
+ Assert.ok(
+ AboutNewTab.activityStreamEnabled,
+ "Activity Stream should be enabled"
+ );
+ Assert.equal(
+ aboutNewTabService.defaultURL,
+ ACTIVITY_STREAM_URL,
+ "Default URL should be the activity stream page"
+ );
+ await notificationPromise;
+
+ // reset twice, only one notification for default URL
+ notificationPromise = nextChangeNotificationPromise(
+ "about:newtab",
+ "reset occurs"
+ );
+ AboutNewTab.resetNewTabURL();
+ await notificationPromise;
+
+ cleanup();
+});
diff --git a/browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js b/browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js
new file mode 100644
index 0000000000..2b2c55b47b
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AboutWelcomeDefaults } = ChromeUtils.import(
+ "resource://activity-stream/aboutwelcome/lib/AboutWelcomeDefaults.jsm"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { AttributionCode } = ChromeUtils.importESModule(
+ "resource:///modules/AttributionCode.sys.mjs"
+);
+const { AddonRepository } = ChromeUtils.importESModule(
+ "resource://gre/modules/addons/AddonRepository.sys.mjs"
+);
+
+const TEST_ATTRIBUTION_DATA = {
+ source: "addons.mozilla.org",
+ medium: "referral",
+ campaign: "non-fx-button",
+ content: "rta:iridium%40particlecore.github.io",
+};
+
+add_task(async function test_handleAddonInfoNotFound() {
+ let sandbox = sinon.createSandbox();
+ const stub = sandbox.stub(AttributionCode, "getAttrDataAsync").resolves(null);
+ let result = await AboutWelcomeDefaults.getAttributionContent();
+ equal(stub.callCount, 1, "Call was made");
+ equal(result, null, "No data is returned");
+
+ sandbox.restore();
+});
+
+add_task(async function test_UAAttribution() {
+ let sandbox = sinon.createSandbox();
+ const stub = sandbox
+ .stub(AttributionCode, "getAttrDataAsync")
+ .resolves({ ua: "test" });
+ let result = await AboutWelcomeDefaults.getAttributionContent();
+ equal(stub.callCount, 1, "Call was made");
+ equal(result.template, undefined, "Template was not returned");
+ equal(result.ua, "test", "UA was returned");
+
+ sandbox.restore();
+});
+
+add_task(async function test_formatAttributionData() {
+ let sandbox = sinon.createSandbox();
+ const TEST_ADDON_INFO = {
+ sourceURI: { scheme: "https", spec: "https://test.xpi" },
+ name: "Test Add-on",
+ icons: { 64: "http://test.svg" },
+ };
+ sandbox
+ .stub(AttributionCode, "getAttrDataAsync")
+ .resolves(TEST_ATTRIBUTION_DATA);
+ sandbox.stub(AddonRepository, "getAddonsByIDs").resolves([TEST_ADDON_INFO]);
+ let result = await AboutWelcomeDefaults.getAttributionContent(
+ TEST_ATTRIBUTION_DATA
+ );
+ equal(AddonRepository.getAddonsByIDs.callCount, 1, "Retrieve addon content");
+ equal(result.template, "return_to_amo", "RTAMO template returned");
+ equal(result.name, TEST_ADDON_INFO.name, "AddonInfo returned");
+
+ sandbox.restore();
+});
diff --git a/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js b/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js
new file mode 100644
index 0000000000..5ecc20f804
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AboutWelcomeTelemetry } = ChromeUtils.import(
+ "resource://activity-stream/aboutwelcome/lib/AboutWelcomeTelemetry.jsm"
+);
+const { AttributionCode } = ChromeUtils.importESModule(
+ "resource:///modules/AttributionCode.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const TELEMETRY_PREF = "browser.newtabpage.activity-stream.telemetry";
+
+add_setup(function setup() {
+ do_get_profile();
+ Services.fog.initializeFOG();
+});
+
+add_task(function test_enabled() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ equal(AWTelemetry.telemetryEnabled, true, "Telemetry should be on");
+
+ Services.prefs.setBoolPref(TELEMETRY_PREF, false);
+
+ equal(AWTelemetry.telemetryEnabled, false, "Telemetry should be off");
+});
+
+add_task(async function test_pingPayload() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+ const AWTelemetry = new AboutWelcomeTelemetry();
+ const stub = sinon.stub(
+ AWTelemetry.pingCentre,
+ "sendStructuredIngestionPing"
+ );
+ sinon.stub(AWTelemetry, "_createPing").resolves({ event: "MOCHITEST" });
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(Glean.messagingSystem.event.testGetValue(), "MOCHITEST");
+ });
+ await AWTelemetry.sendTelemetry();
+
+ equal(stub.callCount, 1, "Call was made");
+ // check the endpoint
+ ok(
+ stub.firstCall.args[1].includes("/messaging-system/onboarding"),
+ "Endpoint is correct"
+ );
+
+ ok(pingSubmitted, "Glean ping was submitted");
+});
+
+add_task(function test_mayAttachAttribution() {
+ const sandbox = sinon.createSandbox();
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ sandbox.stub(AttributionCode, "getCachedAttributionData").returns(null);
+
+ let ping = AWTelemetry._maybeAttachAttribution({});
+
+ equal(ping.attribution, undefined, "Should not set attribution if it's null");
+
+ sandbox.restore();
+ sandbox.stub(AttributionCode, "getCachedAttributionData").returns({});
+ ping = AWTelemetry._maybeAttachAttribution({});
+
+ equal(
+ ping.attribution,
+ undefined,
+ "Should not set attribution if it's empty"
+ );
+
+ const attr = {
+ source: "google.com",
+ medium: "referral",
+ campaign: "Firefox-Brand-US-Chrome",
+ content: "(not set)",
+ experiment: "(not set)",
+ variation: "(not set)",
+ ua: "chrome",
+ };
+ sandbox.restore();
+ sandbox.stub(AttributionCode, "getCachedAttributionData").returns(attr);
+ ping = AWTelemetry._maybeAttachAttribution({});
+
+ equal(ping.attribution, attr, "Should set attribution if it presents");
+});
diff --git a/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js b/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js
new file mode 100644
index 0000000000..a49a6f9382
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AboutWelcomeTelemetry } = ChromeUtils.import(
+ "resource://activity-stream/aboutwelcome/lib/AboutWelcomeTelemetry.jsm"
+);
+const TELEMETRY_PREF = "browser.newtabpage.activity-stream.telemetry";
+
+add_setup(function setup() {
+ do_get_profile();
+ Services.fog.initializeFOG();
+});
+
+// We recognize two kinds of unexpected data that might reach
+// `submitGleanPingForPing`: unknown keys, and keys with unexpectedly-complex
+// data (ie, non-scalar).
+// We report the keys in special metrics to aid in system health monitoring.
+add_task(function test_weird_data() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ const unknownKey = "some_unknown_key";
+ const camelUnknownKey = AWTelemetry._snakeToCamelCase(unknownKey);
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.unknownKeys[camelUnknownKey].testGetValue(),
+ 1,
+ "caught the unknown key"
+ );
+ // TODO(bug 1600008): Also check the for-testing overall count.
+ Assert.equal(Glean.messagingSystem.unknownKeyCount.testGetValue(), 1);
+ });
+ AWTelemetry.submitGleanPingForPing({
+ [unknownKey]: "value doesn't matter",
+ });
+
+ Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
+
+ const invalidNestedDataKey = "event";
+ pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.invalidNestedData[
+ invalidNestedDataKey
+ ].testGetValue("messaging-system"),
+ 1,
+ "caught the invalid nested data"
+ );
+ });
+ AWTelemetry.submitGleanPingForPing({
+ [invalidNestedDataKey]: { this_should: "not be", complex: "data" },
+ });
+
+ Assert.ok(pingSubmitted, "Ping with invalid nested data submitted");
+});
+
+// `event_context` is weird. It's an object, but it might have been stringified
+// before being provided for recording.
+add_task(function test_event_context() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ const eventContext = {
+ reason: "reason",
+ page: "page",
+ source: "source",
+ something_else: "not specifically handled",
+ };
+ const stringifiedEC = JSON.stringify(eventContext);
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventReason.testGetValue(),
+ eventContext.reason,
+ "event_context.reason also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventPage.testGetValue(),
+ eventContext.page,
+ "event_context.page also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventSource.testGetValue(),
+ eventContext.source,
+ "event_context.source also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventContext.testGetValue(),
+ stringifiedEC,
+ "whole event_context added as text."
+ );
+ });
+ AWTelemetry.submitGleanPingForPing({
+ event_context: eventContext,
+ });
+ Assert.ok(pingSubmitted, "Ping with object event_context submitted");
+
+ pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventReason.testGetValue(),
+ eventContext.reason,
+ "event_context.reason also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventPage.testGetValue(),
+ eventContext.page,
+ "event_context.page also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventSource.testGetValue(),
+ eventContext.source,
+ "event_context.source also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventContext.testGetValue(),
+ stringifiedEC,
+ "whole event_context added as text."
+ );
+ });
+ AWTelemetry.submitGleanPingForPing({
+ event_context: stringifiedEC,
+ });
+ Assert.ok(pingSubmitted, "Ping with string event_context submitted");
+});
diff --git a/browser/components/newtab/test/xpcshell/test_CFRMessageProvider.js b/browser/components/newtab/test/xpcshell/test_CFRMessageProvider.js
new file mode 100644
index 0000000000..acdd4a2e2b
--- /dev/null
+++ b/browser/components/newtab/test/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://activity-stream/lib/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/newtab/test/xpcshell/test_InflightAssetsMessageProvider.js b/browser/components/newtab/test/xpcshell/test_InflightAssetsMessageProvider.js
new file mode 100644
index 0000000000..ad1bd1dbff
--- /dev/null
+++ b/browser/components/newtab/test/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.import(
+ "resource://testing-common/InflightAssetsMessageProvider.jsm"
+);
+
+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/newtab/test/xpcshell/test_OnboardingMessageProvider.js b/browser/components/newtab/test/xpcshell/test_OnboardingMessageProvider.js
new file mode 100644
index 0000000000..0ad7a6cbee
--- /dev/null
+++ b/browser/components/newtab/test/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.import(
+ "resource://activity-stream/lib/OnboardingMessageProvider.jsm"
+);
+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",
+ "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",
+ "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/newtab/test/xpcshell/test_PanelTestProvider.js b/browser/components/newtab/test/xpcshell/test_PanelTestProvider.js
new file mode 100644
index 0000000000..d5c5c19f0c
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_PanelTestProvider.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { PanelTestProvider } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/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: 2,
+ pb_newtab: 2,
+ toast_notification: 2,
+ };
+
+ 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/newtab/test/xpcshell/test_reach_experiments.js b/browser/components/newtab/test/xpcshell/test_reach_experiments.js
new file mode 100644
index 0000000000..240bda3594
--- /dev/null
+++ b/browser/components/newtab/test/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.import(
+ "resource://gre/modules/ObjectUtils.jsm"
+);
+
+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(
+ "resource://activity-stream/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/newtab/test/xpcshell/test_remoteExperiments.js b/browser/components/newtab/test/xpcshell/test_remoteExperiments.js
new file mode 100644
index 0000000000..6964d34023
--- /dev/null
+++ b/browser/components/newtab/test/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://activity-stream/lib/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/newtab/test/xpcshell/topstories.json b/browser/components/newtab/test/xpcshell/topstories.json
new file mode 100644
index 0000000000..7d65fcb0e1
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/topstories.json
@@ -0,0 +1,53 @@
+{
+ "status": 1,
+ "settings": {
+ "spocsPerNewTabs": 0.5,
+ "domainAffinityParameterSets": {
+ "default": {
+ "recencyFactor": 0.5,
+ "frequencyFactor": 0.5,
+ "combinedDomainFactor": 0.5,
+ "perfectFrequencyVisits": 10,
+ "perfectCombinedDomainScore": 2,
+ "multiDomainBoost": 0,
+ "itemScoreFactor": 1
+ },
+ "fully-personalized": {
+ "recencyFactor": 0.5,
+ "frequencyFactor": 0.5,
+ "combinedDomainFactor": 0.5,
+ "perfectFrequencyVisits": 10,
+ "perfectCombinedDomainScore": 2,
+ "itemScoreFactor": 0.01,
+ "multiDomainBoost": 0
+ }
+ },
+ "timeSegments": [
+ { "id": "week", "startTime": 604800, "endTime": 0, "weightPosition": 1 },
+ {
+ "id": "month",
+ "startTime": 2592000,
+ "endTime": 604800,
+ "weightPosition": 0.5
+ }
+ ],
+ "recsExpireTime": 5400,
+ "version": "2c2aa06dac65ddb647d8902aaa60263c8e119ff2"
+ },
+ "spocs": [],
+ "recommendations": [
+ {
+ "id": 53093,
+ "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAA/UlEQVR4nO3RMQ0AMAzAsPIn3d5DsBw2gkiZJWV+B/AyJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQmAP4K6zWNUjE4wAAAABJRU5ErkJggg==",
+ "domain": "bbc.com",
+ "title": "Why vegan junk food may be even worse for your health",
+ "excerpt": "While we might switch to a plant-based diet with the best intentions, the unseen risks of vegan fast foods might not show up for years.",
+ "image_src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAA/UlEQVR4nO3RMQ0AMAzAsPIn3d5DsBw2gkiZJWV+B/AyJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQmAP4K6zWNUjE4wAAAABJRU5ErkJggg==",
+ "published_timestamp": "1580277600",
+ "engagement": "",
+ "parameter_set": "default",
+ "domain_affinities": {},
+ "item_score": 1
+ }
+ ]
+}
diff --git a/browser/components/newtab/test/xpcshell/xpcshell.ini b/browser/components/newtab/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..807214219e
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/xpcshell.ini
@@ -0,0 +1,32 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android' # bug 1730213
+prefs =
+ browser.startup.homepage.abouthome_cache.enabled=true
+ browser.startup.homepage.abouthome_cache.testing=true
+
+[test_AboutHomeStartupCacheChild.js]
+[test_AboutHomeStartupCacheWorker.js]
+support-files =
+ ds_layout.json
+ topstories.json
+skip-if =
+ socketprocess_networking # Bug 1759035
+
+[test_AboutNewTab.js]
+[test_AboutWelcomeAttribution.js]
+[test_ASRouterTargeting_attribution.js]
+skip-if =
+ toolkit != "cocoa" # osx specific tests
+ os == "mac" && bits == 64 # See bug 1784121
+[test_ASRouter_getTargetingParameters.js]
+[test_ASRouterTargeting_snapshot.js]
+[test_AboutWelcomeTelemetry.js]
+[test_CFRMessageProvider.js]
+[test_InflightAssetsMessageProvider.js]
+[test_OnboardingMessageProvider.js]
+[test_PanelTestProvider.js]
+[test_reach_experiments.js]
+[test_remoteExperiments.js]
+[test_AboutWelcomeTelemetry_glean.js]