From a90a5cba08fdf6c0ceb95101c275108a152a3aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:35:37 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- .../topsites/test/unit/test_top_sites.js | 3571 ++++++++++++++++++++ .../components/topsites/test/unit/xpcshell.toml | 4 + 2 files changed, 3575 insertions(+) create mode 100644 browser/components/topsites/test/unit/test_top_sites.js create mode 100644 browser/components/topsites/test/unit/xpcshell.toml (limited to 'browser/components/topsites/test') 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"] -- cgit v1.2.3