From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../components/newtab/test/xpcshell/ds_layout.json | 89 +++++ browser/components/newtab/test/xpcshell/head.js | 105 ++++++ .../xpcshell/test_ASRouterTargeting_attribution.js | 98 ++++++ .../xpcshell/test_ASRouterTargeting_snapshot.js | 138 ++++++++ .../test_ASRouter_getTargetingParameters.js | 73 +++++ .../xpcshell/test_AboutHomeStartupCacheChild.js | 33 ++ .../xpcshell/test_AboutHomeStartupCacheWorker.js | 251 ++++++++++++++ .../newtab/test/xpcshell/test_AboutNewTab.js | 359 +++++++++++++++++++++ .../test/xpcshell/test_AboutWelcomeAttribution.js | 69 ++++ .../test/xpcshell/test_AboutWelcomeTelemetry.js | 101 ++++++ .../xpcshell/test_AboutWelcomeTelemetry_glean.js | 143 ++++++++ .../test/xpcshell/test_CFRMessageProvider.js | 32 ++ .../xpcshell/test_InflightAssetsMessageProvider.js | 41 +++ .../xpcshell/test_OnboardingMessageProvider.js | 229 +++++++++++++ .../newtab/test/xpcshell/test_PanelTestProvider.js | 83 +++++ .../newtab/test/xpcshell/test_reach_experiments.js | 97 ++++++ .../newtab/test/xpcshell/test_remoteExperiments.js | 37 +++ .../newtab/test/xpcshell/topstories.json | 53 +++ .../components/newtab/test/xpcshell/xpcshell.ini | 32 ++ 19 files changed, 2063 insertions(+) create mode 100644 browser/components/newtab/test/xpcshell/ds_layout.json create mode 100644 browser/components/newtab/test/xpcshell/head.js create mode 100644 browser/components/newtab/test/xpcshell/test_ASRouterTargeting_attribution.js create mode 100644 browser/components/newtab/test/xpcshell/test_ASRouterTargeting_snapshot.js create mode 100644 browser/components/newtab/test/xpcshell/test_ASRouter_getTargetingParameters.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheChild.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutNewTab.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js create mode 100644 browser/components/newtab/test/xpcshell/test_CFRMessageProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_InflightAssetsMessageProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_OnboardingMessageProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_PanelTestProvider.js create mode 100644 browser/components/newtab/test/xpcshell/test_reach_experiments.js create mode 100644 browser/components/newtab/test/xpcshell/test_remoteExperiments.js create mode 100644 browser/components/newtab/test/xpcshell/topstories.json create mode 100644 browser/components/newtab/test/xpcshell/xpcshell.ini (limited to 'browser/components/newtab/test/xpcshell') 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] -- cgit v1.2.3