From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../newtab/test/unit/lib/TopSitesFeed.test.js | 3020 ++++++++++++++++++++ 1 file changed, 3020 insertions(+) create mode 100644 browser/components/newtab/test/unit/lib/TopSitesFeed.test.js (limited to 'browser/components/newtab/test/unit/lib/TopSitesFeed.test.js') diff --git a/browser/components/newtab/test/unit/lib/TopSitesFeed.test.js b/browser/components/newtab/test/unit/lib/TopSitesFeed.test.js new file mode 100644 index 0000000000..a173c16cde --- /dev/null +++ b/browser/components/newtab/test/unit/lib/TopSitesFeed.test.js @@ -0,0 +1,3020 @@ +"use strict"; + +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { FAKE_GLOBAL_PREFS, FakePrefs, GlobalOverrider } from "test/unit/utils"; +import { + insertPinned, + TOP_SITES_DEFAULT_ROWS, + TOP_SITES_MAX_SITES_PER_ROW, +} from "common/Reducers.sys.mjs"; +import { getDefaultOptions } from "lib/ActivityStreamStorage.jsm"; +import injector from "inject!lib/TopSitesFeed.jsm"; +import { Screenshots } from "lib/Screenshots.jsm"; +import { LinksCache } from "lib/LinksCache.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 REMOTE_SETTING_DEFAULTS_PREF = "browser.topsites.useRemoteSetting"; +const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles"; +const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor"; +const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch"; + +function FakeTippyTopProvider() {} +FakeTippyTopProvider.prototype = { + async init() { + this.initialized = true; + }, + processSite(site) { + return site; + }, +}; + +describe("Top Sites Feed", () => { + let TopSitesFeed; + let DEFAULT_TOP_SITES; + let feed; + let globals; + let sandbox; + let links; + let fakeNewTabUtils; + let fakeScreenshot; + let filterAdultStub; + let shortURLStub; + let fakePageThumbs; + let fetchStub; + let fakeNimbusFeatures; + let fakeSampling; + + beforeEach(() => { + globals = new GlobalOverrider(); + sandbox = globals.sandbox; + fakeNewTabUtils = { + blockedLinks: { + links: [], + isBlocked: () => false, + unblock: sandbox.spy(), + }, + activityStreamLinks: { + getTopSites: sandbox.spy(() => Promise.resolve(links)), + }, + activityStreamProvider: { + _addFavicons: sandbox.spy(l => + Promise.resolve( + l.map(link => { + link.favicon = FAKE_FAVICON; + link.faviconSize = FAKE_FAVICON_SIZE; + return link; + }) + ) + ), + _faviconBytesToDataURI: sandbox.spy(), + }, + pinnedLinks: { + links: [], + isPinned: () => false, + pin: sandbox.spy(), + unpin: sandbox.spy(), + }, + }; + fakeScreenshot = { + getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT)), + maybeCacheScreenshot: sandbox.spy(Screenshots.maybeCacheScreenshot), + _shouldGetScreenshots: sinon.stub().returns(true), + }; + filterAdultStub = { + filter: sinon.stub().returnsArg(0), + }; + shortURLStub = sinon + .stub() + .callsFake(site => + site.url.replace(/(.com|.ca)/, "").replace("https://", "") + ); + const fakeDedupe = function () {}; + fakePageThumbs = { + addExpirationFilter: sinon.stub(), + removeExpirationFilter: sinon.stub(), + }; + fakeNimbusFeatures = { + newtab: { + getVariable: sinon.stub(), + onUpdate: sinon.stub(), + offUpdate: sinon.stub(), + }, + pocketNewtab: { + getVariable: sinon.stub(), + }, + }; + fakeSampling = { + ratioSample: sinon.stub(), + }; + globals.set({ + PageThumbs: fakePageThumbs, + NewTabUtils: fakeNewTabUtils, + gFilterAdultEnabled: false, + NimbusFeatures: fakeNimbusFeatures, + LinksCache, + FilterAdult: filterAdultStub, + Screenshots: fakeScreenshot, + Sampling: fakeSampling, + }); + sandbox.spy(global.XPCOMUtils, "defineLazyGetter"); + FAKE_GLOBAL_PREFS.set("default.sites", "https://foo.com/"); + ({ TopSitesFeed, DEFAULT_TOP_SITES } = injector({ + "lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs }, + "common/Dedupe.jsm": { Dedupe: fakeDedupe }, + "common/Reducers.jsm": { + insertPinned, + TOP_SITES_DEFAULT_ROWS, + TOP_SITES_MAX_SITES_PER_ROW, + }, + "lib/FilterAdult.jsm": { FilterAdult: filterAdultStub }, + "lib/Screenshots.jsm": { Screenshots: fakeScreenshot }, + "lib/TippyTopProvider.sys.mjs": { + TippyTopProvider: FakeTippyTopProvider, + }, + "lib/ShortURL.jsm": { shortURL: shortURLStub }, + "lib/ActivityStreamStorage.jsm": { + ActivityStreamStorage: function Fake() {}, + getDefaultOptions, + }, + })); + feed = new TopSitesFeed(); + const storage = { + init: sandbox.stub().resolves(), + get: sandbox.stub().resolves(), + set: sandbox.stub().resolves(), + }; + // Setup for tests that don't call `init` but require feed.storage + feed._storage = storage; + feed.store = { + dispatch: sinon.spy(), + getState() { + return this.state; + }, + state: { + Prefs: { values: { topSitesRows: 2 } }, + TopSites: { rows: Array(12).fill("site") }, + }, + dbStorage: { getDbTable: sandbox.stub().returns(storage) }, + }; + feed.dedupe.group = (...sites) => sites; + links = FAKE_LINKS; + // Turn off the search shortcuts experiment by default for other tests + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false; + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = + "google,amazon"; + }); + afterEach(() => { + globals.restore(); + sandbox.restore(); + }); + + function stubFaviconsToUseScreenshots() { + fakeNewTabUtils.activityStreamProvider._addFavicons = sandbox.stub(); + } + + describe("#constructor", () => { + it("should defineLazyGetter for log, contextId, and _currentSearchHostname", () => { + assert.calledThrice(global.XPCOMUtils.defineLazyGetter); + + let spyCall = global.XPCOMUtils.defineLazyGetter.getCall(0); + assert.ok(spyCall.calledWith(sinon.match.any, "log", sinon.match.func)); + + spyCall = global.XPCOMUtils.defineLazyGetter.getCall(1); + assert.ok( + spyCall.calledWith(sinon.match.any, "contextId", sinon.match.func) + ); + + spyCall = global.XPCOMUtils.defineLazyGetter.getCall(2); + assert.ok( + spyCall.calledWith(feed, "_currentSearchHostname", sinon.match.func) + ); + }); + }); + + describe("#refreshDefaults", () => { + it("should add defaults on PREFS_INITIAL_VALUES", () => { + feed.onAction({ + type: at.PREFS_INITIAL_VALUES, + data: { "default.sites": "https://foo.com" }, + }); + + assert.isAbove(DEFAULT_TOP_SITES.length, 0); + }); + it("should add defaults on default.sites PREF_CHANGED", () => { + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: "default.sites", value: "https://foo.com" }, + }); + + assert.isAbove(DEFAULT_TOP_SITES.length, 0); + }); + it("should refresh on topSiteRows PREF_CHANGED", () => { + feed.refresh = sinon.spy(); + feed.onAction({ type: at.PREF_CHANGED, data: { name: "topSitesRows" } }); + + assert.calledOnce(feed.refresh); + }); + it("should have default sites with .isDefault = true", () => { + feed.refreshDefaults("https://foo.com"); + + DEFAULT_TOP_SITES.forEach(link => + assert.propertyVal(link, "isDefault", true) + ); + }); + it("should have default sites with appropriate hostname", () => { + feed.refreshDefaults("https://foo.com"); + + DEFAULT_TOP_SITES.forEach(link => + assert.propertyVal(link, "hostname", shortURLStub(link)) + ); + }); + it("should add no defaults on empty pref", () => { + feed.refreshDefaults(""); + + assert.equal(DEFAULT_TOP_SITES.length, 0); + }); + it("should clear defaults", () => { + feed.refreshDefaults("https://foo.com"); + feed.refreshDefaults(""); + + assert.equal(DEFAULT_TOP_SITES.length, 0); + }); + }); + describe("#filterForThumbnailExpiration", () => { + it("should pass rows.urls to the callback provided", () => { + const rows = [ + { url: "foo.com" }, + { url: "bar.com", customScreenshotURL: "custom" }, + ]; + feed.store.state.TopSites = { rows }; + const stub = sinon.stub(); + + feed.filterForThumbnailExpiration(stub); + + assert.calledOnce(stub); + assert.calledWithExactly(stub, ["foo.com", "bar.com", "custom"]); + }); + }); + describe("#getLinksWithDefaults", () => { + beforeEach(() => { + feed.refreshDefaults("https://foo.com"); + }); + + describe("general", () => { + it("should get the links from NewTabUtils", async () => { + const result = await feed.getLinksWithDefaults(); + const reference = links.map(site => + Object.assign({}, site, { + hostname: shortURLStub(site), + typedBonus: true, + }) + ); + + assert.deepEqual(result, reference); + assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites); + }); + it("should indicate the links get typed bonus", async () => { + const result = await feed.getLinksWithDefaults(); + + assert.propertyVal(result[0], "typedBonus", true); + }); + it("should filter out non-pinned adult sites", async () => { + filterAdultStub.filter = sinon.stub().returns([]); + fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }]; + + const result = await feed.getLinksWithDefaults(); + + // The stub filters out everything + assert.calledOnce(filterAdultStub.filter); + assert.equal(result.length, 1); + assert.equal(result[0].url, fakeNewTabUtils.pinnedLinks.links[0].url); + }); + it("should filter out the defaults that have been blocked", async () => { + // 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: shortURLStub({ url }), + typedBonus: true, + url, + }; + const blockedDefaultSite = { url: "https://foo.com" }; + fakeNewTabUtils.activityStreamLinks.getTopSites = () => [topsite]; + fakeNewTabUtils.blockedLinks.isBlocked = site => + site.url === blockedDefaultSite.url; + const result = await feed.getLinksWithDefaults(); + + // what we should be left with is just the top site we added, and not the default site we blocked + assert.lengthOf(result, 1); + assert.deepEqual(result[0], topsite); + assert.notInclude(result, blockedDefaultSite); + }); + it("should call dedupe on the links", async () => { + const stub = sinon.stub(feed.dedupe, "group").callsFake((...id) => id); + + await feed.getLinksWithDefaults(); + + assert.calledOnce(stub); + }); + it("should dedupe the links by hostname", async () => { + const site = { url: "foo", hostname: "bar" }; + const result = feed._dedupeKey(site); + + assert.equal(result, site.hostname); + }); + it("should add defaults if there are are not enough links", async () => { + links = [{ frecency: FAKE_FRECENCY, url: "foo.com" }]; + + const result = await feed.getLinksWithDefaults(); + const reference = [...links, ...DEFAULT_TOP_SITES].map(s => + Object.assign({}, s, { + hostname: shortURLStub(s), + typedBonus: true, + }) + ); + + assert.deepEqual(result, reference); + }); + it("should only add defaults up to the number of visible slots", async () => { + links = []; + const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW; + for (let i = 0; i < numVisible - 1; i++) { + links.push({ frecency: FAKE_FRECENCY, url: `foo${i}.com` }); + } + const result = await feed.getLinksWithDefaults(); + const reference = [...links, DEFAULT_TOP_SITES[0]].map(s => + Object.assign({}, s, { + hostname: shortURLStub(s), + typedBonus: true, + }) + ); + + assert.lengthOf(result, numVisible); + assert.deepEqual(result, reference); + }); + it("should not throw if NewTabUtils returns null", () => { + links = null; + assert.doesNotThrow(() => { + feed.getLinksWithDefaults(); + }); + }); + it("should get more if the user has asked for more", async () => { + links = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW) + .fill(null) + .map((v, i) => ({ + frecency: FAKE_FRECENCY, + url: `http://www.site${i}.com`, + })); + feed.store.state.Prefs.values.topSitesRows = 3; + + const result = await feed.getLinksWithDefaults(); + + assert.propertyVal( + result, + "length", + feed.store.state.Prefs.values.topSitesRows * + TOP_SITES_MAX_SITES_PER_ROW + ); + }); + }); + describe("caching", () => { + it("should reuse the cache on subsequent calls", async () => { + await feed.getLinksWithDefaults(); + await feed.getLinksWithDefaults(); + + assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites); + }); + it("should ignore the cache when requesting more", async () => { + await feed.getLinksWithDefaults(); + feed.store.state.Prefs.values.topSitesRows *= 3; + + await feed.getLinksWithDefaults(); + + assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites); + }); + it("should migrate frecent screenshot data without getting screenshots again", async () => { + feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true; + stubFaviconsToUseScreenshots(); + await feed.getLinksWithDefaults(); + const { callCount } = fakeScreenshot.getScreenshotForURL; + feed.frecentCache.expire(); + + const result = await feed.getLinksWithDefaults(); + + assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites); + assert.callCount(fakeScreenshot.getScreenshotForURL, callCount); + assert.propertyVal(result[0], "screenshot", FAKE_SCREENSHOT); + }); + it("should migrate pinned favicon data without getting favicons again", async () => { + fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }]; + await feed.getLinksWithDefaults(); + const { callCount } = + fakeNewTabUtils.activityStreamProvider._addFavicons; + feed.pinnedCache.expire(); + + const result = await feed.getLinksWithDefaults(); + + assert.callCount( + fakeNewTabUtils.activityStreamProvider._addFavicons, + callCount + ); + assert.propertyVal(result[0], "favicon", FAKE_FAVICON); + assert.propertyVal(result[0], "faviconSize", FAKE_FAVICON_SIZE); + }); + it("should not expose internal link properties", async () => { + const result = await feed.getLinksWithDefaults(); + + const internal = Object.keys(result[0]).filter(key => + key.startsWith("__") + ); + assert.equal(internal.join(""), ""); + }); + it("should copy the screenshot of the frecent site if pinned site doesn't have customScreenshotURL", async () => { + links = [{ url: "https://foo.com/", screenshot: "screenshot" }]; + fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }]; + + const result = await feed.getLinksWithDefaults(); + + assert.equal(result[0].screenshot, links[0].screenshot); + }); + it("should not copy the frecent screenshot if customScreenshotURL is set", async () => { + links = [{ url: "https://foo.com/", screenshot: "screenshot" }]; + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://foo.com/", customScreenshotURL: "custom" }, + ]; + + const result = await feed.getLinksWithDefaults(); + + assert.isUndefined(result[0].screenshot); + }); + it("should keep the same screenshot if no frecent site is found", async () => { + links = []; + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://foo.com/", screenshot: "custom" }, + ]; + + const result = await feed.getLinksWithDefaults(); + + assert.equal(result[0].screenshot, "custom"); + }); + it("should not overwrite pinned site screenshot", async () => { + links = [{ url: "https://foo.com/", screenshot: "foo" }]; + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://foo.com/", screenshot: "bar" }, + ]; + + const result = await feed.getLinksWithDefaults(); + + assert.equal(result[0].screenshot, "bar"); + }); + it("should not set searchTopSite from frecent site", async () => { + links = [ + { + url: "https://foo.com/", + searchTopSite: true, + screenshot: "screenshot", + }, + ]; + fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }]; + + const result = await feed.getLinksWithDefaults(); + + assert.propertyVal(result[0], "searchTopSite", false); + // But it should copy over other properties + assert.propertyVal(result[0], "screenshot", "screenshot"); + }); + describe("concurrency", () => { + beforeEach(() => { + stubFaviconsToUseScreenshots(); + fakeScreenshot.getScreenshotForURL = sandbox + .stub() + .resolves(FAKE_SCREENSHOT); + }); + afterEach(() => { + sandbox.restore(); + }); + + const getTwice = () => + Promise.all([ + feed.getLinksWithDefaults(), + feed.getLinksWithDefaults(), + ]); + + it("should call the backing data once", async () => { + await getTwice(); + + assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites); + }); + it("should get screenshots once per link", async () => { + feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true; + await getTwice(); + + assert.callCount( + fakeScreenshot.getScreenshotForURL, + FAKE_LINKS.length + ); + }); + it("should dispatch once per link screenshot fetched", async () => { + feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true; + feed._requestRichIcon = sinon.stub(); + await getTwice(); + + assert.callCount(feed.store.dispatch, FAKE_LINKS.length); + }); + }); + }); + describe("deduping", () => { + beforeEach(() => { + ({ TopSitesFeed, DEFAULT_TOP_SITES } = injector({ + "lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs }, + "common/Reducers.jsm": { + insertPinned, + TOP_SITES_DEFAULT_ROWS, + TOP_SITES_MAX_SITES_PER_ROW, + }, + "lib/Screenshots.jsm": { Screenshots: fakeScreenshot }, + })); + sandbox.stub(global.Services.eTLD, "getPublicSuffix").returns("com"); + feed = Object.assign(new TopSitesFeed(), { store: feed.store }); + }); + it("should not dedupe pinned sites", async () => { + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://developer.mozilla.org/en-US/docs/Web" }, + { url: "https://developer.mozilla.org/en-US/docs/Learn" }, + ]; + + const sites = await feed.getLinksWithDefaults(); + + assert.lengthOf(sites, 2 * TOP_SITES_MAX_SITES_PER_ROW); + assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url); + assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url); + assert.equal(sites[0].hostname, sites[1].hostname); + }); + it("should prefer pinned sites over links", async () => { + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://developer.mozilla.org/en-US/docs/Web" }, + { url: "https://developer.mozilla.org/en-US/docs/Learn" }, + ]; + // These will be the frecent results. + links = [ + { frecency: FAKE_FRECENCY, url: "https://developer.mozilla.org/" }, + { frecency: FAKE_FRECENCY, url: "https://www.mozilla.org/" }, + ]; + + const sites = await feed.getLinksWithDefaults(); + + // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so + // the frecent with matching hostname as pinned is removed. + assert.lengthOf(sites, 3); + assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url); + assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url); + assert.equal(sites[2].url, links[1].url); + }); + it("should return sites that have a title", async () => { + // Simulate a pinned link with no title. + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://github.com/mozilla/activity-stream" }, + ]; + + const sites = await feed.getLinksWithDefaults(); + + for (const site of sites) { + assert.isDefined(site.hostname); + } + }); + it("should check against null entries", async () => { + fakeNewTabUtils.pinnedLinks.links = [null]; + + await feed.getLinksWithDefaults(); + }); + }); + it("should call _fetchIcon for each link", async () => { + sinon.spy(feed, "_fetchIcon"); + + const results = await feed.getLinksWithDefaults(); + + assert.callCount(feed._fetchIcon, results.length); + results.forEach(link => { + assert.calledWith(feed._fetchIcon, link); + }); + }); + it("should call _fetchScreenshot when customScreenshotURL is set", async () => { + links = []; + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://foo.com", customScreenshotURL: "custom" }, + ]; + sinon.stub(feed, "_fetchScreenshot"); + + await feed.getLinksWithDefaults(); + + assert.calledWith(feed._fetchScreenshot, sinon.match.object, "custom"); + }); + describe("discoverystream", () => { + 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" }], + }, + }, + }, + }); + it("should add a sponsored topsite from discoverystream to all the valid indices", async () => { + for (let i = 0; i < FAKE_LINKS.length; i++) { + feed.store.state.DiscoveryStream = makeStreamData(i); + const result = await feed.getLinksWithDefaults(); + const link = result[i]; + + assert.equal(link.type, "SPOC"); + assert.equal(link.title, "test spoc"); + assert.equal(link.sponsored_position, i + 1); + assert.equal(link.hostname, "test-spoc"); + assert.equal(link.url, "https://test-spoc.com"); + } + }); + }); + }); + describe("#init", () => { + it("should call refresh (broadcast:true)", async () => { + sandbox.stub(feed, "refresh"); + + await feed.init(); + + assert.calledOnce(feed.refresh); + assert.calledWithExactly(feed.refresh, { + broadcast: true, + isStartup: true, + }); + }); + it("should initialise the storage", async () => { + await feed.init(); + + assert.calledOnce(feed.store.dbStorage.getDbTable); + assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs"); + }); + it("should call onUpdate to set up Nimbus update listener", async () => { + await feed.init(); + + assert.calledOnce(fakeNimbusFeatures.newtab.onUpdate); + }); + }); + describe("#refresh", () => { + beforeEach(() => { + sandbox.stub(feed, "_fetchIcon"); + feed._startedUp = true; + }); + it("should wait for tippytop to initialize", async () => { + feed._tippyTopProvider.initialized = false; + sinon.stub(feed._tippyTopProvider, "init").resolves(); + + await feed.refresh(); + + assert.calledOnce(feed._tippyTopProvider.init); + }); + it("should not init the tippyTopProvider if already initialized", async () => { + feed._tippyTopProvider.initialized = true; + sinon.stub(feed._tippyTopProvider, "init").resolves(); + + await feed.refresh(); + + assert.notCalled(feed._tippyTopProvider.init); + }); + it("should broadcast TOP_SITES_UPDATED", async () => { + sinon.stub(feed, "getLinksWithDefaults").returns(Promise.resolve([])); + + await feed.refresh({ broadcast: true }); + + assert.calledOnce(feed.store.dispatch); + assert.calledWithExactly( + feed.store.dispatch, + ac.BroadcastToContent({ + type: at.TOP_SITES_UPDATED, + data: { links: [], pref: { collapsed: false } }, + }) + ); + }); + it("should dispatch an action with the links returned", async () => { + await feed.refresh({ broadcast: true }); + const reference = links.map(site => + Object.assign({}, site, { + hostname: shortURLStub(site), + typedBonus: true, + }) + ); + + assert.calledOnce(feed.store.dispatch); + assert.propertyVal( + feed.store.dispatch.firstCall.args[0], + "type", + at.TOP_SITES_UPDATED + ); + assert.deepEqual( + feed.store.dispatch.firstCall.args[0].data.links, + reference + ); + }); + it("should handle empty slots in the resulting top sites array", async () => { + links = [FAKE_LINKS[0]]; + fakeNewTabUtils.pinnedLinks.links = [ + null, + null, + FAKE_LINKS[1], + null, + null, + null, + null, + null, + FAKE_LINKS[2], + ]; + await feed.refresh({ broadcast: true }); + assert.calledOnce(feed.store.dispatch); + }); + it("should dispatch AlsoToPreloaded when broadcast is false", async () => { + sandbox.stub(feed, "getLinksWithDefaults").returns([]); + await feed.refresh({ broadcast: false }); + + assert.calledOnce(feed.store.dispatch); + assert.calledWithExactly( + feed.store.dispatch, + ac.AlsoToPreloaded({ + type: at.TOP_SITES_UPDATED, + data: { links: [], pref: { collapsed: false } }, + }) + ); + }); + it("should not init storage if it is already initialized", async () => { + feed._storage.initialized = true; + + await feed.refresh({ broadcast: false }); + + assert.notCalled(feed._storage.init); + }); + it("should catch indexedDB errors", async () => { + feed._storage.get.throws(new Error()); + globals.sandbox.spy(global.console, "error"); + + try { + await feed.refresh({ broadcast: false }); + } catch (e) { + assert.fails(); + } + + assert.calledOnce(console.error); + }); + }); + describe("#updateSectionPrefs", () => { + it("should call updateSectionPrefs on UPDATE_SECTION_PREFS", () => { + sandbox.stub(feed, "updateSectionPrefs"); + + feed.onAction({ + type: at.UPDATE_SECTION_PREFS, + data: { id: "topsites" }, + }); + + assert.calledOnce(feed.updateSectionPrefs); + }); + it("should dispatch TOP_SITES_PREFS_UPDATED", async () => { + await feed.updateSectionPrefs({ collapsed: true }); + + assert.calledOnce(feed.store.dispatch); + assert.calledWithExactly( + feed.store.dispatch, + ac.BroadcastToContent({ + type: at.TOP_SITES_PREFS_UPDATED, + data: { pref: { collapsed: true } }, + }) + ); + }); + }); + describe("#getScreenshotPreview", () => { + it("should dispatch preview if request is succesful", async () => { + await feed.getScreenshotPreview("custom", 1234); + + assert.calledOnce(feed.store.dispatch); + assert.calledWithExactly( + feed.store.dispatch, + ac.OnlyToOneContent( + { + data: { preview: FAKE_SCREENSHOT, url: "custom" }, + type: at.PREVIEW_RESPONSE, + }, + 1234 + ) + ); + }); + it("should return empty string if request fails", async () => { + fakeScreenshot.getScreenshotForURL = sandbox + .stub() + .returns(Promise.resolve(null)); + await feed.getScreenshotPreview("custom", 1234); + + assert.calledOnce(feed.store.dispatch); + assert.calledWithExactly( + feed.store.dispatch, + ac.OnlyToOneContent( + { + data: { preview: "", url: "custom" }, + type: at.PREVIEW_RESPONSE, + }, + 1234 + ) + ); + }); + }); + describe("#_fetchIcon", () => { + it("should reuse screenshot on the link", () => { + const link = { screenshot: "reuse.png" }; + + feed._fetchIcon(link); + + assert.notCalled(fakeScreenshot.getScreenshotForURL); + assert.propertyVal(link, "screenshot", "reuse.png"); + }); + it("should reuse existing fetching screenshot on the link", async () => { + const link = { + __sharedCache: { fetchingScreenshot: Promise.resolve("fetching.png") }, + }; + + await feed._fetchIcon(link); + + assert.notCalled(fakeScreenshot.getScreenshotForURL); + }); + it("should get a screenshot if the link is missing it", () => { + feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true; + feed._fetchIcon(Object.assign({ __sharedCache: {} }, FAKE_LINKS[0])); + + assert.calledOnce(fakeScreenshot.getScreenshotForURL); + assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_LINKS[0].url); + }); + it("should not get a screenshot if the link is missing it but top sites aren't shown", () => { + feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = false; + feed._fetchIcon(Object.assign({ __sharedCache: {} }, FAKE_LINKS[0])); + + assert.notCalled(fakeScreenshot.getScreenshotForURL); + }); + it("should update the link's cache with a screenshot", async () => { + feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true; + const updateLink = sandbox.stub(); + const link = { __sharedCache: { updateLink } }; + + await feed._fetchIcon(link); + + assert.calledOnce(updateLink); + assert.calledWith(updateLink, "screenshot", FAKE_SCREENSHOT); + }); + it("should skip getting a screenshot if there is a tippy top icon", () => { + feed._tippyTopProvider.processSite = site => { + site.tippyTopIcon = "icon.png"; + site.backgroundColor = "#fff"; + return site; + }; + const link = { url: "example.com" }; + feed._fetchIcon(link); + assert.propertyVal(link, "tippyTopIcon", "icon.png"); + assert.notProperty(link, "screenshot"); + assert.notCalled(fakeScreenshot.getScreenshotForURL); + }); + it("should skip getting a screenshot if there is an icon of size greater than 96x96 and no tippy top", () => { + const link = { + url: "foo.com", + favicon: "data:foo", + faviconSize: 196, + }; + feed._fetchIcon(link); + assert.notProperty(link, "tippyTopIcon"); + assert.notProperty(link, "screenshot"); + assert.notCalled(fakeScreenshot.getScreenshotForURL); + }); + it("should use the link's rich icon even if there's a tippy top", () => { + feed._tippyTopProvider.processSite = site => { + site.tippyTopIcon = "icon.png"; + site.backgroundColor = "#fff"; + return site; + }; + const link = { + url: "foo.com", + favicon: "data:foo", + faviconSize: 196, + }; + feed._fetchIcon(link); + assert.notProperty(link, "tippyTopIcon"); + }); + }); + describe("#_fetchScreenshot", () => { + it("should call maybeCacheScreenshot", async () => { + feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true; + const updateLink = sinon.stub(); + const link = { + customScreenshotURL: "custom", + __sharedCache: { updateLink }, + }; + await feed._fetchScreenshot(link, "custom"); + + assert.calledOnce(fakeScreenshot.maybeCacheScreenshot); + assert.calledWithExactly( + fakeScreenshot.maybeCacheScreenshot, + link, + link.customScreenshotURL, + "screenshot", + sinon.match.func + ); + }); + it("should not call maybeCacheScreenshot if screenshot is set", async () => { + const updateLink = sinon.stub(); + const link = { + customScreenshotURL: "custom", + __sharedCache: { updateLink }, + screenshot: true, + }; + await feed._fetchScreenshot(link, "custom"); + + assert.notCalled(fakeScreenshot.maybeCacheScreenshot); + }); + }); + describe("#onAction", () => { + it("should call getScreenshotPreview on PREVIEW_REQUEST", () => { + sandbox.stub(feed, "getScreenshotPreview"); + + feed.onAction({ + type: at.PREVIEW_REQUEST, + data: { url: "foo" }, + meta: { fromTarget: 1234 }, + }); + + assert.calledOnce(feed.getScreenshotPreview); + assert.calledWithExactly(feed.getScreenshotPreview, "foo", 1234); + }); + it("should refresh on SYSTEM_TICK", async () => { + sandbox.stub(feed, "refresh"); + + feed.onAction({ type: at.SYSTEM_TICK }); + + assert.calledOnce(feed.refresh); + assert.calledWithExactly(feed.refresh, { broadcast: false }); + }); + it("should call with correct parameters on TOP_SITES_PIN", () => { + const pinAction = { + type: at.TOP_SITES_PIN, + data: { site: { url: "foo.com" }, index: 7 }, + }; + feed.onAction(pinAction); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith( + fakeNewTabUtils.pinnedLinks.pin, + pinAction.data.site, + pinAction.data.index + ); + }); + it("should call pin on TOP_SITES_PIN", () => { + sinon.stub(feed, "pin"); + const pinExistingAction = { + type: at.TOP_SITES_PIN, + data: { site: FAKE_LINKS[4], index: 4 }, + }; + + feed.onAction(pinExistingAction); + + assert.calledOnce(feed.pin); + }); + it("should trigger refresh on TOP_SITES_PIN", async () => { + sinon.stub(feed, "refresh"); + const pinExistingAction = { + type: at.TOP_SITES_PIN, + data: { site: FAKE_LINKS[4], index: 4 }, + }; + + await feed.pin(pinExistingAction); + + assert.calledOnce(feed.refresh); + }); + it("should unblock a previously blocked top site if we are now adding it manually via 'Add a Top Site' option", async () => { + const pinAction = { + type: at.TOP_SITES_PIN, + data: { site: { url: "foo.com" }, index: -1 }, + }; + feed.onAction(pinAction); + assert.calledWith(fakeNewTabUtils.blockedLinks.unblock, { + url: pinAction.data.site.url, + }); + }); + it("should call insert on TOP_SITES_INSERT", async () => { + sinon.stub(feed, "insert"); + const addAction = { + type: at.TOP_SITES_INSERT, + data: { site: { url: "foo.com" } }, + }; + + feed.onAction(addAction); + + assert.calledOnce(feed.insert); + }); + it("should trigger refresh on TOP_SITES_INSERT", async () => { + sinon.stub(feed, "refresh"); + const addAction = { + type: at.TOP_SITES_INSERT, + data: { site: { url: "foo.com" } }, + }; + + await feed.insert(addAction); + + assert.calledOnce(feed.refresh); + }); + it("should call unpin with correct parameters on TOP_SITES_UNPIN", () => { + fakeNewTabUtils.pinnedLinks.links = [ + null, + null, + { url: "foo.com" }, + null, + null, + null, + null, + null, + FAKE_LINKS[0], + ]; + const unpinAction = { + type: at.TOP_SITES_UNPIN, + data: { site: { url: "foo.com" } }, + }; + feed.onAction(unpinAction); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin); + assert.calledWith( + fakeNewTabUtils.pinnedLinks.unpin, + unpinAction.data.site + ); + }); + it("should call refresh without a target if we clear history with PLACES_HISTORY_CLEARED", () => { + sandbox.stub(feed, "refresh"); + + feed.onAction({ type: at.PLACES_HISTORY_CLEARED }); + + assert.calledOnce(feed.refresh); + assert.calledWithExactly(feed.refresh, { broadcast: true }); + }); + it("should call refresh without a target if we remove a Topsite from history", () => { + sandbox.stub(feed, "refresh"); + + feed.onAction({ type: at.PLACES_LINKS_DELETED }); + + assert.calledOnce(feed.refresh); + assert.calledWithExactly(feed.refresh, { broadcast: true }); + }); + it("should still dispatch an action even if there's no target provided", async () => { + sandbox.stub(feed, "_fetchIcon"); + feed._startedUp = true; + await feed.refresh({ broadcast: true }); + assert.calledOnce(feed.store.dispatch); + assert.propertyVal( + feed.store.dispatch.firstCall.args[0], + "type", + at.TOP_SITES_UPDATED + ); + }); + it("should call init on INIT action", async () => { + sinon.stub(feed, "init"); + feed.onAction({ type: at.INIT }); + assert.calledOnce(feed.init); + }); + it("should call refresh on PLACES_LINK_BLOCKED action", async () => { + sinon.stub(feed, "refresh"); + await feed.onAction({ type: at.PLACES_LINK_BLOCKED }); + assert.calledOnce(feed.refresh); + assert.calledWithExactly(feed.refresh, { broadcast: true }); + }); + it("should call refresh on PLACES_LINKS_CHANGED action", async () => { + sinon.stub(feed, "refresh"); + await feed.onAction({ type: at.PLACES_LINKS_CHANGED }); + assert.calledOnce(feed.refresh); + assert.calledWithExactly(feed.refresh, { broadcast: false }); + }); + it("should call pin with correct args on TOP_SITES_INSERT without an index specified", () => { + const addAction = { + type: at.TOP_SITES_INSERT, + data: { site: { url: "foo.bar", label: "foo" } }, + }; + feed.onAction(addAction); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith( + fakeNewTabUtils.pinnedLinks.pin, + addAction.data.site, + 0 + ); + }); + it("should call pin with correct args on TOP_SITES_INSERT", () => { + const dropAction = { + type: at.TOP_SITES_INSERT, + data: { site: { url: "foo.bar", label: "foo" }, index: 3 }, + }; + feed.onAction(dropAction); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith( + fakeNewTabUtils.pinnedLinks.pin, + dropAction.data.site, + 3 + ); + }); + it("should remove the expiration filter on UNINIT", () => { + feed.onAction({ type: "UNINIT" }); + + assert.calledOnce(fakePageThumbs.removeExpirationFilter); + }); + it("should call updatePinnedSearchShortcuts on UPDATE_PINNED_SEARCH_SHORTCUTS action", async () => { + sinon.stub(feed, "updatePinnedSearchShortcuts"); + const addedShortcuts = [ + { + url: "https://google.com", + searchVendor: "google", + label: "google", + searchTopSite: true, + }, + ]; + await feed.onAction({ + type: at.UPDATE_PINNED_SEARCH_SHORTCUTS, + data: { addedShortcuts }, + }); + assert.calledOnce(feed.updatePinnedSearchShortcuts); + }); + it("should refresh from Contile on SHOW_SPONSORED_PREF if Contile is enabled", () => { + sandbox.spy(feed._contile, "refresh"); + const prefChangeAction = { + type: at.PREF_CHANGED, + data: { name: SHOW_SPONSORED_PREF }, + }; + fakeNimbusFeatures.newtab.getVariable.returns(true); + feed.onAction(prefChangeAction); + + assert.calledOnce(feed._contile.refresh); + }); + it("should not refresh from Contile on SHOW_SPONSORED_PREF if Contile is disabled", () => { + sandbox.spy(feed._contile, "refresh"); + const prefChangeAction = { + type: at.PREF_CHANGED, + data: { name: SHOW_SPONSORED_PREF }, + }; + fakeNimbusFeatures.newtab.getVariable.returns(false); + feed.onAction(prefChangeAction); + + assert.notCalled(feed._contile.refresh); + }); + it("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, 15 * 60 * 1000); + Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_PREF, Date.now()); + + sandbox.spy(feed._contile, "refresh"); + const prefChangeAction = { + type: at.PREF_CHANGED, + data: { name: SHOW_SPONSORED_PREF, value: false }, + }; + fakeNimbusFeatures.newtab.getVariable.returns(true); + feed.onAction(prefChangeAction); + + assert.calledOnce(feed._contile.refresh); + + // cached pref values should have reset + assert.isUndefined(Services.prefs.getStringPref(CONTILE_CACHE_PREF)); + assert.isUndefined( + Services.prefs.getIntPref(CONTILE_CACHE_LAST_FETCH_PREF) + ); + assert.isUndefined( + Services.prefs.getIntPref(CONTILE_CACHE_VALID_FOR_PREF) + ); + }); + }); + describe("#add", () => { + it("should pin site in first slot of empty pinned list", () => { + const site = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { site } }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0); + }); + it("should pin site in first slot of pinned list with empty first slot", () => { + fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }]; + const site = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { site } }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0); + }); + it("should move a pinned site in first slot to the next slot: part 1", () => { + const site1 = { url: "example.com" }; + fakeNewTabUtils.pinnedLinks.links = [site1]; + const site = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { site } }); + assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1); + }); + it("should move a pinned site in first slot to the next slot: part 2", () => { + const site1 = { url: "example.com" }; + const site2 = { url: "example.org" }; + fakeNewTabUtils.pinnedLinks.links = [site1, null, site2]; + const site = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { site } }); + assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1); + }); + it("should unpin the last site if all slots are already pinned", () => { + const site1 = { url: "example.com" }; + const site2 = { url: "example.org" }; + const site3 = { url: "example.net" }; + const site4 = { url: "example.biz" }; + const site5 = { url: "example.info" }; + const site6 = { url: "example.news" }; + const site7 = { url: "example.lol" }; + const site8 = { url: "example.golf" }; + fakeNewTabUtils.pinnedLinks.links = [ + site1, + site2, + site3, + site4, + site5, + site6, + site7, + site8, + ]; + feed.store.state.Prefs.values.topSitesRows = 1; + const site = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { site } }); + assert.equal(fakeNewTabUtils.pinnedLinks.pin.callCount, 8); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 2); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site3, 3); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site4, 4); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site5, 5); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site6, 6); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site7, 7); + }); + }); + describe("#pin", () => { + it("should pin site in specified slot empty pinned list", async () => { + const site = { + url: "foo.bar", + label: "foo", + customScreenshotURL: "screenshot", + }; + await feed.pin({ data: { index: 2, site } }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2); + }); + it("should lookup the link object to update the custom screenshot", async () => { + const site = { + url: "foo.bar", + label: "foo", + customScreenshotURL: "screenshot", + }; + sandbox.spy(feed.pinnedCache, "request"); + + await feed.pin({ data: { index: 2, site } }); + + assert.calledOnce(feed.pinnedCache.request); + }); + it("should lookup the link object to update the custom screenshot", async () => { + const site = { url: "foo.bar", label: "foo", customScreenshotURL: null }; + sandbox.spy(feed.pinnedCache, "request"); + + await feed.pin({ data: { index: 2, site } }); + + assert.calledOnce(feed.pinnedCache.request); + }); + it("should not do a link object lookup if custom screenshot field is not set", async () => { + const site = { url: "foo.bar", label: "foo" }; + sandbox.spy(feed.pinnedCache, "request"); + + await feed.pin({ data: { index: 2, site } }); + + assert.notCalled(feed.pinnedCache.request); + }); + it("should pin site in specified slot of pinned list that is free", () => { + fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }]; + const site = { url: "foo.bar", label: "foo" }; + feed.pin({ data: { index: 2, site } }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2); + }); + it("should save the searchTopSite attribute if set", () => { + fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }]; + const site = { url: "foo.bar", label: "foo", searchTopSite: true }; + feed.pin({ data: { index: 2, site } }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.propertyVal( + fakeNewTabUtils.pinnedLinks.pin.firstCall.args[0], + "searchTopSite", + true + ); + }); + it("should NOT move a pinned site in specified slot to the next slot", () => { + fakeNewTabUtils.pinnedLinks.links = [null, null, { url: "example.com" }]; + const site = { url: "foo.bar", label: "foo" }; + feed.pin({ data: { index: 2, site } }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2); + }); + it("should properly update LinksCache object properties between migrations", async () => { + fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }]; + + let pinnedLinks = await feed.pinnedCache.request(); + assert.equal(pinnedLinks.length, 1); + feed.pinnedCache.expire(); + pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo"); + + pinnedLinks = await feed.pinnedCache.request(); + assert.propertyVal(pinnedLinks[0], "screenshot", "foo"); + + // Force cache expiration in order to trigger a migration of objects + feed.pinnedCache.expire(); + pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar"); + + pinnedLinks = await feed.pinnedCache.request(); + assert.propertyVal(pinnedLinks[0], "screenshot", "bar"); + }); + it("should call insert if index < 0", () => { + const site = { url: "foo.bar", label: "foo" }; + const action = { data: { index: -1, site } }; + + sandbox.spy(feed, "insert"); + feed.pin(action); + + assert.calledOnce(feed.insert); + assert.calledWithExactly(feed.insert, action); + }); + it("should not call insert if index == 0", () => { + const site = { url: "foo.bar", label: "foo" }; + const action = { data: { index: 0, site } }; + + sandbox.spy(feed, "insert"); + feed.pin(action); + + assert.notCalled(feed.insert); + }); + }); + describe("clearLinkCustomScreenshot", () => { + it("should remove cached screenshot if custom url changes", async () => { + const stub = sandbox.stub(); + sandbox.stub(feed.pinnedCache, "request").returns( + Promise.resolve([ + { + url: "foo", + customScreenshotURL: "old_screenshot", + __sharedCache: { updateLink: stub }, + }, + ]) + ); + + await feed._clearLinkCustomScreenshot({ + url: "foo", + customScreenshotURL: "new_screenshot", + }); + + assert.calledOnce(stub); + assert.calledWithExactly(stub, "screenshot", undefined); + }); + it("should remove cached screenshot if custom url is removed", async () => { + const stub = sandbox.stub(); + sandbox.stub(feed.pinnedCache, "request").returns( + Promise.resolve([ + { + url: "foo", + customScreenshotURL: "old_screenshot", + __sharedCache: { updateLink: stub }, + }, + ]) + ); + + await feed._clearLinkCustomScreenshot({ + url: "foo", + customScreenshotURL: "new_screenshot", + }); + + assert.calledOnce(stub); + assert.calledWithExactly(stub, "screenshot", undefined); + }); + }); + describe("#drop", () => { + it("should correctly handle different index values", () => { + let index = -1; + const site = { url: "foo.bar", label: "foo" }; + const action = { data: { index, site } }; + + feed.insert(action); + + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0); + + index = undefined; + feed.insert(action); + + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0); + }); + it("should pin site in specified slot that is free", () => { + fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }]; + const site = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { index: 2, site, draggedFromIndex: 0 } }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2); + }); + it("should move a pinned site in specified slot to the next slot", () => { + fakeNewTabUtils.pinnedLinks.links = [null, null, { url: "example.com" }]; + const site = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { index: 2, site, draggedFromIndex: 3 } }); + assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2); + assert.calledWith( + fakeNewTabUtils.pinnedLinks.pin, + { url: "example.com" }, + 3 + ); + }); + it("should move pinned sites in the direction of the dragged site", () => { + const site1 = { url: "foo.bar", label: "foo" }; + const site2 = { url: "example.com", label: "example" }; + fakeNewTabUtils.pinnedLinks.links = [null, null, site2]; + feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 0 } }); + assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 1); + fakeNewTabUtils.pinnedLinks.pin.resetHistory(); + feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 5 } }); + assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2); + assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 3); + }); + it("should not insert past the visible top sites", () => { + const site1 = { url: "foo.bar", label: "foo" }; + feed.insert({ data: { index: 42, site: site1, draggedFromIndex: 0 } }); + assert.notCalled(fakeNewTabUtils.pinnedLinks.pin); + }); + }); + describe("integration", () => { + let resolvers = []; + beforeEach(() => { + feed.store.dispatch = sandbox.stub().callsFake(() => { + resolvers.shift()(); + }); + feed._startedUp = true; + sandbox.stub(feed, "_fetchScreenshot"); + }); + afterEach(() => { + sandbox.restore(); + }); + + const forDispatch = action => + new Promise(resolve => { + resolvers.push(resolve); + feed.onAction(action); + }); + + it("should add a pinned site and remove it", async () => { + feed._requestRichIcon = sinon.stub(); + const url = "https://pin.me"; + fakeNewTabUtils.pinnedLinks.pin = sandbox.stub().callsFake(link => { + fakeNewTabUtils.pinnedLinks.links.push(link); + }); + + await forDispatch({ type: at.TOP_SITES_INSERT, data: { site: { url } } }); + fakeNewTabUtils.pinnedLinks.links.pop(); + await forDispatch({ type: at.PLACES_LINK_BLOCKED }); + + assert.calledTwice(feed.store.dispatch); + assert.equal( + feed.store.dispatch.firstCall.args[0].data.links[0].url, + url + ); + assert.equal( + feed.store.dispatch.secondCall.args[0].data.links[0].url, + FAKE_LINKS[0].url + ); + }); + }); + + describe("improvesearch.noDefaultSearchTile experiment", () => { + const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile"; + beforeEach(() => { + global.Services.search.getDefault = async () => ({ + identifier: "google", + searchForm: "google.com", + }); + feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true; + }); + it("should filter out alexa top 5 search from the default sites", async () => { + const TOP_5_TEST = [ + "google.com", + "search.yahoo.com", + "yahoo.com", + "bing.com", + "ask.com", + "duckduckgo.com", + ]; + links = [{ url: "amazon.com" }, ...TOP_5_TEST.map(url => ({ url }))]; + const urlsReturned = (await feed.getLinksWithDefaults()).map( + link => link.url + ); + assert.include(urlsReturned, "amazon.com"); + TOP_5_TEST.forEach(url => assert.notInclude(urlsReturned, url)); + }); + it("should not filter out alexa, default search from the query results if the experiment pref is off", async () => { + links = [ + { url: "google.com" }, + { url: "foo.com" }, + { url: "duckduckgo" }, + ]; + feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false; + const urlsReturned = (await feed.getLinksWithDefaults()).map( + link => link.url + ); + assert.include(urlsReturned, "google.com"); + }); + it("should filter out the current default search from the default sites", async () => { + feed._currentSearchHostname = "amazon"; + feed.onAction({ + type: at.PREFS_INITIAL_VALUES, + data: { "default.sites": "google.com,amazon.com" }, + }); + links = [{ url: "foo.com" }]; + const urlsReturned = (await feed.getLinksWithDefaults()).map( + link => link.url + ); + assert.notInclude(urlsReturned, "amazon.com"); + }); + it("should not filter out current default search from pinned sites even if it matches the current default search", async () => { + links = [{ url: "foo.com" }]; + fakeNewTabUtils.pinnedLinks.links = [{ url: "google.com" }]; + const urlsReturned = (await feed.getLinksWithDefaults()).map( + link => link.url + ); + assert.include(urlsReturned, "google.com"); + }); + it("should call refresh and set ._currentSearchHostname to the new engine hostname when the the default search engine has been set", () => { + sinon.stub(feed, "refresh"); + sandbox + .stub(global.Services.search, "defaultEngine") + .value({ identifier: "ddg", searchForm: "duckduckgo.com" }); + feed.observe(null, "browser-search-engine-modified", "engine-default"); + assert.equal(feed._currentSearchHostname, "duckduckgo"); + assert.calledOnce(feed.refresh); + }); + it("should call refresh when the experiment pref has changed", () => { + sinon.stub(feed, "refresh"); + + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: true }, + }); + assert.calledOnce(feed.refresh); + + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: false }, + }); + assert.calledTwice(feed.refresh); + }); + }); + + describe("improvesearch.topSitesSearchShortcuts", () => { + beforeEach(() => { + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true; + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] = + "google,amazon"; + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = ""; + const searchEngines = [ + { aliases: ["@google"] }, + { aliases: ["@amazon"] }, + ]; + global.Services.search.getAppProvidedEngines = async () => searchEngines; + fakeNewTabUtils.pinnedLinks.pin = sinon + .stub() + .callsFake((site, index) => { + fakeNewTabUtils.pinnedLinks.links[index] = site; + }); + }); + + it("should properly disable search improvements if the pref is off", async () => { + sandbox.stub(global.Services.prefs, "clearUserPref"); + sandbox.spy(feed.pinnedCache, "expire"); + sandbox.spy(feed, "refresh"); + + // an actual implementation of unpin (until we can get a mochitest for search improvements) + fakeNewTabUtils.pinnedLinks.unpin = sinon.stub().callsFake(site => { + let index = -1; + for (let i = 0; i < fakeNewTabUtils.pinnedLinks.links.length; i++) { + let link = fakeNewTabUtils.pinnedLinks.links[i]; + if (link && link.url === site.url) { + index = i; + } + } + if (index > -1) { + fakeNewTabUtils.pinnedLinks.links[index] = null; + } + }); + + // ensure we've inserted search shorcuts + pin an additional site in space 4 + await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links); + fakeNewTabUtils.pinnedLinks.pin({ url: "https://dontunpinme.com" }, 3); + + // turn the experiment off + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: false }, + }); + + // check we cleared the pref, expired the pinned cache, and refreshed the feed + assert.calledWith( + global.Services.prefs.clearUserPref, + `browser.newtabpage.activity-stream.${SEARCH_SHORTCUTS_HAVE_PINNED_PREF}` + ); + assert.calledOnce(feed.pinnedCache.expire); + assert.calledWith(feed.refresh, { broadcast: true }); + + // check that the search shortcuts were removed from the list of pinned sites + const urlsReturned = fakeNewTabUtils.pinnedLinks.links + .filter(s => s) + .map(link => link.url); + assert.notInclude(urlsReturned, "https://amazon.com"); + assert.notInclude(urlsReturned, "https://google.com"); + assert.include(urlsReturned, "https://dontunpinme.com"); + + // check that the positions where the search shortcuts were null, and the additional pinned site is untouched in space 4 + assert.equal(fakeNewTabUtils.pinnedLinks.links[0], null); + assert.equal(fakeNewTabUtils.pinnedLinks.links[1], null); + assert.equal(fakeNewTabUtils.pinnedLinks.links[2], undefined); + assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], { + url: "https://dontunpinme.com", + }); + }); + + it("should updateCustomSearchShortcuts when experiment pref is turned on", async () => { + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false; + feed.updateCustomSearchShortcuts = sinon.spy(); + + // turn the experiment on + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true }, + }); + + assert.calledOnce(feed.updateCustomSearchShortcuts); + }); + + it("should filter out default top sites that match a hostname of a search shortcut if previously blocked", async () => { + feed.refreshDefaults("https://amazon.ca"); + fakeNewTabUtils.blockedLinks.links = [{ url: "https://amazon.com" }]; + fakeNewTabUtils.blockedLinks.isBlocked = site => + fakeNewTabUtils.blockedLinks.links[0].url === site.url; + const urlsReturned = (await feed.getLinksWithDefaults()).map( + link => link.url + ); + assert.notInclude(urlsReturned, "https://amazon.ca"); + }); + + it("should update frecent search topsite icon", async () => { + feed._tippyTopProvider.processSite = site => { + site.tippyTopIcon = "icon.png"; + site.backgroundColor = "#fff"; + return site; + }; + links = [{ url: "google.com" }]; + + const urlsReturned = await feed.getLinksWithDefaults(); + + const defaultSearchTopsite = urlsReturned.find( + s => s.url === "google.com" + ); + assert.propertyVal(defaultSearchTopsite, "searchTopSite", true); + assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png"); + assert.equal(defaultSearchTopsite.backgroundColor, "#fff"); + }); + it("should update default search topsite icon", async () => { + feed._tippyTopProvider.processSite = site => { + site.tippyTopIcon = "icon.png"; + site.backgroundColor = "#fff"; + return site; + }; + links = [{ url: "foo.com" }]; + feed.onAction({ + type: at.PREFS_INITIAL_VALUES, + data: { "default.sites": "google.com,amazon.com" }, + }); + + const urlsReturned = await feed.getLinksWithDefaults(); + + const defaultSearchTopsite = urlsReturned.find( + s => s.url === "amazon.com" + ); + assert.propertyVal(defaultSearchTopsite, "searchTopSite", true); + assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png"); + assert.equal(defaultSearchTopsite.backgroundColor, "#fff"); + }); + it("should dispatch UPDATE_SEARCH_SHORTCUTS on updateCustomSearchShortcuts", async () => { + feed.store.state.Prefs.values["improvesearch.noDefaultSearchTile"] = true; + await feed.updateCustomSearchShortcuts(); + assert.calledOnce(feed.store.dispatch); + assert.calledWith(feed.store.dispatch, { + data: { + searchShortcuts: [ + { + keyword: "@google", + shortURL: "google", + url: "https://google.com", + }, + { + keyword: "@amazon", + shortURL: "amazon", + url: "https://amazon.com", + }, + ], + }, + meta: { + from: "ActivityStream:Main", + to: "ActivityStream:Content", + isStartup: false, + }, + type: "UPDATE_SEARCH_SHORTCUTS", + }); + }); + + describe("_maybeInsertSearchShortcuts", () => { + beforeEach(() => { + // Default is one row + feed.store.state.Prefs.values.topSitesRows = TOP_SITES_DEFAULT_ROWS; + // Eight slots per row + fakeNewTabUtils.pinnedLinks.links = [ + { url: "" }, + { url: "" }, + { url: "" }, + null, + { url: "" }, + { url: "" }, + null, + { url: "" }, + ]; + }); + + it("should be called on getLinksWithDefaults", async () => { + sandbox.spy(feed, "_maybeInsertSearchShortcuts"); + await feed.getLinksWithDefaults(); + assert.calledOnce(feed._maybeInsertSearchShortcuts); + }); + + it("should do nothing and return false if the experiment is disabled", async () => { + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false; + assert.isFalse( + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ) + ); + assert.notCalled(fakeNewTabUtils.pinnedLinks.pin); + }); + + it("should pin shortcuts in the correct order, into the available unpinned slots", async () => { + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + // The shouldPin pref is "google,amazon" so expect the shortcuts in that order + assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], { + url: "https://google.com", + searchTopSite: true, + label: "@google", + }); + assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[6], { + url: "https://amazon.com", + searchTopSite: true, + label: "@amazon", + }); + }); + + it("should not pin shortcuts for the current default search engine", async () => { + feed._currentSearchHostname = "google"; + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], { + url: "https://amazon.com", + searchTopSite: true, + label: "@amazon", + }); + }); + + it("should only pin the first shortcut if there's only one available slot", async () => { + fakeNewTabUtils.pinnedLinks.links[3] = { url: "" }; + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + // The first item in the shouldPin pref is "google" so expect only Google to be pinned + assert.ok( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://google.com" + ) + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://amazon.com" + ) + ); + }); + + it("should pin none if there's no available slot", async () => { + fakeNewTabUtils.pinnedLinks.links[3] = { url: "" }; + fakeNewTabUtils.pinnedLinks.links[6] = { url: "" }; + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://google.com" + ) + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://amazon.com" + ) + ); + }); + + it("should not pin a shortcut if the corresponding search engine is not available", async () => { + // Make Amazon search engine unavailable + global.Services.search.getAppProvidedEngines = async () => [ + { aliases: ["@google"] }, + ]; + fakeNewTabUtils.pinnedLinks.links.fill(null); + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://amazon.com" + ) + ); + }); + + it("should not pin a search shortcut if it's been pinned before", async () => { + fakeNewTabUtils.pinnedLinks.links.fill(null); + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = + "google,amazon"; + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://google.com" + ) + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://amazon.com" + ) + ); + + fakeNewTabUtils.pinnedLinks.links.fill(null); + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = + "amazon"; + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + assert.ok( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://google.com" + ) + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://amazon.com" + ) + ); + + fakeNewTabUtils.pinnedLinks.links.fill(null); + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = + "google"; + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + assert.notOk( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://google.com" + ) + ); + assert.ok( + fakeNewTabUtils.pinnedLinks.links.find( + s => s && s.url === "https://amazon.com" + ) + ); + }); + + it("should record the insertion of a search shortcut", async () => { + feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = ""; + // Fill up one slot, so there's only one left - to be filled by Google + fakeNewTabUtils.pinnedLinks.links[3] = { url: "" }; + await feed._maybeInsertSearchShortcuts( + fakeNewTabUtils.pinnedLinks.links + ); + assert.calledWithExactly(feed.store.dispatch, { + data: { name: SEARCH_SHORTCUTS_HAVE_PINNED_PREF, value: "google" }, + meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, + type: "SET_PREF", + }); + }); + }); + }); + + describe("updatePinnedSearchShortcuts", () => { + it("should unpin a shortcut in deletedShortcuts", () => { + const deletedShortcuts = [ + { + url: "https://google.com", + searchVendor: "google", + label: "google", + searchTopSite: true, + }, + ]; + const addedShortcuts = []; + fakeNewTabUtils.pinnedLinks.links = [ + null, + null, + { + url: "https://amazon.com", + searchVendor: "amazon", + label: "amazon", + searchTopSite: true, + }, + ]; + feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }); + assert.notCalled(fakeNewTabUtils.pinnedLinks.pin); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin); + assert.calledWith(fakeNewTabUtils.pinnedLinks.unpin, { + url: "https://google.com", + }); + }); + + it("should pin a shortcut in addedShortcuts", () => { + const addedShortcuts = [ + { + url: "https://google.com", + searchVendor: "google", + label: "google", + searchTopSite: true, + }, + ]; + const deletedShortcuts = []; + fakeNewTabUtils.pinnedLinks.links = [ + null, + null, + { + url: "https://amazon.com", + searchVendor: "amazon", + label: "amazon", + searchTopSite: true, + }, + ]; + feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }); + assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin); + assert.calledWith( + fakeNewTabUtils.pinnedLinks.pin, + { + label: "google", + searchTopSite: true, + searchVendor: "google", + url: "https://google.com", + }, + 0 + ); + }); + + it("should pin and unpin in the same action", () => { + const addedShortcuts = [ + { + url: "https://google.com", + searchVendor: "google", + label: "google", + searchTopSite: true, + }, + { + url: "https://ebay.com", + searchVendor: "ebay", + label: "ebay", + searchTopSite: true, + }, + ]; + const deletedShortcuts = [ + { + url: "https://amazon.com", + searchVendor: "amazon", + label: "amazon", + searchTopSite: true, + }, + ]; + fakeNewTabUtils.pinnedLinks.links = [ + { url: "https://foo.com" }, + { + url: "https://amazon.com", + searchVendor: "amazon", + label: "amazon", + searchTopSite: true, + }, + ]; + feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }); + assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin); + assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin); + }); + + it("should pin a shortcut in addedShortcuts even if pinnedLinks is full", () => { + const addedShortcuts = [ + { + url: "https://google.com", + searchVendor: "google", + label: "google", + searchTopSite: true, + }, + ]; + const deletedShortcuts = []; + fakeNewTabUtils.pinnedLinks.links = FAKE_LINKS; + feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }); + assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin); + assert.calledWith( + fakeNewTabUtils.pinnedLinks.pin, + { label: "google", searchTopSite: true, url: "https://google.com" }, + 0 + ); + }); + }); + + describe("#_attachTippyTopIconForSearchShortcut", () => { + beforeEach(() => { + feed._tippyTopProvider.processSite = site => { + if (site.url === "https://www.yandex.ru/") { + site.tippyTopIcon = "yandex-ru.png"; + site.smallFavicon = "yandex-ru.ico"; + } else if ( + site.url === "https://www.yandex.com/" || + site.url === "https://yandex.com" + ) { + site.tippyTopIcon = "yandex.png"; + site.smallFavicon = "yandex.ico"; + } else { + site.tippyTopIcon = "google.png"; + site.smallFavicon = "google.ico"; + } + return site; + }; + }); + + it("should choose the -ru icons for Yandex search shortcut", async () => { + sandbox.stub(global.Services.search, "getEngineByAlias").resolves({ + wrappedJSObject: { _searchForm: "https://www.yandex.ru/" }, + }); + + const link = { url: "https://yandex.com" }; + await feed._attachTippyTopIconForSearchShortcut(link, "@yandex"); + + assert.equal(link.tippyTopIcon, "yandex-ru.png"); + assert.equal(link.smallFavicon, "yandex-ru.ico"); + assert.equal(link.url, "https://yandex.com"); + }); + + it("should choose -com icons for Yandex search shortcut", async () => { + sandbox.stub(global.Services.search, "getEngineByAlias").resolves({ + wrappedJSObject: { _searchForm: "https://www.yandex.com/" }, + }); + + const link = { url: "https://yandex.com" }; + await feed._attachTippyTopIconForSearchShortcut(link, "@yandex"); + + assert.equal(link.tippyTopIcon, "yandex.png"); + assert.equal(link.smallFavicon, "yandex.ico"); + assert.equal(link.url, "https://yandex.com"); + }); + + it("should use the -com icons if can't fetch the search form URL", async () => { + sandbox.stub(global.Services.search, "getEngineByAlias").resolves(null); + + const link = { url: "https://yandex.com" }; + await feed._attachTippyTopIconForSearchShortcut(link, "@yandex"); + + assert.equal(link.tippyTopIcon, "yandex.png"); + assert.equal(link.smallFavicon, "yandex.ico"); + assert.equal(link.url, "https://yandex.com"); + }); + + it("should choose the correct icon for other non-yandex search shortcut", async () => { + sandbox.stub(global.Services.search, "getEngineByAlias").resolves({ + wrappedJSObject: { _searchForm: "https://www.google.com/" }, + }); + + const link = { url: "https://google.com" }; + await feed._attachTippyTopIconForSearchShortcut(link, "@google"); + + assert.equal(link.tippyTopIcon, "google.png"); + assert.equal(link.smallFavicon, "google.ico"); + assert.equal(link.url, "https://google.com"); + }); + }); + + describe("#ContileIntegration", () => { + let getStringPrefStub; + let getIntPrefStub; + beforeEach(() => { + // Turn on sponsored TopSites for testing + feed.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true; + fetchStub = sandbox.stub(); + globals.set("fetch", fetchStub); + + getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref"); + getStringPrefStub + .withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF) + .returns(`["foo","bar"]`); + + getIntPrefStub = sandbox.stub(global.Services.prefs, "getIntPref"); + + fakeNimbusFeatures.newtab.getVariable.returns(true); + sandbox.spy(global.Services.prefs, "setStringPref"); + sandbox.spy(global.Services.prefs, "setIntPref"); + }); + afterEach(() => { + sandbox.restore(); + }); + + it("should fetch sites from Contile", async () => { + 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", + }, + ], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(fetched); + assert.equal(feed._contile.sites.length, 2); + }); + + it("should fetch SOV (Share-of-Voice) settings from Contile", async () => { + const 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", + }, + ], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(fetched); + assert.deepEqual(feed._contile.sov, sov); + assert.equal(feed._contile.sites.length, 2); + }); + + it("should not fetch from Contile if it's not enabled", async () => { + fakeNimbusFeatures.newtab.getVariable.reset(); + fakeNimbusFeatures.newtab.getVariable.returns(false); + const fetched = await feed._contile._fetchSites(); + + assert.notCalled(fetchStub); + assert.ok(!fetched); + assert.equal(feed._contile.sites.length, 0); + }); + + it("should still return two tiles when Contile provides more than 2 tiles and filtering results in more than 2 tiles", async () => { + fakeNimbusFeatures.newtab.getVariable.reset(); + fakeNimbusFeatures.newtab.getVariable.onCall(0).returns(true); + fakeNimbusFeatures.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", + }, + ], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(fetched); + // Both "foo" and "bar" should be filtered + assert.equal(feed._contile.sites.length, 2); + assert.equal(feed._contile.sites[0].url, "https://www.test.com"); + assert.equal(feed._contile.sites[1].url, "https://test1.com"); + }); + + it("should still return two tiles with replacement if the Nimbus variable was unset", async () => { + fakeNimbusFeatures.newtab.getVariable.reset(); + fakeNimbusFeatures.newtab.getVariable.onCall(0).returns(true); + fakeNimbusFeatures.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", + }, + ], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(fetched); + assert.equal(feed._contile.sites.length, 2); + assert.equal(feed._contile.sites[0].url, "https://www.test.com"); + assert.equal(feed._contile.sites[1].url, "https://test1.com"); + }); + + it("should filter the blocked sponsors", async () => { + 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", + }, + ], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(fetched); + // Both "foo" and "bar" should be filtered + assert.equal(feed._contile.sites.length, 1); + assert.equal(feed._contile.sites[0].url, "https://www.test.com"); + }); + + it("should return false when Contile returns with error status and no values are stored in cache prefs", async () => { + fetchStub.resolves({ + ok: false, + status: 500, + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(!fetched); + assert.ok(!feed._contile.sites.length); + }); + + it("should return false when Contile returns with error status and cached tiles are expried", async () => { + getIntPrefStub + .withArgs(CONTILE_CACHE_VALID_FOR_PREF) + .returns(1000 * 60 * 15); + getIntPrefStub + .withArgs(CONTILE_CACHE_LAST_FETCH_PREF) + .returns(Date.now() - 1000 * 60 * 30); + + fetchStub.resolves({ + ok: false, + status: 500, + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(!fetched); + assert.ok(!feed._contile.sites.length); + }); + + it("should handle invalid payload properly from Contile", async () => { + fetchStub.resolves({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + unknown: [], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(!fetched); + assert.ok(!feed._contile.sites.length); + }); + + it("should handle empty payload properly from Contile", async () => { + fetchStub.resolves({ + ok: true, + status: 200, + headers: new Map([ + ["cache-control", "private, max-age=859, stale-if-error=10463"], + ]), + json: () => + Promise.resolve({ + tiles: [], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(fetched); + assert.ok(!feed._contile.sites.length); + }); + + it("should handle no content properly from Contile", async () => { + fetchStub.resolves({ ok: true, status: 204 }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(!fetched); + assert.ok(!feed._contile.sites.length); + }); + + it("should set Caching Prefs after a sucessful request", async () => { + const 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, + }), + }); + + const fetched = await feed._contile._fetchSites(); + assert.ok(fetched); + assert.calledOnce(Services.prefs.setStringPref); + assert.calledTwice(Services.prefs.setIntPref); + + assert.calledWith( + Services.prefs.setStringPref, + CONTILE_CACHE_PREF, + JSON.stringify(tiles) + ); + assert.calledWith( + Services.prefs.setIntPref, + CONTILE_CACHE_VALID_FOR_PREF, + 11322 + ); + }); + + it("should return cached valid tiles when Contile returns error status", async () => { + const 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", + }, + ]; + + getStringPrefStub + .withArgs(CONTILE_CACHE_PREF) + .returns(JSON.stringify(tiles)); + + // valid for 15 mins + getIntPrefStub + .withArgs(CONTILE_CACHE_VALID_FOR_PREF) + .returns(1000 * 60 * 15); + getIntPrefStub + .withArgs(CONTILE_CACHE_LAST_FETCH_PREF) + .returns(Date.now()); + + fetchStub.resolves({ + status: 304, + }); + + const fetched = await feed._contile._fetchSites(); + assert.ok(fetched); + assert.equal(feed._contile.sites.length, 2); + assert.equal(feed._contile.sites[0].url, "https://www.test-cached.com"); + assert.equal(feed._contile.sites[1].url, "https://www.test1-cached.com"); + }); + + it("should not be successful when contile returns an error and no valid tiles are cached", async () => { + getStringPrefStub.withArgs(CONTILE_CACHE_PREF).returns("[]"); + + getIntPrefStub.withArgs(CONTILE_CACHE_VALID_FOR_PREF).returns(0); + getIntPrefStub.withArgs(CONTILE_CACHE_LAST_FETCH_PREF).returns(0); + + fetchStub.resolves({ + status: 500, + }); + + const fetched = await feed._contile._fetchSites(); + assert.ok(!fetched); + }); + + it("should return cached valid tiles filtering blocked tiles when Contile returns error status", async () => { + const 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", + }, + ]; + getStringPrefStub + .withArgs(CONTILE_CACHE_PREF) + .returns(JSON.stringify(tiles)); + + // valid for 15 mins + getIntPrefStub + .withArgs(CONTILE_CACHE_VALID_FOR_PREF) + .returns(1000 * 60 * 15); + getIntPrefStub + .withArgs(CONTILE_CACHE_LAST_FETCH_PREF) + .returns(Date.now()); + + fetchStub.resolves({ + status: 304, + }); + + const fetched = await feed._contile._fetchSites(); + assert.ok(fetched); + assert.equal(feed._contile.sites.length, 1); + assert.equal(feed._contile.sites[0].url, "https://www.test1-cached.com"); + }); + + it("should still return 3 tiles when nimbus variable overrides max num of sponsored contile tiles", async () => { + fakeNimbusFeatures.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", + }, + ], + }), + }); + + const fetched = await feed._contile._fetchSites(); + + assert.ok(fetched); + assert.equal(feed._contile.sites.length, 3); + assert.equal(feed._contile.sites[0].url, "https://www.test.com"); + assert.equal(feed._contile.sites[1].url, "https://test1.com"); + assert.equal(feed._contile.sites[2].url, "https://test2.com"); + }); + }); + + describe("#_mergeSponsoredLinks", () => { + let fakeSponsoredLinks; + let sov; + beforeEach(() => { + fakeSponsoredLinks = { + amp: [ + { + 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", + partner: "amp", + sponsored_position: 1, + }, + { + 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", + partner: "amp", + sponsored_position: 2, + }, + { + url: "https://www.test2.com", + image_url: "images/test2-com.png", + click_url: "https://www.test2-click.com", + impression_url: "https://www.test2-impression.com", + name: "test2", + partner: "amp", + sponsored_position: 2, + }, + ], + "moz-sales": [ + { + 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", + partner: "moz-sales", + pos: 2, + }, + ], + }; + + 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, + }, + ], + }, + ], + }; + }); + afterEach(() => { + sandbox.restore(); + }); + + it("should join sponsored links if the sov object is absent", async () => { + sandbox.stub(feed._contile, "sov").get(() => null); + + const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks); + + assert.deepEqual(sponsored, Object.values(fakeSponsoredLinks).flat()); + }); + + it("should join sponosred links if the SOV Nimbus variable is disabled", async () => { + fakeNimbusFeatures.pocketNewtab.getVariable.returns(false); + const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks); + + assert.deepEqual(sponsored, Object.values(fakeSponsoredLinks).flat()); + }); + + it("should pick sponsored links based on sov configurations", async () => { + sandbox.stub(feed._contile, "sov").get(() => sov); + fakeNimbusFeatures.pocketNewtab.getVariable.reset(); + fakeNimbusFeatures.pocketNewtab.getVariable.onCall(0).returns(true); + fakeNimbusFeatures.pocketNewtab.getVariable.onCall(1).returns(undefined); + global.Sampling.ratioSample.onCall(0).resolves(0); + global.Sampling.ratioSample.onCall(1).resolves(1); + + const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks); + + assert.equal(sponsored.length, 2); + assert.equal(sponsored[0].partner, "amp"); + assert.equal(sponsored[0].sponsored_position, 1); + assert.equal(sponsored[1].partner, "moz-sales"); + assert.equal(sponsored[1].sponsored_position, 2); + assert.equal(sponsored[1].pos, 1); + }); + + it("should add remaining contile tiles when nimbus var contile max num sponsored is present", async () => { + sandbox.stub(feed._contile, "sov").get(() => sov); + fakeNimbusFeatures.pocketNewtab.getVariable.reset(); + fakeNimbusFeatures.pocketNewtab.getVariable.onCall(0).returns(true); + fakeNimbusFeatures.pocketNewtab.getVariable.onCall(1).returns(3); + global.Sampling.ratioSample.resolves(0); + + const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks); + + assert.equal(sponsored.length, 3); + }); + + it("should fall back to other partners if the chosen partner does not have any links", async () => { + sandbox.stub(feed._contile, "sov").get(() => sov); + fakeNimbusFeatures.pocketNewtab.getVariable.returns(true); + global.Sampling.ratioSample.onCall(0).resolves(0); + global.Sampling.ratioSample.onCall(1).resolves(0); + + fakeSponsoredLinks.amp = []; + const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks); + + assert.equal(sponsored.length, 1); + assert.equal(sponsored[0].partner, "moz-sales"); + assert.equal(sponsored[0].sponsored_position, 1); + assert.equal(sponsored[0].pos, 0); + }); + + it("should return an empty array if none of the partners have links", async () => { + sandbox.stub(feed._contile, "sov").get(() => sov); + fakeNimbusFeatures.pocketNewtab.getVariable.returns(true); + global.Sampling.ratioSample.onCall(0).resolves(0); + global.Sampling.ratioSample.onCall(1).resolves(0); + + fakeSponsoredLinks.amp = []; + fakeSponsoredLinks["moz-sales"] = []; + const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks); + + assert.equal(sponsored.length, 0); + }); + }); + + describe("#_readDefaults", () => { + beforeEach(() => { + // Turn on sponsored TopSites for testing + feed.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true; + fetchStub = sandbox.stub(); + globals.set("fetch", fetchStub); + fetchStub.resolves({ ok: true, status: 204 }); + sandbox + .stub(global.Services.prefs, "getBoolPref") + .withArgs(REMOTE_SETTING_DEFAULTS_PREF) + .returns(true); + + sandbox + .stub(global.Services.prefs, "getStringPref") + .withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF) + .returns(`["foo","bar"]`); + sandbox.stub(global.Services.prefs, "prefIsLocked").returns(false); + }); + afterEach(() => { + sandbox.restore(); + }); + + it("should filter all blocked sponsored tiles from RemoteSettings when Contile is disabled", async () => { + sandbox.stub(feed, "_getRemoteConfig").resolves([ + { url: "https://foo.com", title: "foo", sponsored_position: 1 }, + { url: "https://bar.com", title: "bar", sponsored_position: 2 }, + { url: "https://test.com", title: "test", sponsored_position: 3 }, + ]); + fakeNimbusFeatures.newtab.getVariable.returns(false); + await feed._readDefaults(); + + assert.equal(DEFAULT_TOP_SITES.length, 1); + assert.equal(DEFAULT_TOP_SITES[0].label, "test"); + }); + + it("should also filter all blocked sponsored tiles from RemoteSettings when Contile is enabled", async () => { + sandbox.stub(feed, "_getRemoteConfig").resolves([ + { url: "https://foo.com", title: "foo", sponsored_position: 1 }, + { url: "https://bar.com", title: "bar", sponsored_position: 2 }, + { url: "https://test.com", title: "test", sponsored_position: 3 }, + ]); + fakeNimbusFeatures.newtab.getVariable.returns(true); + + await feed._readDefaults(); + + assert.equal(DEFAULT_TOP_SITES.length, 1); + assert.equal(DEFAULT_TOP_SITES[0].label, "test"); + }); + + it("should not filter non-sponsored tiles from RemoteSettings", async () => { + sandbox.stub(feed, "_getRemoteConfig").resolves([ + { url: "https://foo.com", title: "foo", sponsored_position: 1 }, + { url: "https://bar.com", title: "bar", sponsored_position: 2 }, + { url: "https://foo.com", title: "foo" }, + ]); + + await feed._readDefaults(); + + assert.equal(DEFAULT_TOP_SITES.length, 1); + assert.equal(DEFAULT_TOP_SITES[0].label, "foo"); + }); + + it("should take the image from Contile if it's a hi-res one", async () => { + fakeNimbusFeatures.newtab.getVariable.returns(true); + sandbox.stub(feed, "_getRemoteConfig").resolves([]); + + sandbox.stub(feed._contile, "sites").get(() => [ + { + url: "https://test.com", + image_url: "https://images.test.com/test-com.png", + image_size: 192, + click_url: "https://www.test-click.com", + impression_url: "https://www.test-impression.com", + name: "test", + }, + { + url: "https://test1.com", + image_url: "https://images.test1.com/test1-com.png", + image_size: 32, + click_url: "https://www.test1-click.com", + impression_url: "https://www.test1-impression.com", + name: "test1", + }, + ]); + + await feed._readDefaults(); + + const [site1, site2] = DEFAULT_TOP_SITES; + assert.propertyVal( + site1, + "favicon", + "https://images.test.com/test-com.png" + ); + assert.propertyVal(site1, "faviconSize", 192); + + // Should not be taken as it's not hi-res + assert.isUndefined(site2.favicon); + assert.isUndefined(site2.faviconSize); + }); + }); + + describe("#_nimbusChangeListener", () => { + it("should refresh on Nimbus feature updates reasons", () => { + sandbox.spy(feed._contile, "refresh"); + feed._nimbusChangeListener(null, "experiment-updated"); + + assert.calledOnce(feed._contile.refresh); + }); + + it("should not refresh on Nimbus feature loaded reasons", () => { + sandbox.spy(feed._contile, "refresh"); + feed._nimbusChangeListener(null, "feature-experiment-loaded"); + feed._nimbusChangeListener(null, "feature-rollout-loaded"); + + assert.notCalled(feed._contile.refresh); + }); + }); + + describe("#_maybeCapSponsoredLinks", () => { + let sponsoredLinks; + + beforeEach(() => { + sponsoredLinks = [ + { + url: "https://www.test.com", + name: "test", + sponsored_position: 1, + }, + { + url: "https://www.test1.com", + name: "test1", + sponsored_position: 2, + }, + { + url: "https://www.test2.com", + name: "test2", + sponsored_position: 3, + }, + ]; + }); + afterEach(() => { + sandbox.restore(); + }); + + it("should fall back to the default if the Nimbus variable is unspecified", () => { + feed._maybeCapSponsoredLinks(sponsoredLinks); + + assert.equal(sponsoredLinks.length, 2); + }); + it("should cap the links if specified by the Nimbus variable", () => { + fakeNimbusFeatures.pocketNewtab.getVariable.returns(1); + + feed._maybeCapSponsoredLinks(sponsoredLinks); + + assert.equal(sponsoredLinks.length, 1); + }); + it("should leave all the links if the Nimbus variable is equal to what we have", () => { + fakeNimbusFeatures.pocketNewtab.getVariable.returns(3); + + feed._maybeCapSponsoredLinks(sponsoredLinks); + + assert.equal(sponsoredLinks.length, 3); + }); + it("should ignore caps if they are more than what we have", () => { + fakeNimbusFeatures.pocketNewtab.getVariable.returns(10); + + feed._maybeCapSponsoredLinks(sponsoredLinks); + + assert.equal(sponsoredLinks.length, 3); + }); + }); +}); -- cgit v1.2.3