summaryrefslogtreecommitdiffstats
path: root/browser/components/topsites/test/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
commit8dd16259287f58f9273002717ec4d27e97127719 (patch)
tree3863e62a53829a84037444beab3abd4ed9dfc7d0 /browser/components/topsites/test/unit
parentReleasing progress-linux version 126.0.1-1~progress7.99u1. (diff)
downloadfirefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz
firefox-8dd16259287f58f9273002717ec4d27e97127719.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/topsites/test/unit')
-rw-r--r--browser/components/topsites/test/unit/test_top_sites.js3571
-rw-r--r--browser/components/topsites/test/unit/xpcshell.toml4
2 files changed, 3575 insertions, 0 deletions
diff --git a/browser/components/topsites/test/unit/test_top_sites.js b/browser/components/topsites/test/unit/test_top_sites.js
new file mode 100644
index 0000000000..3de5f43262
--- /dev/null
+++ b/browser/components/topsites/test/unit/test_top_sites.js
@@ -0,0 +1,3571 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TopSites, DEFAULT_TOP_SITES } = ChromeUtils.importESModule(
+ "resource:///modules/TopSites.sys.mjs"
+);
+
+const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.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",
+ TestUtils: "resource://testing-common/TestUtils.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 stubTopSites(sandbox) {
+ let cachedStorage = TopSites._storage;
+ let cachedStore = TopSites.store;
+
+ async function cleanup() {
+ if (TopSites._refreshing) {
+ info("Wait for refresh to finish.");
+ // Wait for refresh to finish or else removing the store while a process
+ // is running will result in errors.
+ await TestUtils.topicObserved("topsites-refreshed");
+ }
+ TopSites._tippyTopProvider.initialized = false;
+ TopSites._storage = cachedStorage;
+ TopSites.store = cachedStore;
+ TopSites.pinnedCache.clear();
+ TopSites.frecentCache.clear();
+ info("Finished cleaning up TopSites.");
+ }
+
+ 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
+ TopSites._storage = storage;
+ TopSites.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) },
+ };
+ info("Created mock store for TopSites.");
+ return cleanup;
+}
+
+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() {
+ Assert.ok(TopSites._currentSearchHostname, "_currentSearchHostname defined");
+});
+
+add_task(async function test_refreshDefaults() {
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+ Assert.ok(
+ !DEFAULT_TOP_SITES.length,
+ "Should have 0 DEFAULT_TOP_SITES initially."
+ );
+
+ info("refreshDefaults should add defaults on PREFS_INITIAL_VALUES");
+ TopSites.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");
+ TopSites.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(TopSites, "refresh");
+ TopSites.onAction({ type: at.PREF_CHANGED, data: { name: "topSitesRows" } });
+ Assert.ok(TopSites.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");
+ TopSites.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");
+ TopSites.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");
+ TopSites.refreshDefaults("");
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 0,
+ "Should have 0 DEFAULT_TOP_SITES now."
+ );
+
+ info("refreshDefaults should be able to clear defaults");
+ TopSites.refreshDefaults("https://foo.com");
+ TopSites.refreshDefaults("");
+
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 0,
+ "Should have 0 DEFAULT_TOP_SITES now."
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_filterForThumbnailExpiration() {
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+
+ info(
+ "filterForThumbnailExpiration should pass rows.urls to the callback provided"
+ );
+ const rows = [
+ { url: "foo.com" },
+ { url: "bar.com", customScreenshotURL: "custom" },
+ ];
+ TopSites.store.state.TopSites = { rows };
+ const stub = sandbox.stub();
+ TopSites.filterForThumbnailExpiration(stub);
+ Assert.ok(stub.calledOnce);
+ Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "custom"]));
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_on_SearchService_init_failure() {
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+
+ TopSites.refreshDefaults("https://foo.com");
+
+ gSearchServiceInitStub.rejects(
+ new Error("Simulating search init failures")
+ );
+
+ const result = await TopSites.getLinksWithDefaults();
+ Assert.ok(result);
+
+ gSearchServiceInitStub.resolves();
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults() {
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+
+ TopSites.refreshDefaults("https://foo.com");
+
+ info("getLinksWithDefaults should get the links from NewTabUtils");
+ let result = await TopSites.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();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const result = await TopSites.getLinksWithDefaults();
+ Assert.ok(FilterAdult.filter.calledOnce);
+ Assert.equal(result.length, 1);
+ Assert.equal(result[0].url, TEST_URL);
+
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+ const result = await TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_dedupe() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should call dedupe.group on the links");
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let stub = sandbox.stub(TopSites.dedupe, "group").callsFake((...id) => id);
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(stub.calledOnce, "dedupe.group was called once");
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test__dedupe_key() {
+ let sandbox = sinon.createSandbox();
+
+ info("_dedupeKey should dedupe on hostname instead of url");
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let site = { url: "foo", hostname: "bar" };
+ let result = TopSites._dedupeKey(site);
+
+ Assert.equal(result, site.hostname, "deduped on hostname");
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let result = await TopSites.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();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let result = await TopSites.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();
+ await cleanup();
+ }
+);
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ await TopSites.getLinksWithDefaults();
+ Assert.ok(true, "getLinksWithDefaults did not throw");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const TEST_ROWS = 3;
+ TopSites.store.state.Prefs.values.topSitesRows = TEST_ROWS;
+
+ let result = await TopSites.getLinksWithDefaults();
+ Assert.equal(result.length, TEST_ROWS * TOP_SITES_MAX_SITES_PER_ROW);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_reuse_cache() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should reuse the cache on subsequent calls");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ await TopSites.getLinksWithDefaults();
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ await TopSites.getLinksWithDefaults();
+ TopSites.store.state.Prefs.values.topSitesRows *= 3;
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
+ "getTopSites called twice"
+ );
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ TopSites.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+ await TopSites.getLinksWithDefaults();
+
+ let originalCallCount = Screenshots.getScreenshotForURL.callCount;
+ TopSites.frecentCache.expire();
+
+ let result = await TopSites.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();
+ await cleanup();
+ }
+);
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ await TopSites.getLinksWithDefaults();
+
+ let originalCallCount =
+ NewTabUtils.activityStreamProvider._addFavicons.callCount;
+ TopSites.pinnedCache.expire();
+
+ let result = await TopSites.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();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_no_internal_properties() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not expose internal link properties");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ let internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
+ Assert.equal(internal.join(""), "");
+
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, TEST_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, undefined);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, CUSTOM_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_no_overwrite_pinned_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not overwrite pinned site screenshot");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(
+ async function test_getLinksWithDefaults_no_searchTopSite_from_frecent() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not set searchTopSite from frecent site");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.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();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_concurrency_getTopSites() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults concurrent calls should call the backing data once"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+
+ await Promise.all([
+ TopSites.getLinksWithDefaults(),
+ TopSites.getLinksWithDefaults(),
+ ]);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_concurrency_getScreenshotForURL() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults concurrent calls should call the backing data once"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+ TopSites.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+ Screenshots.getScreenshotForURL.resetHistory();
+
+ await Promise.all([
+ TopSites.getLinksWithDefaults(),
+ TopSites.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"
+ );
+ await cleanup();
+
+ cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+
+ TopSites.refreshDefaults("https://foo.com");
+
+ sandbox.stub(TopSites, "_requestRichIcon");
+ await Promise.all([
+ TopSites.getLinksWithDefaults(),
+ TopSites.getLinksWithDefaults(),
+ ]);
+
+ Assert.equal(
+ TopSites.store.dispatch.callCount,
+ FAKE_LINKS.length,
+ "getLinksWithDefaults concurrent calls should dispatch once per link screenshot fetched"
+ );
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_deduping_no_dedupe_pinned() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not dedupe pinned sites");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_prefer_pinned_sites() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should prefer pinned sites over links");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_title_and_null() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should return sites that have a title");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://github.com/mozilla/activity-stream" }]);
+
+ let sites = await TopSites.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 TopSites.getLinksWithDefaults();
+ Assert.ok(true, "getLinksWithDefaults didn't throw");
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_calls__fetchIcon() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should return sites that have a title");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ sandbox.spy(TopSites, "_fetchIcon");
+ let results = await TopSites.getLinksWithDefaults();
+ Assert.ok(results.length, "Got back some results");
+ Assert.equal(TopSites._fetchIcon.callCount, results.length);
+ for (let result of results) {
+ Assert.ok(TopSites._fetchIcon.calledWith(result));
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ sandbox.stub(TopSites, "_fetchScreenshot");
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(TopSites._fetchScreenshot.calledWith(sinon.match.object, "custom"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+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 cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ for (let i = 0; i < FAKE_LINKS.length; i++) {
+ TopSites.store.state.DiscoveryStream = makeStreamData(i);
+ const result = await TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_init() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox.stub(NimbusFeatures.newtab, "onUpdate");
+
+ let cleanup = stubTopSites(sandbox);
+
+ sandbox.stub(TopSites, "refresh");
+ await TopSites.init();
+
+ info("TopSites.init should call refresh (broadcast: true)");
+ Assert.ok(TopSites.refresh.calledOnce, "refresh called once");
+ Assert.ok(
+ TopSites.refresh.calledWithExactly({
+ broadcast: true,
+ isStartup: true,
+ })
+ );
+
+ info("TopSites.init should initialise the storage");
+ Assert.ok(
+ TopSites.store.dbStorage.getDbTable.calledOnce,
+ "getDbTable called once"
+ );
+ Assert.ok(
+ TopSites.store.dbStorage.getDbTable.calledWithExactly("sectionPrefs")
+ );
+
+ info("TopSites.init should call onUpdate to set up Nimbus update listener");
+
+ Assert.ok(
+ NimbusFeatures.newtab.onUpdate.calledOnce,
+ "NimbusFeatures.newtab.onUpdate called once"
+ );
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox.stub(NimbusFeatures.newtab, "onUpdate");
+
+ let cleanup = stubTopSites(sandbox);
+
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ info("TopSites.refresh should wait for tippytop to initialize");
+ TopSites._tippyTopProvider.initialized = false;
+ sandbox.stub(TopSites._tippyTopProvider, "init").resolves();
+
+ await TopSites.refresh();
+
+ Assert.ok(
+ TopSites._tippyTopProvider.init.calledOnce,
+ "TopSites._tippyTopProvider.init called once"
+ );
+
+ info(
+ "TopSites.refresh should not init the tippyTopProvider if already initialized"
+ );
+ TopSites._tippyTopProvider.initialized = true;
+ TopSites._tippyTopProvider.init.resetHistory();
+
+ await TopSites.refresh();
+
+ Assert.ok(
+ TopSites._tippyTopProvider.init.notCalled,
+ "tippyTopProvider not initted again"
+ );
+
+ info("TopSites.refresh should broadcast TOP_SITES_UPDATED");
+ TopSites.store.dispatch.resetHistory();
+ sandbox.stub(TopSites, "getLinksWithDefaults").resolves([]);
+
+ await TopSites.refresh({ broadcast: true });
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "dispatch called once");
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.BroadcastToContent({
+ type: at.TOP_SITES_UPDATED,
+ data: { links: [], pref: { collapsed: false } },
+ })
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_dispatch() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.refresh should dispatch an action with the links returned");
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ await TopSites.refresh({ broadcast: true });
+ let reference = FAKE_LINKS.map(site =>
+ Object.assign({}, site, {
+ hostname: shortURL(site),
+ typedBonus: true,
+ })
+ );
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "Store.dispatch called once");
+ Assert.equal(
+ TopSites.store.dispatch.firstCall.args[0].type,
+ at.TOP_SITES_UPDATED
+ );
+ Assert.deepEqual(
+ TopSites.store.dispatch.firstCall.args[0].data.links,
+ reference
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_empty_slots() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.refresh should handle empty slots in the resulting top sites array"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._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 TopSites.refresh({ broadcast: true });
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "Store.dispatch called once");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_to_preloaded() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.refresh should dispatch AlsoToPreloaded when broadcast is false"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ gGetTopSitesStub.resolves([]);
+ await TopSites.refresh({ broadcast: false });
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "Store.dispatch called once");
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.AlsoToPreloaded({
+ type: at.TOP_SITES_UPDATED,
+ data: { links: [], pref: { collapsed: false } },
+ })
+ )
+ );
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_init_storage() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.refresh should not init storage of it's already initialized");
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ TopSites._storage.initialized = true;
+
+ await TopSites.refresh({ broadcast: false });
+
+ Assert.ok(
+ TopSites._storage.init.notCalled,
+ "TopSites._storage.init was not called."
+ );
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_handles_indexedDB_errors() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.refresh should dispatch AlsoToPreloaded when broadcast is false"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ TopSites._storage.get.throws(new Error());
+
+ try {
+ await TopSites.refresh({ broadcast: false });
+ Assert.ok(true, "refresh should have succeeded");
+ } catch (e) {
+ Assert.ok(false, "Should not have thrown");
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_updateSectionPrefs_on_UPDATE_SECTION_PREFS() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.onAction should call updateSectionPrefs on UPDATE_SECTION_PREFS"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "updateSectionPrefs");
+ TopSites.onAction({
+ type: at.UPDATE_SECTION_PREFS,
+ data: { id: "topsites" },
+ });
+
+ Assert.ok(
+ TopSites.updateSectionPrefs.calledOnce,
+ "TopSites.updateSectionPrefs called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_updateSectionPrefs_dispatch_TOP_SITES_PREFS_UPDATED() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.updateSectionPrefs should dispatch TOP_SITES_PREFS_UPDATED");
+
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.updateSectionPrefs({ collapsed: true });
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.BroadcastToContent({
+ type: at.TOP_SITES_PREFS_UPDATED,
+ data: { pref: { collapsed: true } },
+ })
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_allocatePositions() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.allocationPositions should allocate positions and dispatch");
+
+ let cleanup = stubTopSites(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(TopSites._contile, "sov").get(() => sov);
+
+ sandbox.stub(Sampling, "ratioSample");
+ Sampling.ratioSample.onCall(0).resolves(0);
+ Sampling.ratioSample.onCall(1).resolves(1);
+
+ await TopSites.allocatePositions();
+
+ Assert.ok(
+ TopSites.store.dispatch.calledOnce,
+ "TopSites.store.dispatch called once"
+ );
+ Assert.ok(
+ TopSites.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 TopSites.allocatePositions();
+
+ Assert.ok(
+ TopSites.store.dispatch.calledTwice,
+ "TopSites.store.dispatch called twice"
+ );
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToMain({
+ type: at.SOV_UPDATED,
+ data: {
+ ready: true,
+ positions: [
+ { position: 1, assignedPartner: "amp" },
+ { position: 2, assignedPartner: "amp" },
+ ],
+ },
+ })
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getScreenshotPreview() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.getScreenshotPreview should dispatch preview if request is succesful"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(TopSites.store.dispatch.calledOnce);
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: FAKE_SCREENSHOT, url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getScreenshotPreview() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.getScreenshotPreview should return empty string if request fails"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
+ await TopSites.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(TopSites.store.dispatch.calledOnce);
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: "", url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_onAction_part_1() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.onAction should call getScreenshotPreview on PREVIEW_REQUEST");
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "getScreenshotPreview");
+ TopSites.onAction({
+ type: at.PREVIEW_REQUEST,
+ data: { url: "foo" },
+ meta: { fromTarget: 1234 },
+ });
+
+ Assert.ok(
+ TopSites.getScreenshotPreview.calledOnce,
+ "TopSites.getScreenshotPreview called once"
+ );
+ Assert.ok(TopSites.getScreenshotPreview.calledWithExactly("foo", 1234));
+
+ info("TopSites.onAction should refresh on SYSTEM_TICK");
+ sandbox.stub(TopSites, "refresh");
+ TopSites.onAction({ type: at.SYSTEM_TICK });
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: false }));
+
+ info(
+ "TopSites.onAction should call with correct parameters on TOP_SITES_PIN"
+ );
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.spy(TopSites, "pin");
+
+ let pinAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: { url: "foo.com" }, index: 7 },
+ };
+ TopSites.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(
+ TopSites.pin.calledOnce,
+ "TopSites.onAction should call pin on TOP_SITES_PIN"
+ );
+
+ info(
+ "TopSites.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 },
+ };
+ TopSites.onAction(pinAction);
+ Assert.ok(
+ NewTabUtils.blockedLinks.unblock.calledWith({
+ url: pinAction.data.site.url,
+ })
+ );
+
+ info("TopSites.onAction should call insert on TOP_SITES_INSERT");
+ sandbox.stub(TopSites, "insert");
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.com" } },
+ };
+
+ TopSites.onAction(addAction);
+ Assert.ok(TopSites.insert.calledOnce, "TopSites.insert called once");
+
+ info(
+ "TopSites.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" } },
+ };
+ TopSites.onAction(unpinAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledOnce,
+ "NewTabUtils.pinnedLinks.unpin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.unpin.calledWith(unpinAction.data.site));
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_onAction_part_2() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.onAction should call refresh without a target if we clear " +
+ "history with PLACES_HISTORY_CLEARED"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "refresh");
+ TopSites.onAction({ type: at.PLACES_HISTORY_CLEARED });
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: true }));
+
+ TopSites.refresh.resetHistory();
+
+ info(
+ "TopSites.onAction should call refresh without a target " +
+ "if we remove a Topsite from history"
+ );
+ TopSites.onAction({ type: at.PLACES_LINKS_DELETED });
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: true }));
+
+ info("TopSites.onAction should call init on INIT action");
+ TopSites.onAction({ type: at.PLACES_LINKS_DELETED });
+ sandbox.stub(TopSites, "init");
+ TopSites.onAction({ type: at.INIT });
+ Assert.ok(TopSites.init.calledOnce, "TopSites.init called once");
+
+ info("TopSites.onAction should call refresh on PLACES_LINK_BLOCKED action");
+ TopSites.refresh.resetHistory();
+ await TopSites.onAction({ type: at.PLACES_LINK_BLOCKED });
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: true }));
+
+ info("TopSites.onAction should call refresh on PLACES_LINKS_CHANGED action");
+ TopSites.refresh.resetHistory();
+ await TopSites.onAction({ type: at.PLACES_LINKS_CHANGED });
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: false }));
+
+ info(
+ "TopSites.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" } },
+ };
+ TopSites.onAction(addAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(addAction.data.site, 0));
+
+ info(
+ "TopSites.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 },
+ };
+ TopSites.onAction(dropAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(dropAction.data.site, 3));
+
+ // TopSites.init needs to actually run in order to register the observers that'll
+ // be removed in the following UNINIT test, otherwise uninit will throw.
+ TopSites.init.restore();
+ TopSites.init();
+
+ info("TopSites.onAction should remove the expiration filter on UNINIT");
+ sandbox.stub(PageThumbs, "removeExpirationFilter");
+ TopSites.onAction({ type: "UNINIT" });
+ Assert.ok(
+ PageThumbs.removeExpirationFilter.calledOnce,
+ "PageThumbs.removeExpirationFilter called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_onAction_part_3() {
+ let sandbox = sinon.createSandbox();
+
+ let cleanup = stubTopSites(sandbox);
+
+ info(
+ "TopSites.onAction should call updatePinnedSearchShortcuts " +
+ "on UPDATE_PINNED_SEARCH_SHORTCUTS action"
+ );
+ sandbox.stub(TopSites, "updatePinnedSearchShortcuts");
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ await TopSites.onAction({
+ type: at.UPDATE_PINNED_SEARCH_SHORTCUTS,
+ data: { addedShortcuts },
+ });
+ Assert.ok(
+ TopSites.updatePinnedSearchShortcuts.calledOnce,
+ "TopSites.updatePinnedSearchShortcuts called once"
+ );
+
+ info(
+ "TopSites.onAction should refresh from Contile on " +
+ "SHOW_SPONSORED_PREF if Contile is enabled"
+ );
+ sandbox.spy(TopSites._contile, "refresh");
+ let prefChangeAction = {
+ type: at.PREF_CHANGED,
+ data: { name: SHOW_SPONSORED_PREF },
+ };
+ sandbox.stub(NimbusFeatures.newtab, "getVariable").returns(true);
+ TopSites.onAction(prefChangeAction);
+
+ Assert.ok(
+ TopSites._contile.refresh.calledOnce,
+ "TopSites._contile.refresh called once"
+ );
+
+ info(
+ "TopSites.onAction should not refresh from Contile on " +
+ "SHOW_SPONSORED_PREF if Contile is disabled"
+ );
+ NimbusFeatures.newtab.getVariable.returns(false);
+ TopSites._contile.refresh.resetHistory();
+ TopSites.onAction(prefChangeAction);
+
+ Assert.ok(
+ !TopSites._contile.refresh.calledOnce,
+ "TopSites._contile.refresh never called"
+ );
+
+ info(
+ "TopSites.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);
+ TopSites._contile.refresh.resetHistory();
+
+ TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_insert_part_1() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ let cleanup = stubTopSites(sandbox);
+
+ info("TopSites.insert should pin site in first slot of empty pinned list");
+ Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
+ await TopSites.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(TopSites.store.dispatch.calledOnce);
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: "", url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
+
+ {
+ info(
+ "TopSites.insert should pin site in first slot of pinned list with " +
+ "empty first slot"
+ );
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.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(
+ "TopSites.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 site = { url: "foo.bar", label: "foo" };
+
+ await TopSites.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(
+ "TopSites.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" };
+ await TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_insert_part_2() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ let cleanup = stubTopSites(sandbox);
+
+ {
+ info(
+ "TopSites.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]);
+ TopSites.store.state.Prefs.values.topSitesRows = 1;
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.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("TopSites.insert should trigger refresh on TOP_SITES_INSERT");
+ sandbox.stub(TopSites, "refresh");
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.com" } },
+ };
+
+ await TopSites.insert(addAction);
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ }
+
+ {
+ info("TopSites.insert should correctly handle different index values");
+ let index = -1;
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index, site } };
+
+ await TopSites.insert(action);
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+
+ index = undefined;
+ await TopSites.insert(action);
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_insert_part_3() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ let cleanup = stubTopSites(sandbox);
+
+ {
+ info("TopSites.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" };
+
+ await TopSites.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(
+ "TopSites.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" };
+
+ await TopSites.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(
+ "TopSites.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]);
+
+ await TopSites.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 TopSites.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("TopSites.insert should not insert past the visible top sites");
+ let site1 = { url: "foo.bar", label: "foo" };
+ await TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_pin_part_1() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.spy(TopSites.pinnedCache, "request");
+ let cleanup = stubTopSites(sandbox);
+
+ {
+ info("TopSites.pin should pin site in specified slot empty pinned list");
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: "screenshot",
+ };
+ Assert.ok(
+ TopSites.pinnedCache.request.notCalled,
+ "TopSites.pinnedCache.request not called"
+ );
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.called,
+ "NewTabUtils.pinnedLinks.pin called"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.pin should lookup the link object to update the custom " +
+ "screenshot"
+ );
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: "screenshot",
+ };
+ Assert.ok(
+ TopSites.pinnedCache.request.notCalled,
+ "TopSites.pinnedCache.request not called"
+ );
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ TopSites.pinnedCache.request.called,
+ "TopSites.pinnedCache.request called"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.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,
+ };
+ Assert.ok(
+ TopSites.pinnedCache.request.notCalled,
+ "TopSites.pinnedCache.request not called"
+ );
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ TopSites.pinnedCache.request.called,
+ "TopSites.pinnedCache.request called"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.pin should not do a link object lookup if custom " +
+ "screenshot field is not set"
+ );
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ !TopSites.pinnedCache.request.called,
+ "TopSites.pinnedCache.request never called"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.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" };
+ await TopSites.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();
+ await cleanup();
+});
+
+add_task(async function test_pin_part_2() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info("TopSites.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 cleanup = stubTopSites(sandbox);
+ await TopSites.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();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.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 cleanup = stubTopSites(sandbox);
+ await TopSites.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();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.pin should properly update LinksCache object " +
+ "properties between migrations"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ let cleanup = stubTopSites(sandbox);
+ let pinnedLinks = await TopSites.pinnedCache.request();
+ Assert.equal(pinnedLinks.length, 1);
+ TopSites.pinnedCache.expire();
+
+ pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo");
+
+ pinnedLinks = await TopSites.pinnedCache.request();
+ Assert.equal(pinnedLinks[0].screenshot, "foo");
+
+ // Force cache expiration in order to trigger a migration of objects
+ TopSites.pinnedCache.expire();
+ pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar");
+
+ pinnedLinks = await TopSites.pinnedCache.request();
+ Assert.equal(pinnedLinks[0].screenshot, "bar");
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_pin_part_3() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.spy(TopSites, "insert");
+
+ {
+ info("TopSites.pin should call insert if index < 0");
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index: -1, site } };
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.pin(action);
+
+ Assert.ok(TopSites.insert.calledOnce, "TopSites.insert called once");
+ Assert.ok(TopSites.insert.calledWithExactly(action));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.insert.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info("TopSites.pin should not call insert if index == 0");
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index: 0, site } };
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.pin(action);
+
+ Assert.ok(!TopSites.insert.called, "TopSites.insert not called");
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info("TopSites.pin should trigger refresh on TOP_SITES_PIN");
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "refresh");
+ let pinExistingAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: FAKE_LINKS[4], index: 4 },
+ };
+
+ await TopSites.pin(pinExistingAction);
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_integration() {
+ let sandbox = sinon.createSandbox();
+
+ info("Test adding a pinned site and removing it with actions");
+ let cleanup = stubTopSites(sandbox);
+
+ let resolvers = [];
+ TopSites.store.dispatch = sandbox.stub().callsFake(() => {
+ resolvers.shift()();
+ });
+ TopSites._startedUp = true;
+ sandbox.stub(TopSites, "_fetchScreenshot");
+
+ let forDispatch = action =>
+ new Promise(resolve => {
+ resolvers.push(resolve);
+ TopSites.onAction(action);
+ });
+
+ TopSites._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(
+ TopSites.store.dispatch.calledTwice,
+ "TopSites.store.dispatch called twice"
+ );
+ Assert.equal(
+ TopSites.store.dispatch.firstCall.args[0].data.links[0].url,
+ url
+ );
+ Assert.equal(
+ TopSites.store.dispatch.secondCall.args[0].data.links[0].url,
+ FAKE_LINKS[0].url
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+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(
+ "TopSites.getLinksWithDefaults should filter out alexa top 5 " +
+ "search from the default sites"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.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);
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should not filter out alexa, default " +
+ "search from the query results if the experiment pref is off"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+
+ Assert.ok(urlsReturned.includes("https://google.com"));
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should filter out the current " +
+ "default search from the default sites"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ sandbox.stub(TopSites, "_currentSearchHostname").get(() => "amazon");
+ TopSites.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "google.com,amazon.com" },
+ });
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+
+ let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(!urlsReturned.includes("https://amazon.com"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should not filter out current " +
+ "default search from pinned sites even if it matches the current " +
+ "default search"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.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 TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(urlsReturned.includes("google.com"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ await cleanup();
+ }
+
+ 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",
+ });
+
+ sandbox.stub(TopSites, "refresh");
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should call refresh and set " +
+ "._currentSearchHostname to the new engine hostname when the " +
+ "default search engine has been set"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ TopSites.observe(
+ null,
+ "browser-search-engine-modified",
+ "engine-default"
+ );
+ Assert.equal(TopSites._currentSearchHostname, "duckduckgo");
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ TopSites.refresh.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should call refresh when the " +
+ "experiment pref has changed"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ TopSites.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: true },
+ });
+ Assert.ok(
+ TopSites.refresh.calledOnce,
+ "TopSites.refresh was called once"
+ );
+
+ TopSites.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: false },
+ });
+ Assert.ok(
+ TopSites.refresh.calledTwice,
+ "TopSites.refresh was called twice"
+ );
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ TopSites.refresh.resetHistory();
+ await cleanup();
+ }
+
+ 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 prepTopSites = () => {
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true;
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] =
+ "google,amazon";
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
+ };
+
+ {
+ info(
+ "TopSites should updateCustomSearchShortcuts when experiment " +
+ "pref is turned on"
+ );
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
+ sandbox.spy(TopSites, "updateCustomSearchShortcuts");
+
+ // turn the experiment on
+ TopSites.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true },
+ });
+
+ Assert.ok(
+ TopSites.updateCustomSearchShortcuts.calledOnce,
+ "TopSites.updateCustomSearchShortcuts called once"
+ );
+ TopSites.updateCustomSearchShortcuts.restore();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites should filter out default top sites that match a " +
+ "hostname of a search shortcut if previously blocked"
+ );
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ TopSites.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 TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(!urlsReturned.includes("https://amazon.ca"));
+ await cleanup();
+ }
+
+ {
+ info("TopSites should update frecent search topsite icon");
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ sandbox.stub(TopSites._tippyTopProvider, "processSite").callsFake(site => {
+ site.tippyTopIcon = "icon.png";
+ site.backgroundColor = "#fff";
+ return site;
+ });
+ gGetTopSitesStub.resolves([{ url: "https://google.com" }]);
+
+ let urlsReturned = await TopSites.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);
+ TopSites._tippyTopProvider.processSite.restore();
+ await cleanup();
+ }
+
+ {
+ info("TopSites should update default search topsite icon");
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ sandbox.stub(TopSites._tippyTopProvider, "processSite").callsFake(site => {
+ site.tippyTopIcon = "icon.png";
+ site.backgroundColor = "#fff";
+ return site;
+ });
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+ TopSites.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "google.com,amazon.com" },
+ });
+
+ let urlsReturned = await TopSites.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);
+ TopSites._tippyTopProvider.processSite.restore();
+ TopSites.store.dispatch.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites should dispatch UPDATE_SEARCH_SHORTCUTS on " +
+ "updateCustomSearchShortcuts"
+ );
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ TopSites.store.state.Prefs.values[
+ "improvesearch.noDefaultSearchTile"
+ ] = true;
+ await TopSites.updateCustomSearchShortcuts();
+ Assert.ok(
+ TopSites.store.dispatch.calledOnce,
+ "TopSites.store.dispatch called once"
+ );
+ Assert.ok(
+ TopSites.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",
+ })
+ );
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_updatePinnedSearchShortcuts() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should unpin a " +
+ "shortcut in deletedShortcuts"
+ );
+ let cleanup = stubTopSites(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,
+ },
+ ]);
+ TopSites.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();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should pin a shortcut " +
+ "in addedShortcuts"
+ );
+ let cleanup = stubTopSites(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,
+ },
+ ]);
+ TopSites.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();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should pin and unpin " +
+ "in the same action"
+ );
+ let cleanup = stubTopSites(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,
+ },
+ ]);
+ TopSites.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();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should pin a shortcut in " +
+ "addedShortcuts even if pinnedLinks is full"
+ );
+ let cleanup = stubTopSites(sandbox);
+
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ let deletedShortcuts = [];
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => FAKE_LINKS);
+ TopSites.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();
+ await cleanup();
+ }
+
+ 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 fetchStub;
+
+ let prepTopSites = () => {
+ TopSites.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true;
+ fetchStub = sandbox.stub(TopSites, "fetch");
+ function cleanupPrep() {
+ TopSites._contile._sites = [];
+ fetchStub.restore();
+ }
+ return cleanupPrep;
+ };
+
+ {
+ info("TopSites._fetchSites should fetch sites from Contile");
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ 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 TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info("TopSites._fetchSites should call allocatePositions");
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ sandbox.stub(TopSites, "allocatePositions").resolves();
+ await TopSites._contile.refresh();
+
+ Assert.ok(
+ TopSites.allocatePositions.calledOnce,
+ "TopSites.allocatePositions called once"
+ );
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should fetch SOV (Share-of-Voice) " +
+ "settings from Contile"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ 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 TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.deepEqual(TopSites._contile.sov, sov);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should not fetch from Contile if " +
+ "it's not enabled"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ NimbusFeatures.newtab.getVariable.reset();
+ NimbusFeatures.newtab.getVariable.returns(false);
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetchStub.notCalled, "TopSites.fetch was not called");
+ Assert.ok(!fetched);
+ Assert.equal(TopSites._contile.sites.length, 0);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should still return two tiles when Contile " +
+ "provides more than 2 tiles and filtering results in more than 2 tiles"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ 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 TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ // Both "foo" and "bar" should be filtered
+ Assert.equal(TopSites._contile.sites.length, 2);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(TopSites._contile.sites[1].url, "https://test1.com");
+
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should still return two tiles with " +
+ "replacement if the Nimbus variable was unset"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ 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 TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(TopSites._contile.sites[1].url, "https://test1.com");
+
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info("TopSites._fetchSites should filter the blocked sponsors");
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ 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 TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ // Both "foo" and "bar" should be filtered
+ Assert.equal(TopSites._contile.sites.length, 1);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._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 cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._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 cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should handle invalid payload " +
+ "properly from Contile"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: () =>
+ Promise.resolve({
+ unknown: [],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should handle empty payload properly " +
+ "from Contile"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ 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 TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info("TopSites._fetchSites should handle no content properly from Contile");
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({ ok: true, status: 204 });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should set Caching Prefs after " +
+ "a successful request"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ 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 TopSites._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
+ );
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should return cached valid tiles " +
+ "when Contile returns error status"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ 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 TopSites._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test-cached.com");
+ Assert.equal(
+ TopSites._contile.sites[1].url,
+ "https://www.test1-cached.com"
+ );
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should not be successful when contile " +
+ "returns an error and no valid tiles are cached"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ 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 TopSites._contile._fetchSites();
+ Assert.ok(!fetched);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should return cached valid tiles " +
+ "filtering blocked tiles when Contile returns error status"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ 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 TopSites._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 1);
+ Assert.equal(
+ TopSites._contile.sites[0].url,
+ "https://www.test1-cached.com"
+ );
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should still return 3 tiles when nimbus " +
+ "variable overrides max num of sponsored contile tiles"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ 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 TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 3);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(TopSites._contile.sites[1].url, "https://test1.com");
+ Assert.equal(TopSites._contile.sites[2].url, "https://test2.com");
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
diff --git a/browser/components/topsites/test/unit/xpcshell.toml b/browser/components/topsites/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..98b0fc5360
--- /dev/null
+++ b/browser/components/topsites/test/unit/xpcshell.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+firefox-appdir = "browser"
+
+["test_top_sites.js"]