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/test_AboutHomeStartupCacheChild.js33
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js255
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutNewTab.js363
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js69
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js90
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js238
-rw-r--r--browser/components/newtab/test/xpcshell/test_HighlightsFeed.js1402
-rw-r--r--browser/components/newtab/test/xpcshell/test_PlacesFeed.js1812
-rw-r--r--browser/components/newtab/test/xpcshell/test_Store.js453
-rw-r--r--browser/components/newtab/test/xpcshell/test_TelemetryFeed.js3285
-rw-r--r--browser/components/newtab/test/xpcshell/test_TopSitesFeed.js3397
-rw-r--r--browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js2023
-rw-r--r--browser/components/newtab/test/xpcshell/topstories.json53
-rw-r--r--browser/components/newtab/test/xpcshell/xpcshell.toml34
14 files changed, 13507 insertions, 0 deletions
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..ce1ee48caa
--- /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.importESModule(
+ "resource:///modules/AboutNewTabService.sys.mjs"
+);
+
+/**
+ * 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..d1f75b78bf
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js
@@ -0,0 +1,255 @@
+/* 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"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { DiscoveryStreamFeed } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/DiscoveryStreamFeed.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const { AboutNewTab } = ChromeUtils.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+const { PREFS_CONFIG } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/ActivityStream.sys.mjs"
+);
+
+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",
+ })
+ );
+
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(DiscoveryStreamFeed.prototype, "generateFeedUrl")
+ .returns("http://example.com/topstories.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(defaultDSConfig)
+ );
+
+ // 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
+ );
+
+ // 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 20 placeholders.
+ equal(
+ Array.from(root.querySelectorAll(".ds-card")).length,
+ 21,
+ "There are 21 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, 20, "There should be 20 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..1eb8081d25
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutNewTab.js
@@ -0,0 +1,363 @@
+/* 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 { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { AboutNewTab } = ChromeUtils.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+
+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..3d83f473d5
--- /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.importESModule(
+ "resource:///modules/aboutwelcome/AboutWelcomeDefaults.sys.mjs"
+);
+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..b8339fb39f
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AboutWelcomeTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs"
+);
+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();
+ 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();
+
+ 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..5191f05d04
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AboutWelcomeTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs"
+);
+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",
+ screen_family: "family",
+ screen_id: "screen_id",
+ screen_index: 0,
+ screen_initlals: "screen_initials",
+ };
+ 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.eventScreenFamily.testGetValue(),
+ eventContext.screen_family,
+ "event_context.screen_family also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenId.testGetValue(),
+ eventContext.screen_id,
+ "event_context.screen_id also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenIndex.testGetValue(),
+ eventContext.screen_index,
+ "event_context.screen_index also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenInitials.testGetValue(),
+ eventContext.screen_initials,
+ "event_context.screen_initials 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.eventScreenFamily.testGetValue(),
+ eventContext.screen_family,
+ "event_context.screen_family also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenId.testGetValue(),
+ eventContext.screen_id,
+ "event_context.screen_id also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenIndex.testGetValue(),
+ eventContext.screen_index,
+ "event_context.screen_index also in own metric."
+ );
+ Assert.equal(
+ Glean.messagingSystem.eventScreenInitials.testGetValue(),
+ eventContext.screen_initials,
+ "event_context.screen_initials 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");
+});
+
+// For event_context to be more useful, we want to make sure we don't error
+// in cases where it doesn't make much sense, such as a plain string that
+// doesnt attempt to represent a valid object.
+add_task(function test_context_errors() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ });
+ Services.prefs.setBoolPref(TELEMETRY_PREF, true);
+
+ const AWTelemetry = new AboutWelcomeTelemetry();
+
+ let weird_context_ping = {
+ event_context: "oops, this string isn't a valid JS object!",
+ };
+
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventContextParseError.testGetValue(),
+ undefined,
+ "this poorly formed context shouldn't register because it was not an object!"
+ );
+ });
+
+ AWTelemetry.submitGleanPingForPing(weird_context_ping);
+
+ Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
+
+ weird_context_ping = {
+ event_context:
+ "{oops : {'this string isn't a valid JS object, but it sure looks like one!}}'",
+ };
+
+ pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+ Assert.equal(
+ Glean.messagingSystem.eventContextParseError.testGetValue(),
+ 1,
+ "this poorly formed context should register because it was not an object!"
+ );
+ });
+
+ AWTelemetry.submitGleanPingForPing(weird_context_ping);
+
+ Assert.ok(pingSubmitted, "Ping with unknown keys was submitted");
+});
diff --git a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js
new file mode 100644
index 0000000000..233eb6df73
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js
@@ -0,0 +1,1402 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { actionTypes: at } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+ PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
+ Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs",
+ SectionsManager: "resource://activity-stream/lib/SectionsManager.sys.mjs",
+ shortURL: "resource://activity-stream/lib/ShortURL.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const {
+ HighlightsFeed,
+ SYNC_BOOKMARKS_FINISHED_EVENT,
+ BOOKMARKS_RESTORE_SUCCESS_EVENT,
+ BOOKMARKS_RESTORE_FAILED_EVENT,
+ SECTION_ID,
+} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm");
+
+const FAKE_LINKS = new Array(20)
+ .fill(null)
+ .map((v, i) => ({ url: `http://www.site${i}.com` }));
+const FAKE_IMAGE = "data123";
+const FAKE_URL = "https://mozilla.org";
+const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg";
+
+function getHighlightsFeedForTest(sandbox) {
+ let feed = new HighlightsFeed();
+ feed.store = {
+ dispatch: sandbox.spy(),
+ getState() {
+ return this.state;
+ },
+ state: {
+ Prefs: {
+ values: {
+ "section.highlights.includePocket": false,
+ "section.highlights.includeDownloads": false,
+ },
+ },
+ TopSites: {
+ initialized: true,
+ rows: Array(12)
+ .fill(null)
+ .map((v, i) => ({ url: `http://www.topsite${i}.com` })),
+ },
+ Sections: [{ id: "highlights", initialized: false }],
+ },
+ subscribe: sandbox.stub().callsFake(cb => {
+ cb();
+ return () => {};
+ }),
+ };
+
+ sandbox
+ .stub(NewTabUtils.activityStreamLinks, "getHighlights")
+ .resolves(FAKE_LINKS);
+ sandbox
+ .stub(NewTabUtils.activityStreamLinks, "deletePocketEntry")
+ .resolves({});
+ sandbox
+ .stub(NewTabUtils.activityStreamLinks, "archivePocketEntry")
+ .resolves({});
+ sandbox
+ .stub(NewTabUtils.activityStreamProvider, "_processHighlights")
+ .callsFake(l => l.slice(0, 1));
+
+ return feed;
+}
+
+async function fetchHighlightsRows(feed, options) {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(SectionsManager, "updateSection");
+ await feed.fetchHighlights(options);
+ let [, { rows }] = SectionsManager.updateSection.firstCall.args;
+
+ sandbox.restore();
+ return rows;
+}
+
+function fetchImage(feed, page) {
+ return feed.fetchImage(
+ Object.assign({ __sharedCache: { updateLink() {} } }, page)
+ );
+}
+
+add_task(function test_construction() {
+ info("HighlightsFeed construction should work");
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(PageThumbs, "addExpirationFilter");
+
+ let feed = getHighlightsFeedForTest(sandbox);
+ Assert.ok(feed, "Was able to create a HighlightsFeed");
+
+ info("HighlightsFeed construction should add a PageThumbs expiration filter");
+ Assert.ok(
+ PageThumbs.addExpirationFilter.calledOnce,
+ "PageThumbs.addExpirationFilter was called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(function test_init_action() {
+ let sandbox = sinon.createSandbox();
+
+ let countObservers = topic => {
+ return [...Services.obs.enumerateObservers(topic)].length;
+ };
+
+ const INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT = countObservers(
+ SYNC_BOOKMARKS_FINISHED_EVENT
+ );
+ const INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT = countObservers(
+ BOOKMARKS_RESTORE_SUCCESS_EVENT
+ );
+ const INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT = countObservers(
+ BOOKMARKS_RESTORE_FAILED_EVENT
+ );
+
+ sandbox
+ .stub(SectionsManager, "onceInitialized")
+ .callsFake(callback => callback());
+ sandbox.stub(SectionsManager, "enableSection");
+
+ let feed = getHighlightsFeedForTest(sandbox);
+ sandbox.stub(feed, "fetchHighlights");
+ sandbox.stub(feed.downloadsManager, "init");
+
+ feed.onAction({ type: at.INIT });
+
+ info("HighlightsFeed.onAction(INIT) should add a sync observer");
+ Assert.equal(
+ countObservers(SYNC_BOOKMARKS_FINISHED_EVENT),
+ INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT + 1
+ );
+ Assert.equal(
+ countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT),
+ INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT + 1
+ );
+ Assert.equal(
+ countObservers(BOOKMARKS_RESTORE_FAILED_EVENT),
+ INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT + 1
+ );
+
+ info(
+ "HighlightsFeed.onAction(INIT) should call SectionsManager.onceInitialized"
+ );
+ Assert.ok(
+ SectionsManager.onceInitialized.calledOnce,
+ "SectionsManager.onceInitialized was called"
+ );
+
+ info("HighlightsFeed.onAction(INIT) should enable its section");
+ Assert.ok(
+ SectionsManager.enableSection.calledOnce,
+ "SectionsManager.enableSection was called"
+ );
+ Assert.ok(SectionsManager.enableSection.calledWith(SECTION_ID));
+
+ info("HighlightsFeed.onAction(INIT) should fetch highlights");
+ Assert.ok(
+ feed.fetchHighlights.calledOnce,
+ "HighlightsFeed.fetchHighlights was called"
+ );
+
+ info("HighlightsFeed.onAction(INIT) should initialize the DownloadsManager");
+ Assert.ok(
+ feed.downloadsManager.init.calledOnce,
+ "HighlightsFeed.downloadsManager.init was called"
+ );
+
+ feed.uninit();
+ // Let's make sure that uninit also removed these observers while we're here.
+ Assert.equal(
+ countObservers(SYNC_BOOKMARKS_FINISHED_EVENT),
+ INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT
+ );
+ Assert.equal(
+ countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT),
+ INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT
+ );
+ Assert.equal(
+ countObservers(BOOKMARKS_RESTORE_FAILED_EVENT),
+ INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_observe_fetch_highlights() {
+ let topicDataPairs = [
+ {
+ description:
+ "should fetch highlights when we are done a sync for bookmarks",
+ shouldFetch: true,
+ topic: SYNC_BOOKMARKS_FINISHED_EVENT,
+ data: "bookmarks",
+ },
+ {
+ description: "should fetch highlights after a successful import",
+ shouldFetch: true,
+ topic: BOOKMARKS_RESTORE_SUCCESS_EVENT,
+ data: "html",
+ },
+ {
+ description: "should fetch highlights after a failed import",
+ shouldFetch: true,
+ topic: BOOKMARKS_RESTORE_FAILED_EVENT,
+ data: "json",
+ },
+ {
+ description:
+ "should not fetch highlights when we are doing a sync for something that is not bookmarks",
+ shouldFetch: false,
+ topic: SYNC_BOOKMARKS_FINISHED_EVENT,
+ data: "tabs",
+ },
+ {
+ description: "should not fetch highlights after a successful import",
+ shouldFetch: false,
+ topic: "someotherevent",
+ data: "bookmarks",
+ },
+ ];
+
+ for (let topicDataPair of topicDataPairs) {
+ info(`HighlightsFeed.observe ${topicDataPair.description}`);
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ sandbox.stub(feed, "fetchHighlights");
+ feed.observe(null, topicDataPair.topic, topicDataPair.data);
+
+ if (topicDataPair.shouldFetch) {
+ Assert.ok(
+ feed.fetchHighlights.calledOnce,
+ "HighlightsFeed.fetchHighlights was called"
+ );
+ Assert.ok(feed.fetchHighlights.calledWith({ broadcast: true }));
+ } else {
+ Assert.ok(
+ feed.fetchHighlights.notCalled,
+ "HighlightsFeed.fetchHighlights was not called"
+ );
+ }
+
+ sandbox.restore();
+ }
+});
+
+add_task(async function test_filterForThumbnailExpiration_calls() {
+ info(
+ "HighlightsFeed.filterForThumbnailExpiration should pass rows.urls " +
+ "to the callback provided"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ let rows = [{ url: "foo.com" }, { url: "bar.com" }];
+
+ feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }];
+ const stub = sinon.stub();
+
+ feed.filterForThumbnailExpiration(stub);
+
+ Assert.ok(stub.calledOnce, "Filter was called");
+ Assert.ok(stub.calledWithExactly(rows.map(r => r.url)));
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_filterForThumbnailExpiration_include_preview_image_url() {
+ info(
+ "HighlightsFeed.filterForThumbnailExpiration should include " +
+ "preview_image_url (if present) in the callback results"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ let rows = [
+ { url: "foo.com" },
+ { url: "bar.com", preview_image_url: "bar.jpg" },
+ ];
+
+ feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }];
+ const stub = sinon.stub();
+
+ feed.filterForThumbnailExpiration(stub);
+
+ Assert.ok(stub.calledOnce, "Filter was called");
+ Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "bar.jpg"]));
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_filterForThumbnailExpiration_not_initialized() {
+ info(
+ "HighlightsFeed.filterForThumbnailExpiration should pass an empty " +
+ "array if not initialized"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ let rows = [{ url: "foo.com" }, { url: "bar.com" }];
+
+ feed.store.state.Sections = [{ rows, initialized: false }];
+ const stub = sinon.stub();
+
+ feed.filterForThumbnailExpiration(stub);
+
+ Assert.ok(stub.calledOnce, "Filter was called");
+ Assert.ok(stub.calledWithExactly([]));
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_TopSites_not_initialized() {
+ info(
+ "HighlightsFeed.fetchHighlights should return early if TopSites are not " +
+ "initialized"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.spy(feed.linksCache, "request");
+
+ feed.store.state.TopSites.initialized = false;
+ feed.store.state.Prefs.values["feeds.topsites"] = true;
+ feed.store.state.Prefs.values["feeds.system.topsites"] = true;
+
+ // Initially TopSites is uninitialised and fetchHighlights should return.
+ await feed.fetchHighlights();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getHighlights.notCalled,
+ "NewTabUtils.activityStreamLinks.getHighlights was not called"
+ );
+ Assert.ok(
+ feed.linksCache.request.notCalled,
+ "HighlightsFeed.linksCache.request was not called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_sections_not_initialized() {
+ info(
+ "HighlightsFeed.fetchHighlights should return early if Sections are not " +
+ "initialized"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.spy(feed.linksCache, "request");
+
+ feed.store.state.TopSites.initialized = true;
+ feed.store.state.Prefs.values["feeds.topsites"] = true;
+ feed.store.state.Prefs.values["feeds.system.topsites"] = true;
+ feed.store.state.Sections = [];
+
+ await feed.fetchHighlights();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getHighlights.notCalled,
+ "NewTabUtils.activityStreamLinks.getHighlights was not called"
+ );
+ Assert.ok(
+ feed.linksCache.request.notCalled,
+ "HighlightsFeed.linksCache.request was not called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_TopSites_initialized() {
+ info(
+ "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites are " +
+ "initialised"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.spy(feed.linksCache, "request");
+
+ // fetchHighlights should continue
+ feed.store.state.TopSites.initialized = true;
+
+ await feed.fetchHighlights();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getHighlights.calledOnce,
+ "NewTabUtils.activityStreamLinks.getHighlights was called"
+ );
+ Assert.ok(
+ feed.linksCache.request.calledOnce,
+ "HighlightsFeed.linksCache.request was called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_chronological_order() {
+ info(
+ "HighlightsFeed.fetchHighlights should chronologically order highlight " +
+ "data types"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [
+ {
+ url: "https://site0.com",
+ type: "bookmark",
+ bookmarkGuid: "1234",
+ date_added: Date.now() - 80,
+ }, // 3rd newest
+ {
+ url: "https://site1.com",
+ type: "history",
+ bookmarkGuid: "1234",
+ date_added: Date.now() - 60,
+ }, // append at the end
+ {
+ url: "https://site2.com",
+ type: "history",
+ date_added: Date.now() - 160,
+ }, // append at the end
+ {
+ url: "https://site3.com",
+ type: "history",
+ date_added: Date.now() - 60,
+ }, // append at the end
+ { url: "https://site4.com", type: "pocket", date_added: Date.now() }, // newest highlight
+ {
+ url: "https://site5.com",
+ type: "pocket",
+ date_added: Date.now() - 100,
+ }, // 4th newest
+ {
+ url: "https://site6.com",
+ type: "bookmark",
+ bookmarkGuid: "1234",
+ date_added: Date.now() - 40,
+ }, // 2nd newest
+ ];
+ let expectedChronological = [4, 6, 0, 5];
+ let expectedHistory = [1, 2, 3];
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ [...expectedChronological, ...expectedHistory].forEach((link, index) => {
+ Assert.equal(
+ highlights[index].url,
+ links[link].url,
+ `highlight[${index}] should be link[${link}]`
+ );
+ });
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_TopSites_not_enabled() {
+ info(
+ "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " +
+ "are not enabled"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.spy(feed.linksCache, "request");
+
+ feed.store.state.Prefs.values["feeds.system.topsites"] = false;
+
+ await feed.fetchHighlights();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getHighlights.calledOnce,
+ "NewTabUtils.activityStreamLinks.getHighlights was called"
+ );
+ Assert.ok(
+ feed.linksCache.request.calledOnce,
+ "HighlightsFeed.linksCache.request was called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_TopSites_not_shown() {
+ info(
+ "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " +
+ "are not shown on NTP"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.spy(feed.linksCache, "request");
+
+ feed.store.state.Prefs.values["feeds.topsites"] = false;
+
+ await feed.fetchHighlights();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getHighlights.calledOnce,
+ "NewTabUtils.activityStreamLinks.getHighlights was called"
+ );
+ Assert.ok(
+ feed.linksCache.request.calledOnce,
+ "HighlightsFeed.linksCache.request was called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_add_hostname_hasImage() {
+ info(
+ "HighlightsFeed.fetchHighlights should add shortURL hostname and hasImage to each link"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [{ url: "https://mozilla.org" }];
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights[0].hostname, shortURL(links[0]));
+ Assert.equal(highlights[0].hasImage, true);
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_add_existing_image() {
+ info(
+ "HighlightsFeed.fetchHighlights should add an existing image if it " +
+ "exists to the link without calling fetchImage"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [{ url: "https://mozilla.org", image: FAKE_IMAGE }];
+ sandbox.spy(feed, "fetchImage");
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights[0].image, FAKE_IMAGE);
+ Assert.ok(feed.fetchImage.notCalled, "HighlightsFeed.fetchImage not called");
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_correct_args() {
+ info(
+ "HighlightsFeed.fetchHighlights should call fetchImage with the correct " +
+ "arguments for new links"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [
+ {
+ url: "https://mozilla.org",
+ preview_image_url: "https://mozilla.org/preview.jog",
+ },
+ ];
+ sandbox.spy(feed, "fetchImage");
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ await fetchHighlightsRows(feed);
+
+ Assert.ok(feed.fetchImage.calledOnce, "HighlightsFeed.fetchImage called");
+
+ let [arg] = feed.fetchImage.firstCall.args;
+ Assert.equal(arg.url, links[0].url);
+ Assert.equal(arg.preview_image_url, links[0].preview_image_url);
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_fetchHighlights_not_include_links_already_in_TopSites() {
+ info(
+ "HighlightsFeed.fetchHighlights should not include any links already in " +
+ "Top Sites"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [
+ { url: "https://mozilla.org" },
+ { url: "http://www.topsite0.com" },
+ { url: "http://www.topsite1.com" },
+ { url: "http://www.topsite2.com" },
+ ];
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 1);
+ Assert.equal(highlights[0].url, links[0].url);
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_fetchHighlights_not_include_history_already_in_TopSites() {
+ info(
+ "HighlightsFeed.fetchHighlights should include bookmark but not " +
+ "history already in Top Sites"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [
+ { url: "http://www.topsite0.com", type: "bookmark" },
+ { url: "http://www.topsite1.com", type: "history" },
+ ];
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 1);
+ Assert.equal(highlights[0].url, links[0].url);
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_fetchHighlights_not_include_history_same_hostname_as_bookmark() {
+ info(
+ "HighlightsFeed.fetchHighlights should not include history of same " +
+ "hostname as a bookmark"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [
+ { url: "https://site.com/bookmark", type: "bookmark" },
+ { url: "https://site.com/history", type: "history" },
+ ];
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 1);
+ Assert.equal(highlights[0].url, links[0].url);
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_fetchHighlights_take_first_history_of_hostname() {
+ info(
+ "HighlightsFeed.fetchHighlights should take the first history of a hostname"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [
+ { url: "https://site.com/first", type: "history" },
+ { url: "https://site.com/second", type: "history" },
+ { url: "https://other", type: "history" },
+ ];
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 2);
+ Assert.equal(highlights[0].url, links[0].url);
+ Assert.equal(highlights[1].url, links[2].url);
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_fetchHighlights_take_bookmark_pocket_download_of_same_hostname() {
+ info(
+ "HighlightsFeed.fetchHighlights should take a bookmark, a pocket, and " +
+ "downloaded item of the same hostname"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let links = [
+ { url: "https://site.com/bookmark", type: "bookmark" },
+ { url: "https://site.com/pocket", type: "pocket" },
+ { url: "https://site.com/download", type: "download" },
+ ];
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 3);
+ Assert.equal(highlights[0].url, links[0].url);
+ Assert.equal(highlights[1].url, links[1].url);
+ Assert.equal(highlights[2].url, links[2].url);
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_fetchHighlights_include_pocket_items() {
+ info(
+ "HighlightsFeed.fetchHighlights should includePocket pocket items when " +
+ "pref is true"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values["section.highlights.includePocket"] = true;
+ sandbox.spy(feed.linksCache, "request");
+
+ await fetchHighlightsRows(feed);
+
+ Assert.ok(
+ feed.linksCache.request.calledOnce,
+ "HighlightsFeed.linksCache.request called"
+ );
+ Assert.ok(
+ !feed.linksCache.request.firstCall.args[0].excludePocket,
+ "Should not be excluding Pocket items"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_do_not_include_pocket_items() {
+ info(
+ "HighlightsFeed.fetchHighlights should not includePocket pocket items " +
+ "when pref is false"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values["section.highlights.includePocket"] = false;
+ sandbox.spy(feed.linksCache, "request");
+
+ await fetchHighlightsRows(feed);
+
+ Assert.ok(
+ feed.linksCache.request.calledOnce,
+ "HighlightsFeed.linksCache.request called"
+ );
+ Assert.ok(
+ feed.linksCache.request.firstCall.args[0].excludePocket,
+ "Should be excluding Pocket items"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_do_not_include_downloads() {
+ info(
+ "HighlightsFeed.fetchHighlights should not include downloads when " +
+ "includeDownloads pref is false"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values["section.highlights.includeDownloads"] = false;
+ feed.downloadsManager.getDownloads = () => [
+ { url: "https://site1.com/download" },
+ { url: "https://site2.com/download" },
+ ];
+
+ let links = [
+ { url: "https://site.com/bookmark", type: "bookmark" },
+ { url: "https://site.com/pocket", type: "pocket" },
+ ];
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 2);
+ Assert.equal(highlights[0].url, links[0].url);
+ Assert.equal(highlights[1].url, links[1].url);
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_include_downloads() {
+ info(
+ "HighlightsFeed.fetchHighlights should include downloads when " +
+ "includeDownloads pref is true"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
+ feed.downloadsManager.getDownloads = () => [
+ { url: "https://site.com/download" },
+ ];
+
+ let links = [
+ { url: "https://site.com/bookmark", type: "bookmark" },
+ { url: "https://site.com/pocket", type: "pocket" },
+ ];
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 3);
+ Assert.equal(highlights[0].url, links[0].url);
+ Assert.equal(highlights[1].url, links[1].url);
+ Assert.equal(highlights[2].url, "https://site.com/download");
+ Assert.equal(highlights[2].type, "download");
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_take_one_download() {
+ info("HighlightsFeed.fetchHighlights should only take 1 download");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
+ feed.downloadsManager.getDownloads = () => [
+ { url: "https://site1.com/download" },
+ { url: "https://site2.com/download" },
+ ];
+
+ let links = [{ url: "https://site.com/bookmark", type: "bookmark" }];
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 2);
+ Assert.equal(highlights[0].url, links[0].url);
+ Assert.equal(highlights[1].url, "https://site1.com/download");
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_chronological_sort() {
+ info(
+ "HighlightsFeed.fetchHighlights should sort bookmarks, pocket, " +
+ "and downloads chronologically"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
+ feed.downloadsManager.getDownloads = () => [
+ {
+ url: "https://site1.com/download",
+ type: "download",
+ date_added: Date.now(),
+ },
+ ];
+
+ let links = [
+ {
+ url: "https://site.com/bookmark",
+ type: "bookmark",
+ date_added: Date.now() - 10000,
+ },
+ {
+ url: "https://site2.com/pocket",
+ type: "pocket",
+ date_added: Date.now() - 5000,
+ },
+ {
+ url: "https://site3.com/visited",
+ type: "history",
+ date_added: Date.now(),
+ },
+ ];
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights.length, 4);
+ Assert.equal(highlights[0].url, "https://site1.com/download");
+ Assert.equal(highlights[1].url, links[1].url);
+ Assert.equal(highlights[2].url, links[0].url);
+ Assert.equal(highlights[3].url, links[2].url); // history item goes last
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_fetchHighlights_set_type_to_bookmark_on_bookmarkGuid() {
+ info(
+ "HighlightsFeed.fetchHighlights should set type to bookmark if there " +
+ "is a bookmarkGuid"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values["section.highlights.includeBookmarks"] = true;
+ feed.downloadsManager.getDownloads = () => [
+ {
+ url: "https://site1.com/download",
+ type: "download",
+ date_added: Date.now(),
+ },
+ ];
+
+ let links = [
+ {
+ url: "https://mozilla.org",
+ type: "history",
+ bookmarkGuid: "1234567890",
+ },
+ ];
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights[0].type, "bookmark");
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_fetchHighlights_keep_history_type_on_bookmarkGuid() {
+ info(
+ "HighlightsFeed.fetchHighlights should keep history type if there is a " +
+ "bookmarkGuid but don't include bookmarks"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.store.state.Prefs.values[
+ "section.highlights.includeBookmarks"
+ ] = false;
+
+ let links = [
+ {
+ url: "https://mozilla.org",
+ type: "history",
+ bookmarkGuid: "1234567890",
+ },
+ ];
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
+
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.equal(highlights[0].type, "history");
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_fetchHighlights_filter_adult() {
+ info("HighlightsFeed.fetchHighlights should filter out adult pages");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.stub(FilterAdult, "filter").returns([]);
+ let highlights = await fetchHighlightsRows(feed);
+
+ Assert.ok(FilterAdult.filter.calledOnce, "FilterAdult.filter called");
+ Assert.equal(highlights.length, 0);
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchHighlights_no_expose_internal_link_props() {
+ info(
+ "HighlightsFeed.fetchHighlights should not expose internal link properties"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ let highlights = await fetchHighlightsRows(feed);
+ let internal = Object.keys(highlights[0]).filter(key => key.startsWith("__"));
+
+ Assert.equal(internal.join(""), "");
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_fetchHighlights_broadcast_when_feed_not_initialized() {
+ info(
+ "HighlightsFeed.fetchHighlights should broadcast if feed is not initialized"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves([]);
+ sandbox.stub(SectionsManager, "updateSection");
+ await feed.fetchHighlights();
+
+ Assert.ok(
+ SectionsManager.updateSection.calledOnce,
+ "SectionsManager.updateSection called once"
+ );
+ Assert.ok(
+ SectionsManager.updateSection.calledWithExactly(
+ SECTION_ID,
+ { rows: [] },
+ true,
+ undefined
+ )
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_fetchHighlights_broadcast_on_broadcast_in_options() {
+ info(
+ "HighlightsFeed.fetchHighlights should broadcast if options.broadcast is true"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves([]);
+ feed.store.state.Sections[0].initialized = true;
+
+ sandbox.stub(SectionsManager, "updateSection");
+ await feed.fetchHighlights({ broadcast: true });
+
+ Assert.ok(
+ SectionsManager.updateSection.calledOnce,
+ "SectionsManager.updateSection called once"
+ );
+ Assert.ok(
+ SectionsManager.updateSection.calledWithExactly(
+ SECTION_ID,
+ { rows: [] },
+ true,
+ undefined
+ )
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_fetchHighlights_no_broadcast() {
+ info(
+ "HighlightsFeed.fetchHighlights should not broadcast if " +
+ "options.broadcast is false and initialized is true"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ NewTabUtils.activityStreamLinks.getHighlights.resolves([]);
+ feed.store.state.Sections[0].initialized = true;
+
+ sandbox.stub(SectionsManager, "updateSection");
+ await feed.fetchHighlights({ broadcast: false });
+
+ Assert.ok(
+ SectionsManager.updateSection.calledOnce,
+ "SectionsManager.updateSection called once"
+ );
+ Assert.ok(
+ SectionsManager.updateSection.calledWithExactly(
+ SECTION_ID,
+ { rows: [] },
+ false,
+ undefined
+ )
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_fetchImage_capture_if_available() {
+ info("HighlightsFeed.fetchImage should capture the image, if available");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.stub(Screenshots, "getScreenshotForURL");
+ sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
+
+ await fetchImage(feed, {
+ preview_image_url: FAKE_IMAGE_URL,
+ url: FAKE_URL,
+ });
+
+ Assert.ok(
+ Screenshots.getScreenshotForURL.calledOnce,
+ "Screenshots.getScreenshotForURL called once"
+ );
+ Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_IMAGE_URL));
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchImage_fallback_to_screenshot() {
+ info("HighlightsFeed.fetchImage should fall back to capturing a screenshot");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.stub(Screenshots, "getScreenshotForURL");
+ sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
+
+ await fetchImage(feed, { url: FAKE_URL });
+
+ Assert.ok(
+ Screenshots.getScreenshotForURL.calledOnce,
+ "Screenshots.getScreenshotForURL called once"
+ );
+ Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_URL));
+
+ sandbox.restore();
+});
+
+add_task(async function test_fetchImage_updateSectionCard_args() {
+ info(
+ "HighlightsFeed.fetchImage should call " +
+ "SectionsManager.updateSectionCard with the right arguments"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.stub(SectionsManager, "updateSectionCard");
+ sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE);
+ sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
+
+ await fetchImage(feed, {
+ preview_image_url: FAKE_IMAGE_URL,
+ url: FAKE_URL,
+ });
+ Assert.ok(
+ SectionsManager.updateSectionCard.calledOnce,
+ "SectionsManager.updateSectionCard called"
+ );
+ Assert.ok(
+ SectionsManager.updateSectionCard.calledWith(
+ "highlights",
+ FAKE_URL,
+ { image: FAKE_IMAGE },
+ true
+ )
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_fetchImage_no_update_card_with_image() {
+ info("HighlightsFeed.fetchImage should not update the card with the image");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.stub(SectionsManager, "updateSectionCard");
+ sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE);
+ sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
+
+ let card = {
+ preview_image_url: FAKE_IMAGE_URL,
+ url: FAKE_URL,
+ };
+ await fetchImage(feed, card);
+ Assert.ok(!card.image, "Image not set on card");
+ sandbox.restore();
+});
+
+add_task(async function test_uninit_disable_section() {
+ info("HighlightsFeed.onAction(UNINIT) should disable its section");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.init();
+
+ sandbox.stub(SectionsManager, "disableSection");
+ feed.onAction({ type: at.UNINIT });
+ Assert.ok(
+ SectionsManager.disableSection.calledOnce,
+ "SectionsManager.disableSection called"
+ );
+ Assert.ok(SectionsManager.disableSection.calledWith(SECTION_ID));
+ sandbox.restore();
+});
+
+add_task(async function test_uninit_remove_expiration_filter() {
+ info("HighlightsFeed.onAction(UNINIT) should remove the expiration filter");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ feed.init();
+
+ sandbox.stub(PageThumbs, "removeExpirationFilter");
+ feed.onAction({ type: at.UNINIT });
+ Assert.ok(
+ PageThumbs.removeExpirationFilter.calledOnce,
+ "PageThumbs.removeExpirationFilter called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_relay_to_DownloadsManager_onAction() {
+ info(
+ "HighlightsFeed.onAction should relay all actions to " +
+ "DownloadsManager.onAction"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+ sandbox.stub(feed.downloadsManager, "onAction");
+
+ let action = {
+ type: at.COPY_DOWNLOAD_LINK,
+ data: { url: "foo.png" },
+ _target: {},
+ };
+ feed.onAction(action);
+
+ Assert.ok(
+ feed.downloadsManager.onAction.calledOnce,
+ "HighlightsFeed.downloadManager.onAction called"
+ );
+ Assert.ok(feed.downloadsManager.onAction.calledWith(action));
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_fetch_highlights_on_SYSTEM_TICK() {
+ info("HighlightsFeed.onAction should fetch highlights on SYSTEM_TICK");
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ await feed.fetchHighlights();
+
+ sandbox.spy(feed, "fetchHighlights");
+ feed.onAction({ type: at.SYSTEM_TICK });
+
+ Assert.ok(
+ feed.fetchHighlights.calledOnce,
+ "HighlightsFeed.fetchHighlights called"
+ );
+ Assert.ok(
+ feed.fetchHighlights.calledWithExactly({
+ broadcast: false,
+ isStartup: false,
+ })
+ );
+ sandbox.restore();
+});
+
+add_task(
+ async function test_onAction_fetch_highlights_on_PREF_CHANGED_for_include() {
+ info(
+ "HighlightsFeed.onAction should fetch highlights on PREF_CHANGED " +
+ "for include prefs"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.spy(feed, "fetchHighlights");
+ feed.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: "section.highlights.includeBookmarks" },
+ });
+
+ Assert.ok(
+ feed.fetchHighlights.calledOnce,
+ "HighlightsFeed.fetchHighlights called"
+ );
+ Assert.ok(feed.fetchHighlights.calledWithExactly({ broadcast: true }));
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_onAction_no_fetch_highlights_on_PREF_CHANGED_for_other() {
+ info(
+ "HighlightsFeed.onAction should not fetch highlights on PREF_CHANGED " +
+ "for other prefs"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ sandbox.spy(feed, "fetchHighlights");
+ feed.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: "section.topstories.pocketCta" },
+ });
+
+ Assert.ok(
+ feed.fetchHighlights.notCalled,
+ "HighlightsFeed.fetchHighlights not called"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_onAction_fetch_highlights_on_actions() {
+ info("HighlightsFeed.onAction should fetch highlights for various actions");
+
+ let actions = [
+ {
+ actionType: "PLACES_HISTORY_CLEARED",
+ expectsExpire: false,
+ expectsBroadcast: true,
+ },
+ {
+ actionType: "DOWNLOAD_CHANGED",
+ expectsExpire: false,
+ expectsBroadcast: true,
+ },
+ {
+ actionType: "PLACES_LINKS_CHANGED",
+ expectsExpire: true,
+ expectsBroadcast: false,
+ },
+ {
+ actionType: "PLACES_LINK_BLOCKED",
+ expectsExpire: false,
+ expectsBroadcast: true,
+ },
+ {
+ actionType: "PLACES_SAVED_TO_POCKET",
+ expectsExpire: true,
+ expectsBroadcast: false,
+ },
+ ];
+ for (let action of actions) {
+ info(
+ `HighlightsFeed.onAction should fetch highlights on ${action.actionType}`
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ await feed.fetchHighlights();
+ sandbox.spy(feed, "fetchHighlights");
+ sandbox.stub(feed.linksCache, "expire");
+
+ feed.onAction({ type: at[action.actionType] });
+ Assert.ok(
+ feed.fetchHighlights.calledOnce,
+ "HighlightsFeed.fetchHighlights called"
+ );
+ Assert.ok(
+ feed.fetchHighlights.calledWith({ broadcast: action.expectsBroadcast })
+ );
+
+ if (action.expectsExpire) {
+ Assert.ok(
+ feed.linksCache.expire.calledOnce,
+ "HighlightsFeed.linksCache.expire called"
+ );
+ }
+
+ sandbox.restore();
+ }
+});
+
+add_task(
+ async function test_onAction_fetch_highlights_no_broadcast_on_TOP_SITES_UPDATED() {
+ info(
+ "HighlightsFeed.onAction should fetch highlights with broadcast " +
+ "false on TOP_SITES_UPDATED"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ await feed.fetchHighlights();
+ sandbox.spy(feed, "fetchHighlights");
+
+ feed.onAction({ type: at.TOP_SITES_UPDATED });
+ Assert.ok(
+ feed.fetchHighlights.calledOnce,
+ "HighlightsFeed.fetchHighlights called"
+ );
+ Assert.ok(
+ feed.fetchHighlights.calledWithExactly({
+ broadcast: false,
+ isStartup: false,
+ })
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_onAction_fetch_highlights_on_deleting_archiving_pocket() {
+ info(
+ "HighlightsFeed.onAction should call fetchHighlights when deleting " +
+ "or archiving from Pocket"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let feed = getHighlightsFeedForTest(sandbox);
+
+ await feed.fetchHighlights();
+ sandbox.spy(feed, "fetchHighlights");
+
+ feed.onAction({
+ type: at.POCKET_LINK_DELETED_OR_ARCHIVED,
+ data: { pocket_id: 12345 },
+ });
+ Assert.ok(
+ feed.fetchHighlights.calledOnce,
+ "HighlightsFeed.fetchHighlights called"
+ );
+ Assert.ok(feed.fetchHighlights.calledWithExactly({ broadcast: true }));
+
+ sandbox.restore();
+ }
+);
diff --git a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js
new file mode 100644
index 0000000000..8e7c42d639
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js
@@ -0,0 +1,1812 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { actionTypes: at, actionCreators: ac } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
+ pktApi: "chrome://pocket/content/pktApi.sys.mjs",
+ PlacesFeed: "resource://activity-stream/lib/PlacesFeed.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ SearchService: "resource://gre/modules/SearchService.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+const { PlacesObserver } = PlacesFeed;
+
+const FAKE_BOOKMARK = {
+ bookmarkGuid: "D3r1sKRobtbW",
+ bookmarkTitle: "Foo",
+ dateAdded: 123214232,
+ url: "foo.com",
+};
+const TYPE_BOOKMARK = 1; // This is fake, for testing
+const SOURCES = {
+ DEFAULT: 0,
+ SYNC: 1,
+ IMPORT: 2,
+ RESTORE: 5,
+ RESTORE_ON_STARTUP: 6,
+};
+
+// The event dispatched in NewTabUtils when a link is blocked;
+const BLOCKED_EVENT = "newtab-linkBlocked";
+
+const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
+const POCKET_SITE_PREF = "extensions.pocket.site";
+
+function getPlacesFeedForTest(sandbox) {
+ let feed = new PlacesFeed();
+ feed.store = {
+ dispatch: sandbox.spy(),
+ feeds: {
+ get: sandbox.stub(),
+ },
+ };
+
+ sandbox.stub(AboutNewTab, "activityStream").value({
+ store: feed.store,
+ });
+
+ return feed;
+}
+
+add_task(async function test_construction() {
+ info("PlacesFeed construction should work");
+ let feed = new PlacesFeed();
+ Assert.ok(feed, "PlacesFeed could be constructed.");
+});
+
+add_task(async function test_PlacesObserver() {
+ info("PlacesFeed should have a PlacesObserver that dispatches to the store");
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+
+ let action = { type: "FOO" };
+ feed.placesObserver.dispatch(action);
+
+ await TestUtils.waitForTick();
+ Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store dispatch called");
+ Assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
+
+ sandbox.restore();
+});
+
+add_task(async function test_addToBlockedTopSitesSponsors_add_to_blocklist() {
+ info(
+ "PlacesFeed.addToBlockedTopSitesSponsors should add the blocked sponsors " +
+ "to the blocklist"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ feed.addToBlockedTopSitesSponsors([
+ { url: "test.com" },
+ { url: "test1.com" },
+ ]);
+
+ let blockedSponsors = JSON.parse(
+ Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF)
+ );
+ Assert.deepEqual(
+ new Set(["foo", "bar", "test", "test1"]),
+ new Set(blockedSponsors)
+ );
+
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
+
+add_task(async function test_addToBlockedTopSitesSponsors_no_dupes() {
+ info(
+ "PlacesFeed.addToBlockedTopSitesSponsors should not add duplicate " +
+ "sponsors to the blocklist"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ feed.addToBlockedTopSitesSponsors([
+ { url: "foo.com" },
+ { url: "bar.com" },
+ { url: "test.com" },
+ ]);
+
+ let blockedSponsors = JSON.parse(
+ Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF)
+ );
+ Assert.deepEqual(new Set(["foo", "bar", "test"]), new Set(blockedSponsors));
+
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_PlacesEvents() {
+ info(
+ "PlacesFeed.onAction should add bookmark, history, places, blocked " +
+ "observers on INIT"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed.placesObserver, "handlePlacesEvent");
+
+ feed.onAction({ type: at.INIT });
+ // The PlacesObserver registration happens at the next tick of the
+ // event loop.
+ await TestUtils.waitForTick();
+
+ // These are some dummy PlacesEvents that we'll pass through the
+ // PlacesObserver service, checking that the handlePlacesEvent receives them
+ // properly.
+ let notifications = [
+ new PlacesBookmarkAddition({
+ dateAdded: 0,
+ guid: "dQFSYrbM5SJN",
+ id: -1,
+ index: 0,
+ isTagging: false,
+ itemType: 1,
+ parentGuid: "n_HOEFys1qsL",
+ parentId: -2,
+ source: 0,
+ title: "test-123",
+ tags: "tags",
+ url: "http://example.com/test-123",
+ frecency: 0,
+ hidden: false,
+ visitCount: 0,
+ lastVisitDate: 0,
+ targetFolderGuid: null,
+ targetFolderItemId: -1,
+ targetFolderTitle: null,
+ }),
+ new PlacesBookmarkRemoved({
+ id: -1,
+ url: "http://example.com/test-123",
+ title: "test-123",
+ itemType: 1,
+ parentId: -2,
+ index: 0,
+ guid: "M3WYgJlm2Jlx",
+ parentGuid: "DO1f97R4KC3Y",
+ source: 0,
+ isTagging: false,
+ isDescendantRemoval: false,
+ }),
+ new PlacesHistoryCleared(),
+ new PlacesVisitRemoved({
+ url: "http://example.com/test-123",
+ pageGuid: "sPVcW2V4H7Rg",
+ reason: PlacesVisitRemoved.REASON_DELETED,
+ transitionType: 0,
+ isRemovedFromStore: true,
+ isPartialVisistsRemoval: false,
+ }),
+ ];
+
+ for (let notification of notifications) {
+ PlacesUtils.observers.notifyListeners([notification]);
+ Assert.ok(
+ feed.placesObserver.handlePlacesEvent.calledOnce,
+ "PlacesFeed.handlePlacesEvent called"
+ );
+ Assert.ok(feed.placesObserver.handlePlacesEvent.calledWith([notification]));
+ feed.placesObserver.handlePlacesEvent.resetHistory();
+ }
+
+ info(
+ "PlacesFeed.onAction remove bookmark, history, places, blocked " +
+ "observers, and timers on UNINIT"
+ );
+
+ let placesChangedTimerCancel = sandbox.spy();
+ feed.placesChangedTimer = {
+ cancel: placesChangedTimerCancel,
+ };
+
+ // Unlike INIT, UNINIT removes the observers synchronously, so no need to
+ // wait for the event loop to tick around again.
+ feed.onAction({ type: at.UNINIT });
+
+ for (let notification of notifications) {
+ PlacesUtils.observers.notifyListeners([notification]);
+ Assert.ok(
+ feed.placesObserver.handlePlacesEvent.notCalled,
+ "PlacesFeed.handlePlacesEvent not called"
+ );
+ feed.placesObserver.handlePlacesEvent.resetHistory();
+ }
+
+ Assert.equal(feed.placesChangedTimer, null);
+ Assert.ok(placesChangedTimerCancel.calledOnce);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_BLOCK_URL() {
+ info("PlacesFeed.onAction should block a url on BLOCK_URL");
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(NewTabUtils.activityStreamLinks, "blockURL");
+
+ feed.onAction({
+ type: at.BLOCK_URL,
+ data: [{ url: "apple.com", pocket_id: 1234 }],
+ });
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.blockURL.calledWith({
+ url: "apple.com",
+ pocket_id: 1234,
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_BLOCK_URL_topsites_sponsors() {
+ info(
+ "PlacesFeed.onAction BLOCK_URL should update the blocked top " +
+ "sites sponsors"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "addToBlockedTopSitesSponsors");
+
+ feed.onAction({
+ type: at.BLOCK_URL,
+ data: [{ url: "foo.com", pocket_id: 1234, isSponsoredTopSite: 1 }],
+ });
+ Assert.ok(feed.addToBlockedTopSitesSponsors.calledWith([{ url: "foo.com" }]));
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_BOOKMARK_URL() {
+ info("PlacesFeed.onAction should bookmark a url on BOOKMARK_URL");
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(NewTabUtils.activityStreamLinks, "addBookmark");
+
+ let data = { url: "pear.com", title: "A pear" };
+ let _target = { browser: { ownerGlobal() {} } };
+ feed.onAction({ type: at.BOOKMARK_URL, data, _target });
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.addBookmark.calledWith(
+ data,
+ _target.browser.ownerGlobal
+ )
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_DELETE_BOOKMARK_BY_ID() {
+ info("PlacesFeed.onAction should delete a bookmark on DELETE_BOOKMARK_BY_ID");
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(NewTabUtils.activityStreamLinks, "deleteBookmark");
+
+ feed.onAction({ type: at.DELETE_BOOKMARK_BY_ID, data: "g123kd" });
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.deleteBookmark.calledWith("g123kd")
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_DELETE_HISTORY_URL() {
+ info(
+ "PlacesFeed.onAction should delete a history entry on DELETE_HISTORY_URL"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(NewTabUtils.activityStreamLinks, "deleteHistoryEntry");
+ sandbox.stub(NewTabUtils.activityStreamLinks, "blockURL");
+
+ feed.onAction({
+ type: at.DELETE_HISTORY_URL,
+ data: { url: "guava.com", forceBlock: null },
+ });
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.deleteHistoryEntry.calledWith("guava.com")
+ );
+ Assert.ok(NewTabUtils.activityStreamLinks.blockURL.notCalled);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_DELETE_HISTORY_URL_and_block() {
+ info(
+ "PlacesFeed.onAction should delete a history entry on " +
+ "DELETE_HISTORY_URL and force a site to be blocked if specified"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(NewTabUtils.activityStreamLinks, "deleteHistoryEntry");
+ sandbox.stub(NewTabUtils.activityStreamLinks, "blockURL");
+
+ feed.onAction({
+ type: at.DELETE_HISTORY_URL,
+ data: { url: "guava.com", forceBlock: "g123kd" },
+ });
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.deleteHistoryEntry.calledWith("guava.com")
+ );
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.blockURL.calledWith({
+ url: "guava.com",
+ pocket_id: undefined,
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_OPEN_NEW_WINDOW() {
+ info(
+ "PlacesFeed.onAction should call openTrustedLinkIn with the " +
+ "correct url, where and params on OPEN_NEW_WINDOW"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub();
+ let openWindowAction = {
+ type: at.OPEN_NEW_WINDOW,
+ data: { url: "https://foo.com" },
+ _target: { browser: { ownerGlobal: { openTrustedLinkIn } } },
+ };
+
+ feed.onAction(openWindowAction);
+
+ Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
+ let [url, where, params] = openTrustedLinkIn.firstCall.args;
+ Assert.equal(url, "https://foo.com");
+ Assert.equal(where, "window");
+ Assert.ok(!params.private);
+ Assert.ok(!params.forceForeground);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_OPEN_PRIVATE_WINDOW() {
+ info(
+ "PlacesFeed.onAction should call openTrustedLinkIn with the " +
+ "correct url, where, params and privacy args on OPEN_PRIVATE_WINDOW"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub();
+ let openWindowAction = {
+ type: at.OPEN_PRIVATE_WINDOW,
+ data: { url: "https://foo.com" },
+ _target: { browser: { ownerGlobal: { openTrustedLinkIn } } },
+ };
+
+ feed.onAction(openWindowAction);
+
+ Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
+ let [url, where, params] = openTrustedLinkIn.firstCall.args;
+ Assert.equal(url, "https://foo.com");
+ Assert.equal(where, "window");
+ Assert.ok(params.private);
+ Assert.ok(!params.forceForeground);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_OPEN_LINK() {
+ info(
+ "PlacesFeed.onAction should call openTrustedLinkIn with the " +
+ "correct url, where and params on OPEN_LINK"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub();
+ let openLinkAction = {
+ type: at.OPEN_LINK,
+ data: { url: "https://foo.com" },
+ _target: {
+ browser: {
+ ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "current" },
+ },
+ },
+ };
+ feed.onAction(openLinkAction);
+
+ Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
+ let [url, where, params] = openTrustedLinkIn.firstCall.args;
+ Assert.equal(url, "https://foo.com");
+ Assert.equal(where, "current");
+ Assert.ok(!params.private);
+ Assert.ok(!params.forceForeground);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_OPEN_LINK_referrer() {
+ info("PlacesFeed.onAction should open link with referrer on OPEN_LINK");
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub();
+ let openLinkAction = {
+ type: at.OPEN_LINK,
+ data: { url: "https://foo.com", referrer: "https://foo.com/ref" },
+ _target: {
+ browser: {
+ ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "tab" },
+ },
+ },
+ };
+ feed.onAction(openLinkAction);
+
+ Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
+ let [, , params] = openTrustedLinkIn.firstCall.args;
+ Assert.equal(params.referrerInfo.referrerPolicy, 5);
+ Assert.equal(
+ params.referrerInfo.originalReferrer.spec,
+ "https://foo.com/ref"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_OPEN_LINK_typed_bonus() {
+ info(
+ "PlacesFeed.onAction should mark link with typed bonus as " +
+ "typed before opening OPEN_LINK"
+ );
+ let sandbox = sinon.createSandbox();
+ let callOrder = [];
+ // We can't stub out PlacesUtils.history.markPageAsTyped, since that's an
+ // XPCOM component. We'll stub out history instead.
+ sandbox.stub(PlacesUtils, "history").get(() => {
+ return {
+ markPageAsTyped: sandbox.stub().callsFake(() => {
+ callOrder.push("markPageAsTyped");
+ }),
+ };
+ });
+
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub().callsFake(() => {
+ callOrder.push("openTrustedLinkIn");
+ });
+ let openLinkAction = {
+ type: at.OPEN_LINK,
+ data: {
+ typedBonus: true,
+ url: "https://foo.com",
+ },
+ _target: {
+ browser: {
+ ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "tab" },
+ },
+ },
+ };
+ feed.onAction(openLinkAction);
+
+ Assert.deepEqual(callOrder, ["markPageAsTyped", "openTrustedLinkIn"]);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_OPEN_LINK_pocket() {
+ info(
+ "PlacesFeed.onAction should open the pocket link if it's a " +
+ "pocket story on OPEN_LINK"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub();
+ let openLinkAction = {
+ type: at.OPEN_LINK,
+ data: {
+ url: "https://foo.com",
+ open_url: "https://getpocket.com/foo",
+ type: "pocket",
+ },
+ _target: {
+ browser: {
+ ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "current" },
+ },
+ },
+ };
+
+ feed.onAction(openLinkAction);
+
+ Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
+ let [url, where, params] = openTrustedLinkIn.firstCall.args;
+ Assert.equal(url, "https://getpocket.com/foo");
+ Assert.equal(where, "current");
+ Assert.ok(!params.private);
+ Assert.ok(!params.forceForeground);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_OPEN_LINK_not_http() {
+ info("PlacesFeed.onAction should not open link if not http");
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub();
+ let openLinkAction = {
+ type: at.OPEN_LINK,
+ data: { url: "file:///foo.com" },
+ _target: {
+ browser: {
+ ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "current" },
+ },
+ },
+ };
+
+ feed.onAction(openLinkAction);
+
+ Assert.ok(openTrustedLinkIn.notCalled, "openTrustedLinkIn not called");
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_FILL_SEARCH_TERM() {
+ info(
+ "PlacesFeed.onAction should call fillSearchTopSiteTerm " +
+ "on FILL_SEARCH_TERM"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "fillSearchTopSiteTerm");
+
+ feed.onAction({ type: at.FILL_SEARCH_TERM });
+
+ Assert.ok(
+ feed.fillSearchTopSiteTerm.calledOnce,
+ "PlacesFeed.fillSearchTopSiteTerm called"
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_ABOUT_SPONSORED_TOP_SITES() {
+ info(
+ "PlacesFeed.onAction should call openTrustedLinkIn with the " +
+ "correct SUMO url on ABOUT_SPONSORED_TOP_SITES"
+ );
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+ let openTrustedLinkIn = sandbox.stub();
+ let openLinkAction = {
+ type: at.ABOUT_SPONSORED_TOP_SITES,
+ _target: {
+ browser: {
+ ownerGlobal: { openTrustedLinkIn },
+ },
+ },
+ };
+
+ feed.onAction(openLinkAction);
+
+ Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
+ let [url, where] = openTrustedLinkIn.firstCall.args;
+ Assert.ok(url.endsWith("sponsor-privacy"));
+ Assert.equal(where, "tab");
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_FILL_SEARCH_TERM() {
+ info(
+ "PlacesFeed.onAction should set the URL bar value to the label value " +
+ "on FILL_SEARCH_TERM"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(SearchService.prototype, "getEngineByAlias").resolves(null);
+
+ let feed = getPlacesFeedForTest(sandbox);
+ let locationBar = { search: sandbox.stub() };
+ let action = {
+ type: at.FILL_SEARCH_TERM,
+ data: { label: "@Foo" },
+ _target: { browser: { ownerGlobal: { gURLBar: locationBar } } },
+ };
+
+ await feed.onAction(action);
+
+ Assert.ok(locationBar.search.calledOnce, "gURLBar.search called");
+ Assert.ok(
+ locationBar.search.calledWithExactly("@Foo", {
+ searchEngine: null,
+ searchModeEntry: "topsites_newtab",
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_SAVE_TO_POCKET() {
+ info("PlacesFeed.onAction should call saveToPocket on SAVE_TO_POCKET");
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "saveToPocket");
+
+ let action = {
+ type: at.SAVE_TO_POCKET,
+ data: { site: { url: "raspberry.com", title: "raspberry" } },
+ _target: { browser: {} },
+ };
+
+ await feed.onAction(action);
+
+ Assert.ok(feed.saveToPocket.calledOnce, "PlacesFeed.saveToPocket called");
+ Assert.ok(
+ feed.saveToPocket.calledWithExactly(
+ action.data.site,
+ action._target.browser
+ )
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_SAVE_TO_POCKET_not_logged_in() {
+ info(
+ "PlacesFeed.onAction should openTrustedLinkIn with sendToPocket " +
+ "if not logged in on SAVE_TO_POCKET"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(pktApi, "isUserLoggedIn").returns(false);
+ sandbox.stub(NimbusFeatures.pocketNewtab, "getVariable").returns(true);
+ sandbox.stub(ExperimentAPI, "getExperiment").returns({
+ slug: "slug",
+ branch: { slug: "branch-slug" },
+ });
+ Services.prefs.setStringPref(POCKET_SITE_PREF, "getpocket.com");
+
+ let feed = getPlacesFeedForTest(sandbox);
+
+ let openTrustedLinkIn = sandbox.stub();
+ let action = {
+ type: at.SAVE_TO_POCKET,
+ data: { site: { url: "raspberry.com", title: "raspberry" } },
+ _target: {
+ browser: {
+ ownerGlobal: {
+ openTrustedLinkIn,
+ },
+ },
+ },
+ };
+
+ await feed.onAction(action);
+
+ Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
+ let [url, where] = openTrustedLinkIn.firstCall.args;
+ Assert.equal(
+ url,
+ "https://getpocket.com/signup?utm_source=firefox_newtab_save_button&utm_campaign=slug&utm_content=branch-slug"
+ );
+ Assert.equal(where, "tab");
+
+ Services.prefs.clearUserPref(POCKET_SITE_PREF);
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_SAVE_TO_POCKET_logged_in() {
+ info(
+ "PlacesFeed.onAction should call " +
+ "NewTabUtils.activityStreamLinks.addPocketEntry if we are saving a " +
+ "pocket story on SAVE_TO_POCKET"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(pktApi, "isUserLoggedIn").returns(true);
+ sandbox.stub(NewTabUtils.activityStreamLinks, "addPocketEntry");
+
+ let feed = getPlacesFeedForTest(sandbox);
+
+ let openTrustedLinkIn = sandbox.stub();
+ let action = {
+ type: at.SAVE_TO_POCKET,
+ data: { site: { url: "raspberry.com", title: "raspberry" } },
+ _target: {
+ browser: {
+ ownerGlobal: {
+ openTrustedLinkIn,
+ },
+ },
+ },
+ };
+
+ await feed.onAction(action);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.addPocketEntry.calledOnce,
+ "NewTabUtils.activityStreamLinks.addPocketEntry called"
+ );
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.addPocketEntry.calledWithExactly(
+ action.data.site.url,
+ action.data.site.title,
+ action._target.browser
+ )
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_saveToPocket_addPocketEntry_rejects() {
+ info(
+ "PlacesFeed.saveToPocket should still resolve if " +
+ "NewTabUtils.activityStreamLinks.addPocketEntry rejects"
+ );
+ let sandbox = sinon.createSandbox();
+ let e = new Error("Error");
+
+ sandbox.stub(NewTabUtils.activityStreamLinks, "addPocketEntry").rejects(e);
+
+ let feed = getPlacesFeedForTest(sandbox);
+
+ let openTrustedLinkIn = sandbox.stub();
+ let action = {
+ data: { site: { url: "raspberry.com", title: "raspberry" } },
+ _target: {
+ browser: {
+ ownerGlobal: {
+ openTrustedLinkIn,
+ },
+ },
+ },
+ };
+
+ try {
+ await feed.saveToPocket(action.data.site, action._target.browser);
+ Assert.ok(true, "PlacesFeed.saveToPocket Promise resolved");
+ } catch {
+ Assert.ok(false, "PlacesFeed.saveToPocket Promise rejected");
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_saveToPocket_broadcast_to_content() {
+ info(
+ "PlacesFeed.saveToPocket should broadcast to content if we " +
+ "successfully added a link to Pocket"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(pktApi, "isUserLoggedIn").returns(true);
+
+ sandbox
+ .stub(NewTabUtils.activityStreamLinks, "addPocketEntry")
+ .resolves({ item: { open_url: "pocket.com/itemID", item_id: 1234 } });
+
+ let feed = getPlacesFeedForTest(sandbox);
+
+ let openTrustedLinkIn = sandbox.stub();
+ let action = {
+ data: { site: { url: "raspberry.com", title: "raspberry" } },
+ _target: {
+ browser: {
+ ownerGlobal: {
+ openTrustedLinkIn,
+ },
+ },
+ },
+ };
+
+ await feed.saveToPocket(action.data.site, action._target.browser);
+ Assert.ok(
+ feed.store.dispatch.calledOnce,
+ "PlacesFeed.store.dispatch was called"
+ );
+ Assert.equal(
+ feed.store.dispatch.firstCall.args[0].type,
+ at.PLACES_SAVED_TO_POCKET
+ );
+ Assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
+ url: "raspberry.com",
+ title: "raspberry",
+ pocket_id: 1234,
+ open_url: "pocket.com/itemID",
+ });
+
+ sandbox.restore();
+});
+
+add_task(async function test_saveToPocket_broadcast_only_on_data() {
+ info(
+ "PlacesFeed.saveToPocket should broadcast to content if we " +
+ "successfully added a link to Pocket"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(pktApi, "isUserLoggedIn").returns(true);
+
+ sandbox
+ .stub(NewTabUtils.activityStreamLinks, "addPocketEntry")
+ .resolves(null);
+
+ let feed = getPlacesFeedForTest(sandbox);
+
+ let openTrustedLinkIn = sandbox.stub();
+ let action = {
+ data: { site: { url: "raspberry.com", title: "raspberry" } },
+ _target: {
+ browser: {
+ ownerGlobal: {
+ openTrustedLinkIn,
+ },
+ },
+ },
+ };
+
+ await feed.saveToPocket(action.data.site, action._target.browser);
+ Assert.ok(
+ feed.store.dispatch.notCalled,
+ "PlacesFeed.store.dispatch was not called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_DELETE_FROM_POCKET() {
+ info(
+ "PlacesFeed.onAction should call deleteFromPocket on DELETE_FROM_POCKET"
+ );
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "deleteFromPocket");
+
+ feed.onAction({
+ type: at.DELETE_FROM_POCKET,
+ data: { pocket_id: 12345 },
+ });
+
+ Assert.ok(
+ feed.deleteFromPocket.calledOnce,
+ "PlacesFeed.deleteFromPocket called"
+ );
+ Assert.ok(feed.deleteFromPocket.calledWithExactly(12345));
+
+ sandbox.restore();
+});
+
+add_task(async function test_deleteFromPocket_resolves() {
+ info(
+ "PlacesFeed.deleteFromPocket should still resolve if deletePocketEntry " +
+ "rejects"
+ );
+ let sandbox = sinon.createSandbox();
+ let e = new Error("Error");
+ sandbox.stub(NewTabUtils.activityStreamLinks, "deletePocketEntry").rejects(e);
+
+ let feed = getPlacesFeedForTest(sandbox);
+ await feed.deleteFromPocket(12345);
+
+ try {
+ await feed.deleteFromPocket(12345);
+ Assert.ok(true, "PlacesFeed.deleteFromPocket Promise resolved");
+ } catch {
+ Assert.ok(false, "PlacesFeed.deleteFromPocket Promise rejected");
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_deleteFromPocket_calls_deletePocketEntry() {
+ info(
+ "PlacesFeed.deleteFromPocket should call " +
+ "NewTabUtils.deletePocketEntry and dispatch " +
+ "POCKET_LINK_DELETED_OR_ARCHIVED when deleting from Pocket"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.activityStreamLinks, "deletePocketEntry");
+
+ let feed = getPlacesFeedForTest(sandbox);
+ await feed.deleteFromPocket(12345);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.deletePocketEntry.calledOnce,
+ "NewTabUtils.activityStreamLinks.deletePocketEntry called"
+ );
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.deletePocketEntry.calledWithExactly(12345)
+ );
+ Assert.ok(
+ feed.store.dispatch.calledOnce,
+ "PlacesFeed.store.dispatch was called"
+ );
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly({
+ type: at.POCKET_LINK_DELETED_OR_ARCHIVED,
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_ARCHIVE_FROM_POCKET() {
+ info(
+ "PlacesFeed.onAction should call archiveFromPocket on ARCHIVE_FROM_POCKET"
+ );
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "archiveFromPocket");
+
+ await feed.onAction({
+ type: at.ARCHIVE_FROM_POCKET,
+ data: { pocket_id: 12345 },
+ });
+
+ Assert.ok(
+ feed.archiveFromPocket.calledOnce,
+ "PlacesFeed.archiveFromPocket called"
+ );
+ Assert.ok(feed.archiveFromPocket.calledWithExactly(12345));
+
+ sandbox.restore();
+});
+
+add_task(async function test_archiveFromPocket_resolves() {
+ info(
+ "PlacesFeed.archiveFromPocket should resolve if archivePocketEntry rejects"
+ );
+ let sandbox = sinon.createSandbox();
+ let e = new Error("Error");
+ sandbox
+ .stub(NewTabUtils.activityStreamLinks, "archivePocketEntry")
+ .rejects(e);
+
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "archiveFromPocket");
+
+ try {
+ await feed.archiveFromPocket(12345);
+ Assert.ok(true, "PlacesFeed.archiveFromPocket Promise resolved");
+ } catch {
+ Assert.ok(false, "PlacesFeed.archiveFromPocket Promise rejected");
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_archiveFromPocket_calls_archivePocketEntry() {
+ info(
+ "PlacesFeed.archiveFromPocket should call " +
+ "NewTabUtils.archivePocketEntry and dispatch " +
+ "POCKET_LINK_DELETED_OR_ARCHIVED when deleting from Pocket"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.activityStreamLinks, "archivePocketEntry");
+
+ let feed = getPlacesFeedForTest(sandbox);
+ await feed.archiveFromPocket(12345);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.archivePocketEntry.calledOnce,
+ "NewTabUtils.activityStreamLinks.archivePocketEntry called"
+ );
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.archivePocketEntry.calledWithExactly(12345)
+ );
+ Assert.ok(
+ feed.store.dispatch.calledOnce,
+ "PlacesFeed.store.dispatch was called"
+ );
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly({
+ type: at.POCKET_LINK_DELETED_OR_ARCHIVED,
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_HANDOFF_SEARCH_TO_AWESOMEBAR() {
+ info(
+ "PlacesFeed.onAction should call handoffSearchToAwesomebar " +
+ "on HANDOFF_SEARCH_TO_AWESOMEBAR"
+ );
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "handoffSearchToAwesomebar");
+
+ let action = {
+ type: at.HANDOFF_SEARCH_TO_AWESOMEBAR,
+ data: { text: "f" },
+ meta: { fromTarget: {} },
+ _target: { browser: { ownerGlobal: { gURLBar: { focus: () => {} } } } },
+ };
+
+ await feed.onAction(action);
+
+ Assert.ok(
+ feed.handoffSearchToAwesomebar.calledOnce,
+ "PlacesFeed.handoffSearchToAwesomebar called"
+ );
+ Assert.ok(feed.handoffSearchToAwesomebar.calledWithExactly(action));
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_PARTNER_LINK_ATTRIBUTION() {
+ info(
+ "PlacesFeed.onAction should call makeAttributionRequest on " +
+ "PARTNER_LINK_ATTRIBUTION"
+ );
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(feed, "makeAttributionRequest");
+
+ let data = { targetURL: "https://partnersite.com", source: "topsites" };
+ feed.onAction({
+ type: at.PARTNER_LINK_ATTRIBUTION,
+ data,
+ });
+
+ Assert.ok(
+ feed.makeAttributionRequest.calledOnce,
+ "PlacesFeed.makeAttributionRequest called"
+ );
+ Assert.ok(feed.makeAttributionRequest.calledWithExactly(data));
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_makeAttributionRequest_PartnerLinkAttribution_makeReq() {
+ info(
+ "PlacesFeed.makeAttributionRequest should call " +
+ "PartnerLinkAttribution.makeRequest"
+ );
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ sandbox.stub(PartnerLinkAttribution, "makeRequest");
+
+ let data = { targetURL: "https://partnersite.com", source: "topsites" };
+ feed.makeAttributionRequest(data);
+
+ Assert.ok(
+ PartnerLinkAttribution.makeRequest.calledOnce,
+ "PartnerLinkAttribution.makeRequest called"
+ );
+
+ sandbox.restore();
+ }
+);
+
+function createFakeURLBar(sandbox) {
+ let fakeURLBar = {
+ focus: sandbox.spy(),
+ handoff: sandbox.spy(),
+ setHiddenFocus: sandbox.spy(),
+ removeHiddenFocus: sandbox.spy(),
+ addEventListener: (ev, cb) => {
+ fakeURLBar.listeners[ev] = cb;
+ },
+ removeEventListener: sandbox.spy(),
+
+ listeners: [],
+ };
+
+ return fakeURLBar;
+}
+
+add_task(async function test_handoffSearchToAwesomebar_no_text() {
+ info(
+ "PlacesFeed.handoffSearchToAwesomebar should properly handle handoff " +
+ "with no text passed in"
+ );
+
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ let fakeURLBar = createFakeURLBar(sandbox);
+
+ sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
+ sandbox.stub(feed, "_getDefaultSearchEngine").returns(null);
+
+ feed.handoffSearchToAwesomebar({
+ _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
+ data: {},
+ meta: { fromTarget: {} },
+ });
+
+ Assert.ok(
+ fakeURLBar.setHiddenFocus.calledOnce,
+ "gURLBar.setHiddenFocus called"
+ );
+ Assert.ok(fakeURLBar.handoff.notCalled, "gURLBar.handoff not called");
+ Assert.ok(
+ feed.store.dispatch.notCalled,
+ "PlacesFeed.store.dispatch not called"
+ );
+
+ // Now type a character.
+ fakeURLBar.listeners.keydown({ key: "f" });
+ Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff called");
+ Assert.ok(
+ fakeURLBar.removeHiddenFocus.calledOnce,
+ "gURLBar.removeHiddenFocus called"
+ );
+ Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
+ Assert.ok(
+ feed.store.dispatch.calledWith({
+ meta: {
+ from: "ActivityStream:Main",
+ skipMain: true,
+ to: "ActivityStream:Content",
+ toTarget: {},
+ },
+ type: "DISABLE_SEARCH",
+ }),
+ "PlacesFeed.store.dispatch called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_handoffSearchToAwesomebar_with_text() {
+ info(
+ "PlacesFeed.handoffSearchToAwesomebar should properly handle handoff " +
+ "with text data passed in"
+ );
+
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ let fakeURLBar = createFakeURLBar(sandbox);
+
+ sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
+ let engine = {};
+ sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
+
+ const SESSION_ID = "decafc0ffee";
+ AboutNewTab.activityStream.store.feeds.get.returns({
+ sessions: {
+ get: () => {
+ return { session_id: SESSION_ID };
+ },
+ },
+ });
+
+ feed.handoffSearchToAwesomebar({
+ _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
+ data: { text: "foo" },
+ meta: { fromTarget: {} },
+ });
+
+ Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
+ Assert.ok(fakeURLBar.handoff.calledWithExactly("foo", engine, SESSION_ID));
+ Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
+ Assert.ok(
+ fakeURLBar.setHiddenFocus.notCalled,
+ "gURLBar.setHiddenFocus not called"
+ );
+
+ // Now call blur listener.
+ fakeURLBar.listeners.blur();
+ Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
+ Assert.ok(
+ feed.store.dispatch.calledWith({
+ meta: {
+ from: "ActivityStream:Main",
+ skipMain: true,
+ to: "ActivityStream:Content",
+ toTarget: {},
+ },
+ type: "SHOW_SEARCH",
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_handoffSearchToAwesomebar_with_text_pb_mode() {
+ info(
+ "PlacesFeed.handoffSearchToAwesomebar should properly handle handoff " +
+ "with text data passed in, in private browsing mode"
+ );
+
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ let fakeURLBar = createFakeURLBar(sandbox);
+
+ sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(true);
+ let engine = {};
+ sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
+
+ feed.handoffSearchToAwesomebar({
+ _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
+ data: { text: "foo" },
+ meta: { fromTarget: {} },
+ });
+ Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
+ Assert.ok(fakeURLBar.handoff.calledWithExactly("foo", engine, undefined));
+ Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
+ Assert.ok(
+ fakeURLBar.setHiddenFocus.notCalled,
+ "gURLBar.setHiddenFocus not called"
+ );
+
+ // Now call blur listener.
+ fakeURLBar.listeners.blur();
+ Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
+ Assert.ok(
+ feed.store.dispatch.calledWith({
+ meta: {
+ from: "ActivityStream:Main",
+ skipMain: true,
+ to: "ActivityStream:Content",
+ toTarget: {},
+ },
+ type: "SHOW_SEARCH",
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_handoffSearchToAwesomebar_SHOW_SEARCH_on_esc() {
+ info(
+ "PlacesFeed.handoffSearchToAwesomebar should SHOW_SEARCH on ESC keydown"
+ );
+
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ let fakeURLBar = createFakeURLBar(sandbox);
+
+ sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
+ let engine = {};
+ sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
+
+ feed.handoffSearchToAwesomebar({
+ _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
+ data: { text: "foo" },
+ meta: { fromTarget: {} },
+ });
+ Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
+ Assert.ok(fakeURLBar.handoff.calledWithExactly("foo", engine, undefined));
+ Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
+
+ // Now call ESC keydown.
+ fakeURLBar.listeners.keydown({ key: "Escape" });
+ Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
+ Assert.ok(
+ feed.store.dispatch.calledWith({
+ meta: {
+ from: "ActivityStream:Main",
+ skipMain: true,
+ to: "ActivityStream:Content",
+ toTarget: {},
+ },
+ type: "SHOW_SEARCH",
+ })
+ );
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_handoffSearchToAwesomebar_with_session_id_no_text() {
+ info(
+ "PlacesFeed.handoffSearchToAwesomebar should properly handoff a " +
+ "newtab session id with no text passed in"
+ );
+
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ let fakeURLBar = createFakeURLBar(sandbox);
+
+ sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
+ let engine = {};
+ sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
+
+ const SESSION_ID = "decafc0ffee";
+ AboutNewTab.activityStream.store.feeds.get.returns({
+ sessions: {
+ get: () => {
+ return { session_id: SESSION_ID };
+ },
+ },
+ });
+
+ feed.handoffSearchToAwesomebar({
+ _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
+ data: {},
+ meta: { fromTarget: {} },
+ });
+
+ Assert.ok(
+ fakeURLBar.setHiddenFocus.calledOnce,
+ "gURLBar.setHiddenFocus was called"
+ );
+ Assert.ok(fakeURLBar.handoff.notCalled, "gURLBar.handoff not called");
+ Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
+ Assert.ok(
+ feed.store.dispatch.notCalled,
+ "PlacesFeed.store.dispatch not called"
+ );
+
+ // Now type a character.
+ fakeURLBar.listeners.keydown({ key: "f" });
+ Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
+ Assert.ok(fakeURLBar.handoff.calledWithExactly("", engine, SESSION_ID));
+
+ Assert.ok(
+ fakeURLBar.removeHiddenFocus.calledOnce,
+ "gURLBar.removeHiddenFocus was called"
+ );
+ Assert.ok(
+ feed.store.dispatch.calledOnce,
+ "PlacesFeed.store.dispatch called"
+ );
+ Assert.ok(
+ feed.store.dispatch.calledWith({
+ meta: {
+ from: "ActivityStream:Main",
+ skipMain: true,
+ to: "ActivityStream:Content",
+ toTarget: {},
+ },
+ type: "DISABLE_SEARCH",
+ })
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_observe_dispatch_PLACES_LINK_BLOCKED() {
+ info(
+ "PlacesFeed.observe should dispatch a PLACES_LINK_BLOCKED action " +
+ "with the url of the blocked link"
+ );
+
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ feed.observe(null, BLOCKED_EVENT, "foo123.com");
+ Assert.equal(
+ feed.store.dispatch.firstCall.args[0].type,
+ at.PLACES_LINK_BLOCKED
+ );
+ Assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
+ url: "foo123.com",
+ });
+
+ sandbox.restore();
+});
+
+add_task(async function test_observe_no_dispatch() {
+ info(
+ "PlacesFeed.observe should not call dispatch if the topic is something " +
+ "other than BLOCKED_EVENT"
+ );
+
+ let sandbox = sinon.createSandbox();
+
+ let feed = getPlacesFeedForTest(sandbox);
+ feed.observe(null, "someotherevent");
+ Assert.ok(
+ feed.store.dispatch.notCalled,
+ "PlacesFeed.store.dispatch not called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_handlePlacesEvent_dispatch_one_PLACES_LINKS_CHANGED() {
+ let events = [
+ {
+ message:
+ "PlacesFeed.handlePlacesEvent should only dispatch 1 PLACES_LINKS_CHANGED action " +
+ "if many bookmark-added notifications happened at once",
+ dispatchCallCount: 5,
+ event: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.DEFAULT,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "https://www.foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ },
+ {
+ message:
+ "PlacesFeed.handlePlacesEvent should only dispatch 1 " +
+ "PLACES_LINKS_CHANGED action if many onItemRemoved notifications " +
+ "happened at once",
+ dispatchCallCount: 5,
+ event: {
+ id: null,
+ parentId: null,
+ index: null,
+ itemType: TYPE_BOOKMARK,
+ url: "foo.com",
+ guid: "rTU_oiklsU7D",
+ parentGuid: "2BzBQXOPFmuU",
+ source: SOURCES.DEFAULT,
+ type: "bookmark-removed",
+ },
+ },
+ {
+ message:
+ "PlacesFeed.handlePlacesEvent should only dispatch 1 " +
+ "PLACES_LINKS_CHANGED action if any page-removed notifications " +
+ "happened at once",
+ dispatchCallCount: 5,
+ event: {
+ type: "page-removed",
+ url: "foo.com",
+ isRemovedFromStore: true,
+ },
+ },
+ ];
+
+ for (let { message, dispatchCallCount, event } of events) {
+ info(message);
+
+ let sandbox = sinon.createSandbox();
+ let feed = getPlacesFeedForTest(sandbox);
+
+ await feed.placesObserver.handlePlacesEvent([event]);
+ await feed.placesObserver.handlePlacesEvent([event]);
+ await feed.placesObserver.handlePlacesEvent([event]);
+ await feed.placesObserver.handlePlacesEvent([event]);
+
+ Assert.ok(feed.placesChangedTimer, "PlacesFeed dispatch timer created");
+
+ // Let's speed things up a bit.
+ feed.placesChangedTimer.delay = 0;
+
+ // Wait for the timer to go off and get cleared
+ await TestUtils.waitForCondition(
+ () => !feed.placesChangedTimer,
+ "PlacesFeed dispatch timer cleared"
+ );
+
+ Assert.equal(
+ feed.store.dispatch.callCount,
+ dispatchCallCount,
+ `PlacesFeed.store.dispatch was called ${dispatchCallCount} times`
+ );
+
+ Assert.ok(
+ feed.store.dispatch.withArgs(
+ ac.OnlyToMain({ type: at.PLACES_LINKS_CHANGED })
+ ).calledOnce,
+ "PlacesFeed.store.dispatch called with PLACES_LINKS_CHANGED once"
+ );
+
+ sandbox.restore();
+ }
+ }
+);
+
+add_task(async function test_PlacesObserver_dispatches() {
+ let events = [
+ {
+ message:
+ "PlacesObserver should dispatch a PLACES_HISTORY_CLEARED action " +
+ "on history-cleared",
+ args: { type: "history-cleared" },
+ expectedAction: { type: at.PLACES_HISTORY_CLEARED },
+ },
+ {
+ message:
+ "PlacesObserver should dispatch a PLACES_LINKS_DELETED action " +
+ "with the right url",
+ args: {
+ type: "page-removed",
+ url: "foo.com",
+ isRemovedFromStore: true,
+ },
+ expectedAction: {
+ type: at.PLACES_LINKS_DELETED,
+ data: { urls: ["foo.com"] },
+ },
+ },
+ {
+ message:
+ "PlacesObserver should dispatch a PLACES_BOOKMARK_ADDED action with " +
+ "the bookmark data - http",
+ args: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.DEFAULT,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "http://www.foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ expectedAction: {
+ type: at.PLACES_BOOKMARK_ADDED,
+ data: {
+ bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,
+ bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,
+ dateAdded: FAKE_BOOKMARK.dateAdded * 1000,
+ url: "http://www.foo.com",
+ },
+ },
+ },
+ {
+ message:
+ "PlacesObserver should dispatch a PLACES_BOOKMARK_ADDED action with " +
+ "the bookmark data - https",
+ args: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.DEFAULT,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "https://www.foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ expectedAction: {
+ type: at.PLACES_BOOKMARK_ADDED,
+ data: {
+ bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,
+ bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,
+ dateAdded: FAKE_BOOKMARK.dateAdded * 1000,
+ url: "https://www.foo.com",
+ },
+ },
+ },
+ ];
+
+ for (let { message, args, expectedAction } of events) {
+ info(message);
+ let sandbox = sinon.createSandbox();
+ let dispatch = sandbox.spy();
+ let observer = new PlacesObserver(dispatch);
+ await observer.handlePlacesEvent([args]);
+ Assert.ok(dispatch.calledWith(expectedAction));
+ sandbox.restore();
+ }
+});
+
+add_task(async function test_PlacesObserver_ignores() {
+ let events = [
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED action - " +
+ "not http/https for bookmark-added",
+ event: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.DEFAULT,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED action - " +
+ "has IMPORT source for bookmark-added",
+ event: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.IMPORT,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED " +
+ "action - has RESTORE source for bookmark-added",
+ event: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.RESTORE,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED " +
+ "action - has RESTORE_ON_STARTUP source for bookmark-added",
+ event: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.RESTORE_ON_STARTUP,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED " +
+ "action - has SYNC source for bookmark-added",
+ event: {
+ itemType: TYPE_BOOKMARK,
+ source: SOURCES.SYNC,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should ignore events that are not of " +
+ "TYPE_BOOKMARK for bookmark-added",
+ event: {
+ itemType: "nottypebookmark",
+ source: SOURCES.DEFAULT,
+ dateAdded: FAKE_BOOKMARK.dateAdded,
+ guid: FAKE_BOOKMARK.bookmarkGuid,
+ title: FAKE_BOOKMARK.bookmarkTitle,
+ url: "https://www.foo.com",
+ isTagging: false,
+ type: "bookmark-added",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should ignore events that are not of " +
+ "TYPE_BOOKMARK for bookmark-removed",
+ event: {
+ id: null,
+ parentId: null,
+ index: null,
+ itemType: "nottypebookmark",
+ url: null,
+ guid: "461Z_7daEqIh",
+ parentGuid: "hkHScG3aI3hh",
+ source: SOURCES.DEFAULT,
+ type: "bookmark-removed",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
+ "action - has SYNC source for bookmark-removed",
+ event: {
+ id: null,
+ parentId: null,
+ index: null,
+ itemType: TYPE_BOOKMARK,
+ url: "foo.com",
+ guid: "uvRE3stjoZOI",
+ parentGuid: "BnsXZl8VMJjB",
+ source: SOURCES.SYNC,
+ type: "bookmark-removed",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
+ "action - has IMPORT source for bookmark-removed",
+ event: {
+ id: null,
+ parentId: null,
+ index: null,
+ itemType: TYPE_BOOKMARK,
+ url: "foo.com",
+ guid: "VF6YwhGpHrOW",
+ parentGuid: "7Vz8v9nKcSoq",
+ source: SOURCES.IMPORT,
+ type: "bookmark-removed",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
+ "action - has RESTORE source for bookmark-removed",
+ event: {
+ id: null,
+ parentId: null,
+ index: null,
+ itemType: TYPE_BOOKMARK,
+ url: "foo.com",
+ guid: "eKozFyXJP97R",
+ parentGuid: "ya8Z2FbjKnD0",
+ source: SOURCES.RESTORE,
+ type: "bookmark-removed",
+ },
+ },
+ {
+ message:
+ "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
+ "action - has RESTORE_ON_STARTUP source for bookmark-removed",
+ event: {
+ id: null,
+ parentId: null,
+ index: null,
+ itemType: TYPE_BOOKMARK,
+ url: "foo.com",
+ guid: "StSGMhrYYfyD",
+ parentGuid: "vL8wsCe2j_eT",
+ source: SOURCES.RESTORE_ON_STARTUP,
+ type: "bookmark-removed",
+ },
+ },
+ ];
+
+ for (let { message, event } of events) {
+ info(message);
+ let sandbox = sinon.createSandbox();
+ let dispatch = sandbox.spy();
+ let observer = new PlacesObserver(dispatch);
+
+ await observer.handlePlacesEvent([event]);
+ Assert.ok(dispatch.notCalled, "PlacesObserver.dispatch not called");
+ sandbox.restore();
+ }
+});
+
+add_task(async function test_PlacesObserver_bookmark_removed() {
+ info(
+ "PlacesObserver should dispatch a PLACES_BOOKMARKS_REMOVED " +
+ "action with the right URL and bookmarkGuid for bookmark-removed"
+ );
+ let sandbox = sinon.createSandbox();
+ let dispatch = sandbox.spy();
+ let observer = new PlacesObserver(dispatch);
+
+ await observer.handlePlacesEvent([
+ {
+ id: null,
+ parentId: null,
+ index: null,
+ itemType: TYPE_BOOKMARK,
+ url: "foo.com",
+ guid: "Xgnxs27I9JnX",
+ parentGuid: "a4k739PL55sP",
+ source: SOURCES.DEFAULT,
+ type: "bookmark-removed",
+ },
+ ]);
+
+ Assert.ok(
+ dispatch.calledWith({
+ type: at.PLACES_BOOKMARKS_REMOVED,
+ data: { urls: ["foo.com"] },
+ })
+ );
+ sandbox.restore();
+});
diff --git a/browser/components/newtab/test/xpcshell/test_Store.js b/browser/components/newtab/test/xpcshell/test_Store.js
new file mode 100644
index 0000000000..b05ad36cd6
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_Store.js
@@ -0,0 +1,453 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Store } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/Store.sys.mjs"
+);
+const { ActivityStreamMessageChannel } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/ActivityStreamMessageChannel.sys.mjs"
+);
+const { ActivityStreamStorage } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// This creates the Redux top-level object.
+/* globals Redux */
+Services.scriptloader.loadSubScript(
+ "resource://activity-stream/vendor/redux.js",
+ this
+);
+
+add_task(async function test_expected_properties() {
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ Assert.equal(store.feeds.constructor.name, "Map", "Should create a Map");
+ Assert.equal(store.feeds.size, 0, "Store should start without any feeds.");
+
+ Assert.ok(store._store, "Has a ._store");
+ Assert.ok(store.dispatch, "Has a .dispatch");
+ Assert.ok(store.getState, "Has a .getState");
+
+ sandbox.restore();
+});
+
+add_task(async function test_messagechannel() {
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(ActivityStreamMessageChannel.prototype, "middleware")
+ .returns(s => next => action => next(action));
+ let store = new Store();
+
+ info(
+ "Store should create a ActivityStreamMessageChannel with the right dispatcher"
+ );
+ Assert.ok(store.getMessageChannel(), "Has a message channel");
+ Assert.equal(
+ store.getMessageChannel().dispatch,
+ store.dispatch,
+ "MessageChannel.dispatch forwards to store.dispatch"
+ );
+ Assert.equal(
+ store.getMessageChannel(),
+ store._messageChannel,
+ "_messageChannel is the member for getMessageChannel()"
+ );
+
+ store.dispatch({ type: "FOO" });
+ Assert.ok(
+ ActivityStreamMessageChannel.prototype.middleware.calledOnce,
+ "Middleware called."
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_initFeed_add_feeds() {
+ info("Store.initFeed should add an instance of the feed to .feeds");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+ class Foo {}
+ store._prefs.set("foo", true);
+ await store.init(new Map([["foo", () => new Foo()]]));
+ store.initFeed("foo");
+
+ Assert.ok(store.feeds.has("foo"), "foo is set");
+ Assert.ok(store.feeds.get("foo") instanceof Foo, "Got registered class");
+ sandbox.restore();
+});
+
+add_task(async function test_initFeed_calls_onAction() {
+ info("Store should call the feed's onAction with uninit action if it exists");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+ let testFeed;
+ let createTestFeed = () => {
+ testFeed = { onAction: sandbox.spy() };
+ return testFeed;
+ };
+ const action = { type: "FOO" };
+ store._feedFactories = new Map([["test", createTestFeed]]);
+
+ store.initFeed("test", action);
+
+ Assert.ok(testFeed.onAction.calledOnce, "onAction called");
+ Assert.ok(
+ testFeed.onAction.calledWith(action),
+ "onAction called with test action"
+ );
+
+ info("Store should add a .store property to the feed");
+ Assert.ok(testFeed.store, "Store exists");
+ Assert.equal(testFeed.store, store, "Feed store is the Store");
+ sandbox.restore();
+});
+
+add_task(async function test_initFeed_on_init() {
+ info("Store should call .initFeed with each key");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ sandbox.stub(store, "initFeed");
+ store._prefs.set("foo", true);
+ store._prefs.set("bar", true);
+ await store.init(
+ new Map([
+ ["foo", () => {}],
+ ["bar", () => {}],
+ ])
+ );
+ Assert.ok(store.initFeed.calledWith("foo"), "First test feed initted");
+ Assert.ok(store.initFeed.calledWith("bar"), "Second test feed initted");
+ sandbox.restore();
+});
+
+add_task(async function test_initFeed_calls__initIndexedDB() {
+ info("Store should call _initIndexedDB");
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ sandbox.spy(store, "_initIndexedDB");
+
+ let dbStub = sandbox.stub(ActivityStreamStorage.prototype, "db");
+ let dbAccessed = false;
+ dbStub.get(() => {
+ dbAccessed = true;
+ return {};
+ });
+
+ store._prefs.set("testfeed", true);
+ await store.init(
+ new Map([
+ [
+ "testfeed",
+ () => {
+ return {};
+ },
+ ],
+ ])
+ );
+
+ Assert.ok(store._initIndexedDB.calledOnce, "_initIndexedDB called once");
+ Assert.ok(
+ store._initIndexedDB.calledWithExactly("feeds.telemetry"),
+ "feeds.telemetry was passed"
+ );
+ // Due to what appears to be a bug in sinon when using calledOnce
+ // with a stubbed getter, we can't just use dbStub.calledOnce here.
+ Assert.ok(dbAccessed, "ActivityStreamStorage was accessed");
+
+ info(
+ "Store should reset ActivityStreamStorage telemetry if opening the db fails"
+ );
+ dbStub.rejects();
+ await store.init(new Map());
+
+ Assert.equal(
+ store.dbStorage.telemetry,
+ null,
+ "Telemetry on storage was cleared"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_disabled_feed() {
+ info("Store should not initialize the feed if the Pref is set to false");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ sandbox.stub(store, "initFeed");
+ store._prefs.set("foo", false);
+ await store.init(new Map([["foo", () => {}]]));
+ Assert.ok(store.initFeed.notCalled, ".initFeed not called");
+
+ store._prefs.set("foo", true);
+
+ sandbox.restore();
+});
+
+add_task(async function test_observe_pref_branch() {
+ info("Store should observe the pref branch");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ sandbox.stub(store._prefs, "observeBranch");
+ await store.init(new Map());
+ Assert.ok(store._prefs.observeBranch.calledOnce, "observeBranch called once");
+ Assert.ok(
+ store._prefs.observeBranch.calledWith(store),
+ "observeBranch passed the store"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_emit_initial_event() {
+ info("Store should emit an initial event if provided");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ const action = { type: "FOO" };
+ sandbox.stub(store, "dispatch");
+ await store.init(new Map(), action);
+ Assert.ok(store.dispatch.calledOnce, "Dispatch called once");
+ Assert.ok(store.dispatch.calledWith(action), "Dispatch called with action");
+
+ sandbox.restore();
+});
+
+add_task(async function test_initialize_telemetry_feed_first() {
+ info("Store should initialize the telemetry feed first");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ store._prefs.set("feeds.foo", true);
+ store._prefs.set("feeds.telemetry", true);
+ const telemetrySpy = sandbox.stub().returns({});
+ const fooSpy = sandbox.stub().returns({});
+ // Intentionally put the telemetry feed as the second item.
+ const feedFactories = new Map([
+ ["feeds.foo", fooSpy],
+ ["feeds.telemetry", telemetrySpy],
+ ]);
+ await store.init(feedFactories);
+ Assert.ok(telemetrySpy.calledBefore(fooSpy), "Telemetry feed initted first");
+
+ sandbox.restore();
+});
+
+add_task(async function test_dispatch_init_load_events() {
+ info("Store should dispatch init/load events");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ sandbox.stub(store.getMessageChannel(), "simulateMessagesForExistingTabs");
+ await store.init(new Map(), { type: "FOO" });
+ Assert.ok(
+ store.getMessageChannel().simulateMessagesForExistingTabs.calledOnce,
+ "simulateMessagesForExistingTabs called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_init_before_load() {
+ info("Store should dispatch INIT before LOAD");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ sandbox.stub(store.getMessageChannel(), "simulateMessagesForExistingTabs");
+ sandbox.stub(store, "dispatch");
+ const init = { type: "INIT" };
+ const load = { type: "TAB_LOAD" };
+ store
+ .getMessageChannel()
+ .simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load));
+ await store.init(new Map(), init);
+
+ Assert.ok(store.dispatch.calledTwice, "Dispatch called twice");
+ Assert.equal(
+ store.dispatch.firstCall.args[0],
+ init,
+ "First dispatch was for init event"
+ );
+ Assert.equal(
+ store.dispatch.secondCall.args[0],
+ load,
+ "Second dispatch was for load event"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_uninit_feeds() {
+ info("uninitFeed should not throw if no feed with that name exists");
+
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ try {
+ store.uninitFeed("does-not-exist");
+ Assert.ok(true, "Didn't throw");
+ } catch (e) {
+ Assert.ok(false, "Should not have thrown");
+ }
+
+ info(
+ "uninitFeed should call the feed's onAction with uninit action if it exists"
+ );
+ let feed;
+ function createFeed() {
+ feed = { onAction: sandbox.spy() };
+ return feed;
+ }
+ const action = { type: "BAR" };
+ store._feedFactories = new Map([["foo", createFeed]]);
+ store.initFeed("foo");
+
+ store.uninitFeed("foo", action);
+
+ Assert.ok(feed.onAction.calledOnce);
+ Assert.ok(feed.onAction.calledWith(action));
+
+ info("uninitFeed should remove the feed from .feeds");
+ Assert.ok(!store.feeds.has("foo"), "foo is not in .feeds");
+
+ sandbox.restore();
+});
+
+add_task(async function test_onPrefChanged() {
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+ let initFeedStub = sandbox.stub(store, "initFeed");
+ let uninitFeedStub = sandbox.stub(store, "uninitFeed");
+ store._prefs.set("foo", false);
+ store.init(new Map([["foo", () => ({})]]));
+
+ info("onPrefChanged should initialize the feed if called with true");
+ store.onPrefChanged("foo", true);
+ Assert.ok(initFeedStub.calledWith("foo"));
+ Assert.ok(!uninitFeedStub.calledOnce);
+ initFeedStub.resetHistory();
+ uninitFeedStub.resetHistory();
+
+ info("onPrefChanged should uninitialize the feed if called with false");
+ store.onPrefChanged("foo", false);
+ Assert.ok(uninitFeedStub.calledWith("foo"));
+ Assert.ok(!initFeedStub.calledOnce);
+ initFeedStub.resetHistory();
+ uninitFeedStub.resetHistory();
+
+ info("onPrefChanged should do nothing if not an expected feed");
+ store.onPrefChanged("bar", false);
+
+ Assert.ok(!initFeedStub.calledOnce);
+ Assert.ok(!uninitFeedStub.calledOnce);
+ sandbox.restore();
+});
+
+add_task(async function test_uninit() {
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+ let dispatchStub = sandbox.stub(store, "dispatch");
+ const action = { type: "BAR" };
+ await store.init(new Map(), null, action);
+ store.uninit();
+
+ Assert.ok(store.dispatch.calledOnce);
+ Assert.ok(store.dispatch.calledWith(action));
+
+ info("Store.uninit should clear .feeds and ._feedFactories");
+ store._prefs.set("a", true);
+ await store.init(
+ new Map([
+ ["a", () => ({})],
+ ["b", () => ({})],
+ ["c", () => ({})],
+ ])
+ );
+
+ store.uninit();
+
+ Assert.equal(store.feeds.size, 0);
+ Assert.equal(store._feedFactories, null);
+
+ info("Store.uninit should emit an uninit event if provided on init");
+ dispatchStub.resetHistory();
+ const uninitAction = { type: "BAR" };
+ await store.init(new Map(), null, uninitAction);
+ store.uninit();
+
+ Assert.ok(store.dispatch.calledOnce);
+ Assert.ok(store.dispatch.calledWith(uninitAction));
+ sandbox.restore();
+});
+
+add_task(async function test_getState() {
+ info("Store.getState should return the redux state");
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+ store._store = Redux.createStore((prevState = 123) => prevState);
+ const { getState } = store;
+ Assert.equal(getState(), 123);
+ sandbox.restore();
+});
+
+/**
+ * addNumberReducer - a simple dummy reducer for testing that adds a number
+ */
+function addNumberReducer(prevState = 0, action) {
+ return action.type === "ADD" ? prevState + action.data : prevState;
+}
+
+add_task(async function test_dispatch() {
+ info("Store.dispatch should call .onAction of each feed");
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+ const { dispatch } = store;
+ const sub = { onAction: sinon.spy() };
+ const action = { type: "FOO" };
+
+ store._prefs.set("sub", true);
+ await store.init(new Map([["sub", () => sub]]));
+
+ dispatch(action);
+
+ Assert.ok(sub.onAction.calledWith(action));
+
+ info("Sandbox.dispatch should call the reducers");
+
+ store._store = Redux.createStore(addNumberReducer);
+ dispatch({ type: "ADD", data: 14 });
+ Assert.equal(store.getState(), 14);
+
+ sandbox.restore();
+});
+
+add_task(async function test_subscribe() {
+ info("Store.subscribe should subscribe to changes to the store");
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+ const sub = sandbox.spy();
+ const action = { type: "FOO" };
+
+ store.subscribe(sub);
+ store.dispatch(action);
+
+ Assert.ok(sub.calledOnce);
+ sandbox.restore();
+});
diff --git a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js
new file mode 100644
index 0000000000..b54d6094ad
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js
@@ -0,0 +1,3285 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.sys.mjs"
+);
+
+const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ActorConstants.sys.mjs"
+);
+
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+const { TelemetryFeed, USER_PREFS_ENCODING } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/TelemetryFeed.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
+ AboutWelcomeTelemetry:
+ "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs",
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+ HomePage: "resource:///modules/HomePage.sys.mjs",
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+ UTEventReporting: "resource://activity-stream/lib/UTEventReporting.sys.mjs",
+});
+
+const FAKE_UUID = "{foo-123-foo}";
+const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId";
+const PREF_TELEMETRY = "browser.newtabpage.activity-stream.telemetry";
+const PREF_EVENT_TELEMETRY =
+ "browser.newtabpage.activity-stream.telemetry.ut.events";
+
+let ASRouterEventPingSchemaPromise;
+let BasePingSchemaPromise;
+let SessionPingSchemaPromise;
+let UserEventPingSchemaPromise;
+
+function assertPingMatchesSchema(pingKind, ping, schema) {
+ // Unlike the validator from JsonSchema.sys.mjs, JsonSchemaValidator
+ // lets us opt-in to having "undefined" properties, which are then
+ // ignored. This is fine because the ping is sent as a JSON string
+ // over an XHR, and undefined properties are culled as part of the
+ // JSON encoding process.
+ let result = JsonSchemaValidator.validate(ping, schema, {
+ allowExplicitUndefinedProperties: true,
+ });
+
+ if (!result.valid) {
+ info(`${pingKind} failed to validate against the schema: ${result.error}`);
+ }
+
+ Assert.ok(result.valid, `${pingKind} is valid against the schema.`);
+}
+
+async function assertSessionPingValid(ping) {
+ let schema = await SessionPingSchemaPromise;
+ assertPingMatchesSchema("SessionPing", ping, schema);
+}
+
+async function assertBasePingValid(ping) {
+ let schema = await BasePingSchemaPromise;
+ assertPingMatchesSchema("BasePing", ping, schema);
+}
+
+async function assertUserEventPingValid(ping) {
+ let schema = await UserEventPingSchemaPromise;
+ assertPingMatchesSchema("UserEventPing", ping, schema);
+}
+
+async function assertASRouterEventPingValid(ping) {
+ let schema = await ASRouterEventPingSchemaPromise;
+ assertPingMatchesSchema("ASRouterEventPing", ping, schema);
+}
+
+add_setup(async function setup() {
+ ASRouterEventPingSchemaPromise = IOUtils.readJSON(
+ do_get_file("../schemas/asrouter_event_ping.schema.json").path
+ );
+
+ BasePingSchemaPromise = IOUtils.readJSON(
+ do_get_file("../schemas/base_ping.schema.json").path
+ );
+
+ SessionPingSchemaPromise = IOUtils.readJSON(
+ do_get_file("../schemas/session_ping.schema.json").path
+ );
+
+ UserEventPingSchemaPromise = IOUtils.readJSON(
+ do_get_file("../schemas/user_event_ping.schema.json").path
+ );
+
+ do_get_profile();
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+
+ await TelemetryController.testReset();
+
+ updateAppInfo({
+ name: "XPCShell",
+ ID: "xpcshell@tests.mozilla.org",
+ version: "122",
+ platformVersion: "122",
+ });
+
+ Services.prefs.setCharPref(
+ "browser.contextual-services.contextId",
+ FAKE_UUID
+ );
+});
+
+add_task(async function test_construction() {
+ let testInstance = new TelemetryFeed();
+ Assert.ok(
+ testInstance,
+ "Should have been able to create an instance of TelemetryFeed."
+ );
+ Assert.ok(
+ testInstance.utEvents instanceof UTEventReporting,
+ "Should add .utEvents, a UTEventReporting instance."
+ );
+ Assert.ok(
+ testInstance._impressionId,
+ "Should create impression id if none exists"
+ );
+});
+
+add_task(async function test_load_impressionId() {
+ info(
+ "Constructing a TelemetryFeed should use a saved impression ID if one exists."
+ );
+ const FAKE_IMPRESSION_ID = "{some-fake-impression-ID}";
+ const IMPRESSION_PREF = "browser.newtabpage.activity-stream.impressionId";
+ Services.prefs.setCharPref(IMPRESSION_PREF, FAKE_IMPRESSION_ID);
+ Assert.equal(new TelemetryFeed()._impressionId, FAKE_IMPRESSION_ID);
+ Services.prefs.clearUserPref(IMPRESSION_PREF);
+});
+
+add_task(async function test_init() {
+ info(
+ "init should make this.browserOpenNewtabStart() observe browser-open-newtab-start"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(TelemetryFeed.prototype, "getOrCreateImpressionId")
+ .returns(FAKE_UUID);
+
+ let instance = new TelemetryFeed();
+ sandbox.stub(instance, "browserOpenNewtabStart");
+ instance.init();
+
+ Services.obs.notifyObservers(null, "browser-open-newtab-start");
+ Assert.ok(
+ instance.browserOpenNewtabStart.calledOnce,
+ "browserOpenNewtabStart called once."
+ );
+
+ info("init should create impression id if none exists");
+ Assert.equal(instance._impressionId, FAKE_UUID);
+
+ instance.uninit();
+ sandbox.restore();
+});
+
+add_task(async function test_saved_impression_id() {
+ const FAKE_IMPRESSION_ID = "fakeImpressionId";
+ Services.prefs.setCharPref(PREF_IMPRESSION_ID, FAKE_IMPRESSION_ID);
+ Assert.equal(new TelemetryFeed()._impressionId, FAKE_IMPRESSION_ID);
+ Services.prefs.clearUserPref(PREF_IMPRESSION_ID);
+});
+
+add_task(async function test_telemetry_prefs() {
+ info("Telemetry pref changes from false to true");
+ Services.prefs.setBoolPref(PREF_TELEMETRY, false);
+ let instance = new TelemetryFeed();
+ Assert.ok(!instance.telemetryEnabled, "Telemetry disabled");
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ Assert.ok(instance.telemetryEnabled, "Telemetry enabled");
+
+ info("Event telemetry pref changes from false to true");
+
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, false);
+ Assert.ok(!instance.eventTelemetryEnabled, "Event telemetry disabled");
+
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
+ Assert.ok(instance.eventTelemetryEnabled, "Event telemetry enabled");
+
+ Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+});
+
+add_task(async function test_deletionRequest_scalars() {
+ info("TelemetryFeed.init should set two scalars for deletion-request");
+
+ Services.telemetry.clearScalars();
+ let instance = new TelemetryFeed();
+ instance.init();
+
+ let snapshot = Services.telemetry.getSnapshotForScalars(
+ "deletion-request",
+ false
+ ).parent;
+ TelemetryTestUtils.assertScalar(
+ snapshot,
+ "deletion.request.impression_id",
+ instance._impressionId
+ );
+ TelemetryTestUtils.assertScalar(
+ snapshot,
+ "deletion.request.context_id",
+ FAKE_UUID
+ );
+ instance.uninit();
+});
+
+add_task(async function test_metrics_on_initialization() {
+ info("TelemetryFeed.init should record initial metrics from newtab prefs");
+ Services.fog.testResetFOG();
+ const ENABLED_SETTING = true;
+ const TOP_SITES_ROWS = 3;
+ const BLOCKED_SPONSORS = ["mozilla"];
+
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.topsites",
+ ENABLED_SETTING
+ );
+ Services.prefs.setIntPref(
+ "browser.newtabpage.activity-stream.topSitesRows",
+ TOP_SITES_ROWS
+ );
+ Services.prefs.setCharPref(
+ "browser.topsites.blockedSponsors",
+ JSON.stringify(BLOCKED_SPONSORS)
+ );
+
+ let instance = new TelemetryFeed();
+ instance.init();
+
+ Assert.equal(Glean.topsites.enabled.testGetValue(), ENABLED_SETTING);
+ Assert.equal(Glean.topsites.rows.testGetValue(), TOP_SITES_ROWS);
+ Assert.deepEqual(
+ Glean.newtab.blockedSponsors.testGetValue(),
+ BLOCKED_SPONSORS
+ );
+
+ instance.uninit();
+
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.feeds.topsites"
+ );
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.topSitesRows"
+ );
+ Services.prefs.clearUserPref("browser.topsites.blockedSponsors");
+});
+
+add_task(async function test_metrics_with_bad_json() {
+ info(
+ "TelemetryFeed.init should not record blocked sponsor metrics when " +
+ "bad json string is passed"
+ );
+ Services.fog.testResetFOG();
+ Services.prefs.setCharPref("browser.topsites.blockedSponsors", "BAD[JSON]");
+
+ let instance = new TelemetryFeed();
+ instance.init();
+
+ Assert.equal(Glean.newtab.blockedSponsors.testGetValue(), null);
+
+ instance.uninit();
+
+ Services.prefs.clearUserPref("browser.topsites.blockedSponsors");
+});
+
+add_task(async function test_metrics_on_pref_changes() {
+ info("TelemetryFeed.init should record new metrics for newtab pref changes");
+ const INITIAL_TOP_SITES_ROWS = 3;
+ const INITIAL_BLOCKED_SPONSORS = [];
+ Services.fog.testResetFOG();
+ Services.prefs.setIntPref(
+ "browser.newtabpage.activity-stream.topSitesRows",
+ INITIAL_TOP_SITES_ROWS
+ );
+ Services.prefs.setCharPref(
+ "browser.topsites.blockedSponsors",
+ JSON.stringify(INITIAL_BLOCKED_SPONSORS)
+ );
+
+ let instance = new TelemetryFeed();
+ instance.init();
+
+ Assert.equal(Glean.topsites.rows.testGetValue(), INITIAL_TOP_SITES_ROWS);
+ Assert.deepEqual(
+ Glean.newtab.blockedSponsors.testGetValue(),
+ INITIAL_BLOCKED_SPONSORS
+ );
+
+ const NEXT_TOP_SITES_ROWS = 2;
+ const NEXT_BLOCKED_SPONSORS = ["mozilla"];
+
+ Services.prefs.setIntPref(
+ "browser.newtabpage.activity-stream.topSitesRows",
+ NEXT_TOP_SITES_ROWS
+ );
+
+ Services.prefs.setStringPref(
+ "browser.topsites.blockedSponsors",
+ JSON.stringify(NEXT_BLOCKED_SPONSORS)
+ );
+
+ Assert.equal(Glean.topsites.rows.testGetValue(), NEXT_TOP_SITES_ROWS);
+ Assert.deepEqual(
+ Glean.newtab.blockedSponsors.testGetValue(),
+ NEXT_BLOCKED_SPONSORS
+ );
+
+ instance.uninit();
+
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.topSitesRows"
+ );
+ Services.prefs.clearUserPref("browser.topsites.blockedSponsors");
+});
+
+add_task(async function test_events_on_pref_changes() {
+ info("TelemetryFeed.init should record events for some newtab pref changes");
+ // We only record events for browser.newtabpage.activity-stream.feeds.topsites and
+ // browser.newtabpage.activity-stream.showSponsoredTopSites being changed.
+ const INITIAL_TOPSITES_ENABLED = false;
+ const INITIAL_SHOW_SPONSORED_TOP_SITES = true;
+ Services.fog.testResetFOG();
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.topsites",
+ INITIAL_TOPSITES_ENABLED
+ );
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.showSponsoredTopSites",
+ INITIAL_SHOW_SPONSORED_TOP_SITES
+ );
+
+ let instance = new TelemetryFeed();
+ instance.init();
+
+ const NEXT_TOPSITES_ENABLED = true;
+ const NEXT_SHOW_SPONSORED_TOP_SITES = false;
+
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.topsites",
+ NEXT_TOPSITES_ENABLED
+ );
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.showSponsoredTopSites",
+ NEXT_SHOW_SPONSORED_TOP_SITES
+ );
+
+ let prefChangeEvents = Glean.topsites.prefChanged.testGetValue();
+ Assert.deepEqual(prefChangeEvents[0].extra, {
+ pref_name: "browser.newtabpage.activity-stream.feeds.topsites",
+ new_value: String(NEXT_TOPSITES_ENABLED),
+ });
+ Assert.deepEqual(prefChangeEvents[1].extra, {
+ pref_name: "browser.newtabpage.activity-stream.showSponsoredTopSites",
+ new_value: String(NEXT_SHOW_SPONSORED_TOP_SITES),
+ });
+
+ instance.uninit();
+
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.feeds.topsites"
+ );
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.showSponsoredTopSites"
+ );
+});
+
+add_task(async function test_browserOpenNewtabStart() {
+ info(
+ "TelemetryFeed.browserOpenNewtabStart should call " +
+ "ChromeUtils.addProfilerMarker with browser-open-newtab-start"
+ );
+
+ let instance = new TelemetryFeed();
+
+ let entries = 10000;
+ let interval = 1;
+ let threads = ["GeckoMain"];
+ let features = [];
+ await Services.profiler.StartProfiler(entries, interval, features, threads);
+ instance.browserOpenNewtabStart();
+
+ let profileArrayBuffer =
+ await Services.profiler.getProfileDataAsArrayBuffer();
+ await Services.profiler.StopProfiler();
+
+ let profileUint8Array = new Uint8Array(profileArrayBuffer);
+ let textDecoder = new TextDecoder("utf-8", { fatal: true });
+ let profileString = textDecoder.decode(profileUint8Array);
+ let profile = JSON.parse(profileString);
+ Assert.ok(profile.threads);
+ Assert.equal(profile.threads.length, 1);
+
+ let foundMarker = profile.threads[0].markers.data.find(marker => {
+ return marker[5]?.name === "browser-open-newtab-start";
+ });
+
+ Assert.ok(foundMarker, "Found the browser-open-newtab-start marker");
+});
+
+add_task(async function test_addSession_and_get_session() {
+ info("TelemetryFeed.addSession should add a session and return it");
+ let instance = new TelemetryFeed();
+ let session = instance.addSession("foo");
+
+ Assert.equal(instance.sessions.get("foo"), session);
+
+ info("TelemetryFeed.addSession should set a session_id");
+ Assert.ok(session.session_id, "Should have a session_id set");
+});
+
+add_task(async function test_addSession_url_param() {
+ info("TelemetryFeed.addSession should set the page if a url param is given");
+ let instance = new TelemetryFeed();
+ let session = instance.addSession("foo", "about:monkeys");
+ Assert.equal(session.page, "about:monkeys");
+
+ info(
+ "TelemetryFeed.assSession should set the page prop to 'unknown' " +
+ "if no URL param given"
+ );
+ session = instance.addSession("test2");
+ Assert.equal(session.page, "unknown");
+});
+
+add_task(async function test_addSession_perf_properties() {
+ info(
+ "TelemetryFeed.addSession should set the perf type when lacking " +
+ "timestamp"
+ );
+ let instance = new TelemetryFeed();
+ let session = instance.addSession("foo");
+ Assert.equal(session.perf.load_trigger_type, "unexpected");
+
+ info(
+ "TelemetryFeed.addSession should set load_trigger_type to " +
+ "first_window_opened on the first about:home seen"
+ );
+ session = instance.addSession("test2", "about:home");
+ Assert.equal(session.perf.load_trigger_type, "first_window_opened");
+
+ info(
+ "TelemetryFeed.addSession should set load_trigger_ts to the " +
+ "value of the process start timestamp"
+ );
+ Assert.equal(
+ session.perf.load_trigger_ts,
+ Services.startup.getStartupInfo().process.getTime(),
+ "Should have set a timestamp to be the process start time"
+ );
+
+ info(
+ "TelemetryFeed.addSession should NOT set load_trigger_type to " +
+ "first_window_opened on the second about:home seen"
+ );
+ let session2 = instance.addSession("test2", "about:home");
+ Assert.notEqual(session2.perf.load_trigger_type, "first_window_opened");
+});
+
+add_task(async function test_addSession_valid_ping_on_first_abouthome() {
+ info(
+ "TelemetryFeed.addSession should create a valid session ping " +
+ "on the first about:home seen"
+ );
+ let instance = new TelemetryFeed();
+ // Add a session
+ const PORT_ID = "foo";
+ let session = instance.addSession(PORT_ID, "about:home");
+
+ // Create a ping referencing the session
+ let ping = instance.createSessionEndEvent(session);
+ await assertSessionPingValid(ping);
+});
+
+add_task(async function test_addSession_valid_ping_data_late_by_ms() {
+ info(
+ "TelemetryFeed.addSession should create a valid session ping " +
+ "with the data_late_by_ms perf"
+ );
+ let instance = new TelemetryFeed();
+ // Add a session
+ const PORT_ID = "foo";
+ let session = instance.addSession(PORT_ID, "about:home");
+
+ const TOPSITES_LATE_BY_MS = 10;
+ const HIGHLIGHTS_LATE_BY_MS = 20;
+ instance.saveSessionPerfData("foo", {
+ topsites_data_late_by_ms: TOPSITES_LATE_BY_MS,
+ });
+ instance.saveSessionPerfData("foo", {
+ highlights_data_late_by_ms: HIGHLIGHTS_LATE_BY_MS,
+ });
+
+ // Create a ping referencing the session
+ let ping = instance.createSessionEndEvent(session);
+ await assertSessionPingValid(ping);
+ Assert.equal(session.perf.topsites_data_late_by_ms, TOPSITES_LATE_BY_MS);
+ Assert.equal(session.perf.highlights_data_late_by_ms, HIGHLIGHTS_LATE_BY_MS);
+});
+
+add_task(async function test_addSession_valid_ping_topsites_stats_perf() {
+ info(
+ "TelemetryFeed.addSession should create a valid session ping " +
+ "with the topsites stats perf"
+ );
+ let instance = new TelemetryFeed();
+ // Add a session
+ const PORT_ID = "foo";
+ let session = instance.addSession(PORT_ID, "about:home");
+
+ const SCREENSHOT_WITH_ICON = 2;
+ const TOPSITES_PINNED = 3;
+ const TOPSITES_SEARCH_SHORTCUTS = 2;
+
+ instance.saveSessionPerfData("foo", {
+ topsites_icon_stats: {
+ custom_screenshot: 0,
+ screenshot_with_icon: SCREENSHOT_WITH_ICON,
+ screenshot: 1,
+ tippytop: 2,
+ rich_icon: 1,
+ no_image: 0,
+ },
+ topsites_pinned: TOPSITES_PINNED,
+ topsites_search_shortcuts: TOPSITES_SEARCH_SHORTCUTS,
+ });
+
+ // Create a ping referencing the session
+ let ping = instance.createSessionEndEvent(session);
+ await assertSessionPingValid(ping);
+ Assert.equal(
+ instance.sessions.get("foo").perf.topsites_icon_stats.screenshot_with_icon,
+ SCREENSHOT_WITH_ICON
+ );
+ Assert.equal(
+ instance.sessions.get("foo").perf.topsites_pinned,
+ TOPSITES_PINNED
+ );
+ Assert.equal(
+ instance.sessions.get("foo").perf.topsites_search_shortcuts,
+ TOPSITES_SEARCH_SHORTCUTS
+ );
+});
+
+add_task(async function test_endSession_no_throw_on_bad_session() {
+ info(
+ "TelemetryFeed.endSession should not throw if there is no " +
+ "session for a given port ID"
+ );
+ let instance = new TelemetryFeed();
+ try {
+ instance.endSession("doesn't exist");
+ Assert.ok(true, "Did not throw.");
+ } catch (e) {
+ Assert.ok(false, "Should not have thrown.");
+ }
+});
+
+add_task(async function test_endSession_session_duration() {
+ info(
+ "TelemetryFeed.endSession should add a session_duration integer " +
+ "if there is a visibility_event_rcvd_ts"
+ );
+ let instance = new TelemetryFeed();
+ let session = instance.addSession("foo");
+ session.perf.visibility_event_rcvd_ts = 444.4732;
+ instance.endSession("foo");
+
+ Assert.ok(
+ Number.isInteger(session.session_duration),
+ "session_duration should be an integer"
+ );
+});
+
+add_task(async function test_endSession_no_ping_on_no_visibility_event() {
+ info(
+ "TelemetryFeed.endSession shouldn't send session ping if there's " +
+ "no visibility_event_rcvd_ts"
+ );
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
+ let instance = new TelemetryFeed();
+ instance.addSession("foo");
+
+ Services.telemetry.clearEvents();
+ instance.endSession("foo");
+ TelemetryTestUtils.assertNumberOfEvents(0);
+
+ info("TelemetryFeed.endSession should remove the session from .sessions");
+ Assert.ok(!instance.sessions.has("foo"));
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
+});
+
+add_task(async function test_endSession_send_ping() {
+ info(
+ "TelemetryFeed.endSession should call createSessionSendEvent with the " +
+ "session if visibilty_event_rcvd_ts was set"
+ );
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
+ let instance = new TelemetryFeed();
+
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(instance, "createSessionEndEvent");
+ sandbox.stub(instance.utEvents, "sendSessionEndEvent");
+
+ let session = instance.addSession("foo");
+
+ session.perf.visibility_event_rcvd_ts = 444.4732;
+ instance.endSession("foo");
+
+ Assert.ok(instance.createSessionEndEvent.calledWith(session));
+ let sessionEndEvent = instance.createSessionEndEvent.firstCall.returnValue;
+ Assert.ok(instance.utEvents.sendSessionEndEvent.calledWith(sessionEndEvent));
+
+ info("TelemetryFeed.endSession should remove the session from .sessions");
+ Assert.ok(!instance.sessions.has("foo"));
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
+
+ sandbox.restore();
+});
+
+add_task(async function test_createPing_valid_base_if_no_portID() {
+ info(
+ "TelemetryFeed.createPing should create a valid base ping " +
+ "without a session if no portID is supplied"
+ );
+ let instance = new TelemetryFeed();
+ let ping = await instance.createPing();
+ await assertBasePingValid(ping);
+ Assert.ok(!ping.session_id);
+ Assert.ok(!ping.page);
+});
+
+add_task(async function test_createPing_valid_base_if_portID() {
+ info(
+ "TelemetryFeed.createPing should create a valid base ping " +
+ "with session info if a portID is supplied"
+ );
+ // Add a session
+ const PORT_ID = "foo";
+ let instance = new TelemetryFeed();
+ instance.addSession(PORT_ID, "about:home");
+ let sessionID = instance.sessions.get(PORT_ID).session_id;
+
+ // Create a ping referencing the session
+ let ping = await instance.createPing(PORT_ID);
+ await assertBasePingValid(ping);
+
+ // Make sure we added the right session-related stuff to the ping
+ Assert.equal(ping.session_id, sessionID);
+ Assert.equal(ping.page, "about:home");
+});
+
+add_task(async function test_createPing_no_session_yet_portID() {
+ info(
+ "TelemetryFeed.createPing should create an 'unexpected' base ping " +
+ "if no session yet portID is supplied"
+ );
+ let instance = new TelemetryFeed();
+ let ping = await instance.createPing("foo");
+ await assertBasePingValid(ping);
+
+ Assert.equal(ping.page, "unknown");
+ Assert.equal(
+ instance.sessions.get("foo").perf.load_trigger_type,
+ "unexpected"
+ );
+});
+
+add_task(async function test_createPing_includes_userPrefs() {
+ info("TelemetryFeed.createPing should create a base ping with user_prefs");
+ let expectedUserPrefs = 0;
+
+ for (let pref of Object.keys(USER_PREFS_ENCODING)) {
+ Services.prefs.setBoolPref(
+ `browser.newtabpage.activity-stream.${pref}`,
+ true
+ );
+ expectedUserPrefs |= USER_PREFS_ENCODING[pref];
+ }
+
+ let instance = new TelemetryFeed();
+ let ping = await instance.createPing("foo");
+ await assertBasePingValid(ping);
+ Assert.equal(ping.user_prefs, expectedUserPrefs);
+
+ for (const pref of Object.keys(USER_PREFS_ENCODING)) {
+ Services.prefs.clearUserPref(`browser.newtabpage.activity-stream.${pref}`);
+ }
+});
+
+add_task(async function test_createUserEvent_is_valid() {
+ info(
+ "TelemetryFeed.createUserEvent should create a valid user event ping " +
+ "with the right session_id"
+ );
+ const PORT_ID = "foo";
+
+ let instance = new TelemetryFeed();
+ let data = { source: "TOP_SITES", event: "CLICK" };
+ let action = ac.AlsoToMain(ac.UserEvent(data), PORT_ID);
+ let session = instance.addSession(PORT_ID);
+
+ let ping = await instance.createUserEvent(action);
+
+ // Is it valid?
+ await assertUserEventPingValid(ping);
+ // Does it have the right session_id?
+ Assert.equal(ping.session_id, session.session_id);
+});
+
+add_task(async function test_createSessionEndEvent_is_valid() {
+ info(
+ "TelemetryFeed.createSessionEndEvent should create a valid session ping"
+ );
+ const FAKE_DURATION = 12345;
+ let instance = new TelemetryFeed();
+ let ping = await instance.createSessionEndEvent({
+ session_id: FAKE_UUID,
+ page: "about:newtab",
+ session_duration: FAKE_DURATION,
+ perf: {
+ load_trigger_ts: 10,
+ load_trigger_type: "menu_plus_or_keyboard",
+ visibility_event_rcvd_ts: 20,
+ is_preloaded: true,
+ },
+ });
+
+ // Is it valid?
+ await assertSessionPingValid(ping);
+ Assert.equal(ping.session_id, FAKE_UUID);
+ Assert.equal(ping.page, "about:newtab");
+ Assert.equal(ping.session_duration, FAKE_DURATION);
+});
+
+add_task(async function test_createSessionEndEvent_with_unexpected_is_valid() {
+ info(
+ "TelemetryFeed.createSessionEndEvent should create a valid 'unexpected' " +
+ "session ping"
+ );
+ const FAKE_DURATION = 12345;
+ const FAKE_TRIGGER_TYPE = "unexpected";
+
+ let instance = new TelemetryFeed();
+ let ping = await instance.createSessionEndEvent({
+ session_id: FAKE_UUID,
+ page: "about:newtab",
+ session_duration: FAKE_DURATION,
+ perf: {
+ load_trigger_type: FAKE_TRIGGER_TYPE,
+ is_preloaded: true,
+ },
+ });
+
+ // Is it valid?
+ await assertSessionPingValid(ping);
+ Assert.equal(ping.session_id, FAKE_UUID);
+ Assert.equal(ping.page, "about:newtab");
+ Assert.equal(ping.session_duration, FAKE_DURATION);
+ Assert.equal(ping.perf.load_trigger_type, FAKE_TRIGGER_TYPE);
+});
+
+add_task(async function test_applyCFRPolicy_prerelease() {
+ info(
+ "TelemetryFeed.applyCFRPolicy should use client_id and message_id " +
+ "in prerelease"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("nightly");
+
+ let instance = new TelemetryFeed();
+
+ let data = {
+ action: "cfr_user_event",
+ event: "IMPRESSION",
+ message_id: "cfr_message_01",
+ bucket_id: "cfr_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyCFRPolicy(data);
+
+ Assert.equal(pingType, "cfr");
+ Assert.equal(ping.impression_id, undefined);
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(ping.bucket_id, "cfr_bucket_01");
+ Assert.equal(ping.message_id, "cfr_message_01");
+
+ sandbox.restore();
+});
+
+add_task(async function test_applyCFRPolicy_release() {
+ info(
+ "TelemetryFeed.applyCFRPolicy should use impression_id and bucket_id " +
+ "in release"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release");
+ sandbox
+ .stub(TelemetryFeed.prototype, "getOrCreateImpressionId")
+ .returns(FAKE_UUID);
+
+ let instance = new TelemetryFeed();
+
+ let data = {
+ action: "cfr_user_event",
+ event: "IMPRESSION",
+ message_id: "cfr_message_01",
+ bucket_id: "cfr_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyCFRPolicy(data);
+
+ Assert.equal(pingType, "cfr");
+ Assert.equal(ping.impression_id, FAKE_UUID);
+ Assert.equal(ping.client_id, undefined);
+ Assert.equal(ping.bucket_id, "cfr_bucket_01");
+ Assert.equal(ping.message_id, "n/a");
+
+ sandbox.restore();
+});
+
+add_task(async function test_applyCFRPolicy_experiment_release() {
+ info(
+ "TelemetryFeed.applyCFRPolicy should use impression_id and bucket_id " +
+ "in release"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release");
+ sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
+ slug: "SOME-CFR-EXP",
+ });
+
+ let instance = new TelemetryFeed();
+
+ let data = {
+ action: "cfr_user_event",
+ event: "IMPRESSION",
+ message_id: "cfr_message_01",
+ bucket_id: "cfr_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyCFRPolicy(data);
+
+ Assert.equal(pingType, "cfr");
+ Assert.equal(ping.impression_id, undefined);
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(ping.bucket_id, "cfr_bucket_01");
+ Assert.equal(ping.message_id, "cfr_message_01");
+
+ sandbox.restore();
+});
+
+add_task(async function test_applyCFRPolicy_release_private_browsing() {
+ info(
+ "TelemetryFeed.applyCFRPolicy should use impression_id and bucket_id " +
+ "in Private Browsing in release"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release");
+ sandbox
+ .stub(TelemetryFeed.prototype, "getOrCreateImpressionId")
+ .returns(FAKE_UUID);
+
+ let instance = new TelemetryFeed();
+
+ let data = {
+ action: "cfr_user_event",
+ event: "IMPRESSION",
+ is_private: true,
+ message_id: "cfr_message_01",
+ bucket_id: "cfr_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyCFRPolicy(data);
+
+ Assert.equal(pingType, "cfr");
+ Assert.equal(ping.impression_id, FAKE_UUID);
+ Assert.equal(ping.client_id, undefined);
+ Assert.equal(ping.bucket_id, "cfr_bucket_01");
+ Assert.equal(ping.message_id, "n/a");
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_applyCFRPolicy_release_experiment_private_browsing() {
+ info(
+ "TelemetryFeed.applyCFRPolicy should use client_id and message_id in the " +
+ "experiment cohort in Private Browsing in release"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release");
+ sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
+ slug: "SOME-CFR-EXP",
+ });
+
+ let instance = new TelemetryFeed();
+
+ let data = {
+ action: "cfr_user_event",
+ event: "IMPRESSION",
+ is_private: true,
+ message_id: "cfr_message_01",
+ bucket_id: "cfr_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyCFRPolicy(data);
+
+ Assert.equal(pingType, "cfr");
+ Assert.equal(ping.impression_id, undefined);
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(ping.bucket_id, "cfr_bucket_01");
+ Assert.equal(ping.message_id, "cfr_message_01");
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_applyWhatsNewPolicy() {
+ info(
+ "TelemetryFeed.applyWhatsNewPolicy should set client_id and set pingType"
+ );
+ let instance = new TelemetryFeed();
+ let { ping, pingType } = await instance.applyWhatsNewPolicy({});
+
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(pingType, "whats-new-panel");
+});
+
+add_task(async function test_applyInfoBarPolicy() {
+ info(
+ "TelemetryFeed.applyInfoBarPolicy should set client_id and set pingType"
+ );
+ let instance = new TelemetryFeed();
+ let { ping, pingType } = await instance.applyInfoBarPolicy({});
+
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(pingType, "infobar");
+});
+
+add_task(async function test_applyToastNotificationPolicy() {
+ info(
+ "TelemetryFeed.applyToastNotificationPolicy should set client_id " +
+ "and set pingType"
+ );
+ let instance = new TelemetryFeed();
+ let { ping, pingType } = await instance.applyToastNotificationPolicy({});
+
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(pingType, "toast_notification");
+});
+
+add_task(async function test_applySpotlightPolicy() {
+ info(
+ "TelemetryFeed.applySpotlightPolicy should set client_id " +
+ "and set pingType"
+ );
+ let instance = new TelemetryFeed();
+ let { ping, pingType } = await instance.applySpotlightPolicy({
+ action: "foo",
+ });
+
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(pingType, "spotlight");
+ Assert.equal(ping.action, undefined);
+});
+
+add_task(async function test_applyMomentsPolicy_prerelease() {
+ info(
+ "TelemetryFeed.applyMomentsPolicy should use client_id and " +
+ "message_id in prerelease"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("nightly");
+
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "moments_user_event",
+ event: "IMPRESSION",
+ message_id: "moments_message_01",
+ bucket_id: "moments_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyMomentsPolicy(data);
+
+ Assert.equal(pingType, "moments");
+ Assert.equal(ping.impression_id, undefined);
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(ping.bucket_id, "moments_bucket_01");
+ Assert.equal(ping.message_id, "moments_message_01");
+
+ sandbox.restore();
+});
+
+add_task(async function test_applyMomentsPolicy_release() {
+ info(
+ "TelemetryFeed.applyMomentsPolicy should use impression_id and " +
+ "bucket_id in release"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release");
+ sandbox
+ .stub(TelemetryFeed.prototype, "getOrCreateImpressionId")
+ .returns(FAKE_UUID);
+
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "moments_user_event",
+ event: "IMPRESSION",
+ message_id: "moments_message_01",
+ bucket_id: "moments_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyMomentsPolicy(data);
+
+ Assert.equal(pingType, "moments");
+ Assert.equal(ping.impression_id, FAKE_UUID);
+ Assert.equal(ping.client_id, undefined);
+ Assert.equal(ping.bucket_id, "moments_bucket_01");
+ Assert.equal(ping.message_id, "n/a");
+
+ sandbox.restore();
+});
+
+add_task(async function test_applyMomentsPolicy_experiment_release() {
+ info(
+ "TelemetryFeed.applyMomentsPolicy client_id and message_id in " +
+ "the experiment cohort in release"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release");
+ sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
+ slug: "SOME-CFR-EXP",
+ });
+
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "moments_user_event",
+ event: "IMPRESSION",
+ message_id: "moments_message_01",
+ bucket_id: "moments_bucket_01",
+ };
+ let { ping, pingType } = await instance.applyMomentsPolicy(data);
+
+ Assert.equal(pingType, "moments");
+ Assert.equal(ping.impression_id, undefined);
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(ping.bucket_id, "moments_bucket_01");
+ Assert.equal(ping.message_id, "moments_message_01");
+
+ sandbox.restore();
+});
+
+add_task(async function test_applyOnboardingPolicy_includes_client_id() {
+ info("TelemetryFeed.applyOnboardingPolicy should include client_id");
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "onboarding_user_event",
+ event: "CLICK_BUTTION",
+ message_id: "onboarding_message_01",
+ };
+ let { ping, pingType } = await instance.applyOnboardingPolicy(data);
+
+ Assert.equal(pingType, "onboarding");
+ Assert.equal(
+ ping.client_id,
+ Services.prefs.getCharPref("toolkit.telemetry.cachedClientID")
+ );
+ Assert.equal(ping.message_id, "onboarding_message_01");
+ Assert.equal(
+ ping.browser_session_id,
+ TelemetrySession.getMetadata("").sessionId
+ );
+});
+
+add_task(async function test_applyOnboardingPolicy_with_session() {
+ info(
+ "TelemetryFeed.applyOnboardingPolicy should include page to " +
+ "event_context if there is a session"
+ );
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "onboarding_user_event",
+ event: "CLICK_BUTTION",
+ message_id: "onboarding_message_01",
+ };
+ let session = { page: "about:welcome" };
+ let { ping, pingType } = await instance.applyOnboardingPolicy(data, session);
+
+ Assert.equal(pingType, "onboarding");
+ Assert.equal(ping.event_context, JSON.stringify({ page: "about:welcome" }));
+ Assert.equal(ping.message_id, "onboarding_message_01");
+});
+
+add_task(async function test_applyOnboardingPolicy_only_allowed_pages() {
+ info(
+ "TelemetryFeed.applyOnboardingPolicy should not set page if it is " +
+ "not in ONBOARDING_ALLOWED_PAGE_VALUES"
+ );
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "onboarding_user_event",
+ event: "CLICK_BUTTION",
+ message_id: "onboarding_message_01",
+ };
+ let session = { page: "foo" };
+ let { ping, pingType } = await instance.applyOnboardingPolicy(data, session);
+
+ Assert.equal(pingType, "onboarding");
+ Assert.equal(ping.event_context, JSON.stringify({}));
+ Assert.equal(ping.message_id, "onboarding_message_01");
+});
+
+add_task(
+ async function test_applyOnboardingPolicy_append_page_to_event_context() {
+ info(
+ "TelemetryFeed.applyOnboardingPolicy should append page to event_context " +
+ "if it is not empty"
+ );
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "onboarding_user_event",
+ event: "CLICK_BUTTION",
+ message_id: "onboarding_message_01",
+ event_context: JSON.stringify({ foo: "bar" }),
+ };
+ let session = { page: "about:welcome" };
+ let { ping, pingType } = await instance.applyOnboardingPolicy(
+ data,
+ session
+ );
+
+ Assert.equal(pingType, "onboarding");
+ Assert.equal(
+ ping.event_context,
+ JSON.stringify({ foo: "bar", page: "about:welcome" })
+ );
+ Assert.equal(ping.message_id, "onboarding_message_01");
+ }
+);
+
+add_task(
+ async function test_applyOnboardingPolicy_append_page_to_event_context() {
+ info(
+ "TelemetryFeed.applyOnboardingPolicy should append page to event_context " +
+ "if it is not a JSON serialized string"
+ );
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "onboarding_user_event",
+ event: "CLICK_BUTTION",
+ message_id: "onboarding_message_01",
+ event_context: "foo",
+ };
+ let session = { page: "about:welcome" };
+ let { ping, pingType } = await instance.applyOnboardingPolicy(
+ data,
+ session
+ );
+
+ Assert.equal(pingType, "onboarding");
+ Assert.equal(
+ ping.event_context,
+ JSON.stringify({ value: "foo", page: "about:welcome" })
+ );
+ Assert.equal(ping.message_id, "onboarding_message_01");
+ }
+);
+
+add_task(async function test_applyUndesiredEventPolicy() {
+ info(
+ "TelemetryFeed.applyUndesiredEventPolicy should exclude client_id " +
+ "and use impression_id"
+ );
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(TelemetryFeed.prototype, "getOrCreateImpressionId")
+ .returns(FAKE_UUID);
+
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "asrouter_undesired_event",
+ event: "RS_MISSING_DATA",
+ };
+ let { ping, pingType } = await instance.applyUndesiredEventPolicy(data);
+
+ Assert.equal(pingType, "undesired-events");
+ Assert.equal(ping.client_id, undefined);
+ Assert.equal(ping.impression_id, FAKE_UUID);
+
+ sandbox.restore();
+});
+
+add_task(async function test_createASRouterEvent_valid_ping() {
+ info(
+ "TelemetryFeed.createASRouterEvent should create a valid " +
+ "ASRouterEventPing ping"
+ );
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "cfr_user_event",
+ event: "CLICK",
+ message_id: "cfr_message_01",
+ };
+ let action = ac.ASRouterUserEvent(data);
+ let { ping } = await instance.createASRouterEvent(action);
+
+ await assertASRouterEventPingValid(ping);
+ Assert.equal(ping.event, "CLICK");
+});
+
+add_task(async function test_createASRouterEvent_call_correctPolicy() {
+ let testCallCorrectPolicy = async (expectedPolicyFnName, data) => {
+ info(
+ `TelemetryFeed.createASRouterEvent should call ${expectedPolicyFnName} ` +
+ `on action ${data.action} and event ${data.event}`
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ sandbox.stub(instance, expectedPolicyFnName);
+
+ let action = ac.ASRouterUserEvent(data);
+ await instance.createASRouterEvent(action);
+ Assert.ok(
+ instance[expectedPolicyFnName].calledOnce,
+ `TelemetryFeed.${expectedPolicyFnName} called`
+ );
+
+ sandbox.restore();
+ };
+
+ testCallCorrectPolicy("applyCFRPolicy", {
+ action: "cfr_user_event",
+ event: "IMPRESSION",
+ message_id: "cfr_message_01",
+ });
+
+ testCallCorrectPolicy("applyOnboardingPolicy", {
+ action: "onboarding_user_event",
+ event: "CLICK_BUTTON",
+ message_id: "onboarding_message_01",
+ });
+
+ testCallCorrectPolicy("applyWhatsNewPolicy", {
+ action: "whats-new-panel_user_event",
+ event: "CLICK_BUTTON",
+ message_id: "whats-new-panel_message_01",
+ });
+
+ testCallCorrectPolicy("applyMomentsPolicy", {
+ action: "moments_user_event",
+ event: "CLICK_BUTTON",
+ message_id: "moments_message_01",
+ });
+
+ testCallCorrectPolicy("applySpotlightPolicy", {
+ action: "spotlight_user_event",
+ event: "CLICK",
+ message_id: "SPOTLIGHT_MESSAGE_93",
+ });
+
+ testCallCorrectPolicy("applyToastNotificationPolicy", {
+ action: "toast_notification_user_event",
+ event: "IMPRESSION",
+ message_id: "TEST_TOAST_NOTIFICATION1",
+ });
+
+ testCallCorrectPolicy("applyUndesiredEventPolicy", {
+ action: "asrouter_undesired_event",
+ event: "UNDESIRED_EVENT",
+ });
+});
+
+add_task(async function test_createASRouterEvent_stringify_event_context() {
+ info(
+ "TelemetryFeed.createASRouterEvent should stringify event_context if " +
+ "it is an Object"
+ );
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "asrouter_undesired_event",
+ event: "UNDESIRED_EVENT",
+ event_context: { foo: "bar" },
+ };
+ let action = ac.ASRouterUserEvent(data);
+ let { ping } = await instance.createASRouterEvent(action);
+
+ Assert.equal(ping.event_context, JSON.stringify({ foo: "bar" }));
+});
+
+add_task(async function test_createASRouterEvent_not_stringify_event_context() {
+ info(
+ "TelemetryFeed.createASRouterEvent should not stringify event_context " +
+ "if it is a String"
+ );
+ let instance = new TelemetryFeed();
+ let data = {
+ action: "asrouter_undesired_event",
+ event: "UNDESIRED_EVENT",
+ event_context: "foo",
+ };
+ let action = ac.ASRouterUserEvent(data);
+ let { ping } = await instance.createASRouterEvent(action);
+
+ Assert.equal(ping.event_context, "foo");
+});
+
+add_task(async function test_sendUTEvent_call_right_function() {
+ info("TelemetryFeed.sendUTEvent should call the UT event function passed in");
+ let sandbox = sinon.createSandbox();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
+
+ let event = {};
+ let instance = new TelemetryFeed();
+ sandbox.stub(instance.utEvents, "sendUserEvent");
+ instance.addSession("foo");
+
+ await instance.sendUTEvent(event, instance.utEvents.sendUserEvent);
+ Assert.ok(instance.utEvents.sendUserEvent.calledWith(event));
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
+ sandbox.restore();
+});
+
+add_task(async function test_setLoadTriggerInfo() {
+ info(
+ "TelemetryFeed.setLoadTriggerInfo should call saveSessionPerfData " +
+ "w/load_trigger_{ts,type} data"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ sandbox.stub(instance, "saveSessionPerfData");
+
+ instance.browserOpenNewtabStart();
+ instance.addSession("port123");
+ instance.setLoadTriggerInfo("port123");
+
+ Assert.ok(
+ instance.saveSessionPerfData.calledWith(
+ "port123",
+ sinon.match({
+ load_trigger_type: "menu_plus_or_keyboard",
+ load_trigger_ts: sinon.match.number,
+ })
+ ),
+ "TelemetryFeed.saveSessionPerfData was called with the right arguments"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_setLoadTriggerInfo_no_saveSessionPerfData() {
+ info(
+ "TelemetryFeed.setLoadTriggerInfo should not call saveSessionPerfData " +
+ "when getting mark throws"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ sandbox.stub(instance, "saveSessionPerfData");
+
+ instance.addSession("port123");
+ instance.setLoadTriggerInfo("port123");
+
+ Assert.ok(
+ instance.saveSessionPerfData.notCalled,
+ "TelemetryFeed.saveSessionPerfData was not called"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_saveSessionPerfData_updates_session_with_data() {
+ info(
+ "TelemetryFeed.saveSessionPerfData should update the given session " +
+ "with the given data"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ instance.addSession("port123");
+ Assert.equal(instance.sessions.get("port123").fake_ts, undefined);
+ let data = { fake_ts: 456, other_fake_ts: 789 };
+ instance.saveSessionPerfData("port123", data);
+
+ let sessionPerfData = instance.sessions.get("port123").perf;
+ Assert.equal(sessionPerfData.fake_ts, 456);
+ Assert.equal(sessionPerfData.other_fake_ts, 789);
+
+ sandbox.restore();
+});
+
+add_task(async function test_saveSessionPerfData_calls_setLoadTriggerInfo() {
+ info(
+ "TelemetryFeed.saveSessionPerfData should call setLoadTriggerInfo if " +
+ "data has visibility_event_rcvd_ts"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ sandbox.stub(instance, "setLoadTriggerInfo");
+ instance.addSession("port123");
+ let data = { visibility_event_rcvd_ts: 444455 };
+
+ instance.saveSessionPerfData("port123", data);
+
+ Assert.ok(
+ instance.setLoadTriggerInfo.calledOnce,
+ "TelemetryFeed.setLoadTriggerInfo was called once"
+ );
+ Assert.ok(instance.setLoadTriggerInfo.calledWithExactly("port123"));
+
+ Assert.equal(
+ instance.sessions.get("port123").perf.visibility_event_rcvd_ts,
+ 444455
+ );
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_saveSessionPerfData_does_not_call_setLoadTriggerInfo() {
+ info(
+ "TelemetryFeed.saveSessionPerfData shouldn't call setLoadTriggerInfo if " +
+ "data has no visibility_event_rcvd_ts"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ sandbox.stub(instance, "setLoadTriggerInfo");
+ instance.addSession("port123");
+ instance.saveSessionPerfData("port123", { monkeys_ts: 444455 });
+
+ Assert.ok(
+ instance.setLoadTriggerInfo.notCalled,
+ "TelemetryFeed.setLoadTriggerInfo was not called"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_saveSessionPerfData_does_not_call_setLoadTriggerInfo_about_home() {
+ info(
+ "TelemetryFeed.saveSessionPerfData should not call setLoadTriggerInfo when " +
+ "url is about:home"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ sandbox.stub(instance, "setLoadTriggerInfo");
+ instance.addSession("port123", "about:home");
+ let data = { visibility_event_rcvd_ts: 444455 };
+ instance.saveSessionPerfData("port123", data);
+
+ Assert.ok(
+ instance.setLoadTriggerInfo.notCalled,
+ "TelemetryFeed.setLoadTriggerInfo was not called"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_saveSessionPerfData_calls_maybeRecordTopsitesPainted() {
+ info(
+ "TelemetryFeed.saveSessionPerfData should call maybeRecordTopsitesPainted " +
+ "when url is about:home and topsites_first_painted_ts is given"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ const TOPSITES_FIRST_PAINTED_TS = 44455;
+ let data = { topsites_first_painted_ts: TOPSITES_FIRST_PAINTED_TS };
+
+ sandbox.stub(AboutNewTab, "maybeRecordTopsitesPainted");
+ instance.addSession("port123", "about:home");
+ instance.saveSessionPerfData("port123", data);
+
+ Assert.ok(
+ AboutNewTab.maybeRecordTopsitesPainted.calledOnce,
+ "AboutNewTab.maybeRecordTopsitesPainted called once"
+ );
+ Assert.ok(
+ AboutNewTab.maybeRecordTopsitesPainted.calledWith(
+ TOPSITES_FIRST_PAINTED_TS
+ )
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_saveSessionPerfData_records_Glean_newtab_opened_event() {
+ info(
+ "TelemetryFeed.saveSessionPerfData should record a Glean newtab.opened event " +
+ "with the correct visit_id when visibility event received"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ const SESSION_ID = "decafc0ffee";
+ const PAGE = "about:newtab";
+ let session = { page: PAGE, perf: {}, session_id: SESSION_ID };
+ let data = { visibility_event_rcvd_ts: 444455 };
+
+ sandbox.stub(instance.sessions, "get").returns(session);
+ instance.saveSessionPerfData("port123", data);
+
+ let newtabOpenedEvents = Glean.newtab.opened.testGetValue();
+ Assert.deepEqual(newtabOpenedEvents[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ source: PAGE,
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_uninit_calls_utEvents_uninit() {
+ info("TelemetryFeed.uninit should call .utEvents.uninit");
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ sandbox.stub(instance.utEvents, "uninit");
+
+ instance.uninit();
+ Assert.ok(
+ instance.utEvents.uninit.calledOnce,
+ "TelemetryFeed.utEvents.uninit should be called"
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_uninit_deregisters_observer() {
+ info(
+ "TelemetryFeed.uninit should make this.browserOpenNewtabStart() stop " +
+ "observing browser-open-newtab-start"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ let countObservers = () => {
+ return [...Services.obs.enumerateObservers("browser-open-newtab-start")]
+ .length;
+ };
+
+ const ORIGINAL_COUNT = countObservers();
+ instance.init();
+ Assert.equal(countObservers(), ORIGINAL_COUNT + 1, "Observer was added");
+
+ instance.uninit();
+ Assert.equal(countObservers(), ORIGINAL_COUNT, "Observer was removed");
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_basic_actions() {
+ let browser = Services.appShell
+ .createWindowlessBrowser(false)
+ .document.createElement("browser");
+
+ let testOnAction = (setupFn, action, checkFn) => {
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ setupFn(sandbox, instance);
+
+ instance.onAction(action);
+ checkFn(instance);
+ sandbox.restore();
+ };
+
+ info("TelemetryFeed.onAction should call .init() on an INIT action");
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "init");
+ sandbox.stub(instance, "sendPageTakeoverData");
+ },
+ { type: at.INIT },
+ instance => {
+ Assert.ok(instance.init.calledOnce, "TelemetryFeed.init called once");
+ Assert.ok(
+ instance.sendPageTakeoverData.calledOnce,
+ "TelemetryFeed.sendPageTakeoverData called once"
+ );
+ }
+ );
+
+ info("TelemetryFeed.onAction should call .uninit() on an UNINIT action");
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "uninit");
+ },
+ { type: at.UNINIT },
+ instance => {
+ Assert.ok(instance.uninit.calledOnce, "TelemetryFeed.uninit called once");
+ }
+ );
+
+ info(
+ "TelemetryFeed.onAction should call .handleNewTabInit on a " +
+ "NEW_TAB_INIT action"
+ );
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "handleNewTabInit");
+ },
+ ac.AlsoToMain({
+ type: at.NEW_TAB_INIT,
+ data: { url: "about:newtab", browser },
+ }),
+ instance => {
+ Assert.ok(
+ instance.handleNewTabInit.calledOnce,
+ "TelemetryFeed.handleNewTabInit called once"
+ );
+ }
+ );
+
+ info(
+ "TelemetryFeed.onAction should call .addSession() on a " +
+ "NEW_TAB_INIT action"
+ );
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "addSession").returns({ perf: {} });
+ sandbox.stub(instance, "setLoadTriggerInfo");
+ },
+ ac.AlsoToMain(
+ {
+ type: at.NEW_TAB_INIT,
+ data: { url: "about:monkeys", browser },
+ },
+ "port123"
+ ),
+ instance => {
+ Assert.ok(
+ instance.addSession.calledOnce,
+ "TelemetryFeed.addSession called once"
+ );
+ Assert.ok(instance.addSession.calledWith("port123", "about:monkeys"));
+ }
+ );
+
+ info(
+ "TelemetryFeed.onAction should call .endSession() on a " +
+ "NEW_TAB_UNLOAD action"
+ );
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "endSession");
+ },
+ ac.AlsoToMain({ type: at.NEW_TAB_UNLOAD }, "port123"),
+ instance => {
+ Assert.ok(
+ instance.endSession.calledOnce,
+ "TelemetryFeed.endSession called once"
+ );
+ Assert.ok(instance.endSession.calledWith("port123"));
+ }
+ );
+
+ info(
+ "TelemetryFeed.onAction should call .saveSessionPerfData " +
+ "on SAVE_SESSION_PERF_DATA"
+ );
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "saveSessionPerfData");
+ },
+ ac.AlsoToMain(
+ { type: at.SAVE_SESSION_PERF_DATA, data: { some_ts: 10 } },
+ "port123"
+ ),
+ instance => {
+ Assert.ok(
+ instance.saveSessionPerfData.calledOnce,
+ "TelemetryFeed.saveSessionPerfData called once"
+ );
+ Assert.ok(
+ instance.saveSessionPerfData.calledWith("port123", { some_ts: 10 })
+ );
+ }
+ );
+
+ info(
+ "TelemetryFeed.onAction should send an event on a TELEMETRY_USER_EVENT " +
+ "action"
+ );
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "createUserEvent");
+ sandbox.stub(instance.utEvents, "sendUserEvent");
+ },
+ { type: at.TELEMETRY_USER_EVENT },
+ instance => {
+ Assert.ok(
+ instance.createUserEvent.calledOnce,
+ "TelemetryFeed.createUserEvent called once"
+ );
+ Assert.ok(
+ instance.createUserEvent.calledWith({ type: at.TELEMETRY_USER_EVENT })
+ );
+ Assert.ok(
+ instance.utEvents.sendUserEvent.calledOnce,
+ "TelemetryFeed.utEvents.sendUserEvent called once"
+ );
+ Assert.ok(
+ instance.utEvents.sendUserEvent.calledWith(
+ instance.createUserEvent.returnValue
+ )
+ );
+ }
+ );
+ Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+
+ info(
+ "TelemetryFeed.onAction should send an event on a " +
+ "DISCOVERY_STREAM_USER_EVENT action"
+ );
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ testOnAction(
+ (sandbox, instance) => {
+ sandbox.stub(instance, "createUserEvent");
+ sandbox.stub(instance.utEvents, "sendUserEvent");
+ },
+ { type: at.DISCOVERY_STREAM_USER_EVENT },
+ instance => {
+ Assert.ok(
+ instance.createUserEvent.calledOnce,
+ "TelemetryFeed.createUserEvent called once"
+ );
+ Assert.ok(
+ instance.createUserEvent.calledWith({
+ type: at.DISCOVERY_STREAM_USER_EVENT,
+ data: {
+ value: {
+ pocket_logged_in_status: Glean.pocket.isSignedIn.testGetValue(),
+ },
+ },
+ })
+ );
+ Assert.ok(
+ instance.utEvents.sendUserEvent.calledOnce,
+ "TelemetryFeed.utEvents.sendUserEvent called once"
+ );
+ Assert.ok(
+ instance.utEvents.sendUserEvent.calledWith(
+ instance.createUserEvent.returnValue
+ )
+ );
+ }
+ );
+ Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+});
+
+add_task(async function test_onAction_calls_handleASRouterUserEvent() {
+ let actions = [
+ at.AS_ROUTER_TELEMETRY_USER_EVENT,
+ msg.TOOLBAR_BADGE_TELEMETRY,
+ msg.TOOLBAR_PANEL_TELEMETRY,
+ msg.MOMENTS_PAGE_TELEMETRY,
+ msg.DOORHANGER_TELEMETRY,
+ ];
+ Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ actions.forEach(type => {
+ info(`Testing ${type} action`);
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ const eventHandler = sandbox.spy(instance, "handleASRouterUserEvent");
+ const action = {
+ type,
+ data: { event: "CLICK" },
+ };
+
+ instance.onAction(action);
+
+ Assert.ok(eventHandler.calledWith(action));
+ sandbox.restore();
+ });
+ Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+});
+
+add_task(
+ async function test_onAction_calls_handleDiscoveryStreamImpressionStats_ds() {
+ info(
+ "TelemetryFeed.onAction should call " +
+ ".handleDiscoveryStreamImpressionStats on a " +
+ "DISCOVERY_STREAM_IMPRESSION_STATS action"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ let session = {};
+ sandbox.stub(instance.sessions, "get").returns(session);
+ let data = { source: "foo", tiles: [{ id: 1 }] };
+ let action = { type: at.DISCOVERY_STREAM_IMPRESSION_STATS, data };
+ sandbox.spy(instance, "handleDiscoveryStreamImpressionStats");
+
+ instance.onAction(ac.AlsoToMain(action, "port123"));
+
+ Assert.ok(
+ instance.handleDiscoveryStreamImpressionStats.calledWith("port123", data)
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_onAction_calls_handleTopSitesSponsoredImpressionStats() {
+ info(
+ "TelemetryFeed.onAction should call " +
+ ".handleTopSitesSponsoredImpressionStats on a " +
+ "TOP_SITES_SPONSORED_IMPRESSION_STATS action"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ let session = {};
+ sandbox.stub(instance.sessions, "get").returns(session);
+ let data = { type: "impression", tile_id: 42, position: 1 };
+ let action = { type: at.TOP_SITES_SPONSORED_IMPRESSION_STATS, data };
+ sandbox.spy(instance, "handleTopSitesSponsoredImpressionStats");
+
+ instance.onAction(ac.AlsoToMain(action));
+
+ Assert.ok(
+ instance.handleTopSitesSponsoredImpressionStats.calledOnce,
+ "TelemetryFeed.handleTopSitesSponsoredImpressionStats called once"
+ );
+ Assert.deepEqual(
+ instance.handleTopSitesSponsoredImpressionStats.firstCall.args[0].data,
+ data
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_onAction_calls_handleAboutSponsoredTopSites() {
+ info(
+ "TelemetryFeed.onAction should call " +
+ ".handleAboutSponsoredTopSites on a " +
+ "ABOUT_SPONSORED_TOP_SITES action"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ let data = { position: 0, advertiser_name: "moo", tile_id: 42 };
+ let action = { type: at.ABOUT_SPONSORED_TOP_SITES, data };
+ sandbox.spy(instance, "handleAboutSponsoredTopSites");
+
+ instance.onAction(ac.AlsoToMain(action));
+
+ Assert.ok(
+ instance.handleAboutSponsoredTopSites.calledOnce,
+ "TelemetryFeed.handleAboutSponsoredTopSites called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_calls_handleBlockUrl() {
+ info(
+ "TelemetryFeed.onAction should call #handleBlockUrl on a BLOCK_URL action"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ let data = { position: 0, advertiser_name: "moo", tile_id: 42 };
+ let action = { type: at.BLOCK_URL, data };
+ sandbox.spy(instance, "handleBlockUrl");
+
+ instance.onAction(ac.AlsoToMain(action));
+
+ Assert.ok(
+ instance.handleBlockUrl.calledOnce,
+ "TelemetryFeed.handleBlockUrl called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_onAction_calls_handleTopSitesOrganicImpressionStats() {
+ info(
+ "TelemetryFeed.onAction should call .handleTopSitesOrganicImpressionStats " +
+ "on a TOP_SITES_ORGANIC_IMPRESSION_STATS action"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ let session = {};
+ sandbox.stub(instance.sessions, "get").returns(session);
+
+ let data = { type: "impression", position: 1 };
+ let action = { type: at.TOP_SITES_ORGANIC_IMPRESSION_STATS, data };
+ sandbox.spy(instance, "handleTopSitesOrganicImpressionStats");
+
+ instance.onAction(ac.AlsoToMain(action));
+
+ Assert.ok(
+ instance.handleTopSitesOrganicImpressionStats.calledOnce,
+ "TelemetryFeed.handleTopSitesOrganicImpressionStats called once"
+ );
+ Assert.deepEqual(
+ instance.handleTopSitesOrganicImpressionStats.firstCall.args[0].data,
+ data
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_handleNewTabInit_sets_preloaded_session() {
+ info(
+ "TelemetryFeed.handleNewTabInit should set the session as preloaded " +
+ "if the browser is preloaded"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ let session = { perf: {} };
+ let preloadedBrowser = {
+ getAttribute() {
+ return "preloaded";
+ },
+ };
+ sandbox.stub(instance, "addSession").returns(session);
+
+ instance.onAction(
+ ac.AlsoToMain({
+ type: at.NEW_TAB_INIT,
+ data: { url: "about:newtab", browser: preloadedBrowser },
+ })
+ );
+
+ Assert.ok(session.perf.is_preloaded, "is_preloaded property was set");
+
+ sandbox.restore();
+});
+
+add_task(async function test_handleNewTabInit_sets_nonpreloaded_session() {
+ info(
+ "TelemetryFeed.handleNewTabInit should set the session as non-preloaded " +
+ "if the browser is non-preloaded"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ let session = { perf: {} };
+ let preloadedBrowser = {
+ getAttribute() {
+ return "";
+ },
+ };
+ sandbox.stub(instance, "addSession").returns(session);
+
+ instance.onAction(
+ ac.AlsoToMain({
+ type: at.NEW_TAB_INIT,
+ data: { url: "about:newtab", browser: preloadedBrowser },
+ })
+ );
+
+ Assert.ok(!session.perf.is_preloaded, "is_preloaded property is not true");
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_SendASRouterUndesiredEvent_calls_handleASRouterUserEvent() {
+ info(
+ "TelemetryFeed.SendASRouterUndesiredEvent should call " +
+ "handleASRouterUserEvent"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ sandbox.stub(instance, "handleASRouterUserEvent");
+
+ instance.SendASRouterUndesiredEvent({ foo: "bar" });
+
+ Assert.ok(
+ instance.handleASRouterUserEvent.calledOnce,
+ "TelemetryFeed.handleASRouterUserEvent was called once"
+ );
+ let [payload] = instance.handleASRouterUserEvent.firstCall.args;
+ Assert.equal(payload.data.action, "asrouter_undesired_event");
+ Assert.equal(payload.data.foo, "bar");
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_sendPageTakeoverData_homepage_category() {
+ info(
+ "TelemetryFeed.sendPageTakeoverData should call " +
+ "handleASRouterUserEvent"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ sandbox.stub(HomePage, "get").returns("https://searchprovider.com");
+ instance._classifySite = () => Promise.resolve("other");
+
+ await instance.sendPageTakeoverData();
+ Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "other");
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+});
+
+add_task(async function test_sendPageTakeoverData_newtab_category_custom() {
+ info(
+ "TelemetryFeed.sendPageTakeoverData should send correct newtab " +
+ "category for about:newtab set to custom URL"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ sandbox.stub(AboutNewTab, "newTabURLOverridden").get(() => true);
+ sandbox
+ .stub(AboutNewTab, "newTabURL")
+ .get(() => "https://searchprovider.com");
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ instance._classifySite = () => Promise.resolve("other");
+
+ await instance.sendPageTakeoverData();
+ Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "other");
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+});
+
+add_task(async function test_sendPageTakeoverData_newtab_category_custom() {
+ info(
+ "TelemetryFeed.sendPageTakeoverData should not set home|newtab " +
+ "category if neither about:{home,newtab} are set to custom URL"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ instance._classifySite = () => Promise.resolve("other");
+
+ await instance.sendPageTakeoverData();
+ Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "enabled");
+ Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "enabled");
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+});
+
+add_task(async function test_sendPageTakeoverData_newtab_category_extension() {
+ info(
+ "TelemetryFeed.sendPageTakeoverData should set correct home|newtab " +
+ "category when changed by extension"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ const ID = "{abc-foo-bar}";
+ sandbox.stub(ExtensionSettingsStore, "getSetting").returns({ id: ID });
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ instance._classifySite = () => Promise.resolve("other");
+
+ await instance.sendPageTakeoverData();
+ Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "extension");
+ Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "extension");
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+});
+
+add_task(async function test_sendPageTakeoverData_newtab_disabled() {
+ info(
+ "TelemetryFeed.sendPageTakeoverData instruments when newtab is disabled"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ Services.prefs.setBoolPref("browser.newtabpage.enabled", false);
+ instance._classifySite = () => Promise.resolve("other");
+
+ await instance.sendPageTakeoverData();
+ Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "disabled");
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ Services.prefs.clearUserPref("browser.newtabpage.enabled");
+ sandbox.restore();
+});
+
+add_task(async function test_sendPageTakeoverData_homepage_disabled() {
+ info(
+ "TelemetryFeed.sendPageTakeoverData instruments when homepage is disabled"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+ sandbox.stub(HomePage, "overridden").get(() => true);
+
+ await instance.sendPageTakeoverData();
+ Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "disabled");
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+});
+
+add_task(async function test_sendPageTakeoverData_newtab_ping() {
+ info("TelemetryFeed.sendPageTakeoverData should send a 'newtab' ping");
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+
+ let pingSubmitted = new Promise(resolve => {
+ GleanPings.newtab.testBeforeNextSubmit(reason => {
+ Assert.equal(reason, "component_init");
+ resolve();
+ });
+ });
+
+ await instance.sendPageTakeoverData();
+ await pingSubmitted;
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+});
+
+add_task(
+ async function test_handleDiscoveryStreamImpressionStats_should_throw() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamImpressionStats should throw " +
+ "for a missing session"
+ );
+
+ let instance = new TelemetryFeed();
+ try {
+ instance.handleDiscoveryStreamImpressionStats("a_missing_port", {});
+ Assert.ok(false, "Should not have reached here.");
+ } catch (e) {
+ Assert.ok(true, "Should have thrown for a missing session.");
+ }
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamImpressionStats_instrument_pocket_impressions() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamImpressionStats should throw " +
+ "for a missing session"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ const SESSION_ID = "1337cafe";
+ const POS_1 = 1;
+ const POS_2 = 4;
+ const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ let pingSubmitted = new Promise(resolve => {
+ GleanPings.spoc.testBeforeNextSubmit(reason => {
+ Assert.equal(reason, "impression");
+
+ let pocketImpressions = Glean.pocket.impression.testGetValue();
+ Assert.equal(pocketImpressions.length, 2);
+ Assert.deepEqual(pocketImpressions[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(false),
+ position: String(POS_1),
+ recommendation_id: "decaf-c0ff33",
+ tile_id: String(1),
+ });
+ Assert.deepEqual(pocketImpressions[1].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(true),
+ position: String(POS_2),
+ tile_id: String(2),
+ });
+ Assert.equal(Glean.pocket.shim.testGetValue(), SHIM);
+
+ resolve();
+ });
+ });
+
+ instance.handleDiscoveryStreamImpressionStats("_", {
+ source: "foo",
+ tiles: [
+ {
+ id: 1,
+ pos: POS_1,
+ type: "organic",
+ recommendation_id: "decaf-c0ff33",
+ },
+ {
+ id: 2,
+ pos: POS_2,
+ type: "spoc",
+ recommendation_id: undefined,
+ shim: SHIM,
+ },
+ ],
+ window_inner_width: 1000,
+ window_inner_height: 900,
+ });
+
+ await pingSubmitted;
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleASRouterUserEvent_calls_submitGleanPingForPing() {
+ info(
+ "TelemetryFeed.handleASRouterUserEvent should call " +
+ "submitGleanPingForPing on known pingTypes when telemetry is enabled"
+ );
+
+ let data = {
+ action: "onboarding_user_event",
+ event: "IMPRESSION",
+ message_id: "12345",
+ };
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+
+ sandbox.spy(AboutWelcomeTelemetry.prototype, "submitGleanPingForPing");
+
+ await instance.handleASRouterUserEvent({ data });
+
+ Assert.ok(
+ AboutWelcomeTelemetry.prototype.submitGleanPingForPing.calledOnce,
+ "AboutWelcomeTelemetry.submitGleanPingForPing called once"
+ );
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleASRouterUserEvent_no_submit_unknown_pingTypes() {
+ info(
+ "TelemetryFeed.handleASRouterUserEvent not submit pings on unknown pingTypes"
+ );
+
+ let data = {
+ action: "unknown_event",
+ event: "IMPRESSION",
+ message_id: "12345",
+ };
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY, true);
+
+ sandbox.spy(AboutWelcomeTelemetry.prototype, "submitGleanPingForPing");
+
+ await instance.handleASRouterUserEvent({ data });
+
+ Assert.ok(
+ AboutWelcomeTelemetry.prototype.submitGleanPingForPing.notCalled,
+ "AboutWelcomeTelemetry.submitGleanPingForPing not called"
+ );
+
+ Services.prefs.clearUserPref(PREF_TELEMETRY);
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_isInCFRCohort_return_false_for_no_CFR_experiment() {
+ info(
+ "TelemetryFeed.isInCFRCohort should return false if there " +
+ "is no CFR experiment registered"
+ );
+ let instance = new TelemetryFeed();
+ Assert.ok(
+ !instance.isInCFRCohort,
+ "Should not be in CFR cohort by default"
+ );
+ }
+);
+
+add_task(
+ async function test_isInCFRCohort_return_true_for_registered_CFR_experiment() {
+ info(
+ "TelemetryFeed.isInCFRCohort should return true if there " +
+ "is a CFR experiment registered"
+ );
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+
+ sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
+ slug: "SOME-CFR-EXP",
+ });
+
+ Assert.ok(instance.isInCFRCohort, "Should be in a CFR cohort");
+ Assert.equal(
+ ExperimentAPI.getExperimentMetaData.firstCall.args[0].featureId,
+ "cfr"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesSponsoredImpressionStats_add_keyed_scalar() {
+ info(
+ "TelemetryFeed.handleTopSitesSponsoredImpressionStats should add to " +
+ "keyed scalar on an impression event"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.telemetry.clearScalars();
+
+ let data = {
+ type: "impression",
+ tile_id: 42,
+ source: "newtab",
+ position: 0,
+ reporting_url: "https://test.reporting.net/",
+ };
+ await instance.handleTopSitesSponsoredImpressionStats({ data });
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "contextual.services.topsites.impression",
+ "newtab_1",
+ 1
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesSponsoredImpressionStats_add_keyed_scalar_click() {
+ info(
+ "TelemetryFeed.handleTopSitesSponsoredImpressionStats should add to " +
+ "keyed scalar on a click event"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.telemetry.clearScalars();
+
+ let data = {
+ type: "click",
+ tile_id: 42,
+ source: "newtab",
+ position: 0,
+ reporting_url: "https://test.reporting.net/",
+ };
+ await instance.handleTopSitesSponsoredImpressionStats({ data });
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "contextual.services.topsites.click",
+ "newtab_1",
+ 1
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesSponsoredImpressionStats_record_glean_impression() {
+ info(
+ "TelemetryFeed.handleTopSitesSponsoredImpressionStats should record a " +
+ "Glean topsites.impression event on an impression event"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let data = {
+ type: "impression",
+ tile_id: 42,
+ source: "newtab",
+ position: 1,
+ reporting_url: "https://test.reporting.net/",
+ advertiser: "adnoid ads",
+ };
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+ await instance.handleTopSitesSponsoredImpressionStats({ data });
+ let impressions = Glean.topsites.impression.testGetValue();
+ Assert.equal(impressions.length, 1, "Should have recorded 1 impression");
+
+ Assert.deepEqual(impressions[0].extra, {
+ advertiser_name: "adnoid ads",
+ tile_id: data.tile_id,
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(true),
+ position: String(1),
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesSponsoredImpressionStats_record_glean_click() {
+ info(
+ "TelemetryFeed.handleTopSitesSponsoredImpressionStats should record " +
+ "a Glean topsites.click event on a click event"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let data = {
+ type: "click",
+ advertiser: "test advertiser",
+ tile_id: 42,
+ source: "newtab",
+ position: 0,
+ reporting_url: "https://test.reporting.net/",
+ };
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+ await instance.handleTopSitesSponsoredImpressionStats({ data });
+ let clicks = Glean.topsites.click.testGetValue();
+ Assert.equal(clicks.length, 1, "Should have recorded 1 click");
+
+ Assert.deepEqual(clicks[0].extra, {
+ advertiser_name: "test advertiser",
+ tile_id: data.tile_id,
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(true),
+ position: String(0),
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesSponsoredImpressionStats_no_submit_unknown_pingType() {
+ info(
+ "TelemetryFeed.handleTopSitesSponsoredImpressionStats should not " +
+ "submit on unknown pingTypes"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let data = { type: "unknown_type" };
+
+ await instance.handleTopSitesSponsoredImpressionStats({ data });
+ let impressions = Glean.topsites.impression.testGetValue();
+ Assert.ok(!impressions, "Should not have recorded any impressions");
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesOrganicImpressionStats_record_glean_topsites_impression() {
+ info(
+ "TelemetryFeed.handleTopSitesOrganicImpressionStats should record a " +
+ "Glean topsites.impression event on an impression event"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let data = {
+ type: "impression",
+ source: "newtab",
+ position: 0,
+ };
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ await instance.handleTopSitesOrganicImpressionStats({ data });
+ let impressions = Glean.topsites.impression.testGetValue();
+ Assert.equal(impressions.length, 1, "Recorded 1 impression");
+
+ Assert.deepEqual(impressions[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(false),
+ position: String(0),
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesOrganicImpressionStats_record_glean_topsites_click() {
+ info(
+ "TelemetryFeed.handleTopSitesOrganicImpressionStats should record a " +
+ "Glean topsites.click event on a click event"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let data = {
+ type: "click",
+ source: "newtab",
+ position: 0,
+ };
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ await instance.handleTopSitesOrganicImpressionStats({ data });
+ let clicks = Glean.topsites.click.testGetValue();
+ Assert.equal(clicks.length, 1, "Recorded 1 click");
+
+ Assert.deepEqual(clicks[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(false),
+ position: String(0),
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesOrganicImpressionStats_no_recording() {
+ info(
+ "TelemetryFeed.handleTopSitesOrganicImpressionStats should not " +
+ "record events on an unknown session"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ sandbox.stub(instance.sessions, "get").returns(false);
+
+ await instance.handleTopSitesOrganicImpressionStats({});
+ Assert.ok(!Glean.topsites.click.testGetValue(), "Click was not recorded");
+ Assert.ok(
+ !Glean.topsites.impression.testGetValue(),
+ "Impression was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleTopSitesOrganicImpressionStats_no_recording_with_session() {
+ info(
+ "TelemetryFeed.handleTopSitesOrganicImpressionStats should not record " +
+ "events on an unknown impressionStats action"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ await instance.handleTopSitesOrganicImpressionStats({ type: "unknown" });
+ Assert.ok(!Glean.topsites.click.testGetValue(), "Click was not recorded");
+ Assert.ok(
+ !Glean.topsites.impression.testGetValue(),
+ "Impression was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_no_recording_with_session() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
+ "action with no `data`"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let action = ac.DiscoveryStreamUserEvent();
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+ Assert.ok(
+ !Glean.pocket.topicClick.testGetValue(),
+ "Pocket topicClick was not recorded"
+ );
+ Assert.ok(
+ !Glean.pocket.click.testGetValue(),
+ "Pocket click was not recorded"
+ );
+ Assert.ok(
+ !Glean.pocket.save.testGetValue(),
+ "Pocket save was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_click_with_no_value() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
+ "CLICK data with no value"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "POPULAR_TOPICS",
+ });
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+ let topicClicks = Glean.pocket.topicClick.testGetValue();
+ Assert.equal(topicClicks.length, 1, "Recorded 1 click");
+ Assert.deepEqual(topicClicks[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_non_popular_click_with_no_value() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
+ "non-POPULAR_TOPICS CLICK data with no value"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "not-POPULAR_TOPICS",
+ });
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+ Assert.ok(
+ !Glean.pocket.topicClick.testGetValue(),
+ "Pocket topicClick was not recorded"
+ );
+ Assert.ok(
+ !Glean.pocket.click.testGetValue(),
+ "Pocket click was not recorded"
+ );
+ Assert.ok(
+ !Glean.pocket.save.testGetValue(),
+ "Pocket save was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_non_popular_click() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
+ "CLICK data with non-POPULAR_TOPICS source"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ const TOPIC = "atopic";
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "not-POPULAR_TOPICS",
+ value: {
+ card_type: "topics_widget",
+ topic: TOPIC,
+ },
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+ let topicClicks = Glean.pocket.topicClick.testGetValue();
+ Assert.equal(topicClicks.length, 1, "Recorded 1 click");
+ Assert.deepEqual(topicClicks[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ topic: TOPIC,
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_without_card_type() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent doesn't instrument " +
+ "a CLICK without a card_type"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "not-POPULAR_TOPICS",
+ value: {
+ card_type: "not spoc, organic, or topics_widget",
+ },
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+
+ Assert.ok(
+ !Glean.pocket.topicClick.testGetValue(),
+ "Pocket topicClick was not recorded"
+ );
+ Assert.ok(
+ !Glean.pocket.click.testGetValue(),
+ "Pocket click was not recorded"
+ );
+ Assert.ok(
+ !Glean.pocket.save.testGetValue(),
+ "Pocket save was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_handleDiscoveryStreamUserEvent_popular_click() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a popular " +
+ "topic click"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ const TOPIC = "entertainment";
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ source: "POPULAR_TOPICS",
+ value: {
+ card_type: "topics_widget",
+ topic: TOPIC,
+ },
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+ let topicClicks = Glean.pocket.topicClick.testGetValue();
+ Assert.equal(topicClicks.length, 1, "Recorded 1 click");
+ Assert.deepEqual(topicClicks[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ topic: TOPIC,
+ });
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_organic_top_stories_click() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent instruments an organic " +
+ "top stories click"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ const ACTION_POSITION = 42;
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ action_position: ACTION_POSITION,
+ value: {
+ card_type: "organic",
+ recommendation_id: "decaf-c0ff33",
+ tile_id: 314623757745896,
+ },
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+
+ let clicks = Glean.pocket.click.testGetValue();
+ Assert.equal(clicks.length, 1, "Recorded 1 click");
+ Assert.deepEqual(clicks[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(false),
+ position: ACTION_POSITION,
+ recommendation_id: "decaf-c0ff33",
+ tile_id: String(314623757745896),
+ });
+
+ Assert.ok(
+ !Glean.pocket.shim.testGetValue(),
+ "Pocket shim was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_sponsored_top_stories_click() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a sponsored " +
+ "top stories click"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ const ACTION_POSITION = 42;
+ const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "CLICK",
+ action_position: ACTION_POSITION,
+ value: {
+ card_type: "spoc",
+ recommendation_id: undefined,
+ tile_id: 448685088,
+ shim: SHIM,
+ },
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ let pingSubmitted = new Promise(resolve => {
+ GleanPings.spoc.testBeforeNextSubmit(reason => {
+ Assert.equal(reason, "click");
+ resolve();
+ });
+ });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+
+ let clicks = Glean.pocket.click.testGetValue();
+ Assert.equal(clicks.length, 1, "Recorded 1 click");
+ Assert.deepEqual(clicks[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(true),
+ position: ACTION_POSITION,
+ tile_id: String(448685088),
+ });
+
+ await pingSubmitted;
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_organic_top_stories_save() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a save of an " +
+ "organic top story"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ const ACTION_POSITION = 42;
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "SAVE_TO_POCKET",
+ action_position: ACTION_POSITION,
+ value: {
+ card_type: "organic",
+ recommendation_id: "decaf-c0ff33",
+ tile_id: 314623757745896,
+ },
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+
+ let saves = Glean.pocket.save.testGetValue();
+ Assert.equal(saves.length, 1, "Recorded 1 save");
+ Assert.deepEqual(saves[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(false),
+ position: ACTION_POSITION,
+ recommendation_id: "decaf-c0ff33",
+ tile_id: String(314623757745896),
+ });
+ Assert.ok(
+ !Glean.pocket.shim.testGetValue(),
+ "Pocket shim was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_sponsored_top_stories_save() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a save of a " +
+ "sponsored top story"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ const ACTION_POSITION = 42;
+ const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "SAVE_TO_POCKET",
+ action_position: ACTION_POSITION,
+ value: {
+ card_type: "spoc",
+ recommendation_id: undefined,
+ tile_id: 448685088,
+ shim: SHIM,
+ },
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+ let pingSubmitted = new Promise(resolve => {
+ GleanPings.spoc.testBeforeNextSubmit(reason => {
+ Assert.equal(reason, "save");
+ Assert.equal(
+ Glean.pocket.shim.testGetValue(),
+ SHIM,
+ "Pocket shim was recorded"
+ );
+
+ resolve();
+ });
+ });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+
+ let saves = Glean.pocket.save.testGetValue();
+ Assert.equal(saves.length, 1, "Recorded 1 save");
+ Assert.deepEqual(saves[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(true),
+ position: ACTION_POSITION,
+ tile_id: String(448685088),
+ });
+
+ await pingSubmitted;
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleDiscoveryStreamUserEvent_sponsored_top_stories_save_no_value() {
+ info(
+ "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a save of a " +
+ "sponsored top story, without `value`"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+ const ACTION_POSITION = 42;
+ let action = ac.DiscoveryStreamUserEvent({
+ event: "SAVE_TO_POCKET",
+ action_position: ACTION_POSITION,
+ });
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleDiscoveryStreamUserEvent(action);
+
+ let saves = Glean.pocket.save.testGetValue();
+ Assert.equal(saves.length, 1, "Recorded 1 save");
+ Assert.deepEqual(saves[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(false),
+ position: ACTION_POSITION,
+ });
+ Assert.ok(
+ !Glean.pocket.shim.testGetValue(),
+ "Pocket shim was not recorded"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleAboutSponsoredTopSites_record_showPrivacyClick() {
+ info(
+ "TelemetryFeed.handleAboutSponsoredTopSites should record a Glean " +
+ "topsites.showPrivacyClick event on action"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let data = {
+ position: 42,
+ advertiser_name: "mozilla",
+ tile_id: 4567,
+ };
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ instance.handleAboutSponsoredTopSites({ data });
+
+ let clicks = Glean.topsites.showPrivacyClick.testGetValue();
+ Assert.equal(clicks.length, 1, "Recorded 1 click");
+ Assert.deepEqual(clicks[0].extra, {
+ advertiser_name: data.advertiser_name,
+ tile_id: String(data.tile_id),
+ newtab_visit_id: SESSION_ID,
+ position: String(data.position),
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_handleAboutSponsoredTopSites_no_record_showPrivacyClick() {
+ info(
+ "TelemetryFeed.handleAboutSponsoredTopSites should not record a Glean " +
+ "topsites.showPrivacyClick event if there's no session"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ let data = {
+ position: 42,
+ advertiser_name: "mozilla",
+ tile_id: 4567,
+ };
+
+ sandbox.stub(instance.sessions, "get").returns(null);
+
+ instance.handleAboutSponsoredTopSites({ data });
+
+ let clicks = Glean.topsites.showPrivacyClick.testGetValue();
+ Assert.ok(!clicks, "Did not record any clicks");
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_handleBlockUrl_no_record_dismisses() {
+ info(
+ "TelemetryFeed.handleBlockUrl shouldn't record events for pocket " +
+ "cards' dismisses"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ let data = [
+ {
+ // Shouldn't record anything for this one
+ is_pocket_card: true,
+ position: 43,
+ tile_id: undefined,
+ },
+ ];
+
+ await instance.handleBlockUrl({ data });
+
+ Assert.ok(
+ !Glean.topsites.dismiss.testGetValue(),
+ "Should not record a dismiss for Pocket cards"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_handleBlockUrl_record_dismiss_on_action() {
+ info(
+ "TelemetryFeed.handleBlockUrl should record a topsites.dismiss event " +
+ "on action"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ let data = [
+ {
+ is_pocket_card: false,
+ position: 42,
+ advertiser_name: "mozilla",
+ tile_id: 4567,
+ isSponsoredTopSite: 1, // for some reason this is an int.
+ },
+ ];
+
+ await instance.handleBlockUrl({ data });
+
+ let dismisses = Glean.topsites.dismiss.testGetValue();
+ Assert.equal(dismisses.length, 1, "Should have recorded 1 dismiss");
+ Assert.deepEqual(dismisses[0].extra, {
+ advertiser_name: data[0].advertiser_name,
+ tile_id: String(data[0].tile_id),
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(!!data[0].isSponsoredTopSite),
+ position: String(data[0].position),
+ });
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_handleBlockUrl_record_dismiss_on_nonsponsored_action() {
+ info(
+ "TelemetryFeed.handleBlockUrl should record a Glean topsites.dismiss " +
+ "event on action on non-sponsored topsite"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ const SESSION_ID = "decafc0ffee";
+ sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
+
+ let data = [
+ {
+ is_pocket_card: false,
+ position: 42,
+ tile_id: undefined,
+ },
+ ];
+
+ await instance.handleBlockUrl({ data });
+
+ let dismisses = Glean.topsites.dismiss.testGetValue();
+ Assert.equal(dismisses.length, 1, "Should have recorded 1 dismiss");
+ Assert.deepEqual(dismisses[0].extra, {
+ newtab_visit_id: SESSION_ID,
+ is_sponsored: String(false),
+ position: String(data[0].position),
+ });
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_handleBlockUrl_no_record_dismiss_on_no_session() {
+ info(
+ "TelemetryFeed.handleBlockUrl should not record a Glean " +
+ "topsites.dismiss event if there's no session"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let instance = new TelemetryFeed();
+ Services.fog.testResetFOG();
+
+ sandbox.stub(instance.sessions, "get").returns(null);
+
+ let data = {};
+
+ await instance.handleBlockUrl({ data });
+
+ Assert.ok(
+ !Glean.topsites.dismiss.testGetValue(),
+ "Should not have recorded a dismiss"
+ );
+
+ sandbox.restore();
+});
diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js
new file mode 100644
index 0000000000..860e8758a5
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js
@@ -0,0 +1,3397 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TopSitesFeed, DEFAULT_TOP_SITES } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/TopSitesFeed.sys.mjs"
+);
+
+const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
+ shortURL: "resource://activity-stream/lib/ShortURL.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs",
+ Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs",
+ SearchService: "resource://gre/modules/SearchService.sys.mjs",
+ TOP_SITES_DEFAULT_ROWS: "resource://activity-stream/common/Reducers.sys.mjs",
+ TOP_SITES_MAX_SITES_PER_ROW:
+ "resource://activity-stream/common/Reducers.sys.mjs",
+});
+
+const FAKE_FAVICON = "data987";
+const FAKE_FAVICON_SIZE = 128;
+const FAKE_FRECENCY = 200;
+const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW)
+ .fill(null)
+ .map((v, i) => ({
+ frecency: FAKE_FRECENCY,
+ url: `http://www.site${i}.com`,
+ }));
+const FAKE_SCREENSHOT = "data123";
+const SEARCH_SHORTCUTS_EXPERIMENT_PREF = "improvesearch.topSiteSearchShortcuts";
+const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF =
+ "improvesearch.topSiteSearchShortcuts.searchEngines";
+const SEARCH_SHORTCUTS_HAVE_PINNED_PREF =
+ "improvesearch.topSiteSearchShortcuts.havePinned";
+const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
+const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
+const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
+const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
+
+// This pref controls how long the contile cache is valid for in seconds.
+const CONTILE_CACHE_VALID_FOR_SECONDS_PREF =
+ "browser.topsites.contile.cacheValidFor";
+// This pref records when the last contile fetch occurred, as a UNIX timestamp
+// in seconds.
+const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch";
+
+function FakeTippyTopProvider() {}
+FakeTippyTopProvider.prototype = {
+ async init() {
+ this.initialized = true;
+ },
+ processSite(site) {
+ return site;
+ },
+};
+
+let gSearchServiceInitStub;
+let gGetTopSitesStub;
+
+function getTopSitesFeedForTest(sandbox) {
+ let feed = new TopSitesFeed();
+ const storage = {
+ init: sandbox.stub().resolves(),
+ get: sandbox.stub().resolves(),
+ set: sandbox.stub().resolves(),
+ };
+
+ // Setup for tests that don't call `init` but require feed.storage
+ feed._storage = storage;
+ feed.store = {
+ dispatch: sinon.spy(),
+ getState() {
+ return this.state;
+ },
+ state: {
+ Prefs: { values: { topSitesRows: 2 } },
+ TopSites: { rows: Array(12).fill("site") },
+ },
+ dbStorage: { getDbTable: sandbox.stub().returns(storage) },
+ };
+
+ return feed;
+}
+
+add_setup(async () => {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(SearchService.prototype, "defaultEngine").get(() => {
+ return { identifier: "ddg", searchForm: "https://duckduckgo.com" };
+ });
+
+ gGetTopSitesStub = sandbox
+ .stub(NewTabUtils.activityStreamLinks, "getTopSites")
+ .resolves(FAKE_LINKS);
+
+ gSearchServiceInitStub = sandbox
+ .stub(SearchService.prototype, "init")
+ .resolves();
+
+ sandbox.stub(NewTabUtils.activityStreamProvider, "_faviconBytesToDataURI");
+
+ sandbox
+ .stub(NewTabUtils.activityStreamProvider, "_addFavicons")
+ .callsFake(l => {
+ return Promise.resolve(
+ l.map(link => {
+ link.favicon = FAKE_FAVICON;
+ link.faviconSize = FAKE_FAVICON_SIZE;
+ return link;
+ })
+ );
+ });
+
+ sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_SCREENSHOT);
+ sandbox.spy(Screenshots, "maybeCacheScreenshot");
+ sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_construction() {
+ let feed = new TopSitesFeed();
+ Assert.ok(feed, "Could construct a TopSitesFeed");
+ Assert.ok(feed._currentSearchHostname, "_currentSearchHostname defined");
+});
+
+add_task(async function test_refreshDefaults() {
+ let sandbox = sinon.createSandbox();
+ let feed = new TopSitesFeed();
+ Assert.ok(
+ !DEFAULT_TOP_SITES.length,
+ "Should have 0 DEFAULT_TOP_SITES initially."
+ );
+
+ info("refreshDefaults should add defaults on PREFS_INITIAL_VALUES");
+ feed.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "https://foo.com" },
+ });
+
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have 1 DEFAULT_TOP_SITES now."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should add defaults on default.sites PREF_CHANGED");
+ feed.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: "default.sites", value: "https://foo.com" },
+ });
+
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have 1 DEFAULT_TOP_SITES now."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should refresh on topSiteRows PREF_CHANGED");
+ let refreshStub = sandbox.stub(feed, "refresh");
+ feed.onAction({ type: at.PREF_CHANGED, data: { name: "topSitesRows" } });
+ Assert.ok(feed.refresh.calledOnce, "refresh called");
+ refreshStub.restore();
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should have default sites with .isDefault = true");
+ feed.refreshDefaults("https://foo.com");
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have a DEFAULT_TOP_SITES now."
+ );
+ Assert.ok(
+ DEFAULT_TOP_SITES[0].isDefault,
+ "Lone top site should be the default."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should have default sites with appropriate hostname");
+ feed.refreshDefaults("https://foo.com");
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have a DEFAULT_TOP_SITES now."
+ );
+ let [site] = DEFAULT_TOP_SITES;
+ Assert.equal(
+ site.hostname,
+ shortURL(site),
+ "Lone top site should have the right hostname."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should add no defaults on empty pref");
+ feed.refreshDefaults("");
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 0,
+ "Should have 0 DEFAULT_TOP_SITES now."
+ );
+
+ info("refreshDefaults should be able to clear defaults");
+ feed.refreshDefaults("https://foo.com");
+ feed.refreshDefaults("");
+
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 0,
+ "Should have 0 DEFAULT_TOP_SITES now."
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_filterForThumbnailExpiration() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ info(
+ "filterForThumbnailExpiration should pass rows.urls to the callback provided"
+ );
+ const rows = [
+ { url: "foo.com" },
+ { url: "bar.com", customScreenshotURL: "custom" },
+ ];
+ feed.store.state.TopSites = { rows };
+ const stub = sandbox.stub();
+ feed.filterForThumbnailExpiration(stub);
+ Assert.ok(stub.calledOnce);
+ Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "custom"]));
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_on_SearchService_init_failure() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed.refreshDefaults("https://foo.com");
+
+ gSearchServiceInitStub.rejects(new Error("Simulating search init failure"));
+
+ const result = await feed.getLinksWithDefaults();
+ Assert.ok(result);
+
+ gSearchServiceInitStub.resolves();
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults() {
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed.refreshDefaults("https://foo.com");
+
+ info("getLinksWithDefaults should get the links from NewTabUtils");
+ let result = await feed.getLinksWithDefaults();
+
+ const reference = FAKE_LINKS.map(site =>
+ Object.assign({}, site, {
+ hostname: shortURL(site),
+ typedBonus: true,
+ })
+ );
+
+ Assert.deepEqual(result, reference);
+ Assert.ok(NewTabUtils.activityStreamLinks.getTopSites.calledOnce);
+
+ info("getLinksWithDefaults should indicate the links get typed bonus");
+ Assert.ok(result[0].typedBonus, "Expected typed bonus property to be true.");
+
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_filterAdult() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should filter out non-pinned adult sites");
+
+ sandbox.stub(FilterAdult, "filter").returns([]);
+ const TEST_URL = "https://foo.com/";
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [{ url: TEST_URL }]);
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ const result = await feed.getLinksWithDefaults();
+ Assert.ok(FilterAdult.filter.calledOnce);
+ Assert.equal(result.length, 1);
+ Assert.equal(result[0].url, TEST_URL);
+
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_caching() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should filter out the defaults that have been blocked"
+ );
+ // make sure we only have one top site, and we block the only default site we have to show
+ const url = "www.myonlytopsite.com";
+ const topsite = {
+ frecency: FAKE_FRECENCY,
+ hostname: shortURL({ url }),
+ typedBonus: true,
+ url,
+ };
+
+ const blockedDefaultSite = { url: "https://foo.com" };
+ gGetTopSitesStub.resolves([topsite]);
+ sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
+ return site.url === blockedDefaultSite.url;
+ });
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+ const result = await feed.getLinksWithDefaults();
+
+ // what we should be left with is just the top site we added, and not the default site we blocked
+ Assert.equal(result.length, 1);
+ Assert.deepEqual(result[0], topsite);
+ let foundBlocked = result.find(site => site.url === blockedDefaultSite.url);
+ Assert.ok(!foundBlocked, "Should not have found blocked site.");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_dedupe() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should call dedupe.group on the links");
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ let stub = sandbox.stub(feed.dedupe, "group").callsFake((...id) => id);
+ await feed.getLinksWithDefaults();
+
+ Assert.ok(stub.calledOnce, "dedupe.group was called once");
+ sandbox.restore();
+});
+
+add_task(async function test__dedupe_key() {
+ let sandbox = sinon.createSandbox();
+
+ info("_dedupeKey should dedupe on hostname instead of url");
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ let site = { url: "foo", hostname: "bar" };
+ let result = feed._dedupeKey(site);
+
+ Assert.equal(result, site.hostname, "deduped on hostname");
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_adds_defaults() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should add defaults if there are are not enough links"
+ );
+ const TEST_LINKS = [{ frecency: FAKE_FRECENCY, url: "foo.com" }];
+ gGetTopSitesStub.resolves(TEST_LINKS);
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ let result = await feed.getLinksWithDefaults();
+
+ let reference = [...TEST_LINKS, ...DEFAULT_TOP_SITES].map(s =>
+ Object.assign({}, s, {
+ hostname: shortURL(s),
+ typedBonus: true,
+ })
+ );
+
+ Assert.deepEqual(result, reference);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_adds_defaults_for_visible_slots() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should only add defaults up to the number of visible slots"
+ );
+ const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
+ let testLinks = [];
+ for (let i = 0; i < numVisible - 1; i++) {
+ testLinks.push({ frecency: FAKE_FRECENCY, url: `foo${i}.com` });
+ }
+ gGetTopSitesStub.resolves(testLinks);
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ let result = await feed.getLinksWithDefaults();
+
+ let reference = [...testLinks, DEFAULT_TOP_SITES[0]].map(s =>
+ Object.assign({}, s, {
+ hostname: shortURL(s),
+ typedBonus: true,
+ })
+ );
+
+ Assert.equal(result.length, numVisible);
+ Assert.deepEqual(result, reference);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_no_throw_on_no_links() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should not throw if NewTabUtils returns null");
+ gGetTopSitesStub.resolves(null);
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ feed.getLinksWithDefaults();
+ Assert.ok(true, "getLinksWithDefaults did not throw");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_get_more_on_request() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should get more if the user has asked for more");
+ let testLinks = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW)
+ .fill(null)
+ .map((v, i) => ({
+ frecency: FAKE_FRECENCY,
+ url: `http://www.site${i}.com`,
+ }));
+ gGetTopSitesStub.resolves(testLinks);
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ const TEST_ROWS = 3;
+ feed.store.state.Prefs.values.topSitesRows = TEST_ROWS;
+
+ let result = await feed.getLinksWithDefaults();
+ Assert.equal(result.length, TEST_ROWS * TOP_SITES_MAX_SITES_PER_ROW);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_reuse_cache() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should reuse the cache on subsequent calls");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ await feed.getLinksWithDefaults();
+ await feed.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_ignore_cache_on_requesting_more() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should ignore the cache when requesting more");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ await feed.getLinksWithDefaults();
+ feed.store.state.Prefs.values.topSitesRows *= 3;
+ await feed.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
+ "getTopSites called twice"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_getLinksWithDefaults_migrate_frecent_screenshot_data() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should migrate frecent screenshot data without getting screenshots again"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+ await feed.getLinksWithDefaults();
+
+ let originalCallCount = Screenshots.getScreenshotForURL.callCount;
+ feed.frecentCache.expire();
+
+ let result = await feed.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
+ "getTopSites called twice"
+ );
+ Assert.equal(
+ Screenshots.getScreenshotForURL.callCount,
+ originalCallCount,
+ "getScreenshotForURL was not called again."
+ );
+ Assert.equal(result[0].screenshot, FAKE_SCREENSHOT);
+
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_getLinksWithDefaults_migrate_pinned_favicon_data() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should migrate pinned favicon data without getting favicons again"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ await feed.getLinksWithDefaults();
+
+ let originalCallCount =
+ NewTabUtils.activityStreamProvider._addFavicons.callCount;
+ feed.pinnedCache.expire();
+
+ let result = await feed.getLinksWithDefaults();
+
+ Assert.equal(
+ NewTabUtils.activityStreamProvider._addFavicons.callCount,
+ originalCallCount,
+ "_addFavicons was not called again."
+ );
+ Assert.equal(result[0].favicon, FAKE_FAVICON);
+ Assert.equal(result[0].faviconSize, FAKE_FAVICON_SIZE);
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_no_internal_properties() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not expose internal link properties");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ let result = await feed.getLinksWithDefaults();
+
+ let internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
+ Assert.equal(internal.join(""), "");
+
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_copy_frecent_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should copy the screenshot of the frecent site if " +
+ "pinned site doesn't have customScreenshotURL"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ const TEST_SCREENSHOT = "screenshot";
+
+ gGetTopSitesStub.resolves([
+ { url: "https://foo.com/", screenshot: TEST_SCREENSHOT },
+ ]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ let result = await feed.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, TEST_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_no_copy_frecent_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should not copy the frecent screenshot if " +
+ "customScreenshotURL is set"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resolves([
+ { url: "https://foo.com/", screenshot: "screenshot" },
+ ]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/", customScreenshotURL: "custom" }]);
+
+ let result = await feed.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, undefined);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_persist_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should keep the same screenshot if no frecent site is found"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ const CUSTOM_SCREENSHOT = "custom";
+
+ gGetTopSitesStub.resolves([]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/", screenshot: CUSTOM_SCREENSHOT }]);
+
+ let result = await feed.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, CUSTOM_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_no_overwrite_pinned_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not overwrite pinned site screenshot");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ const EXISTING_SCREENSHOT = "some-screenshot";
+
+ gGetTopSitesStub.resolves([{ url: "https://foo.com/", screenshot: "foo" }]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ { url: "https://foo.com/", screenshot: EXISTING_SCREENSHOT },
+ ]);
+
+ let result = await feed.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_getLinksWithDefaults_no_searchTopSite_from_frecent() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not set searchTopSite from frecent site");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ const EXISTING_SCREENSHOT = "some-screenshot";
+
+ gGetTopSitesStub.resolves([
+ {
+ url: "https://foo.com/",
+ searchTopSite: true,
+ screenshot: EXISTING_SCREENSHOT,
+ },
+ ]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ let result = await feed.getLinksWithDefaults();
+
+ Assert.ok(!result[0].searchTopSite);
+ // But it should copy over other properties
+ Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_concurrency_getTopSites() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults concurrent calls should call the backing data once"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+
+ await Promise.all([feed.getLinksWithDefaults(), feed.getLinksWithDefaults()]);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_concurrency_getScreenshotForURL() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults concurrent calls should call the backing data once"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+ feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+ Screenshots.getScreenshotForURL.resetHistory();
+
+ await Promise.all([
+ feed.getLinksWithDefaults(),
+ feed.getLinksWithDefaults(),
+ ]);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ Assert.equal(
+ Screenshots.getScreenshotForURL.callCount,
+ FAKE_LINKS.length,
+ "getLinksWithDefaults concurrent calls should get screenshots once per link"
+ );
+
+ feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+
+ feed.refreshDefaults("https://foo.com");
+
+ sandbox.stub(feed, "_requestRichIcon");
+ await Promise.all([
+ feed.getLinksWithDefaults(),
+ feed.getLinksWithDefaults(),
+ ]);
+
+ Assert.equal(
+ feed.store.dispatch.callCount,
+ FAKE_LINKS.length,
+ "getLinksWithDefaults concurrent calls should dispatch once per link screenshot fetched"
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_deduping_no_dedupe_pinned() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not dedupe pinned sites");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults("https://foo.com");
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ { url: "https://developer.mozilla.org/en-US/docs/Web" },
+ { url: "https://developer.mozilla.org/en-US/docs/Learn" },
+ ]);
+
+ let sites = await feed.getLinksWithDefaults();
+ Assert.equal(sites.length, 2 * TOP_SITES_MAX_SITES_PER_ROW);
+ Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
+ Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
+ Assert.equal(sites[0].hostname, sites[1].hostname);
+
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_prefer_pinned_sites() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should prefer pinned sites over links");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ { url: "https://developer.mozilla.org/en-US/docs/Web" },
+ { url: "https://developer.mozilla.org/en-US/docs/Learn" },
+ ]);
+
+ const SECOND_TOP_SITE_URL = "https://www.mozilla.org/";
+
+ gGetTopSitesStub.resolves([
+ { frecency: FAKE_FRECENCY, url: "https://developer.mozilla.org/" },
+ { frecency: FAKE_FRECENCY, url: SECOND_TOP_SITE_URL },
+ ]);
+
+ let sites = await feed.getLinksWithDefaults();
+
+ // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so
+ // the frecent with matching hostname as pinned is removed.
+ Assert.equal(sites.length, 3);
+ Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
+ Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
+ Assert.equal(sites[2].url, SECOND_TOP_SITE_URL);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_title_and_null() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should return sites that have a title");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://github.com/mozilla/activity-stream" }]);
+
+ let sites = await feed.getLinksWithDefaults();
+ for (let site of sites) {
+ Assert.ok(site.hostname);
+ }
+
+ info("getLinksWithDefaults should not throw for null entries");
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [null]);
+ await feed.getLinksWithDefaults();
+ Assert.ok(true, "getLinksWithDefaults didn't throw");
+
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_calls__fetchIcon() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should return sites that have a title");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults();
+
+ sandbox.spy(feed, "_fetchIcon");
+ let results = await feed.getLinksWithDefaults();
+ Assert.ok(results.length, "Got back some results");
+ Assert.equal(feed._fetchIcon.callCount, results.length);
+ for (let result of results) {
+ Assert.ok(feed._fetchIcon.calledWith(result));
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_calls__fetchScreenshot() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should call _fetchScreenshot when customScreenshotURL is set"
+ );
+
+ gGetTopSitesStub.resolves([]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com", customScreenshotURL: "custom" }]);
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults();
+
+ sandbox.stub(feed, "_fetchScreenshot");
+ await feed.getLinksWithDefaults();
+
+ Assert.ok(feed._fetchScreenshot.calledWith(sinon.match.object, "custom"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_getLinksWithDefaults_with_DiscoveryStream() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should add a sponsored topsite from discoverystream to all the valid indices"
+ );
+
+ let makeStreamData = index => ({
+ layout: [
+ {
+ components: [
+ {
+ placement: {
+ name: "sponsored-topsites",
+ },
+ spocs: {
+ positions: [{ index }],
+ },
+ },
+ ],
+ },
+ ],
+ spocs: {
+ data: {
+ "sponsored-topsites": {
+ items: [{ title: "test spoc", url: "https://test-spoc.com" }],
+ },
+ },
+ },
+ });
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.refreshDefaults();
+
+ for (let i = 0; i < FAKE_LINKS.length; i++) {
+ feed.store.state.DiscoveryStream = makeStreamData(i);
+ const result = await feed.getLinksWithDefaults();
+ const link = result[i];
+
+ Assert.equal(link.type, "SPOC");
+ Assert.equal(link.title, "test spoc");
+ Assert.equal(link.sponsored_position, i + 1);
+ Assert.equal(link.hostname, "test-spoc");
+ Assert.equal(link.url, "https://test-spoc.com");
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_init() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox.stub(NimbusFeatures.newtab, "onUpdate");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ sandbox.stub(feed, "refresh");
+ await feed.init();
+
+ info("TopSitesFeed.init should call refresh (broadcast: true)");
+ Assert.ok(feed.refresh.calledOnce, "refresh called once");
+ Assert.ok(
+ feed.refresh.calledWithExactly({
+ broadcast: true,
+ isStartup: true,
+ })
+ );
+
+ info("TopSitesFeed.init should initialise the storage");
+ Assert.ok(
+ feed.store.dbStorage.getDbTable.calledOnce,
+ "getDbTable called once"
+ );
+ Assert.ok(feed.store.dbStorage.getDbTable.calledWithExactly("sectionPrefs"));
+
+ info(
+ "TopSitesFeed.init should call onUpdate to set up Nimbus update listener"
+ );
+
+ Assert.ok(
+ NimbusFeatures.newtab.onUpdate.calledOnce,
+ "NimbusFeatures.newtab.onUpdate called once"
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_refresh() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox.stub(NimbusFeatures.newtab, "onUpdate");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ sandbox.stub(feed, "_fetchIcon");
+ feed._startedUp = true;
+
+ info("TopSitesFeed.refresh should wait for tippytop to initialize");
+ feed._tippyTopProvider.initialized = false;
+ sandbox.stub(feed._tippyTopProvider, "init").resolves();
+
+ await feed.refresh();
+
+ Assert.ok(
+ feed._tippyTopProvider.init.calledOnce,
+ "feed._tippyTopProvider.init called once"
+ );
+
+ info(
+ "TopSitesFeed.refresh should not init the tippyTopProvider if already initialized"
+ );
+ feed._tippyTopProvider.initialized = true;
+ feed._tippyTopProvider.init.resetHistory();
+
+ await feed.refresh();
+
+ Assert.ok(
+ feed._tippyTopProvider.init.notCalled,
+ "tippyTopProvider not initted again"
+ );
+
+ info("TopSitesFeed.refresh should broadcast TOP_SITES_UPDATED");
+ feed.store.dispatch.resetHistory();
+ sandbox.stub(feed, "getLinksWithDefaults").resolves([]);
+
+ await feed.refresh({ broadcast: true });
+
+ Assert.ok(feed.store.dispatch.calledOnce, "dispatch called once");
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.BroadcastToContent({
+ type: at.TOP_SITES_UPDATED,
+ data: { links: [], pref: { collapsed: false } },
+ })
+ )
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_refresh_dispatch() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.refresh should dispatch an action with the links returned"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "_fetchIcon");
+ feed._startedUp = true;
+
+ await feed.refresh({ broadcast: true });
+ let reference = FAKE_LINKS.map(site =>
+ Object.assign({}, site, {
+ hostname: shortURL(site),
+ typedBonus: true,
+ })
+ );
+
+ Assert.ok(feed.store.dispatch.calledOnce, "Store.dispatch called once");
+ Assert.equal(
+ feed.store.dispatch.firstCall.args[0].type,
+ at.TOP_SITES_UPDATED
+ );
+ Assert.deepEqual(feed.store.dispatch.firstCall.args[0].data.links, reference);
+
+ sandbox.restore();
+});
+
+add_task(async function test_refresh_empty_slots() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.refresh should handle empty slots in the resulting top sites array"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "_fetchIcon");
+ feed._startedUp = true;
+
+ gGetTopSitesStub.resolves([FAKE_LINKS[0]]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ null,
+ null,
+ FAKE_LINKS[1],
+ null,
+ null,
+ null,
+ null,
+ null,
+ FAKE_LINKS[2],
+ ]);
+
+ await feed.refresh({ broadcast: true });
+
+ Assert.ok(feed.store.dispatch.calledOnce, "Store.dispatch called once");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_refresh_to_preloaded() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.refresh should dispatch AlsoToPreloaded when broadcast is false"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "_fetchIcon");
+ feed._startedUp = true;
+
+ gGetTopSitesStub.resolves([]);
+ await feed.refresh({ broadcast: false });
+
+ Assert.ok(feed.store.dispatch.calledOnce, "Store.dispatch called once");
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.AlsoToPreloaded({
+ type: at.TOP_SITES_UPDATED,
+ data: { links: [], pref: { collapsed: false } },
+ })
+ )
+ );
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+});
+
+add_task(async function test_refresh_init_storage() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.refresh should not init storage of it's already initialized"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "_fetchIcon");
+ feed._startedUp = true;
+
+ feed._storage.initialized = true;
+
+ await feed.refresh({ broadcast: false });
+
+ Assert.ok(feed._storage.init.notCalled, "feed._storage.init was not called.");
+ sandbox.restore();
+});
+
+add_task(async function test_refresh_handles_indexedDB_errors() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.refresh should dispatch AlsoToPreloaded when broadcast is false"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "_fetchIcon");
+ feed._startedUp = true;
+
+ feed._storage.get.throws(new Error());
+
+ try {
+ await feed.refresh({ broadcast: false });
+ Assert.ok(true, "refresh should have succeeded");
+ } catch (e) {
+ Assert.ok(false, "Should not have thrown");
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_updateSectionPrefs_on_UPDATE_SECTION_PREFS() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.onAction should call updateSectionPrefs on UPDATE_SECTION_PREFS"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "updateSectionPrefs");
+ feed.onAction({
+ type: at.UPDATE_SECTION_PREFS,
+ data: { id: "topsites" },
+ });
+
+ Assert.ok(
+ feed.updateSectionPrefs.calledOnce,
+ "feed.updateSectionPrefs called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_updateSectionPrefs_dispatch_TOP_SITES_PREFS_UPDATED() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.updateSectionPrefs should dispatch TOP_SITES_PREFS_UPDATED"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ await feed.updateSectionPrefs({ collapsed: true });
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.BroadcastToContent({
+ type: at.TOP_SITES_PREFS_UPDATED,
+ data: { pref: { collapsed: true } },
+ })
+ )
+ );
+
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_allocatePositions() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.allocationPositions should allocate positions and dispatch"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ let sov = {
+ name: "SOV-20230518215316",
+ allocations: [
+ {
+ position: 1,
+ allocation: [
+ {
+ partner: "amp",
+ percentage: 100,
+ },
+ {
+ partner: "moz-sales",
+ percentage: 0,
+ },
+ ],
+ },
+ {
+ position: 2,
+ allocation: [
+ {
+ partner: "amp",
+ percentage: 80,
+ },
+ {
+ partner: "moz-sales",
+ percentage: 20,
+ },
+ ],
+ },
+ ],
+ };
+
+ sandbox.stub(feed._contile, "sov").get(() => sov);
+
+ sandbox.stub(Sampling, "ratioSample");
+ Sampling.ratioSample.onCall(0).resolves(0);
+ Sampling.ratioSample.onCall(1).resolves(1);
+
+ await feed.allocatePositions();
+
+ Assert.ok(feed.store.dispatch.calledOnce, "feed.store.dispatch called once");
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.OnlyToMain({
+ type: at.SOV_UPDATED,
+ data: {
+ ready: true,
+ positions: [
+ { position: 1, assignedPartner: "amp" },
+ { position: 2, assignedPartner: "moz-sales" },
+ ],
+ },
+ })
+ )
+ );
+
+ Sampling.ratioSample.onCall(2).resolves(0);
+ Sampling.ratioSample.onCall(3).resolves(0);
+
+ await feed.allocatePositions();
+
+ Assert.ok(
+ feed.store.dispatch.calledTwice,
+ "feed.store.dispatch called twice"
+ );
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.OnlyToMain({
+ type: at.SOV_UPDATED,
+ data: {
+ ready: true,
+ positions: [
+ { position: 1, assignedPartner: "amp" },
+ { position: 2, assignedPartner: "amp" },
+ ],
+ },
+ })
+ )
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_getScreenshotPreview() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.getScreenshotPreview should dispatch preview if request is succesful"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ await feed.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(feed.store.dispatch.calledOnce);
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: FAKE_SCREENSHOT, url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_getScreenshotPreview() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.getScreenshotPreview should return empty string if request fails"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
+ await feed.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(feed.store.dispatch.calledOnce);
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: "", url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_part_1() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.onAction should call getScreenshotPreview on PREVIEW_REQUEST"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "getScreenshotPreview");
+
+ feed.onAction({
+ type: at.PREVIEW_REQUEST,
+ data: { url: "foo" },
+ meta: { fromTarget: 1234 },
+ });
+
+ Assert.ok(
+ feed.getScreenshotPreview.calledOnce,
+ "feed.getScreenshotPreview called once"
+ );
+ Assert.ok(feed.getScreenshotPreview.calledWithExactly("foo", 1234));
+
+ info("TopSitesFeed.onAction should refresh on SYSTEM_TICK");
+ sandbox.stub(feed, "refresh");
+ feed.onAction({ type: at.SYSTEM_TICK });
+
+ Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
+ Assert.ok(feed.refresh.calledWithExactly({ broadcast: false }));
+
+ info(
+ "TopSitesFeed.onAction should call with correct parameters on TOP_SITES_PIN"
+ );
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.spy(feed, "pin");
+
+ let pinAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: { url: "foo.com" }, index: 7 },
+ };
+ feed.onAction(pinAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWithExactly(
+ pinAction.data.site,
+ pinAction.data.index
+ )
+ );
+ Assert.ok(
+ feed.pin.calledOnce,
+ "TopSitesFeed.onAction should call pin on TOP_SITES_PIN"
+ );
+
+ info(
+ "TopSitesFeed.onAction should unblock a previously blocked top site if " +
+ "we are now adding it manually via 'Add a Top Site' option"
+ );
+ sandbox.stub(NewTabUtils.blockedLinks, "unblock");
+ pinAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: { url: "foo.com" }, index: -1 },
+ };
+ feed.onAction(pinAction);
+ Assert.ok(
+ NewTabUtils.blockedLinks.unblock.calledWith({
+ url: pinAction.data.site.url,
+ })
+ );
+
+ info("TopSitesFeed.onAction should call insert on TOP_SITES_INSERT");
+ sandbox.stub(feed, "insert");
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.com" } },
+ };
+
+ feed.onAction(addAction);
+ Assert.ok(feed.insert.calledOnce, "TopSitesFeed.insert called once");
+
+ info(
+ "TopSitesFeed.onAction should call unpin with correct parameters " +
+ "on TOP_SITES_UNPIN"
+ );
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ null,
+ null,
+ { url: "foo.com" },
+ null,
+ null,
+ null,
+ null,
+ null,
+ FAKE_LINKS[0],
+ ]);
+ sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
+
+ let unpinAction = {
+ type: at.TOP_SITES_UNPIN,
+ data: { site: { url: "foo.com" } },
+ };
+ feed.onAction(unpinAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledOnce,
+ "NewTabUtils.pinnedLinks.unpin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.unpin.calledWith(unpinAction.data.site));
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_part_2() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSitesFeed.onAction should call refresh without a target if we clear " +
+ "history with PLACES_HISTORY_CLEARED"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "refresh");
+ feed.onAction({ type: at.PLACES_HISTORY_CLEARED });
+
+ Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
+ Assert.ok(feed.refresh.calledWithExactly({ broadcast: true }));
+
+ feed.refresh.resetHistory();
+
+ info(
+ "TopSitesFeed.onAction should call refresh without a target " +
+ "if we remove a Topsite from history"
+ );
+ feed.onAction({ type: at.PLACES_LINKS_DELETED });
+
+ Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
+ Assert.ok(feed.refresh.calledWithExactly({ broadcast: true }));
+
+ info("TopSitesFeed.onAction should call init on INIT action");
+ feed.onAction({ type: at.PLACES_LINKS_DELETED });
+ sandbox.stub(feed, "init");
+ feed.onAction({ type: at.INIT });
+ Assert.ok(feed.init.calledOnce, "TopSitesFeed.init called once");
+
+ info(
+ "TopSitesFeed.onAction should call refresh on PLACES_LINK_BLOCKED action"
+ );
+ feed.refresh.resetHistory();
+ await feed.onAction({ type: at.PLACES_LINK_BLOCKED });
+ Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
+ Assert.ok(feed.refresh.calledWithExactly({ broadcast: true }));
+
+ info(
+ "TopSitesFeed.onAction should call refresh on PLACES_LINKS_CHANGED action"
+ );
+ feed.refresh.resetHistory();
+ await feed.onAction({ type: at.PLACES_LINKS_CHANGED });
+ Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
+ Assert.ok(feed.refresh.calledWithExactly({ broadcast: false }));
+
+ info(
+ "TopSitesFeed.onAction should call pin with correct args on " +
+ "TOP_SITES_INSERT without an index specified"
+ );
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.bar", label: "foo" } },
+ };
+ feed.onAction(addAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(addAction.data.site, 0));
+
+ info(
+ "TopSitesFeed.onAction should call pin with correct args on " +
+ "TOP_SITES_INSERT"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ let dropAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.bar", label: "foo" }, index: 3 },
+ };
+ feed.onAction(dropAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(dropAction.data.site, 3));
+
+ // feed.init needs to actually run in order to register the observers that'll
+ // be removed in the following UNINIT test, otherwise uninit will throw.
+ feed.init.restore();
+ feed.init();
+
+ info("TopSitesFeed.onAction should remove the expiration filter on UNINIT");
+ sandbox.stub(PageThumbs, "removeExpirationFilter");
+ feed.onAction({ type: "UNINIT" });
+ Assert.ok(
+ PageThumbs.removeExpirationFilter.calledOnce,
+ "PageThumbs.removeExpirationFilter called once"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_part_3() {
+ let sandbox = sinon.createSandbox();
+
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ info(
+ "TopSitesFeed.onAction should call updatePinnedSearchShortcuts " +
+ "on UPDATE_PINNED_SEARCH_SHORTCUTS action"
+ );
+ sandbox.stub(feed, "updatePinnedSearchShortcuts");
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ await feed.onAction({
+ type: at.UPDATE_PINNED_SEARCH_SHORTCUTS,
+ data: { addedShortcuts },
+ });
+ Assert.ok(
+ feed.updatePinnedSearchShortcuts.calledOnce,
+ "TopSitesFeed.updatePinnedSearchShortcuts called once"
+ );
+
+ info(
+ "TopSitesFeed.onAction should refresh from Contile on " +
+ "SHOW_SPONSORED_PREF if Contile is enabled"
+ );
+ sandbox.spy(feed._contile, "refresh");
+ let prefChangeAction = {
+ type: at.PREF_CHANGED,
+ data: { name: SHOW_SPONSORED_PREF },
+ };
+ sandbox.stub(NimbusFeatures.newtab, "getVariable").returns(true);
+ feed.onAction(prefChangeAction);
+
+ Assert.ok(
+ feed._contile.refresh.calledOnce,
+ "TopSitesFeed._contile.refresh called once"
+ );
+
+ info(
+ "TopSitesFeed.onAction should not refresh from Contile on " +
+ "SHOW_SPONSORED_PREF if Contile is disabled"
+ );
+ NimbusFeatures.newtab.getVariable.returns(false);
+ feed._contile.refresh.resetHistory();
+ feed.onAction(prefChangeAction);
+
+ Assert.ok(
+ !feed._contile.refresh.calledOnce,
+ "TopSitesFeed._contile.refresh never called"
+ );
+
+ info(
+ "TopSitesFeed.onAction should reset Contile cache prefs " +
+ "when SHOW_SPONSORED_PREF is false"
+ );
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 15 * 60);
+ prefChangeAction = {
+ type: at.PREF_CHANGED,
+ data: { name: SHOW_SPONSORED_PREF, value: false },
+ };
+ NimbusFeatures.newtab.getVariable.returns(true);
+ feed._contile.refresh.resetHistory();
+
+ feed.onAction(prefChangeAction);
+ Assert.ok(!Services.prefs.prefHasUserValue(CONTILE_CACHE_PREF));
+ Assert.ok(!Services.prefs.prefHasUserValue(CONTILE_CACHE_LAST_FETCH_PREF));
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(CONTILE_CACHE_VALID_FOR_SECONDS_PREF)
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_insert_part_1() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info(
+ "TopSitesFeed.insert should pin site in first slot of empty pinned list"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
+ await feed.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(feed.store.dispatch.calledOnce);
+ Assert.ok(
+ feed.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: "", url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
+ }
+
+ {
+ info(
+ "TopSitesFeed.insert should pin site in first slot of pinned list with " +
+ "empty first slot"
+ );
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+ let site = { url: "foo.bar", label: "foo" };
+ await feed.insert({ data: { site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.insert should move a pinned site in first slot to the " +
+ "next slot: part 1"
+ );
+ let site1 = { url: "example.com" };
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [site1]);
+ let feed = getTopSitesFeedForTest(sandbox);
+ let site = { url: "foo.bar", label: "foo" };
+
+ await feed.insert({ data: { site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.insert should move a pinned site in first slot to the " +
+ "next slot: part 2"
+ );
+ let site1 = { url: "example.com" };
+ let site2 = { url: "example.org" };
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [site1, null, site2]);
+ let site = { url: "foo.bar", label: "foo" };
+ let feed = getTopSitesFeedForTest(sandbox);
+ await feed.insert({ data: { site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_insert_part_2() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info(
+ "TopSitesFeed.insert should unpin the last site if all slots are " +
+ "already pinned"
+ );
+ let site1 = { url: "example.com" };
+ let site2 = { url: "example.org" };
+ let site3 = { url: "example.net" };
+ let site4 = { url: "example.biz" };
+ let site5 = { url: "example.info" };
+ let site6 = { url: "example.news" };
+ let site7 = { url: "example.lol" };
+ let site8 = { url: "example.golf" };
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [site1, site2, site3, site4, site5, site6, site7, site8]);
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values.topSitesRows = 1;
+ let site = { url: "foo.bar", label: "foo" };
+ await feed.insert({ data: { site } });
+ Assert.equal(
+ NewTabUtils.pinnedLinks.pin.callCount,
+ 8,
+ "NewTabUtils.pinnedLinks.pin called 8 times"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 2));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site3, 3));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site4, 4));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site5, 5));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site6, 6));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site7, 7));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info("TopSitesFeed.insert should trigger refresh on TOP_SITES_INSERT");
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "refresh");
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.com" } },
+ };
+
+ await feed.insert(addAction);
+
+ Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
+ }
+
+ {
+ info("TopSitesFeed.insert should correctly handle different index values");
+ let index = -1;
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index, site } };
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ await feed.insert(action);
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+
+ index = undefined;
+ await feed.insert(action);
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_insert_part_3() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info("TopSitesFeed.insert should pin site in specified slot that is free");
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ await feed.insert({ data: { index: 2, site, draggedFromIndex: 0 } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.insert should move a pinned site in specified slot " +
+ "to the next slot"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ await feed.insert({ data: { index: 2, site, draggedFromIndex: 3 } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWith({ url: "example.com" }, 3)
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.insert should move pinned sites in the direction " +
+ "of the dragged site"
+ );
+
+ let site1 = { url: "foo.bar", label: "foo" };
+ let site2 = { url: "example.com", label: "example" };
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, null, site2]);
+
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ await feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 0 } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 1));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+
+ await feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 5 } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 3));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info("TopSitesFeed.insert should not insert past the visible top sites");
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ let site1 = { url: "foo.bar", label: "foo" };
+ await feed.insert({
+ data: { index: 42, site: site1, draggedFromIndex: 0 },
+ });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.notCalled,
+ "NewTabUtils.pinnedLinks.pin wasn't called"
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_pin_part_1() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info(
+ "TopSitesFeed.pin should pin site in specified slot empty pinned " +
+ "list"
+ );
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: "screenshot",
+ };
+ let feed = getTopSitesFeedForTest(sandbox);
+ await feed.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.pin should lookup the link object to update the custom " +
+ "screenshot"
+ );
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: "screenshot",
+ };
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.spy(feed.pinnedCache, "request");
+ await feed.pin({ data: { index: 2, site } });
+
+ Assert.ok(
+ feed.pinnedCache.request.calledOnce,
+ "feed.pinnedCache.request called once"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.pin should lookup the link object to update the custom " +
+ "screenshot when the custom screenshot is initially null"
+ );
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: null,
+ };
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.spy(feed.pinnedCache, "request");
+ await feed.pin({ data: { index: 2, site } });
+
+ Assert.ok(
+ feed.pinnedCache.request.calledOnce,
+ "feed.pinnedCache.request called once"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.pin should not do a link object lookup if custom " +
+ "screenshot field is not set"
+ );
+ let site = { url: "foo.bar", label: "foo" };
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.spy(feed.pinnedCache, "request");
+ await feed.pin({ data: { index: 2, site } });
+
+ Assert.ok(
+ !feed.pinnedCache.request.called,
+ "feed.pinnedCache.request never called"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.pin should pin site in specified slot of pinned " +
+ "list that is free"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+ let feed = getTopSitesFeedForTest(sandbox);
+ await feed.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_pin_part_2() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info("TopSitesFeed.pin should save the searchTopSite attribute if set");
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo", searchTopSite: true };
+ let feed = getTopSitesFeedForTest(sandbox);
+ await feed.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.firstCall.args[0].searchTopSite);
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.pin should NOT move a pinned site in specified " +
+ "slot to the next slot"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+ let feed = getTopSitesFeedForTest(sandbox);
+ await feed.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.pin should properly update LinksCache object " +
+ "properties between migrations"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ let feed = getTopSitesFeedForTest(sandbox);
+ let pinnedLinks = await feed.pinnedCache.request();
+ Assert.equal(pinnedLinks.length, 1);
+ feed.pinnedCache.expire();
+
+ pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo");
+
+ pinnedLinks = await feed.pinnedCache.request();
+ Assert.equal(pinnedLinks[0].screenshot, "foo");
+
+ // Force cache expiration in order to trigger a migration of objects
+ feed.pinnedCache.expire();
+ pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar");
+
+ pinnedLinks = await feed.pinnedCache.request();
+ Assert.equal(pinnedLinks[0].screenshot, "bar");
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_pin_part_3() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info("TopSitesFeed.pin should call insert if index < 0");
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index: -1, site } };
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.spy(feed, "insert");
+ await feed.pin(action);
+
+ Assert.ok(feed.insert.calledOnce, "feed.insert called once");
+ Assert.ok(feed.insert.calledWithExactly(action));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info("TopSitesFeed.pin should not call insert if index == 0");
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index: 0, site } };
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.spy(feed, "insert");
+ await feed.pin(action);
+
+ Assert.ok(!feed.insert.called, "feed.insert not called");
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info("TopSitesFeed.pin should trigger refresh on TOP_SITES_PIN");
+ let feed = getTopSitesFeedForTest(sandbox);
+ sandbox.stub(feed, "refresh");
+ let pinExistingAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: FAKE_LINKS[4], index: 4 },
+ };
+
+ await feed.pin(pinExistingAction);
+
+ Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_integration() {
+ let sandbox = sinon.createSandbox();
+
+ info("Test adding a pinned site and removing it with actions");
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ let resolvers = [];
+ feed.store.dispatch = sandbox.stub().callsFake(() => {
+ resolvers.shift()();
+ });
+ feed._startedUp = true;
+ sandbox.stub(feed, "_fetchScreenshot");
+
+ let forDispatch = action =>
+ new Promise(resolve => {
+ resolvers.push(resolve);
+ feed.onAction(action);
+ });
+
+ feed._requestRichIcon = sandbox.stub();
+ let url = "https://pin.me";
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake(link => {
+ NewTabUtils.pinnedLinks.links.push(link);
+ });
+
+ await forDispatch({ type: at.TOP_SITES_INSERT, data: { site: { url } } });
+ NewTabUtils.pinnedLinks.links.pop();
+ await forDispatch({ type: at.PLACES_LINK_BLOCKED });
+
+ Assert.ok(
+ feed.store.dispatch.calledTwice,
+ "feed.store.dispatch called twice"
+ );
+ Assert.equal(feed.store.dispatch.firstCall.args[0].data.links[0].url, url);
+ Assert.equal(
+ feed.store.dispatch.secondCall.args[0].data.links[0].url,
+ FAKE_LINKS[0].url
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_improvesearch_noDefaultSearchTile_experiment() {
+ let sandbox = sinon.createSandbox();
+ const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
+
+ sandbox.stub(SearchService.prototype, "getDefault").resolves({
+ identifier: "google",
+ searchForm: "google.com",
+ });
+
+ {
+ info(
+ "TopSitesFeed.getLinksWithDefaults should filter out alexa top 5 " +
+ "search from the default sites"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+ let top5Test = [
+ "https://google.com",
+ "https://search.yahoo.com",
+ "https://yahoo.com",
+ "https://bing.com",
+ "https://ask.com",
+ "https://duckduckgo.com",
+ ];
+
+ gGetTopSitesStub.resolves([
+ { url: "https://amazon.com" },
+ ...top5Test.map(url => ({ url })),
+ ]);
+
+ const urlsReturned = (await feed.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(
+ urlsReturned.includes("https://amazon.com"),
+ "amazon included in default links"
+ );
+ top5Test.forEach(url =>
+ Assert.ok(!urlsReturned.includes(url), `Should not include ${url}`)
+ );
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ {
+ info(
+ "TopSitesFeed.getLinksWithDefaults should not filter out alexa, default " +
+ "search from the query results if the experiment pref is off"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;
+
+ gGetTopSitesStub.resolves([
+ { url: "https://google.com" },
+ { url: "https://foo.com" },
+ { url: "https://duckduckgo" },
+ ]);
+ let urlsReturned = (await feed.getLinksWithDefaults()).map(
+ link => link.url
+ );
+
+ Assert.ok(urlsReturned.includes("https://google.com"));
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ {
+ info(
+ "TopSitesFeed.getLinksWithDefaults should filter out the current " +
+ "default search from the default sites"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ sandbox.stub(feed, "_currentSearchHostname").get(() => "amazon");
+ feed.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "google.com,amazon.com" },
+ });
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+
+ let urlsReturned = (await feed.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(!urlsReturned.includes("https://amazon.com"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ {
+ info(
+ "TopSitesFeed.getLinksWithDefaults should not filter out current " +
+ "default search from pinned sites even if it matches the current " +
+ "default search"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "google.com" }]);
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+
+ let urlsReturned = (await feed.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(urlsReturned.includes("google.com"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_improvesearch_noDefaultSearchTile_experiment_part_2() {
+ let sandbox = sinon.createSandbox();
+ const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
+
+ sandbox.stub(SearchService.prototype, "getDefault").resolves({
+ identifier: "google",
+ searchForm: "google.com",
+ });
+
+ {
+ info(
+ "TopSitesFeed.getLinksWithDefaults should call refresh and set " +
+ "._currentSearchHostname to the new engine hostname when the " +
+ "default search engine has been set"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+ sandbox.stub(feed, "refresh");
+
+ feed.observe(null, "browser-search-engine-modified", "engine-default");
+ Assert.equal(feed._currentSearchHostname, "duckduckgo");
+ Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ {
+ info(
+ "TopSitesFeed.getLinksWithDefaults should call refresh when the " +
+ "experiment pref has changed"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+ sandbox.stub(feed, "refresh");
+
+ feed.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: true },
+ });
+ Assert.ok(feed.refresh.calledOnce, "feed.refresh was called once");
+
+ feed.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: false },
+ });
+ Assert.ok(feed.refresh.calledTwice, "feed.refresh was called twice");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ sandbox.restore();
+ }
+);
+
+// eslint-disable-next-line max-statements
+add_task(async function test_improvesearch_topSitesSearchShortcuts() {
+ let sandbox = sinon.createSandbox();
+ let searchEngines = [{ aliases: ["@google"] }, { aliases: ["@amazon"] }];
+ sandbox
+ .stub(SearchService.prototype, "getAppProvidedEngines")
+ .resolves(searchEngines);
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake((site, index) => {
+ NewTabUtils.pinnedLinks.links[index] = site;
+ });
+
+ let prepFeed = feed => {
+ feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true;
+ feed.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] =
+ "google,amazon";
+ feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
+ return feed;
+ };
+
+ {
+ info(
+ "TopSitesFeed should updateCustomSearchShortcuts when experiment " +
+ "pref is turned on"
+ );
+ let feed = prepFeed(getTopSitesFeedForTest(sandbox));
+ feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
+ feed.updateCustomSearchShortcuts = sandbox.spy();
+
+ // turn the experiment on
+ feed.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true },
+ });
+
+ Assert.ok(
+ feed.updateCustomSearchShortcuts.calledOnce,
+ "feed.updateCustomSearchShortcuts called once"
+ );
+ }
+
+ {
+ info(
+ "TopSitesFeed should filter out default top sites that match a " +
+ "hostname of a search shortcut if previously blocked"
+ );
+ let feed = prepFeed(getTopSitesFeedForTest(sandbox));
+ feed.refreshDefaults("https://amazon.ca");
+ sandbox
+ .stub(NewTabUtils.blockedLinks, "links")
+ .value([{ url: "https://amazon.com" }]);
+ sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
+ return NewTabUtils.blockedLinks.links[0].url === site.url;
+ });
+
+ let urlsReturned = (await feed.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(!urlsReturned.includes("https://amazon.ca"));
+ }
+
+ {
+ info("TopSitesFeed should update frecent search topsite icon");
+ let feed = prepFeed(getTopSitesFeedForTest(sandbox));
+ feed._tippyTopProvider.processSite = site => {
+ site.tippyTopIcon = "icon.png";
+ site.backgroundColor = "#fff";
+ return site;
+ };
+ gGetTopSitesStub.resolves([{ url: "https://google.com" }]);
+
+ let urlsReturned = await feed.getLinksWithDefaults();
+
+ let defaultSearchTopsite = urlsReturned.find(
+ s => s.url === "https://google.com"
+ );
+ Assert.ok(defaultSearchTopsite.searchTopSite);
+ Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
+ Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ {
+ info("TopSitesFeed should update default search topsite icon");
+ let feed = prepFeed(getTopSitesFeedForTest(sandbox));
+ feed._tippyTopProvider.processSite = site => {
+ site.tippyTopIcon = "icon.png";
+ site.backgroundColor = "#fff";
+ return site;
+ };
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+ feed.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "google.com,amazon.com" },
+ });
+
+ let urlsReturned = await feed.getLinksWithDefaults();
+
+ let defaultSearchTopsite = urlsReturned.find(
+ s => s.url === "https://amazon.com"
+ );
+ Assert.ok(defaultSearchTopsite.searchTopSite);
+ Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
+ Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ }
+
+ {
+ info(
+ "TopSitesFeed should dispatch UPDATE_SEARCH_SHORTCUTS on " +
+ "updateCustomSearchShortcuts"
+ );
+ let feed = prepFeed(getTopSitesFeedForTest(sandbox));
+ feed.store.state.Prefs.values["improvesearch.noDefaultSearchTile"] = true;
+ await feed.updateCustomSearchShortcuts();
+ Assert.ok(
+ feed.store.dispatch.calledOnce,
+ "feed.store.dispatch called once"
+ );
+ Assert.ok(
+ feed.store.dispatch.calledWith({
+ data: {
+ searchShortcuts: [
+ {
+ keyword: "@google",
+ shortURL: "google",
+ url: "https://google.com",
+ backgroundColor: undefined,
+ smallFavicon:
+ "chrome://activity-stream/content/data/content/tippytop/favicons/google-com.ico",
+ tippyTopIcon:
+ "chrome://activity-stream/content/data/content/tippytop/images/google-com@2x.png",
+ },
+ {
+ keyword: "@amazon",
+ shortURL: "amazon",
+ url: "https://amazon.com",
+ backgroundColor: undefined,
+ smallFavicon:
+ "chrome://activity-stream/content/data/content/tippytop/favicons/amazon.ico",
+ tippyTopIcon:
+ "chrome://activity-stream/content/data/content/tippytop/images/amazon@2x.png",
+ },
+ ],
+ },
+ meta: {
+ from: "ActivityStream:Main",
+ to: "ActivityStream:Content",
+ isStartup: false,
+ },
+ type: "UPDATE_SEARCH_SHORTCUTS",
+ })
+ );
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_updatePinnedSearchShortcuts() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
+
+ {
+ info(
+ "TopSitesFeed.updatePinnedSearchShortcuts should unpin a " +
+ "shortcut in deletedShortcuts"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ let deletedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ let addedShortcuts = [];
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
+ null,
+ null,
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ]);
+
+ feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.notCalled,
+ "NewTabUtils.pinnedLinks.pin not called"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledOnce,
+ "NewTabUtils.pinnedLinks.unpin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledWith({
+ url: "https://google.com",
+ })
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.updatePinnedSearchShortcuts should pin a shortcut " +
+ "in addedShortcuts"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ let deletedShortcuts = [];
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
+ null,
+ null,
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ]);
+ feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.notCalled,
+ "NewTabUtils.pinnedLinks.unpin not called"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWith(
+ {
+ label: "google",
+ searchTopSite: true,
+ searchVendor: "google",
+ url: "https://google.com",
+ },
+ 0
+ )
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.updatePinnedSearchShortcuts should pin and unpin " +
+ "in the same action"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ {
+ url: "https://ebay.com",
+ searchVendor: "ebay",
+ label: "ebay",
+ searchTopSite: true,
+ },
+ ];
+ let deletedShortcuts = [
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ];
+
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
+ { url: "https://foo.com" },
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ]);
+
+ feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledOnce,
+ "NewTabUtils.pinnedLinks.unpin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSitesFeed.updatePinnedSearchShortcuts should pin a shortcut in " +
+ "addedShortcuts even if pinnedLinks is full"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ let deletedShortcuts = [];
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => FAKE_LINKS);
+ feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.notCalled,
+ "NewTabUtils.pinnedLinks.unpin not called"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWith(
+ { label: "google", searchTopSite: true, url: "https://google.com" },
+ 0
+ ),
+ "NewTabUtils.pinnedLinks.unpin not called"
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ }
+
+ sandbox.restore();
+});
+
+// eslint-disable-next-line max-statements
+add_task(async function test_ContileIntegration() {
+ let sandbox = sinon.createSandbox();
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+ sandbox.stub(NimbusFeatures.newtab, "getVariable").returns(true);
+
+ let prepFeed = feed => {
+ feed.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true;
+ let fetchStub = sandbox.stub(feed, "fetch");
+ return { feed, fetchStub };
+ };
+
+ {
+ info("TopSitesFeed._fetchSites should fetch sites from Contile");
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+ }
+
+ {
+ info("TopSitesFeed._fetchSites should call allocatePositions");
+ let { feed } = prepFeed(getTopSitesFeedForTest(sandbox));
+ sandbox.stub(feed, "allocatePositions").resolves();
+ await feed._contile.refresh();
+
+ Assert.ok(
+ feed.allocatePositions.calledOnce,
+ "feed.allocatePositions called once"
+ );
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should fetch SOV (Share-of-Voice) " +
+ "settings from Contile"
+ );
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ let sov = {
+ name: "SOV-20230518215316",
+ allocations: [
+ {
+ position: 1,
+ allocation: [
+ {
+ partner: "foo",
+ percentage: 100,
+ },
+ {
+ partner: "bar",
+ percentage: 0,
+ },
+ ],
+ },
+ {
+ position: 2,
+ allocation: [
+ {
+ partner: "foo",
+ percentage: 80,
+ },
+ {
+ partner: "bar",
+ percentage: 20,
+ },
+ ],
+ },
+ ],
+ };
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ sov: btoa(JSON.stringify(sov)),
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.deepEqual(feed._contile.sov, sov);
+ Assert.equal(feed._contile.sites.length, 2);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should not fetch from Contile if " +
+ "it's not enabled"
+ );
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ NimbusFeatures.newtab.getVariable.reset();
+ NimbusFeatures.newtab.getVariable.returns(false);
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetchStub.notCalled, "TopSitesFeed.fetch was not called");
+ Assert.ok(!fetched);
+ Assert.equal(feed._contile.sites.length, 0);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should still return two tiles when Contile " +
+ "provides more than 2 tiles and filtering results in more than 2 tiles"
+ );
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ NimbusFeatures.newtab.getVariable.reset();
+ NimbusFeatures.newtab.getVariable.onCall(0).returns(true);
+ NimbusFeatures.newtab.getVariable.onCall(1).returns(true);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://bar.com",
+ image_url: "images/bar-com.png",
+ click_url: "https://www.bar-click.com",
+ impression_url: "https://www.bar-impression.com",
+ name: "bar",
+ },
+ {
+ url: "https://test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ {
+ url: "https://test2.com",
+ image_url: "images/test2-com.png",
+ click_url: "https://www.test2-click.com",
+ impression_url: "https://www.test2-impression.com",
+ name: "test2",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ // Both "foo" and "bar" should be filtered
+ Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(feed._contile.sites[1].url, "https://test1.com");
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should still return two tiles with " +
+ "replacement if the Nimbus variable was unset"
+ );
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ NimbusFeatures.newtab.getVariable.reset();
+ NimbusFeatures.newtab.getVariable.onCall(0).returns(true);
+ NimbusFeatures.newtab.getVariable.onCall(1).returns(undefined);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(feed._contile.sites[1].url, "https://test1.com");
+ }
+
+ {
+ info("TopSitesFeed._fetchSites should filter the blocked sponsors");
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://bar.com",
+ image_url: "images/bar-com.png",
+ click_url: "https://www.bar-click.com",
+ impression_url: "https://www.bar-impression.com",
+ name: "bar",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ // Both "foo" and "bar" should be filtered
+ Assert.equal(feed._contile.sites.length, 1);
+ Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should return false when Contile returns " +
+ "with error status and no values are stored in cache prefs"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, 0);
+
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!feed._contile.sites.length);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should return false when Contile " +
+ "returns with error status and cached tiles are expried"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ const THIRTY_MINUTES_AGO_IN_SECONDS =
+ Math.round(Date.now() / 1000) - 60 * 30;
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ THIRTY_MINUTES_AGO_IN_SECONDS
+ );
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
+
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!feed._contile.sites.length);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should handle invalid payload " +
+ "properly from Contile"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: () =>
+ Promise.resolve({
+ unknown: [],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!feed._contile.sites.length);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should handle empty payload properly " +
+ "from Contile"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.ok(!feed._contile.sites.length);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should handle no content properly " +
+ "from Contile"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+ fetchStub.resolves({ ok: true, status: 204 });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!feed._contile.sites.length);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should set Caching Prefs after " +
+ "a successful request"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ let tiles = [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ];
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles,
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(
+ Services.prefs.getStringPref(CONTILE_CACHE_PREF),
+ JSON.stringify(tiles)
+ );
+ Assert.equal(
+ Services.prefs.getIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF),
+ 11322
+ );
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should return cached valid tiles " +
+ "when Contile returns error status"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+ let tiles = [
+ {
+ url: "https://www.test-cached.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1-cached.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ];
+
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles));
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+
+ fetchStub.resolves({
+ status: 304,
+ });
+
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites[0].url, "https://www.test-cached.com");
+ Assert.equal(feed._contile.sites[1].url, "https://www.test1-cached.com");
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should not be successful when contile " +
+ "returns an error and no valid tiles are cached"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 0);
+ Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, 0);
+
+ fetchStub.resolves({
+ status: 500,
+ });
+
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(!fetched);
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should return cached valid tiles " +
+ "filtering blocked tiles when Contile returns error status"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ let tiles = [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://www.test1-cached.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ];
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles));
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+
+ fetchStub.resolves({
+ status: 304,
+ });
+
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 1);
+ Assert.equal(feed._contile.sites[0].url, "https://www.test1-cached.com");
+ }
+
+ {
+ info(
+ "TopSitesFeed._fetchSites should still return 3 tiles when nimbus " +
+ "variable overrides max num of sponsored contile tiles"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ sandbox.stub(NimbusFeatures.pocketNewtab, "getVariable").returns(3);
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ {
+ url: "https://test2.com",
+ image_url: "images/test2-com.png",
+ click_url: "https://www.test2-click.com",
+ impression_url: "https://www.test2-impression.com",
+ name: "test2",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 3);
+ Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(feed._contile.sites[1].url, "https://test1.com");
+ Assert.equal(feed._contile.sites[2].url, "https://test2.com");
+ }
+
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js
new file mode 100644
index 0000000000..5d13df0eb0
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js
@@ -0,0 +1,2023 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ SearchService: "resource://gre/modules/SearchService.sys.mjs",
+ TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs",
+});
+
+const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
+const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
+const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
+const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor";
+const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch";
+const NIMBUS_VARIABLE_MAX_SPONSORED = "topSitesMaxSponsored";
+const NIMBUS_VARIABLE_CONTILE_POSITIONS = "contileTopsitesPositions";
+const NIMBUS_VARIABLE_CONTILE_ENABLED = "topSitesContileEnabled";
+const NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED = "topSitesContileMaxSponsored";
+
+let contileTile1 = {
+ id: 74357,
+ name: "Brand1",
+ url: "https://www.brand1.com",
+ click_url: "https://clickurl.com",
+ image_url: "https://contile-images.jpg",
+ image_size: 200,
+ impression_url: "https://impression_url.com",
+};
+let contileTile2 = {
+ id: 74925,
+ name: "Brand2",
+ url: "https://www.brand2.com",
+ click_url: "https://click_url.com",
+ image_url: "https://contile-images.jpg",
+ image_size: 200,
+ impression_url: "https://impression_url.com",
+};
+let contileTile3 = {
+ id: 75001,
+ name: "Brand3",
+ url: "https://www.brand3.com",
+ click_url: "https://click_url.com",
+ image_url: "https://contile-images.jpg",
+ image_size: 200,
+ impression_url: "https://impression_url.com",
+};
+let mozSalesTile = [
+ {
+ label: "MozSales Title",
+ title: "MozSales Title",
+ url: "https://mozsale.net",
+ sponsored_position: 1,
+ partner: "moz-sales",
+ },
+];
+
+function getTopSitesFeedForTest(sandbox) {
+ let feed = new TopSitesFeed();
+ const storage = {
+ init: sandbox.stub().resolves(),
+ get: sandbox.stub().resolves(),
+ set: sandbox.stub().resolves(),
+ };
+
+ feed._storage = storage;
+ feed.store = {
+ dispatch: sinon.spy(),
+ getState() {
+ return this.state;
+ },
+ state: {
+ Prefs: { values: { topSitesRows: 2 } },
+ TopSites: { rows: Array(12).fill("site") },
+ },
+ dbStorage: { getDbTable: sandbox.stub().returns(storage) },
+ };
+
+ return feed;
+}
+
+function prepFeed(feed, sandbox) {
+ feed.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true;
+ let fetchStub = sandbox.stub(feed, "fetch");
+ return { feed, fetchStub };
+}
+
+function setNimbusVariablesForNumTiles(nimbusPocketStub, numTiles) {
+ nimbusPocketStub.withArgs(NIMBUS_VARIABLE_MAX_SPONSORED).returns(numTiles);
+ nimbusPocketStub
+ .withArgs(NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED)
+ .returns(numTiles);
+ // when setting num tiles to > 2 need to set the positions or the > 2 has no effect.
+ // can be defaulted to undefined
+ let positionsArray = Array.from(
+ { length: numTiles },
+ (value, index) => index
+ );
+ nimbusPocketStub
+ .withArgs(NIMBUS_VARIABLE_CONTILE_POSITIONS)
+ .returns(positionsArray.toString());
+}
+
+add_setup(async () => {
+ do_get_profile();
+ Services.fog.initializeFOG();
+
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(SearchService.prototype, "init").resolves();
+
+ const nimbusStub = sandbox.stub(NimbusFeatures.newtab, "getVariable");
+ nimbusStub.withArgs(NIMBUS_VARIABLE_CONTILE_ENABLED).returns(true);
+
+ sandbox.spy(Glean.topsites.sponsoredTilesConfigured, "set");
+ sandbox.spy(Glean.topsites.sponsoredTilesReceived, "set");
+
+ // Temporarily setting isInAutomation to false.
+ // If Cu.isInAutomation is true then the check for Cu.isInAutomation in
+ // ContileIntegration._readDefaults passes, bypassing Contile, resulting in
+ // not being able use stubbed values.
+ if (Cu.isInAutomation) {
+ Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ false
+ );
+
+ if (Cu.isInAutomation) {
+ // This condition is unexpected, because it is enforced at:
+ // https://searchfox.org/mozilla-central/rev/ea65de7c/js/xpconnect/src/xpcpublic.h#753-759
+ throw new Error("Failed to set isInAutomation to false");
+ }
+ }
+ registerCleanupFunction(() => {
+ if (!Cu.isInAutomation) {
+ Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ true
+ );
+
+ if (!Cu.isInAutomation) {
+ // This condition is unexpected, because it is enforced at:
+ // https://searchfox.org/mozilla-central/rev/ea65de7c/js/xpconnect/src/xpcpublic.h#753-759
+ throw new Error("Failed to set isInAutomation to true");
+ }
+ }
+
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_set_contile_tile_to_oversold() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+
+ feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
+ feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_moz_sale_tile_to_oversold() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "determineFilteredTilesAndSetToOversold should set moz-sale tile to oversold when_contile tiles are displayed"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2]);
+ feed._telemetryUtility.setTiles(mozSalesTile);
+
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+
+ feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
+ feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "mozsales title",
+ provider: "moz-sales",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_contile_tile_to_oversold() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "determineFilteredTilesAndSetToOversold should set contile tile to oversold when moz-sale tile is displayed"
+ );
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2]);
+ feed._telemetryUtility.setTiles(mozSalesTile);
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ label: "MozSales Title",
+ title: "MozSales Title",
+ url: "https://mozsale.net",
+ sponsored_position: 2,
+ partner: "moz-sales",
+ },
+ ];
+
+ feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
+ feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ {
+ advertiser: "mozsales title",
+ provider: "moz-sales",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_contile_tiles_to_dismissed() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+
+ feed._telemetryUtility.determineFilteredTilesAndSetToDismissed(mergedTiles);
+ feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_all_contile_tiles_to_dismissed() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ let mergedTiles = [];
+
+ feed._telemetryUtility.determineFilteredTilesAndSetToDismissed(mergedTiles);
+ feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_moz_sales_tiles_to_dismissed() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2]);
+ feed._telemetryUtility.setTiles(mozSalesTile);
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+
+ feed._telemetryUtility.determineFilteredTilesAndSetToDismissed(mergedTiles);
+ feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "mozsales title",
+ provider: "moz-sales",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_tiles_to_dismissed_then_updated() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+
+ // Step 1: Set initial tiles
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ // Step 2: Set all tiles to dismissed
+ feed._telemetryUtility.determineFilteredTilesAndSetToDismissed([]);
+
+ let updatedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+
+ // Step 3: Finalize with the updated list of tiles.
+ feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_tile_positions_after_updated_list() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+
+ // Step 1: Set initial tiles
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ // Step 2: Set 1 tile to oversold (brand3)
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+ feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
+
+ // Step 3: Finalize with the updated list of tiles.
+ let updatedTiles = [
+ {
+ url: "https://www.replacement.com",
+ label: "replacement",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+ feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_tile_positions_after_updated_list_all_tiles() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+
+ // Step 1: Set initial tiles
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ // Step 2: Set 1 tile to oversold (brand3)
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+ feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
+
+ // Step 3: Finalize with the updated list of tiles.
+ let updatedTiles = [
+ {
+ url: "https://www.replacement.com",
+ label: "replacement",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.replacement2.com",
+ label: "replacement2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+ feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(
+ async function test_set_tile_positions_after_no_refresh_no_tiles_changed() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+
+ // Step 1: Set initial tiles
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ // Step 2: Set 1 tile to oversold (brand3)
+ let mergedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+ feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
+
+ // Step 3: Finalize with the updated list of tiles.
+ let updatedTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ ];
+ feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_set_contile_tile_to_unresolved() {
+ let sandbox = sinon.createSandbox();
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ // Create the error state, need to bypass existing checks.
+ feed._telemetryUtility.allSponsoredTiles = {
+ ampbrand1: {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: "oversold",
+ },
+ ampbrand2: {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: null,
+ },
+ };
+
+ feed._telemetryUtility._detectErrorConditionAndSetUnresolved();
+
+ let result = JSON.stringify({
+ sponsoredTilesReceived: Object.values(
+ feed._telemetryUtility.allSponsoredTiles
+ ),
+ });
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "unresolved",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "unresolved",
+ },
+ ],
+ };
+ Assert.equal(result, JSON.stringify(expectedResult));
+ sandbox.restore();
+});
+
+add_task(async function test_set_position_to_value_gt_3() {
+ let sandbox = sinon.createSandbox();
+ info("Test setTilePositions uses sponsored_position value, not array index.");
+ let feed = getTopSitesFeedForTest(sandbox);
+
+ feed._telemetryUtility.setSponsoredTilesConfigured();
+ feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+
+ let filteredContileTiles = [
+ {
+ url: "https://www.brand1.com",
+ label: "brand1",
+ sponsored_position: 1,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand2.com",
+ label: "brand2",
+ sponsored_position: 2,
+ partner: "amp",
+ },
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 6,
+ partner: "amp",
+ },
+ ];
+
+ feed._telemetryUtility.determineFilteredTilesAndSetToOversold(
+ filteredContileTiles
+ );
+ feed._telemetryUtility.finalizeNewtabPingFields(filteredContileTiles);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 6,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_all_tiles_displayed() {
+ let sandbox = sinon.createSandbox();
+ info("if all provided tiles are displayed, the display_fail_reason is null");
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.brand1.com",
+ image_url: "images/brand1-com.png",
+ click_url: "https://www.brand1-click.com",
+ impression_url: "https://www.brand1-impression.com",
+ name: "brand1",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ ],
+ }),
+ });
+
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_one_tile_display_fail_reason_to_oversold() {
+ let sandbox = sinon.createSandbox();
+
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.brand1.com",
+ image_url: "images/brand1-com.png",
+ click_url: "https://www.brand1-click.com",
+ impression_url: "https://www.brand1-impression.com",
+ name: "brand1",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brnad3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ ],
+ }),
+ });
+
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_set_one_tile_display_fail_reason_to_dismissed() {
+ let sandbox = sinon.createSandbox();
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brnad2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ ],
+ }),
+ });
+
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
+
+add_task(
+ async function test_set_one_tile_to_dismissed_and_one_tile_to_oversold() {
+ let sandbox = sinon.createSandbox();
+ let { feed, fetchStub } = prepFeed(
+ getTopSitesFeedForTest(sandbox),
+ sandbox
+ );
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ {
+ url: "https://www.brand4.com",
+ image_url: "images/brand4-com.png",
+ click_url: "https://www.brand4-click.com",
+ impression_url: "https://www.brand4-impression.com",
+ name: "brand4",
+ },
+ ],
+ }),
+ });
+
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function test_set_one_cached_tile_display_fail_reason_to_dismissed() {
+ let sandbox = sinon.createSandbox();
+ info("confirm the telemetry is valid when using cached tiles.");
+
+ let { feed, fetchStub } = prepFeed(
+ getTopSitesFeedForTest(sandbox),
+ sandbox
+ );
+
+ const tiles = [
+ {
+ url: "https://www.brand1.com",
+ image_url: "images/brand1-com.png",
+ click_url: "https://www.brand1-click.com",
+ impression_url: "https://www.brand1-impression.com",
+ name: "brand1",
+ },
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ ];
+
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles));
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_PREF, 60 * 15);
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ fetchStub.resolves({
+ status: 304,
+ });
+
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 1);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_update_tile_count() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "the tile count should update when topSitesMaxSponsored is updated by Nimbus"
+ );
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.brand1.com",
+ image_url: "images/brand1-com.png",
+ click_url: "https://www.brand1-click.com",
+ impression_url: "https://www.brand1-impression.com",
+ name: "brand1",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ ],
+ }),
+ });
+
+ // 1. Initially the Nimbus pref is set to 2 tiles
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ // 2. Set the Numbus pref to 3, confirm previous count still used.
+ const nimbusPocketStub = sandbox.stub(
+ NimbusFeatures.pocketNewtab,
+ "getVariable"
+ );
+ setNimbusVariablesForNumTiles(nimbusPocketStub, 3);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ // 3. Confirm the new count is applied when data pulled from Contile., 3 tiles displayed
+ fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 3);
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
+
+ expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_update_tile_count_sourced_from_cache() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "the tile count should update from cache when topSitesMaxSponsored is updated by Nimbus"
+ );
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ const tiles = [
+ {
+ url: "https://www.brand1.com",
+ image_url: "images/brand1-com.png",
+ click_url: "https://www.brand1-click.com",
+ impression_url: "https://www.brand1-impression.com",
+ name: "brand1",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ ];
+
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles));
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_PREF, 60 * 15);
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+
+ fetchStub.resolves({
+ status: 304,
+ });
+
+ // 1. Initially the Nimbus pref is set to 2 tiles
+ // Ensure ContileIntegration._fetchSites is working populate _sites and initilize TelemetryUtility
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 3);
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ // 2. Set the Numbus pref to 3, confirm previous count still used.
+ const nimbusPocketStub = sandbox.stub(
+ NimbusFeatures.pocketNewtab,
+ "getVariable"
+ );
+ setNimbusVariablesForNumTiles(nimbusPocketStub, 3);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ // 3. Confirm the new count is applied when data pulled from Contile, 3 tiles displayed
+ fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 3);
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
+
+ expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(
+ async function test_update_telemetry_fields_if_dismissed_brands_list_is_updated() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "if the user dismisses a brand, that dismissed tile shoudl be represented in the next ping."
+ );
+ let { feed, fetchStub } = prepFeed(
+ getTopSitesFeedForTest(sandbox),
+ sandbox
+ );
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ ],
+ }),
+ });
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar","brand2"]`
+ );
+
+ fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 1);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+ }
+);
+
+add_task(async function test_sponsoredTilesReceived_not_set() {
+ let sandbox = sinon.createSandbox();
+ info("sponsoredTilesReceived should be empty if tiles service returns 204");
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ fetchStub.resolves({ ok: true, status: 204 });
+
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(!fetched);
+ Assert.ok(!feed._contile.sites.length);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ let expectedResult = { sponsoredTilesReceived: [] };
+
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_telemetry_data_updates() {
+ let sandbox = sinon.createSandbox();
+ info("sponsoredTilesReceived should update when new tiles received.");
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ ],
+ }),
+ });
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://bar.com",
+ image_url: "images/bar-com.png",
+ click_url: "https://www.bar-click.com",
+ impression_url: "https://www.bar-impression.com",
+ name: "bar",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand3",
+ },
+ ],
+ }),
+ });
+
+ await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 1);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "bar",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
+
+add_task(async function test_reset_telemetry_data() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "sponsoredTilesReceived should be cleared when no tiles received and cache refreshed."
+ );
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "test3",
+ },
+ ],
+ }),
+ });
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+
+ let fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "foo",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+
+ fetchStub.resolves({ ok: true, status: 204 });
+
+ fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.ok(!feed._contile.sites.length);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+
+ expectedResult = { sponsoredTilesReceived: [] };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
+
+add_task(async function test_set_telemetry_for_moz_sales_tiles() {
+ let sandbox = sinon.createSandbox();
+ let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox), sandbox);
+
+ sandbox.stub(feed, "fetchDiscoveryStreamSpocs").returns([
+ {
+ label: "MozSales Title",
+ title: "MozSales Title",
+ url: "https://mozsale.net",
+ sponsored_position: 1,
+ partner: "moz-sales",
+ },
+ ]);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.brand1.com",
+ image_url: "images/brand1-com.png",
+ click_url: "https://www.brand1-click.com",
+ impression_url: "https://www.brand1-impression.com",
+ name: "brand1",
+ },
+ {
+ url: "https://www.brand2.com",
+ image_url: "images/brand2-com.png",
+ click_url: "https://www.brand2-click.com",
+ impression_url: "https://www.brand2-impression.com",
+ name: "brand2",
+ },
+ ],
+ }),
+ });
+ const fetched = await feed._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(feed._contile.sites.length, 2);
+
+ await feed._readDefaults();
+ await feed.getLinksWithDefaults(false);
+
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ let expectedResult = {
+ sponsoredTilesReceived: [
+ {
+ advertiser: "brand1",
+ provider: "amp",
+ display_position: 1,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand2",
+ provider: "amp",
+ display_position: 2,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "mozsales title",
+ provider: "moz-sales",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
+ ],
+ };
+ Assert.equal(
+ Glean.topsites.sponsoredTilesReceived.testGetValue(),
+ JSON.stringify(expectedResult)
+ );
+ sandbox.restore();
+});
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.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..a8470913af
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/xpcshell.toml
@@ -0,0 +1,34 @@
+[DEFAULT]
+firefox-appdir = "browser"
+skip-if = ["os == '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 = ["topstories.json"]
+skip-if = ["socketprocess_networking"] # Bug 1759035
+
+["test_AboutNewTab.js"]
+
+["test_AboutWelcomeAttribution.js"]
+
+["test_AboutWelcomeTelemetry.js"]
+
+["test_AboutWelcomeTelemetry_glean.js"]
+
+["test_HighlightsFeed.js"]
+
+["test_PlacesFeed.js"]
+
+["test_Store.js"]
+
+["test_TelemetryFeed.js"]
+support-files = ["../schemas/*.schema.json"]
+
+["test_TopSitesFeed.js"]
+
+["test_TopSitesFeed_glean.js"]