/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { actionTypes: at } = ChromeUtils.importESModule( "resource://activity-stream/common/Actions.mjs" ); ChromeUtils.defineESModuleGetters(this, { FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs", NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs", SectionsManager: "resource://activity-stream/lib/SectionsManager.sys.mjs", shortURL: "resource://activity-stream/lib/ShortURL.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", }); const { HighlightsFeed, SYNC_BOOKMARKS_FINISHED_EVENT, BOOKMARKS_RESTORE_SUCCESS_EVENT, BOOKMARKS_RESTORE_FAILED_EVENT, SECTION_ID, } = ChromeUtils.importESModule( "resource://activity-stream/lib/HighlightsFeed.sys.mjs" ); const FAKE_LINKS = new Array(20) .fill(null) .map((v, i) => ({ url: `http://www.site${i}.com` })); const FAKE_IMAGE = "data123"; const FAKE_URL = "https://mozilla.org"; const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg"; function getHighlightsFeedForTest(sandbox) { let feed = new HighlightsFeed(); feed.store = { dispatch: sandbox.spy(), getState() { return this.state; }, state: { Prefs: { values: { "section.highlights.includePocket": false, "section.highlights.includeDownloads": false, }, }, TopSites: { initialized: true, rows: Array(12) .fill(null) .map((v, i) => ({ url: `http://www.topsite${i}.com` })), }, Sections: [{ id: "highlights", initialized: false }], }, subscribe: sandbox.stub().callsFake(cb => { cb(); return () => {}; }), }; sandbox .stub(NewTabUtils.activityStreamLinks, "getHighlights") .resolves(FAKE_LINKS); sandbox .stub(NewTabUtils.activityStreamLinks, "deletePocketEntry") .resolves({}); sandbox .stub(NewTabUtils.activityStreamLinks, "archivePocketEntry") .resolves({}); sandbox .stub(NewTabUtils.activityStreamProvider, "_processHighlights") .callsFake(l => l.slice(0, 1)); return feed; } async function fetchHighlightsRows(feed, options) { let sandbox = sinon.createSandbox(); sandbox.stub(SectionsManager, "updateSection"); await feed.fetchHighlights(options); let [, { rows }] = SectionsManager.updateSection.firstCall.args; sandbox.restore(); return rows; } function fetchImage(feed, page) { return feed.fetchImage( Object.assign({ __sharedCache: { updateLink() {} } }, page) ); } add_task(function test_construction() { info("HighlightsFeed construction should work"); let sandbox = sinon.createSandbox(); sandbox.stub(PageThumbs, "addExpirationFilter"); let feed = getHighlightsFeedForTest(sandbox); Assert.ok(feed, "Was able to create a HighlightsFeed"); info("HighlightsFeed construction should add a PageThumbs expiration filter"); Assert.ok( PageThumbs.addExpirationFilter.calledOnce, "PageThumbs.addExpirationFilter was called once" ); sandbox.restore(); }); add_task(function test_init_action() { let sandbox = sinon.createSandbox(); let countObservers = topic => { return [...Services.obs.enumerateObservers(topic)].length; }; const INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT = countObservers( SYNC_BOOKMARKS_FINISHED_EVENT ); const INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT = countObservers( BOOKMARKS_RESTORE_SUCCESS_EVENT ); const INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT = countObservers( BOOKMARKS_RESTORE_FAILED_EVENT ); sandbox .stub(SectionsManager, "onceInitialized") .callsFake(callback => callback()); sandbox.stub(SectionsManager, "enableSection"); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(feed, "fetchHighlights"); sandbox.stub(feed.downloadsManager, "init"); feed.onAction({ type: at.INIT }); info("HighlightsFeed.onAction(INIT) should add a sync observer"); Assert.equal( countObservers(SYNC_BOOKMARKS_FINISHED_EVENT), INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT + 1 ); Assert.equal( countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT), INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT + 1 ); Assert.equal( countObservers(BOOKMARKS_RESTORE_FAILED_EVENT), INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT + 1 ); info( "HighlightsFeed.onAction(INIT) should call SectionsManager.onceInitialized" ); Assert.ok( SectionsManager.onceInitialized.calledOnce, "SectionsManager.onceInitialized was called" ); info("HighlightsFeed.onAction(INIT) should enable its section"); Assert.ok( SectionsManager.enableSection.calledOnce, "SectionsManager.enableSection was called" ); Assert.ok(SectionsManager.enableSection.calledWith(SECTION_ID)); info("HighlightsFeed.onAction(INIT) should fetch highlights"); Assert.ok( feed.fetchHighlights.calledOnce, "HighlightsFeed.fetchHighlights was called" ); info("HighlightsFeed.onAction(INIT) should initialize the DownloadsManager"); Assert.ok( feed.downloadsManager.init.calledOnce, "HighlightsFeed.downloadsManager.init was called" ); feed.uninit(); // Let's make sure that uninit also removed these observers while we're here. Assert.equal( countObservers(SYNC_BOOKMARKS_FINISHED_EVENT), INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT ); Assert.equal( countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT), INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT ); Assert.equal( countObservers(BOOKMARKS_RESTORE_FAILED_EVENT), INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT ); sandbox.restore(); }); add_task(async function test_observe_fetch_highlights() { let topicDataPairs = [ { description: "should fetch highlights when we are done a sync for bookmarks", shouldFetch: true, topic: SYNC_BOOKMARKS_FINISHED_EVENT, data: "bookmarks", }, { description: "should fetch highlights after a successful import", shouldFetch: true, topic: BOOKMARKS_RESTORE_SUCCESS_EVENT, data: "html", }, { description: "should fetch highlights after a failed import", shouldFetch: true, topic: BOOKMARKS_RESTORE_FAILED_EVENT, data: "json", }, { description: "should not fetch highlights when we are doing a sync for something that is not bookmarks", shouldFetch: false, topic: SYNC_BOOKMARKS_FINISHED_EVENT, data: "tabs", }, { description: "should not fetch highlights after a successful import", shouldFetch: false, topic: "someotherevent", data: "bookmarks", }, ]; for (let topicDataPair of topicDataPairs) { info(`HighlightsFeed.observe ${topicDataPair.description}`); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(feed, "fetchHighlights"); feed.observe(null, topicDataPair.topic, topicDataPair.data); if (topicDataPair.shouldFetch) { Assert.ok( feed.fetchHighlights.calledOnce, "HighlightsFeed.fetchHighlights was called" ); Assert.ok(feed.fetchHighlights.calledWith({ broadcast: true })); } else { Assert.ok( feed.fetchHighlights.notCalled, "HighlightsFeed.fetchHighlights was not called" ); } sandbox.restore(); } }); add_task(async function test_filterForThumbnailExpiration_calls() { info( "HighlightsFeed.filterForThumbnailExpiration should pass rows.urls " + "to the callback provided" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let rows = [{ url: "foo.com" }, { url: "bar.com" }]; feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }]; const stub = sinon.stub(); feed.filterForThumbnailExpiration(stub); Assert.ok(stub.calledOnce, "Filter was called"); Assert.ok(stub.calledWithExactly(rows.map(r => r.url))); sandbox.restore(); }); add_task( async function test_filterForThumbnailExpiration_include_preview_image_url() { info( "HighlightsFeed.filterForThumbnailExpiration should include " + "preview_image_url (if present) in the callback results" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let rows = [ { url: "foo.com" }, { url: "bar.com", preview_image_url: "bar.jpg" }, ]; feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }]; const stub = sinon.stub(); feed.filterForThumbnailExpiration(stub); Assert.ok(stub.calledOnce, "Filter was called"); Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "bar.jpg"])); sandbox.restore(); } ); add_task(async function test_filterForThumbnailExpiration_not_initialized() { info( "HighlightsFeed.filterForThumbnailExpiration should pass an empty " + "array if not initialized" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let rows = [{ url: "foo.com" }, { url: "bar.com" }]; feed.store.state.Sections = [{ rows, initialized: false }]; const stub = sinon.stub(); feed.filterForThumbnailExpiration(stub); Assert.ok(stub.calledOnce, "Filter was called"); Assert.ok(stub.calledWithExactly([])); sandbox.restore(); }); add_task(async function test_fetchHighlights_TopSites_not_initialized() { info( "HighlightsFeed.fetchHighlights should return early if TopSites are not " + "initialized" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.spy(feed.linksCache, "request"); feed.store.state.TopSites.initialized = false; feed.store.state.Prefs.values["feeds.topsites"] = true; feed.store.state.Prefs.values["feeds.system.topsites"] = true; // Initially TopSites is uninitialised and fetchHighlights should return. await feed.fetchHighlights(); Assert.ok( NewTabUtils.activityStreamLinks.getHighlights.notCalled, "NewTabUtils.activityStreamLinks.getHighlights was not called" ); Assert.ok( feed.linksCache.request.notCalled, "HighlightsFeed.linksCache.request was not called" ); sandbox.restore(); }); add_task(async function test_fetchHighlights_sections_not_initialized() { info( "HighlightsFeed.fetchHighlights should return early if Sections are not " + "initialized" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.spy(feed.linksCache, "request"); feed.store.state.TopSites.initialized = true; feed.store.state.Prefs.values["feeds.topsites"] = true; feed.store.state.Prefs.values["feeds.system.topsites"] = true; feed.store.state.Sections = []; await feed.fetchHighlights(); Assert.ok( NewTabUtils.activityStreamLinks.getHighlights.notCalled, "NewTabUtils.activityStreamLinks.getHighlights was not called" ); Assert.ok( feed.linksCache.request.notCalled, "HighlightsFeed.linksCache.request was not called" ); sandbox.restore(); }); add_task(async function test_fetchHighlights_TopSites_initialized() { info( "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites are " + "initialised" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.spy(feed.linksCache, "request"); // fetchHighlights should continue feed.store.state.TopSites.initialized = true; await feed.fetchHighlights(); Assert.ok( NewTabUtils.activityStreamLinks.getHighlights.calledOnce, "NewTabUtils.activityStreamLinks.getHighlights was called" ); Assert.ok( feed.linksCache.request.calledOnce, "HighlightsFeed.linksCache.request was called" ); sandbox.restore(); }); add_task(async function test_fetchHighlights_chronological_order() { info( "HighlightsFeed.fetchHighlights should chronologically order highlight " + "data types" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [ { url: "https://site0.com", type: "bookmark", bookmarkGuid: "1234", date_added: Date.now() - 80, }, // 3rd newest { url: "https://site1.com", type: "history", bookmarkGuid: "1234", date_added: Date.now() - 60, }, // append at the end { url: "https://site2.com", type: "history", date_added: Date.now() - 160, }, // append at the end { url: "https://site3.com", type: "history", date_added: Date.now() - 60, }, // append at the end { url: "https://site4.com", type: "pocket", date_added: Date.now() }, // newest highlight { url: "https://site5.com", type: "pocket", date_added: Date.now() - 100, }, // 4th newest { url: "https://site6.com", type: "bookmark", bookmarkGuid: "1234", date_added: Date.now() - 40, }, // 2nd newest ]; let expectedChronological = [4, 6, 0, 5]; let expectedHistory = [1, 2, 3]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); [...expectedChronological, ...expectedHistory].forEach((link, index) => { Assert.equal( highlights[index].url, links[link].url, `highlight[${index}] should be link[${link}]` ); }); sandbox.restore(); }); add_task(async function test_fetchHighlights_TopSites_not_enabled() { info( "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " + "are not enabled" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.spy(feed.linksCache, "request"); feed.store.state.Prefs.values["feeds.system.topsites"] = false; await feed.fetchHighlights(); Assert.ok( NewTabUtils.activityStreamLinks.getHighlights.calledOnce, "NewTabUtils.activityStreamLinks.getHighlights was called" ); Assert.ok( feed.linksCache.request.calledOnce, "HighlightsFeed.linksCache.request was called" ); sandbox.restore(); }); add_task(async function test_fetchHighlights_TopSites_not_shown() { info( "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " + "are not shown on NTP" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.spy(feed.linksCache, "request"); feed.store.state.Prefs.values["feeds.topsites"] = false; await feed.fetchHighlights(); Assert.ok( NewTabUtils.activityStreamLinks.getHighlights.calledOnce, "NewTabUtils.activityStreamLinks.getHighlights was called" ); Assert.ok( feed.linksCache.request.calledOnce, "HighlightsFeed.linksCache.request was called" ); sandbox.restore(); }); add_task(async function test_fetchHighlights_add_hostname_hasImage() { info( "HighlightsFeed.fetchHighlights should add shortURL hostname and hasImage to each link" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [{ url: "https://mozilla.org" }]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights[0].hostname, shortURL(links[0])); Assert.equal(highlights[0].hasImage, true); sandbox.restore(); }); add_task(async function test_fetchHighlights_add_existing_image() { info( "HighlightsFeed.fetchHighlights should add an existing image if it " + "exists to the link without calling fetchImage" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [{ url: "https://mozilla.org", image: FAKE_IMAGE }]; sandbox.spy(feed, "fetchImage"); NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights[0].image, FAKE_IMAGE); Assert.ok(feed.fetchImage.notCalled, "HighlightsFeed.fetchImage not called"); sandbox.restore(); }); add_task(async function test_fetchHighlights_correct_args() { info( "HighlightsFeed.fetchHighlights should call fetchImage with the correct " + "arguments for new links" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [ { url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog", }, ]; sandbox.spy(feed, "fetchImage"); NewTabUtils.activityStreamLinks.getHighlights.resolves(links); await fetchHighlightsRows(feed); Assert.ok(feed.fetchImage.calledOnce, "HighlightsFeed.fetchImage called"); let [arg] = feed.fetchImage.firstCall.args; Assert.equal(arg.url, links[0].url); Assert.equal(arg.preview_image_url, links[0].preview_image_url); sandbox.restore(); }); add_task( async function test_fetchHighlights_not_include_links_already_in_TopSites() { info( "HighlightsFeed.fetchHighlights should not include any links already in " + "Top Sites" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [ { url: "https://mozilla.org" }, { url: "http://www.topsite0.com" }, { url: "http://www.topsite1.com" }, { url: "http://www.topsite2.com" }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 1); Assert.equal(highlights[0].url, links[0].url); sandbox.restore(); } ); add_task( async function test_fetchHighlights_not_include_history_already_in_TopSites() { info( "HighlightsFeed.fetchHighlights should include bookmark but not " + "history already in Top Sites" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [ { url: "http://www.topsite0.com", type: "bookmark" }, { url: "http://www.topsite1.com", type: "history" }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 1); Assert.equal(highlights[0].url, links[0].url); sandbox.restore(); } ); add_task( async function test_fetchHighlights_not_include_history_same_hostname_as_bookmark() { info( "HighlightsFeed.fetchHighlights should not include history of same " + "hostname as a bookmark" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [ { url: "https://site.com/bookmark", type: "bookmark" }, { url: "https://site.com/history", type: "history" }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 1); Assert.equal(highlights[0].url, links[0].url); sandbox.restore(); } ); add_task(async function test_fetchHighlights_take_first_history_of_hostname() { info( "HighlightsFeed.fetchHighlights should take the first history of a hostname" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [ { url: "https://site.com/first", type: "history" }, { url: "https://site.com/second", type: "history" }, { url: "https://other", type: "history" }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 2); Assert.equal(highlights[0].url, links[0].url); Assert.equal(highlights[1].url, links[2].url); sandbox.restore(); }); add_task( async function test_fetchHighlights_take_bookmark_pocket_download_of_same_hostname() { info( "HighlightsFeed.fetchHighlights should take a bookmark, a pocket, and " + "downloaded item of the same hostname" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let links = [ { url: "https://site.com/bookmark", type: "bookmark" }, { url: "https://site.com/pocket", type: "pocket" }, { url: "https://site.com/download", type: "download" }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 3); Assert.equal(highlights[0].url, links[0].url); Assert.equal(highlights[1].url, links[1].url); Assert.equal(highlights[2].url, links[2].url); sandbox.restore(); } ); add_task(async function test_fetchHighlights_include_pocket_items() { info( "HighlightsFeed.fetchHighlights should includePocket pocket items when " + "pref is true" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values["section.highlights.includePocket"] = true; sandbox.spy(feed.linksCache, "request"); await fetchHighlightsRows(feed); Assert.ok( feed.linksCache.request.calledOnce, "HighlightsFeed.linksCache.request called" ); Assert.ok( !feed.linksCache.request.firstCall.args[0].excludePocket, "Should not be excluding Pocket items" ); sandbox.restore(); }); add_task(async function test_fetchHighlights_do_not_include_pocket_items() { info( "HighlightsFeed.fetchHighlights should not includePocket pocket items " + "when pref is false" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values["section.highlights.includePocket"] = false; sandbox.spy(feed.linksCache, "request"); await fetchHighlightsRows(feed); Assert.ok( feed.linksCache.request.calledOnce, "HighlightsFeed.linksCache.request called" ); Assert.ok( feed.linksCache.request.firstCall.args[0].excludePocket, "Should be excluding Pocket items" ); sandbox.restore(); }); add_task(async function test_fetchHighlights_do_not_include_downloads() { info( "HighlightsFeed.fetchHighlights should not include downloads when " + "includeDownloads pref is false" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values["section.highlights.includeDownloads"] = false; feed.downloadsManager.getDownloads = () => [ { url: "https://site1.com/download" }, { url: "https://site2.com/download" }, ]; let links = [ { url: "https://site.com/bookmark", type: "bookmark" }, { url: "https://site.com/pocket", type: "pocket" }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 2); Assert.equal(highlights[0].url, links[0].url); Assert.equal(highlights[1].url, links[1].url); sandbox.restore(); }); add_task(async function test_fetchHighlights_include_downloads() { info( "HighlightsFeed.fetchHighlights should include downloads when " + "includeDownloads pref is true" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true; feed.downloadsManager.getDownloads = () => [ { url: "https://site.com/download" }, ]; let links = [ { url: "https://site.com/bookmark", type: "bookmark" }, { url: "https://site.com/pocket", type: "pocket" }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 3); Assert.equal(highlights[0].url, links[0].url); Assert.equal(highlights[1].url, links[1].url); Assert.equal(highlights[2].url, "https://site.com/download"); Assert.equal(highlights[2].type, "download"); sandbox.restore(); }); add_task(async function test_fetchHighlights_take_one_download() { info("HighlightsFeed.fetchHighlights should only take 1 download"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true; feed.downloadsManager.getDownloads = () => [ { url: "https://site1.com/download" }, { url: "https://site2.com/download" }, ]; let links = [{ url: "https://site.com/bookmark", type: "bookmark" }]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 2); Assert.equal(highlights[0].url, links[0].url); Assert.equal(highlights[1].url, "https://site1.com/download"); sandbox.restore(); }); add_task(async function test_fetchHighlights_chronological_sort() { info( "HighlightsFeed.fetchHighlights should sort bookmarks, pocket, " + "and downloads chronologically" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true; feed.downloadsManager.getDownloads = () => [ { url: "https://site1.com/download", type: "download", date_added: Date.now(), }, ]; let links = [ { url: "https://site.com/bookmark", type: "bookmark", date_added: Date.now() - 10000, }, { url: "https://site2.com/pocket", type: "pocket", date_added: Date.now() - 5000, }, { url: "https://site3.com/visited", type: "history", date_added: Date.now(), }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights.length, 4); Assert.equal(highlights[0].url, "https://site1.com/download"); Assert.equal(highlights[1].url, links[1].url); Assert.equal(highlights[2].url, links[0].url); Assert.equal(highlights[3].url, links[2].url); // history item goes last sandbox.restore(); }); add_task( async function test_fetchHighlights_set_type_to_bookmark_on_bookmarkGuid() { info( "HighlightsFeed.fetchHighlights should set type to bookmark if there " + "is a bookmarkGuid" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values["section.highlights.includeBookmarks"] = true; feed.downloadsManager.getDownloads = () => [ { url: "https://site1.com/download", type: "download", date_added: Date.now(), }, ]; let links = [ { url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890", }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights[0].type, "bookmark"); sandbox.restore(); } ); add_task( async function test_fetchHighlights_keep_history_type_on_bookmarkGuid() { info( "HighlightsFeed.fetchHighlights should keep history type if there is a " + "bookmarkGuid but don't include bookmarks" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.store.state.Prefs.values[ "section.highlights.includeBookmarks" ] = false; let links = [ { url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890", }, ]; NewTabUtils.activityStreamLinks.getHighlights.resolves(links); let highlights = await fetchHighlightsRows(feed); Assert.equal(highlights[0].type, "history"); sandbox.restore(); } ); add_task(async function test_fetchHighlights_filter_adult() { info("HighlightsFeed.fetchHighlights should filter out adult pages"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(FilterAdult, "filter").returns([]); let highlights = await fetchHighlightsRows(feed); Assert.ok(FilterAdult.filter.calledOnce, "FilterAdult.filter called"); Assert.equal(highlights.length, 0); sandbox.restore(); }); add_task(async function test_fetchHighlights_no_expose_internal_link_props() { info( "HighlightsFeed.fetchHighlights should not expose internal link properties" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); let highlights = await fetchHighlightsRows(feed); let internal = Object.keys(highlights[0]).filter(key => key.startsWith("__")); Assert.equal(internal.join(""), ""); sandbox.restore(); }); add_task( async function test_fetchHighlights_broadcast_when_feed_not_initialized() { info( "HighlightsFeed.fetchHighlights should broadcast if feed is not initialized" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); NewTabUtils.activityStreamLinks.getHighlights.resolves([]); sandbox.stub(SectionsManager, "updateSection"); await feed.fetchHighlights(); Assert.ok( SectionsManager.updateSection.calledOnce, "SectionsManager.updateSection called once" ); Assert.ok( SectionsManager.updateSection.calledWithExactly( SECTION_ID, { rows: [] }, true, undefined ) ); sandbox.restore(); } ); add_task( async function test_fetchHighlights_broadcast_on_broadcast_in_options() { info( "HighlightsFeed.fetchHighlights should broadcast if options.broadcast is true" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); NewTabUtils.activityStreamLinks.getHighlights.resolves([]); feed.store.state.Sections[0].initialized = true; sandbox.stub(SectionsManager, "updateSection"); await feed.fetchHighlights({ broadcast: true }); Assert.ok( SectionsManager.updateSection.calledOnce, "SectionsManager.updateSection called once" ); Assert.ok( SectionsManager.updateSection.calledWithExactly( SECTION_ID, { rows: [] }, true, undefined ) ); sandbox.restore(); } ); add_task(async function test_fetchHighlights_no_broadcast() { info( "HighlightsFeed.fetchHighlights should not broadcast if " + "options.broadcast is false and initialized is true" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); NewTabUtils.activityStreamLinks.getHighlights.resolves([]); feed.store.state.Sections[0].initialized = true; sandbox.stub(SectionsManager, "updateSection"); await feed.fetchHighlights({ broadcast: false }); Assert.ok( SectionsManager.updateSection.calledOnce, "SectionsManager.updateSection called once" ); Assert.ok( SectionsManager.updateSection.calledWithExactly( SECTION_ID, { rows: [] }, false, undefined ) ); sandbox.restore(); }); add_task(async function test_fetchImage_capture_if_available() { info("HighlightsFeed.fetchImage should capture the image, if available"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(Screenshots, "getScreenshotForURL"); sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); await fetchImage(feed, { preview_image_url: FAKE_IMAGE_URL, url: FAKE_URL, }); Assert.ok( Screenshots.getScreenshotForURL.calledOnce, "Screenshots.getScreenshotForURL called once" ); Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_IMAGE_URL)); sandbox.restore(); }); add_task(async function test_fetchImage_fallback_to_screenshot() { info("HighlightsFeed.fetchImage should fall back to capturing a screenshot"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(Screenshots, "getScreenshotForURL"); sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); await fetchImage(feed, { url: FAKE_URL }); Assert.ok( Screenshots.getScreenshotForURL.calledOnce, "Screenshots.getScreenshotForURL called once" ); Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_URL)); sandbox.restore(); }); add_task(async function test_fetchImage_updateSectionCard_args() { info( "HighlightsFeed.fetchImage should call " + "SectionsManager.updateSectionCard with the right arguments" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(SectionsManager, "updateSectionCard"); sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE); sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); await fetchImage(feed, { preview_image_url: FAKE_IMAGE_URL, url: FAKE_URL, }); Assert.ok( SectionsManager.updateSectionCard.calledOnce, "SectionsManager.updateSectionCard called" ); Assert.ok( SectionsManager.updateSectionCard.calledWith( "highlights", FAKE_URL, { image: FAKE_IMAGE }, true ) ); sandbox.restore(); }); add_task(async function test_fetchImage_no_update_card_with_image() { info("HighlightsFeed.fetchImage should not update the card with the image"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(SectionsManager, "updateSectionCard"); sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE); sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); let card = { preview_image_url: FAKE_IMAGE_URL, url: FAKE_URL, }; await fetchImage(feed, card); Assert.ok(!card.image, "Image not set on card"); sandbox.restore(); }); add_task(async function test_uninit_disable_section() { info("HighlightsFeed.onAction(UNINIT) should disable its section"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.init(); sandbox.stub(SectionsManager, "disableSection"); feed.onAction({ type: at.UNINIT }); Assert.ok( SectionsManager.disableSection.calledOnce, "SectionsManager.disableSection called" ); Assert.ok(SectionsManager.disableSection.calledWith(SECTION_ID)); sandbox.restore(); }); add_task(async function test_uninit_remove_expiration_filter() { info("HighlightsFeed.onAction(UNINIT) should remove the expiration filter"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); feed.init(); sandbox.stub(PageThumbs, "removeExpirationFilter"); feed.onAction({ type: at.UNINIT }); Assert.ok( PageThumbs.removeExpirationFilter.calledOnce, "PageThumbs.removeExpirationFilter called" ); sandbox.restore(); }); add_task(async function test_onAction_relay_to_DownloadsManager_onAction() { info( "HighlightsFeed.onAction should relay all actions to " + "DownloadsManager.onAction" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.stub(feed.downloadsManager, "onAction"); let action = { type: at.COPY_DOWNLOAD_LINK, data: { url: "foo.png" }, _target: {}, }; feed.onAction(action); Assert.ok( feed.downloadsManager.onAction.calledOnce, "HighlightsFeed.downloadManager.onAction called" ); Assert.ok(feed.downloadsManager.onAction.calledWith(action)); sandbox.restore(); }); add_task(async function test_onAction_fetch_highlights_on_SYSTEM_TICK() { info("HighlightsFeed.onAction should fetch highlights on SYSTEM_TICK"); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); await feed.fetchHighlights(); sandbox.spy(feed, "fetchHighlights"); feed.onAction({ type: at.SYSTEM_TICK }); Assert.ok( feed.fetchHighlights.calledOnce, "HighlightsFeed.fetchHighlights called" ); Assert.ok( feed.fetchHighlights.calledWithExactly({ broadcast: false, isStartup: false, }) ); sandbox.restore(); }); add_task( async function test_onAction_fetch_highlights_on_PREF_CHANGED_for_include() { info( "HighlightsFeed.onAction should fetch highlights on PREF_CHANGED " + "for include prefs" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.spy(feed, "fetchHighlights"); feed.onAction({ type: at.PREF_CHANGED, data: { name: "section.highlights.includeBookmarks" }, }); Assert.ok( feed.fetchHighlights.calledOnce, "HighlightsFeed.fetchHighlights called" ); Assert.ok(feed.fetchHighlights.calledWithExactly({ broadcast: true })); sandbox.restore(); } ); add_task( async function test_onAction_no_fetch_highlights_on_PREF_CHANGED_for_other() { info( "HighlightsFeed.onAction should not fetch highlights on PREF_CHANGED " + "for other prefs" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); sandbox.spy(feed, "fetchHighlights"); feed.onAction({ type: at.PREF_CHANGED, data: { name: "section.topstories.pocketCta" }, }); Assert.ok( feed.fetchHighlights.notCalled, "HighlightsFeed.fetchHighlights not called" ); sandbox.restore(); } ); add_task(async function test_onAction_fetch_highlights_on_actions() { info("HighlightsFeed.onAction should fetch highlights for various actions"); let actions = [ { actionType: "PLACES_HISTORY_CLEARED", expectsExpire: false, expectsBroadcast: true, }, { actionType: "DOWNLOAD_CHANGED", expectsExpire: false, expectsBroadcast: true, }, { actionType: "PLACES_LINKS_CHANGED", expectsExpire: true, expectsBroadcast: false, }, { actionType: "PLACES_LINK_BLOCKED", expectsExpire: false, expectsBroadcast: true, }, { actionType: "PLACES_SAVED_TO_POCKET", expectsExpire: true, expectsBroadcast: false, }, ]; for (let action of actions) { info( `HighlightsFeed.onAction should fetch highlights on ${action.actionType}` ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); await feed.fetchHighlights(); sandbox.spy(feed, "fetchHighlights"); sandbox.stub(feed.linksCache, "expire"); feed.onAction({ type: at[action.actionType] }); Assert.ok( feed.fetchHighlights.calledOnce, "HighlightsFeed.fetchHighlights called" ); Assert.ok( feed.fetchHighlights.calledWith({ broadcast: action.expectsBroadcast }) ); if (action.expectsExpire) { Assert.ok( feed.linksCache.expire.calledOnce, "HighlightsFeed.linksCache.expire called" ); } sandbox.restore(); } }); add_task( async function test_onAction_fetch_highlights_no_broadcast_on_TOP_SITES_UPDATED() { info( "HighlightsFeed.onAction should fetch highlights with broadcast " + "false on TOP_SITES_UPDATED" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); await feed.fetchHighlights(); sandbox.spy(feed, "fetchHighlights"); feed.onAction({ type: at.TOP_SITES_UPDATED }); Assert.ok( feed.fetchHighlights.calledOnce, "HighlightsFeed.fetchHighlights called" ); Assert.ok( feed.fetchHighlights.calledWithExactly({ broadcast: false, isStartup: false, }) ); sandbox.restore(); } ); add_task( async function test_onAction_fetch_highlights_on_deleting_archiving_pocket() { info( "HighlightsFeed.onAction should call fetchHighlights when deleting " + "or archiving from Pocket" ); let sandbox = sinon.createSandbox(); let feed = getHighlightsFeedForTest(sandbox); await feed.fetchHighlights(); sandbox.spy(feed, "fetchHighlights"); feed.onAction({ type: at.POCKET_LINK_DELETED_OR_ARCHIVED, data: { pocket_id: 12345 }, }); Assert.ok( feed.fetchHighlights.calledOnce, "HighlightsFeed.fetchHighlights called" ); Assert.ok(feed.fetchHighlights.calledWithExactly({ broadcast: true })); sandbox.restore(); } );