From f6aeb7e743338307322914635ab8bb27999540c7 Mon Sep 17 00:00:00 2001 From: Mike Conley Date: Mon, 22 Jan 2024 14:22:35 +0000 Subject: [PATCH] Bug 1873969 - Convert HighlightsFeed.test.js to an xpcshell test. r=thecount Differential Revision: https://phabricator.services.mozilla.com/D198322 --- .../test/unit/lib/HighlightsFeed.test.js | 822 ---------- .../test/xpcshell/test_HighlightsFeed.js | 1414 +++++++++++++++++ .../newtab/test/xpcshell/xpcshell.toml | 2 + 3 files changed, 1416 insertions(+), 822 deletions(-) delete mode 100644 browser/components/newtab/test/unit/lib/HighlightsFeed.test.js create mode 100644 browser/components/newtab/test/xpcshell/test_HighlightsFeed.js diff --git a/browser/components/newtab/test/unit/lib/HighlightsFeed.test.js b/browser/components/newtab/test/unit/lib/HighlightsFeed.test.js deleted file mode 100644 index f0cd2450b794f..0000000000000 --- a/browser/components/newtab/test/unit/lib/HighlightsFeed.test.js +++ /dev/null @@ -1,822 +0,0 @@ -"use strict"; - -import { actionTypes as at } from "common/Actions.sys.mjs"; -import { Dedupe } from "common/Dedupe.sys.mjs"; -import { GlobalOverrider } from "test/unit/utils"; -import injector from "inject!lib/HighlightsFeed.jsm"; -import { Screenshots } from "lib/Screenshots.jsm"; -import { LinksCache } from "lib/LinksCache.sys.mjs"; - -const FAKE_LINKS = new Array(20) - .fill(null) - .map((v, i) => ({ url: `http://www.site${i}.com` })); -const FAKE_IMAGE = "data123"; - -describe("Highlights Feed", () => { - let HighlightsFeed; - let SECTION_ID; - let SYNC_BOOKMARKS_FINISHED_EVENT; - let BOOKMARKS_RESTORE_SUCCESS_EVENT; - let BOOKMARKS_RESTORE_FAILED_EVENT; - let feed; - let globals; - let sandbox; - let links; - let fakeScreenshot; - let fakeNewTabUtils; - let filterAdultStub; - let sectionsManagerStub; - let downloadsManagerStub; - let shortURLStub; - let fakePageThumbs; - - beforeEach(() => { - globals = new GlobalOverrider(); - sandbox = globals.sandbox; - fakeNewTabUtils = { - activityStreamLinks: { - getHighlights: sandbox.spy(() => Promise.resolve(links)), - deletePocketEntry: sandbox.spy(() => Promise.resolve({})), - archivePocketEntry: sandbox.spy(() => Promise.resolve({})), - }, - activityStreamProvider: { - _processHighlights: sandbox.spy(l => l.slice(0, 1)), - }, - }; - sectionsManagerStub = { - onceInitialized: sinon.stub().callsFake(callback => callback()), - enableSection: sinon.spy(), - disableSection: sinon.spy(), - updateSection: sinon.spy(), - updateSectionCard: sinon.spy(), - sections: new Map([["highlights", { id: "highlights" }]]), - }; - downloadsManagerStub = sinon.stub().returns({ - getDownloads: () => [{ url: "https://site.com/download" }], - onAction: sinon.spy(), - init: sinon.spy(), - }); - fakeScreenshot = { - getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE)), - maybeCacheScreenshot: Screenshots.maybeCacheScreenshot, - _shouldGetScreenshots: sinon.stub().returns(true), - }; - filterAdultStub = { - filter: sinon.stub().returnsArg(0), - }; - shortURLStub = sinon - .stub() - .callsFake(site => site.url.match(/\/([^/]+)/)[1]); - fakePageThumbs = { - addExpirationFilter: sinon.stub(), - removeExpirationFilter: sinon.stub(), - }; - - globals.set({ - NewTabUtils: fakeNewTabUtils, - PageThumbs: fakePageThumbs, - gFilterAdultEnabled: false, - LinksCache, - DownloadsManager: downloadsManagerStub, - FilterAdult: filterAdultStub, - Screenshots: fakeScreenshot, - }); - ({ - HighlightsFeed, - SECTION_ID, - SYNC_BOOKMARKS_FINISHED_EVENT, - BOOKMARKS_RESTORE_SUCCESS_EVENT, - BOOKMARKS_RESTORE_FAILED_EVENT, - } = injector({ - "lib/FilterAdult.jsm": { FilterAdult: filterAdultStub }, - "lib/ShortURL.jsm": { shortURL: shortURLStub }, - "lib/SectionsManager.jsm": { SectionsManager: sectionsManagerStub }, - "lib/Screenshots.jsm": { Screenshots: fakeScreenshot }, - "common/Dedupe.jsm": { Dedupe }, - "lib/DownloadsManager.jsm": { DownloadsManager: downloadsManagerStub }, - })); - sandbox.spy(global.Services.obs, "addObserver"); - sandbox.spy(global.Services.obs, "removeObserver"); - feed = new HighlightsFeed(); - feed.store = { - dispatch: sinon.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: sinon.stub().callsFake(cb => { - cb(); - return () => {}; - }), - }; - links = FAKE_LINKS; - }); - afterEach(() => { - globals.restore(); - }); - - describe("#init", () => { - it("should create a HighlightsFeed", () => { - assert.instanceOf(feed, HighlightsFeed); - }); - it("should register a expiration filter", () => { - assert.calledOnce(fakePageThumbs.addExpirationFilter); - }); - it("should add the sync observer", () => { - feed.onAction({ type: at.INIT }); - assert.calledWith( - global.Services.obs.addObserver, - feed, - SYNC_BOOKMARKS_FINISHED_EVENT - ); - assert.calledWith( - global.Services.obs.addObserver, - feed, - BOOKMARKS_RESTORE_SUCCESS_EVENT - ); - assert.calledWith( - global.Services.obs.addObserver, - feed, - BOOKMARKS_RESTORE_FAILED_EVENT - ); - }); - it("should call SectionsManager.onceInitialized on INIT", () => { - feed.onAction({ type: at.INIT }); - assert.calledOnce(sectionsManagerStub.onceInitialized); - }); - it("should enable its section", () => { - feed.onAction({ type: at.INIT }); - assert.calledOnce(sectionsManagerStub.enableSection); - assert.calledWith(sectionsManagerStub.enableSection, SECTION_ID); - }); - it("should fetch highlights on postInit", () => { - feed.fetchHighlights = sinon.spy(); - feed.postInit(); - assert.calledOnce(feed.fetchHighlights); - }); - it("should hook up the store for the DownloadsManager", () => { - feed.onAction({ type: at.INIT }); - assert.calledOnce(feed.downloadsManager.init); - }); - }); - describe("#observe", () => { - beforeEach(() => { - feed.fetchHighlights = sinon.spy(); - }); - it("should fetch higlights when we are done a sync for bookmarks", () => { - feed.observe(null, SYNC_BOOKMARKS_FINISHED_EVENT, "bookmarks"); - assert.calledWith(feed.fetchHighlights, { broadcast: true }); - }); - it("should fetch highlights after a successful import", () => { - feed.observe(null, BOOKMARKS_RESTORE_SUCCESS_EVENT, "html"); - assert.calledWith(feed.fetchHighlights, { broadcast: true }); - }); - it("should fetch highlights after a failed import", () => { - feed.observe(null, BOOKMARKS_RESTORE_FAILED_EVENT, "json"); - assert.calledWith(feed.fetchHighlights, { broadcast: true }); - }); - it("should not fetch higlights when we are doing a sync for something that is not bookmarks", () => { - feed.observe(null, SYNC_BOOKMARKS_FINISHED_EVENT, "tabs"); - assert.notCalled(feed.fetchHighlights); - }); - it("should not fetch higlights for other events", () => { - feed.observe(null, "someotherevent", "bookmarks"); - assert.notCalled(feed.fetchHighlights); - }); - }); - describe("#filterForThumbnailExpiration", () => { - it("should pass rows.urls to the callback provided", () => { - const rows = [{ url: "foo.com" }, { url: "bar.com" }]; - feed.store.state.Sections = [ - { id: "highlights", rows, initialized: true }, - ]; - const stub = sinon.stub(); - - feed.filterForThumbnailExpiration(stub); - - assert.calledOnce(stub); - assert.calledWithExactly( - stub, - rows.map(r => r.url) - ); - }); - it("should include preview_image_url (if present) in the callback results", () => { - const 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.calledOnce(stub); - assert.calledWithExactly(stub, ["foo.com", "bar.com", "bar.jpg"]); - }); - it("should pass an empty array if not initialized", () => { - const rows = [{ url: "foo.com" }, { url: "bar.com" }]; - feed.store.state.Sections = [{ rows, initialized: false }]; - const stub = sinon.stub(); - - feed.filterForThumbnailExpiration(stub); - - assert.calledOnce(stub); - assert.calledWithExactly(stub, []); - }); - }); - describe("#fetchHighlights", () => { - const fetchHighlights = async options => { - await feed.fetchHighlights(options); - return sectionsManagerStub.updateSection.firstCall.args[1].rows; - }; - it("should return early if TopSites are not initialised", async () => { - 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.notCalled(fakeNewTabUtils.activityStreamLinks.getHighlights); - assert.notCalled(feed.linksCache.request); - }); - it("should return early if Sections are not initialised", async () => { - 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.notCalled(fakeNewTabUtils.activityStreamLinks.getHighlights); - assert.notCalled(feed.linksCache.request); - }); - it("should fetch Highlights if TopSites are initialised", async () => { - sandbox.spy(feed.linksCache, "request"); - // fetchHighlights should continue - feed.store.state.TopSites.initialized = true; - - await feed.fetchHighlights(); - - assert.calledOnce(feed.linksCache.request); - assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights); - }); - it("should chronologically order highlight data types", async () => { - 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 - ]; - const expectedChronological = [4, 6, 0, 5]; - const expectedHistory = [1, 2, 3]; - - let highlights = await fetchHighlights(); - - [...expectedChronological, ...expectedHistory].forEach((link, index) => { - assert.propertyVal( - highlights[index], - "url", - links[link].url, - `highlight[${index}] should be link[${link}]` - ); - }); - }); - it("should fetch Highlights if TopSites are not enabled", async () => { - sandbox.spy(feed.linksCache, "request"); - feed.store.state.Prefs.values["feeds.system.topsites"] = false; - - await feed.fetchHighlights(); - - assert.calledOnce(feed.linksCache.request); - assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights); - }); - it("should fetch Highlights if TopSites are not shown on NTP", async () => { - sandbox.spy(feed.linksCache, "request"); - feed.store.state.Prefs.values["feeds.topsites"] = false; - - await feed.fetchHighlights(); - - assert.calledOnce(feed.linksCache.request); - assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights); - }); - it("should add hostname and hasImage to each link", async () => { - links = [{ url: "https://mozilla.org" }]; - - const highlights = await fetchHighlights(); - - assert.equal(highlights[0].hostname, "mozilla.org"); - assert.equal(highlights[0].hasImage, true); - }); - it("should add an existing image if it exists to the link without calling fetchImage", async () => { - links = [{ url: "https://mozilla.org", image: FAKE_IMAGE }]; - sinon.spy(feed, "fetchImage"); - - const highlights = await fetchHighlights(); - - assert.equal(highlights[0].image, FAKE_IMAGE); - assert.notCalled(feed.fetchImage); - }); - it("should call fetchImage with the correct arguments for new links", async () => { - links = [ - { - url: "https://mozilla.org", - preview_image_url: "https://mozilla.org/preview.jog", - }, - ]; - sinon.spy(feed, "fetchImage"); - - await feed.fetchHighlights(); - - assert.calledOnce(feed.fetchImage); - const [arg] = feed.fetchImage.firstCall.args; - assert.propertyVal(arg, "url", links[0].url); - assert.propertyVal(arg, "preview_image_url", links[0].preview_image_url); - }); - it("should not include any links already in Top Sites", async () => { - links = [ - { url: "https://mozilla.org" }, - { url: "http://www.topsite0.com" }, - { url: "http://www.topsite1.com" }, - { url: "http://www.topsite2.com" }, - ]; - - const highlights = await fetchHighlights(); - - assert.equal(highlights.length, 1); - assert.equal(highlights[0].url, links[0].url); - }); - it("should include bookmark but not history already in Top Sites", async () => { - links = [ - { url: "http://www.topsite0.com", type: "bookmark" }, - { url: "http://www.topsite1.com", type: "history" }, - ]; - - const highlights = await fetchHighlights(); - - assert.equal(highlights.length, 1); - assert.equal(highlights[0].url, links[0].url); - }); - it("should not include history of same hostname as a bookmark", async () => { - links = [ - { url: "https://site.com/bookmark", type: "bookmark" }, - { url: "https://site.com/history", type: "history" }, - ]; - - const highlights = await fetchHighlights(); - - assert.equal(highlights.length, 1); - assert.equal(highlights[0].url, links[0].url); - }); - it("should take the first history of a hostname", async () => { - links = [ - { url: "https://site.com/first", type: "history" }, - { url: "https://site.com/second", type: "history" }, - { url: "https://other", type: "history" }, - ]; - - const highlights = await fetchHighlights(); - - assert.equal(highlights.length, 2); - assert.equal(highlights[0].url, links[0].url); - assert.equal(highlights[1].url, links[2].url); - }); - it("should take a bookmark, a pocket, and downloaded item of the same hostname", async () => { - links = [ - { url: "https://site.com/bookmark", type: "bookmark" }, - { url: "https://site.com/pocket", type: "pocket" }, - { url: "https://site.com/download", type: "download" }, - ]; - - const highlights = await fetchHighlights(); - - 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); - }); - it("should includePocket pocket items when pref is true", async () => { - feed.store.state.Prefs.values["section.highlights.includePocket"] = true; - sandbox.spy(feed.linksCache, "request"); - await feed.fetchHighlights(); - - assert.propertyVal( - feed.linksCache.request.firstCall.args[0], - "excludePocket", - false - ); - }); - it("should not includePocket pocket items when pref is false", async () => { - sandbox.spy(feed.linksCache, "request"); - await feed.fetchHighlights(); - - assert.propertyVal( - feed.linksCache.request.firstCall.args[0], - "excludePocket", - true - ); - }); - it("should not include downloads when includeDownloads pref is false", async () => { - links = [ - { url: "https://site.com/bookmark", type: "bookmark" }, - { url: "https://site.com/pocket", type: "pocket" }, - ]; - - // Check that we don't have the downloaded item in highlights - const highlights = await fetchHighlights(); - assert.equal(highlights.length, 2); - assert.equal(highlights[0].url, links[0].url); - assert.equal(highlights[1].url, links[1].url); - }); - it("should include downloads when includeDownloads pref is true", async () => { - feed.store.state.Prefs.values[ - "section.highlights.includeDownloads" - ] = true; - links = [ - { url: "https://site.com/bookmark", type: "bookmark" }, - { url: "https://site.com/pocket", type: "pocket" }, - ]; - - // Check that we did get the downloaded item in highlights - const highlights = await fetchHighlights(); - 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.propertyVal(highlights[2], "type", "download"); - }); - it("should only take 1 download", async () => { - feed.store.state.Prefs.values[ - "section.highlights.includeDownloads" - ] = true; - feed.downloadsManager.getDownloads = () => [ - { url: "https://site1.com/download" }, - { url: "https://site2.com/download" }, - ]; - links = [{ url: "https://site.com/bookmark", type: "bookmark" }]; - - // Check that we did get the most single recent downloaded item in highlights - const highlights = await fetchHighlights(); - assert.equal(highlights.length, 2); - assert.equal(highlights[0].url, links[0].url); - assert.equal(highlights[1].url, "https://site1.com/download"); - }); - it("should sort bookmarks, pocket, and downloads chronologically", async () => { - feed.store.state.Prefs.values[ - "section.highlights.includeDownloads" - ] = true; - feed.downloadsManager.getDownloads = () => [ - { - url: "https://site1.com/download", - type: "download", - date_added: Date.now(), - }, - ]; - 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(), - }, - ]; - - // Check that the higlights are ordered chronologically by their 'date_added' - const highlights = await fetchHighlights(); - 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 - }); - it("should set type to bookmark if there is a bookmarkGuid", async () => { - feed.store.state.Prefs.values[ - "section.highlights.includeBookmarks" - ] = true; - links = [ - { - url: "https://mozilla.org", - type: "history", - bookmarkGuid: "1234567890", - }, - ]; - - const highlights = await fetchHighlights(); - - assert.equal(highlights[0].type, "bookmark"); - }); - it("should keep history type if there is a bookmarkGuid but don't include bookmarks", async () => { - feed.store.state.Prefs.values[ - "section.highlights.includeBookmarks" - ] = false; - links = [ - { - url: "https://mozilla.org", - type: "history", - bookmarkGuid: "1234567890", - }, - ]; - - const highlights = await fetchHighlights(); - - assert.propertyVal(highlights[0], "type", "history"); - }); - it("should filter out adult pages", async () => { - filterAdultStub.filter = sinon.stub().returns([]); - const highlights = await fetchHighlights(); - - // The stub filters out everything - assert.calledOnce(filterAdultStub.filter); - assert.equal(highlights.length, 0); - }); - it("should not expose internal link properties", async () => { - const highlights = await fetchHighlights(); - - const internal = Object.keys(highlights[0]).filter(key => - key.startsWith("__") - ); - assert.equal(internal.join(""), ""); - }); - it("should broadcast if feed is not initialized", async () => { - links = []; - await fetchHighlights(); - - assert.calledOnce(sectionsManagerStub.updateSection); - assert.calledWithExactly( - sectionsManagerStub.updateSection, - SECTION_ID, - { rows: [] }, - true, - undefined - ); - }); - it("should broadcast if options.broadcast is true", async () => { - links = []; - feed.store.state.Sections[0].initialized = true; - await fetchHighlights({ broadcast: true }); - - assert.calledOnce(sectionsManagerStub.updateSection); - assert.calledWithExactly( - sectionsManagerStub.updateSection, - SECTION_ID, - { rows: [] }, - true, - undefined - ); - }); - it("should not broadcast if options.broadcast is false and initialized is true", async () => { - links = []; - feed.store.state.Sections[0].initialized = true; - await fetchHighlights({ broadcast: false }); - - assert.calledOnce(sectionsManagerStub.updateSection); - assert.calledWithExactly( - sectionsManagerStub.updateSection, - SECTION_ID, - { rows: [] }, - false, - undefined - ); - }); - }); - describe("#fetchImage", () => { - const FAKE_URL = "https://mozilla.org"; - const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg"; - function fetchImage(page) { - return feed.fetchImage( - Object.assign({ __sharedCache: { updateLink() {} } }, page) - ); - } - it("should capture the image, if available", async () => { - await fetchImage({ - preview_image_url: FAKE_IMAGE_URL, - url: FAKE_URL, - }); - - assert.calledOnce(fakeScreenshot.getScreenshotForURL); - assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_IMAGE_URL); - }); - it("should fall back to capturing a screenshot", async () => { - await fetchImage({ url: FAKE_URL }); - - assert.calledOnce(fakeScreenshot.getScreenshotForURL); - assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_URL); - }); - it("should call SectionsManager.updateSectionCard with the right arguments", async () => { - await fetchImage({ - preview_image_url: FAKE_IMAGE_URL, - url: FAKE_URL, - }); - - assert.calledOnce(sectionsManagerStub.updateSectionCard); - assert.calledWith( - sectionsManagerStub.updateSectionCard, - "highlights", - FAKE_URL, - { image: FAKE_IMAGE }, - true - ); - }); - it("should not update the card with the image", async () => { - const card = { - preview_image_url: FAKE_IMAGE_URL, - url: FAKE_URL, - }; - - await fetchImage(card); - - assert.notProperty(card, "image"); - }); - }); - describe("#uninit", () => { - it("should disable its section", () => { - feed.onAction({ type: at.UNINIT }); - assert.calledOnce(sectionsManagerStub.disableSection); - assert.calledWith(sectionsManagerStub.disableSection, SECTION_ID); - }); - it("should remove the expiration filter", () => { - feed.onAction({ type: at.UNINIT }); - assert.calledOnce(fakePageThumbs.removeExpirationFilter); - }); - it("should remove the sync and Places observers", () => { - feed.onAction({ type: at.UNINIT }); - assert.calledWith( - global.Services.obs.removeObserver, - feed, - SYNC_BOOKMARKS_FINISHED_EVENT - ); - assert.calledWith( - global.Services.obs.removeObserver, - feed, - BOOKMARKS_RESTORE_SUCCESS_EVENT - ); - assert.calledWith( - global.Services.obs.removeObserver, - feed, - BOOKMARKS_RESTORE_FAILED_EVENT - ); - }); - }); - describe("#onAction", () => { - it("should relay all actions to DownloadsManager.onAction", () => { - let action = { - type: at.COPY_DOWNLOAD_LINK, - data: { url: "foo.png" }, - _target: {}, - }; - feed.onAction(action); - assert.calledWith(feed.downloadsManager.onAction, action); - }); - it("should fetch highlights on SYSTEM_TICK", async () => { - await feed.fetchHighlights(); - feed.fetchHighlights = sinon.spy(); - feed.onAction({ type: at.SYSTEM_TICK }); - - assert.calledOnce(feed.fetchHighlights); - assert.calledWithExactly(feed.fetchHighlights, { - broadcast: false, - isStartup: false, - }); - }); - it("should fetch highlights on PREF_CHANGED for include prefs", async () => { - feed.fetchHighlights = sinon.spy(); - - feed.onAction({ - type: at.PREF_CHANGED, - data: { name: "section.highlights.includeBookmarks" }, - }); - - assert.calledOnce(feed.fetchHighlights); - assert.calledWith(feed.fetchHighlights, { broadcast: true }); - }); - it("should not fetch highlights on PREF_CHANGED for other prefs", async () => { - feed.fetchHighlights = sinon.spy(); - - feed.onAction({ - type: at.PREF_CHANGED, - data: { name: "section.topstories.pocketCta" }, - }); - - assert.notCalled(feed.fetchHighlights); - }); - it("should fetch highlights on PLACES_HISTORY_CLEARED", async () => { - await feed.fetchHighlights(); - feed.fetchHighlights = sinon.spy(); - feed.onAction({ type: at.PLACES_HISTORY_CLEARED }); - assert.calledOnce(feed.fetchHighlights); - assert.calledWith(feed.fetchHighlights, { broadcast: true }); - }); - it("should fetch highlights on DOWNLOAD_CHANGED", async () => { - await feed.fetchHighlights(); - feed.fetchHighlights = sinon.spy(); - feed.onAction({ type: at.DOWNLOAD_CHANGED }); - assert.calledOnce(feed.fetchHighlights); - assert.calledWith(feed.fetchHighlights, { broadcast: true }); - }); - it("should fetch highlights on PLACES_LINKS_CHANGED", async () => { - await feed.fetchHighlights(); - feed.fetchHighlights = sinon.spy(); - sandbox.stub(feed.linksCache, "expire"); - - feed.onAction({ type: at.PLACES_LINKS_CHANGED }); - assert.calledOnce(feed.fetchHighlights); - assert.calledWith(feed.fetchHighlights, { broadcast: false }); - assert.calledOnce(feed.linksCache.expire); - }); - it("should fetch highlights on PLACES_LINK_BLOCKED", async () => { - await feed.fetchHighlights(); - feed.fetchHighlights = sinon.spy(); - feed.onAction({ type: at.PLACES_LINK_BLOCKED }); - assert.calledOnce(feed.fetchHighlights); - assert.calledWith(feed.fetchHighlights, { broadcast: true }); - }); - it("should fetch highlights and expire the cache on PLACES_SAVED_TO_POCKET", async () => { - await feed.fetchHighlights(); - feed.fetchHighlights = sinon.spy(); - sandbox.stub(feed.linksCache, "expire"); - - feed.onAction({ type: at.PLACES_SAVED_TO_POCKET }); - assert.calledOnce(feed.fetchHighlights); - assert.calledWith(feed.fetchHighlights, { broadcast: false }); - assert.calledOnce(feed.linksCache.expire); - }); - it("should call fetchHighlights with broadcast false on TOP_SITES_UPDATED", () => { - sandbox.stub(feed, "fetchHighlights"); - feed.onAction({ type: at.TOP_SITES_UPDATED }); - - assert.calledOnce(feed.fetchHighlights); - assert.calledWithExactly(feed.fetchHighlights, { - broadcast: false, - isStartup: false, - }); - }); - it("should call fetchHighlights when deleting or archiving from Pocket", async () => { - feed.fetchHighlights = sinon.spy(); - feed.onAction({ - type: at.POCKET_LINK_DELETED_OR_ARCHIVED, - data: { pocket_id: 12345 }, - }); - - assert.calledOnce(feed.fetchHighlights); - assert.calledWithExactly(feed.fetchHighlights, { broadcast: true }); - }); - }); -}); diff --git a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js new file mode 100644 index 0000000000000..63ad5106835d2 --- /dev/null +++ b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js @@ -0,0 +1,1414 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { actionTypes: at } = ChromeUtils.importESModule( + "resource://activity-stream/common/Actions.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const { FilterAdult } = ChromeUtils.import( + "resource://activity-stream/lib/FilterAdult.jsm" +); + +const { Screenshots } = ChromeUtils.import( + "resource://activity-stream/lib/Screenshots.jsm" +); + +const { SectionsManager } = ChromeUtils.import( + "resource://activity-stream/lib/SectionsManager.jsm" +); + +const { shortURL } = ChromeUtils.import( + "resource://activity-stream/lib/ShortURL.jsm" +); + +const { + HighlightsFeed, + SYNC_BOOKMARKS_FINISHED_EVENT, + BOOKMARKS_RESTORE_SUCCESS_EVENT, + BOOKMARKS_RESTORE_FAILED_EVENT, + SECTION_ID, +} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm"); + +const FAKE_LINKS = new Array(20) + .fill(null) + .map((v, i) => ({ url: `http://www.site${i}.com` })); +const FAKE_IMAGE = "data123"; +const FAKE_URL = "https://mozilla.org"; +const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg"; + +function getHighlightsFeedForTest(sandbox) { + let feed = new HighlightsFeed(); + feed.store = { + dispatch: sandbox.spy(), + getState() { + return this.state; + }, + state: { + Prefs: { + values: { + "section.highlights.includePocket": false, + "section.highlights.includeDownloads": false, + }, + }, + TopSites: { + initialized: true, + rows: Array(12) + .fill(null) + .map((v, i) => ({ url: `http://www.topsite${i}.com` })), + }, + Sections: [{ id: "highlights", initialized: false }], + }, + subscribe: sandbox.stub().callsFake(cb => { + cb(); + return () => {}; + }), + }; + + sandbox + .stub(NewTabUtils.activityStreamLinks, "getHighlights") + .resolves(FAKE_LINKS); + sandbox + .stub(NewTabUtils.activityStreamLinks, "deletePocketEntry") + .resolves({}); + sandbox + .stub(NewTabUtils.activityStreamLinks, "archivePocketEntry") + .resolves({}); + sandbox + .stub(NewTabUtils.activityStreamProvider, "_processHighlights") + .callsFake(l => l.slice(0, 1)); + + return feed; +} + +async function fetchHighlightsRows(feed, options) { + let sandbox = sinon.createSandbox(); + sandbox.stub(SectionsManager, "updateSection"); + await feed.fetchHighlights(options); + let [, { rows }] = SectionsManager.updateSection.firstCall.args; + + sandbox.restore(); + return rows; +} + +function fetchImage(feed, page) { + return feed.fetchImage( + Object.assign({ __sharedCache: { updateLink() {} } }, page) + ); +} + +add_task(function test_construction() { + info("HighlightsFeed construction should work"); + let sandbox = sinon.createSandbox(); + sandbox.stub(PageThumbs, "addExpirationFilter"); + + let feed = getHighlightsFeedForTest(sandbox); + Assert.ok(feed, "Was able to create a HighlightsFeed"); + + info("HighlightsFeed construction should add a PageThumbs expiration filter"); + Assert.ok( + PageThumbs.addExpirationFilter.calledOnce, + "PageThumbs.addExpirationFilter was called once" + ); + + sandbox.restore(); +}); + +add_task(function test_init_action() { + let sandbox = sinon.createSandbox(); + + let countObservers = topic => { + return [...Services.obs.enumerateObservers(topic)].length; + }; + + const INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT = countObservers( + SYNC_BOOKMARKS_FINISHED_EVENT + ); + const INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT = countObservers( + BOOKMARKS_RESTORE_SUCCESS_EVENT + ); + const INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT = countObservers( + BOOKMARKS_RESTORE_FAILED_EVENT + ); + + sandbox + .stub(SectionsManager, "onceInitialized") + .callsFake(callback => callback()); + sandbox.stub(SectionsManager, "enableSection"); + + let feed = getHighlightsFeedForTest(sandbox); + sandbox.stub(feed, "fetchHighlights"); + sandbox.stub(feed.downloadsManager, "init"); + + feed.onAction({ type: at.INIT }); + + info("HighlightsFeed.onAction(INIT) should add a sync observer"); + Assert.equal( + countObservers(SYNC_BOOKMARKS_FINISHED_EVENT), + INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT + 1 + ); + Assert.equal( + countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT), + INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT + 1 + ); + Assert.equal( + countObservers(BOOKMARKS_RESTORE_FAILED_EVENT), + INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT + 1 + ); + + info( + "HighlightsFeed.onAction(INIT) should call SectionsManager.onceInitialized" + ); + Assert.ok( + SectionsManager.onceInitialized.calledOnce, + "SectionsManager.onceInitialized was called" + ); + + info("HighlightsFeed.onAction(INIT) should enable its section"); + Assert.ok( + SectionsManager.enableSection.calledOnce, + "SectionsManager.enableSection was called" + ); + Assert.ok(SectionsManager.enableSection.calledWith(SECTION_ID)); + + info("HighlightsFeed.onAction(INIT) should fetch highlights"); + Assert.ok( + feed.fetchHighlights.calledOnce, + "HighlightsFeed.fetchHighlights was called" + ); + + info("HighlightsFeed.onAction(INIT) should initialize the DownloadsManager"); + Assert.ok( + feed.downloadsManager.init.calledOnce, + "HighlightsFeed.downloadsManager.init was called" + ); + + feed.uninit(); + // Let's make sure that uninit also removed these observers while we're here. + Assert.equal( + countObservers(SYNC_BOOKMARKS_FINISHED_EVENT), + INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT + ); + Assert.equal( + countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT), + INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT + ); + Assert.equal( + countObservers(BOOKMARKS_RESTORE_FAILED_EVENT), + INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT + ); + + sandbox.restore(); +}); + +add_task(async function test_observe_fetch_highlights() { + let topicDataPairs = [ + { + description: + "should fetch highlights when we are done a sync for bookmarks", + shouldFetch: true, + topic: SYNC_BOOKMARKS_FINISHED_EVENT, + data: "bookmarks", + }, + { + description: "should fetch highlights after a successful import", + shouldFetch: true, + topic: BOOKMARKS_RESTORE_SUCCESS_EVENT, + data: "html", + }, + { + description: "should fetch highlights after a failed import", + shouldFetch: true, + topic: BOOKMARKS_RESTORE_FAILED_EVENT, + data: "json", + }, + { + description: + "should not fetch highlights when we are doing a sync for something that is not bookmarks", + shouldFetch: false, + topic: SYNC_BOOKMARKS_FINISHED_EVENT, + data: "tabs", + }, + { + description: "should not fetch highlights after a successful import", + shouldFetch: false, + topic: "someotherevent", + data: "bookmarks", + }, + ]; + + for (let topicDataPair of topicDataPairs) { + info(`HighlightsFeed.observe ${topicDataPair.description}`); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + sandbox.stub(feed, "fetchHighlights"); + feed.observe(null, topicDataPair.topic, topicDataPair.data); + + if (topicDataPair.shouldFetch) { + Assert.ok( + feed.fetchHighlights.calledOnce, + "HighlightsFeed.fetchHighlights was called" + ); + Assert.ok(feed.fetchHighlights.calledWith({ broadcast: true })); + } else { + Assert.ok( + feed.fetchHighlights.notCalled, + "HighlightsFeed.fetchHighlights was not called" + ); + } + + sandbox.restore(); + } +}); + +add_task(async function test_filterForThumbnailExpiration_calls() { + info( + "HighlightsFeed.filterForThumbnailExpiration should pass rows.urls " + + "to the callback provided" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + let rows = [{ url: "foo.com" }, { url: "bar.com" }]; + + feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }]; + const stub = sinon.stub(); + + feed.filterForThumbnailExpiration(stub); + + Assert.ok(stub.calledOnce, "Filter was called"); + Assert.ok(stub.calledWithExactly(rows.map(r => r.url))); + + sandbox.restore(); +}); + +add_task( + async function test_filterForThumbnailExpiration_include_preview_image_url() { + info( + "HighlightsFeed.filterForThumbnailExpiration should include " + + "preview_image_url (if present) in the callback results" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + let rows = [ + { url: "foo.com" }, + { url: "bar.com", preview_image_url: "bar.jpg" }, + ]; + + feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }]; + const stub = sinon.stub(); + + feed.filterForThumbnailExpiration(stub); + + Assert.ok(stub.calledOnce, "Filter was called"); + Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "bar.jpg"])); + + sandbox.restore(); + } +); + +add_task(async function test_filterForThumbnailExpiration_not_initialized() { + info( + "HighlightsFeed.filterForThumbnailExpiration should pass an empty " + + "array if not initialized" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + let rows = [{ url: "foo.com" }, { url: "bar.com" }]; + + feed.store.state.Sections = [{ rows, initialized: false }]; + const stub = sinon.stub(); + + feed.filterForThumbnailExpiration(stub); + + Assert.ok(stub.calledOnce, "Filter was called"); + Assert.ok(stub.calledWithExactly([])); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_TopSites_not_initialized() { + info( + "HighlightsFeed.fetchHighlights should return early if TopSites are not " + + "initialized" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.spy(feed.linksCache, "request"); + + feed.store.state.TopSites.initialized = false; + feed.store.state.Prefs.values["feeds.topsites"] = true; + feed.store.state.Prefs.values["feeds.system.topsites"] = true; + + // Initially TopSites is uninitialised and fetchHighlights should return. + await feed.fetchHighlights(); + + Assert.ok( + NewTabUtils.activityStreamLinks.getHighlights.notCalled, + "NewTabUtils.activityStreamLinks.getHighlights was not called" + ); + Assert.ok( + feed.linksCache.request.notCalled, + "HighlightsFeed.linksCache.request was not called" + ); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_sections_not_initialized() { + info( + "HighlightsFeed.fetchHighlights should return early if Sections are not " + + "initialized" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.spy(feed.linksCache, "request"); + + feed.store.state.TopSites.initialized = true; + feed.store.state.Prefs.values["feeds.topsites"] = true; + feed.store.state.Prefs.values["feeds.system.topsites"] = true; + feed.store.state.Sections = []; + + await feed.fetchHighlights(); + + Assert.ok( + NewTabUtils.activityStreamLinks.getHighlights.notCalled, + "NewTabUtils.activityStreamLinks.getHighlights was not called" + ); + Assert.ok( + feed.linksCache.request.notCalled, + "HighlightsFeed.linksCache.request was not called" + ); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_TopSites_initialized() { + info( + "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites are " + + "initialised" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.spy(feed.linksCache, "request"); + + // fetchHighlights should continue + feed.store.state.TopSites.initialized = true; + + await feed.fetchHighlights(); + + Assert.ok( + NewTabUtils.activityStreamLinks.getHighlights.calledOnce, + "NewTabUtils.activityStreamLinks.getHighlights was called" + ); + Assert.ok( + feed.linksCache.request.calledOnce, + "HighlightsFeed.linksCache.request was called" + ); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_chronological_order() { + info( + "HighlightsFeed.fetchHighlights should chronologically order highlight " + + "data types" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [ + { + url: "https://site0.com", + type: "bookmark", + bookmarkGuid: "1234", + date_added: Date.now() - 80, + }, // 3rd newest + { + url: "https://site1.com", + type: "history", + bookmarkGuid: "1234", + date_added: Date.now() - 60, + }, // append at the end + { + url: "https://site2.com", + type: "history", + date_added: Date.now() - 160, + }, // append at the end + { + url: "https://site3.com", + type: "history", + date_added: Date.now() - 60, + }, // append at the end + { url: "https://site4.com", type: "pocket", date_added: Date.now() }, // newest highlight + { + url: "https://site5.com", + type: "pocket", + date_added: Date.now() - 100, + }, // 4th newest + { + url: "https://site6.com", + type: "bookmark", + bookmarkGuid: "1234", + date_added: Date.now() - 40, + }, // 2nd newest + ]; + let expectedChronological = [4, 6, 0, 5]; + let expectedHistory = [1, 2, 3]; + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + [...expectedChronological, ...expectedHistory].forEach((link, index) => { + Assert.equal( + highlights[index].url, + links[link].url, + `highlight[${index}] should be link[${link}]` + ); + }); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_TopSites_not_enabled() { + info( + "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " + + "are not enabled" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.spy(feed.linksCache, "request"); + + feed.store.state.Prefs.values["feeds.system.topsites"] = false; + + await feed.fetchHighlights(); + + Assert.ok( + NewTabUtils.activityStreamLinks.getHighlights.calledOnce, + "NewTabUtils.activityStreamLinks.getHighlights was called" + ); + Assert.ok( + feed.linksCache.request.calledOnce, + "HighlightsFeed.linksCache.request was called" + ); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_TopSites_not_shown() { + info( + "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " + + "are not shown on NTP" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.spy(feed.linksCache, "request"); + + feed.store.state.Prefs.values["feeds.topsites"] = false; + + await feed.fetchHighlights(); + + Assert.ok( + NewTabUtils.activityStreamLinks.getHighlights.calledOnce, + "NewTabUtils.activityStreamLinks.getHighlights was called" + ); + Assert.ok( + feed.linksCache.request.calledOnce, + "HighlightsFeed.linksCache.request was called" + ); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_add_hostname_hasImage() { + info( + "HighlightsFeed.fetchHighlights should add shortURL hostname and hasImage to each link" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [{ url: "https://mozilla.org" }]; + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights[0].hostname, shortURL(links[0])); + Assert.equal(highlights[0].hasImage, true); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_add_existing_image() { + info( + "HighlightsFeed.fetchHighlights should add an existing image if it " + + "exists to the link without calling fetchImage" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [{ url: "https://mozilla.org", image: FAKE_IMAGE }]; + sandbox.spy(feed, "fetchImage"); + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights[0].image, FAKE_IMAGE); + Assert.ok(feed.fetchImage.notCalled, "HighlightsFeed.fetchImage not called"); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_correct_args() { + info( + "HighlightsFeed.fetchHighlights should call fetchImage with the correct " + + "arguments for new links" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [ + { + url: "https://mozilla.org", + preview_image_url: "https://mozilla.org/preview.jog", + }, + ]; + sandbox.spy(feed, "fetchImage"); + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + await fetchHighlightsRows(feed); + + Assert.ok(feed.fetchImage.calledOnce, "HighlightsFeed.fetchImage called"); + + let [arg] = feed.fetchImage.firstCall.args; + Assert.equal(arg.url, links[0].url); + Assert.equal(arg.preview_image_url, links[0].preview_image_url); + + sandbox.restore(); +}); + +add_task( + async function test_fetchHighlights_not_include_links_already_in_TopSites() { + info( + "HighlightsFeed.fetchHighlights should not include any links already in " + + "Top Sites" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [ + { url: "https://mozilla.org" }, + { url: "http://www.topsite0.com" }, + { url: "http://www.topsite1.com" }, + { url: "http://www.topsite2.com" }, + ]; + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 1); + Assert.equal(highlights[0].url, links[0].url); + + sandbox.restore(); + } +); + +add_task( + async function test_fetchHighlights_not_include_history_already_in_TopSites() { + info( + "HighlightsFeed.fetchHighlights should include bookmark but not " + + "history already in Top Sites" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [ + { url: "http://www.topsite0.com", type: "bookmark" }, + { url: "http://www.topsite1.com", type: "history" }, + ]; + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 1); + Assert.equal(highlights[0].url, links[0].url); + + sandbox.restore(); + } +); + +add_task( + async function test_fetchHighlights_not_include_history_same_hostname_as_bookmark() { + info( + "HighlightsFeed.fetchHighlights should not include history of same " + + "hostname as a bookmark" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [ + { url: "https://site.com/bookmark", type: "bookmark" }, + { url: "https://site.com/history", type: "history" }, + ]; + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 1); + Assert.equal(highlights[0].url, links[0].url); + + sandbox.restore(); + } +); + +add_task(async function test_fetchHighlights_take_first_history_of_hostname() { + info( + "HighlightsFeed.fetchHighlights should take the first history of a hostname" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [ + { url: "https://site.com/first", type: "history" }, + { url: "https://site.com/second", type: "history" }, + { url: "https://other", type: "history" }, + ]; + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 2); + Assert.equal(highlights[0].url, links[0].url); + Assert.equal(highlights[1].url, links[2].url); + + sandbox.restore(); +}); + +add_task( + async function test_fetchHighlights_take_bookmark_pocket_download_of_same_hostname() { + info( + "HighlightsFeed.fetchHighlights should take a bookmark, a pocket, and " + + "downloaded item of the same hostname" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let links = [ + { url: "https://site.com/bookmark", type: "bookmark" }, + { url: "https://site.com/pocket", type: "pocket" }, + { url: "https://site.com/download", type: "download" }, + ]; + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 3); + Assert.equal(highlights[0].url, links[0].url); + Assert.equal(highlights[1].url, links[1].url); + Assert.equal(highlights[2].url, links[2].url); + + sandbox.restore(); + } +); + +add_task(async function test_fetchHighlights_include_pocket_items() { + info( + "HighlightsFeed.fetchHighlights should includePocket pocket items when " + + "pref is true" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values["section.highlights.includePocket"] = true; + sandbox.spy(feed.linksCache, "request"); + + await fetchHighlightsRows(feed); + + Assert.ok( + feed.linksCache.request.calledOnce, + "HighlightsFeed.linksCache.request called" + ); + Assert.ok( + !feed.linksCache.request.firstCall.args[0].excludePocket, + "Should not be excluding Pocket items" + ); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_do_not_include_pocket_items() { + info( + "HighlightsFeed.fetchHighlights should not includePocket pocket items " + + "when pref is false" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values["section.highlights.includePocket"] = false; + sandbox.spy(feed.linksCache, "request"); + + await fetchHighlightsRows(feed); + + Assert.ok( + feed.linksCache.request.calledOnce, + "HighlightsFeed.linksCache.request called" + ); + Assert.ok( + feed.linksCache.request.firstCall.args[0].excludePocket, + "Should be excluding Pocket items" + ); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_do_not_include_downloads() { + info( + "HighlightsFeed.fetchHighlights should not include downloads when " + + "includeDownloads pref is false" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values["section.highlights.includeDownloads"] = false; + feed.downloadsManager.getDownloads = () => [ + { url: "https://site1.com/download" }, + { url: "https://site2.com/download" }, + ]; + + let links = [ + { url: "https://site.com/bookmark", type: "bookmark" }, + { url: "https://site.com/pocket", type: "pocket" }, + ]; + + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 2); + Assert.equal(highlights[0].url, links[0].url); + Assert.equal(highlights[1].url, links[1].url); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_include_downloads() { + info( + "HighlightsFeed.fetchHighlights should include downloads when " + + "includeDownloads pref is true" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true; + feed.downloadsManager.getDownloads = () => [ + { url: "https://site.com/download" }, + ]; + + let links = [ + { url: "https://site.com/bookmark", type: "bookmark" }, + { url: "https://site.com/pocket", type: "pocket" }, + ]; + + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 3); + Assert.equal(highlights[0].url, links[0].url); + Assert.equal(highlights[1].url, links[1].url); + Assert.equal(highlights[2].url, "https://site.com/download"); + Assert.equal(highlights[2].type, "download"); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_take_one_download() { + info("HighlightsFeed.fetchHighlights should only take 1 download"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true; + feed.downloadsManager.getDownloads = () => [ + { url: "https://site1.com/download" }, + { url: "https://site2.com/download" }, + ]; + + let links = [{ url: "https://site.com/bookmark", type: "bookmark" }]; + + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 2); + Assert.equal(highlights[0].url, links[0].url); + Assert.equal(highlights[1].url, "https://site1.com/download"); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_chronological_sort() { + info( + "HighlightsFeed.fetchHighlights should sort bookmarks, pocket, " + + "and downloads chronologically" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true; + feed.downloadsManager.getDownloads = () => [ + { + url: "https://site1.com/download", + type: "download", + date_added: Date.now(), + }, + ]; + + let links = [ + { + url: "https://site.com/bookmark", + type: "bookmark", + date_added: Date.now() - 10000, + }, + { + url: "https://site2.com/pocket", + type: "pocket", + date_added: Date.now() - 5000, + }, + { + url: "https://site3.com/visited", + type: "history", + date_added: Date.now(), + }, + ]; + + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights.length, 4); + Assert.equal(highlights[0].url, "https://site1.com/download"); + Assert.equal(highlights[1].url, links[1].url); + Assert.equal(highlights[2].url, links[0].url); + Assert.equal(highlights[3].url, links[2].url); // history item goes last + + sandbox.restore(); +}); + +add_task( + async function test_fetchHighlights_set_type_to_bookmark_on_bookmarkGuid() { + info( + "HighlightsFeed.fetchHighlights should set type to bookmark if there " + + "is a bookmarkGuid" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values["section.highlights.includeBookmarks"] = true; + feed.downloadsManager.getDownloads = () => [ + { + url: "https://site1.com/download", + type: "download", + date_added: Date.now(), + }, + ]; + + let links = [ + { + url: "https://mozilla.org", + type: "history", + bookmarkGuid: "1234567890", + }, + ]; + + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights[0].type, "bookmark"); + + sandbox.restore(); + } +); + +add_task( + async function test_fetchHighlights_keep_history_type_on_bookmarkGuid() { + info( + "HighlightsFeed.fetchHighlights should keep history type if there is a " + + "bookmarkGuid but don't include bookmarks" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.store.state.Prefs.values[ + "section.highlights.includeBookmarks" + ] = false; + + let links = [ + { + url: "https://mozilla.org", + type: "history", + bookmarkGuid: "1234567890", + }, + ]; + + NewTabUtils.activityStreamLinks.getHighlights.resolves(links); + + let highlights = await fetchHighlightsRows(feed); + + Assert.equal(highlights[0].type, "history"); + + sandbox.restore(); + } +); + +add_task(async function test_fetchHighlights_filter_adult() { + info("HighlightsFeed.fetchHighlights should filter out adult pages"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.stub(FilterAdult, "filter").returns([]); + let highlights = await fetchHighlightsRows(feed); + + Assert.ok(FilterAdult.filter.calledOnce, "FilterAdult.filter called"); + Assert.equal(highlights.length, 0); + + sandbox.restore(); +}); + +add_task(async function test_fetchHighlights_no_expose_internal_link_props() { + info( + "HighlightsFeed.fetchHighlights should not expose internal link properties" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + let highlights = await fetchHighlightsRows(feed); + let internal = Object.keys(highlights[0]).filter(key => key.startsWith("__")); + + Assert.equal(internal.join(""), ""); + + sandbox.restore(); +}); + +add_task( + async function test_fetchHighlights_broadcast_when_feed_not_initialized() { + info( + "HighlightsFeed.fetchHighlights should broadcast if feed is not initialized" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + NewTabUtils.activityStreamLinks.getHighlights.resolves([]); + sandbox.stub(SectionsManager, "updateSection"); + await feed.fetchHighlights(); + + Assert.ok( + SectionsManager.updateSection.calledOnce, + "SectionsManager.updateSection called once" + ); + Assert.ok( + SectionsManager.updateSection.calledWithExactly( + SECTION_ID, + { rows: [] }, + true, + undefined + ) + ); + sandbox.restore(); + } +); + +add_task( + async function test_fetchHighlights_broadcast_on_broadcast_in_options() { + info( + "HighlightsFeed.fetchHighlights should broadcast if options.broadcast is true" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + NewTabUtils.activityStreamLinks.getHighlights.resolves([]); + feed.store.state.Sections[0].initialized = true; + + sandbox.stub(SectionsManager, "updateSection"); + await feed.fetchHighlights({ broadcast: true }); + + Assert.ok( + SectionsManager.updateSection.calledOnce, + "SectionsManager.updateSection called once" + ); + Assert.ok( + SectionsManager.updateSection.calledWithExactly( + SECTION_ID, + { rows: [] }, + true, + undefined + ) + ); + sandbox.restore(); + } +); + +add_task(async function test_fetchHighlights_no_broadcast() { + info( + "HighlightsFeed.fetchHighlights should not broadcast if " + + "options.broadcast is false and initialized is true" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + NewTabUtils.activityStreamLinks.getHighlights.resolves([]); + feed.store.state.Sections[0].initialized = true; + + sandbox.stub(SectionsManager, "updateSection"); + await feed.fetchHighlights({ broadcast: false }); + + Assert.ok( + SectionsManager.updateSection.calledOnce, + "SectionsManager.updateSection called once" + ); + Assert.ok( + SectionsManager.updateSection.calledWithExactly( + SECTION_ID, + { rows: [] }, + false, + undefined + ) + ); + sandbox.restore(); +}); + +add_task(async function test_fetchImage_capture_if_available() { + info("HighlightsFeed.fetchImage should capture the image, if available"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.stub(Screenshots, "getScreenshotForURL"); + sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); + + await fetchImage(feed, { + preview_image_url: FAKE_IMAGE_URL, + url: FAKE_URL, + }); + + Assert.ok( + Screenshots.getScreenshotForURL.calledOnce, + "Screenshots.getScreenshotForURL called once" + ); + Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_IMAGE_URL)); + + sandbox.restore(); +}); + +add_task(async function test_fetchImage_fallback_to_screenshot() { + info("HighlightsFeed.fetchImage should fall back to capturing a screenshot"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.stub(Screenshots, "getScreenshotForURL"); + sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); + + await fetchImage(feed, { url: FAKE_URL }); + + Assert.ok( + Screenshots.getScreenshotForURL.calledOnce, + "Screenshots.getScreenshotForURL called once" + ); + Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_URL)); + + sandbox.restore(); +}); + +add_task(async function test_fetchImage_updateSectionCard_args() { + info( + "HighlightsFeed.fetchImage should call " + + "SectionsManager.updateSectionCard with the right arguments" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.stub(SectionsManager, "updateSectionCard"); + sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE); + sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); + + await fetchImage(feed, { + preview_image_url: FAKE_IMAGE_URL, + url: FAKE_URL, + }); + Assert.ok( + SectionsManager.updateSectionCard.calledOnce, + "SectionsManager.updateSectionCard called" + ); + Assert.ok( + SectionsManager.updateSectionCard.calledWith( + "highlights", + FAKE_URL, + { image: FAKE_IMAGE }, + true + ) + ); + sandbox.restore(); +}); + +add_task(async function test_fetchImage_no_update_card_with_image() { + info("HighlightsFeed.fetchImage should not update the card with the image"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.stub(SectionsManager, "updateSectionCard"); + sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE); + sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true); + + let card = { + preview_image_url: FAKE_IMAGE_URL, + url: FAKE_URL, + }; + await fetchImage(feed, card); + Assert.ok(!card.image, "Image not set on card"); + sandbox.restore(); +}); + +add_task(async function test_uninit_disable_section() { + info("HighlightsFeed.onAction(UNINIT) should disable its section"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.init(); + + sandbox.stub(SectionsManager, "disableSection"); + feed.onAction({ type: at.UNINIT }); + Assert.ok( + SectionsManager.disableSection.calledOnce, + "SectionsManager.disableSection called" + ); + Assert.ok(SectionsManager.disableSection.calledWith(SECTION_ID)); + sandbox.restore(); +}); + +add_task(async function test_uninit_remove_expiration_filter() { + info("HighlightsFeed.onAction(UNINIT) should remove the expiration filter"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + feed.init(); + + sandbox.stub(PageThumbs, "removeExpirationFilter"); + feed.onAction({ type: at.UNINIT }); + Assert.ok( + PageThumbs.removeExpirationFilter.calledOnce, + "PageThumbs.removeExpirationFilter called" + ); + + sandbox.restore(); +}); + +add_task(async function test_onAction_relay_to_DownloadsManager_onAction() { + info( + "HighlightsFeed.onAction should relay all actions to " + + "DownloadsManager.onAction" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + sandbox.stub(feed.downloadsManager, "onAction"); + + let action = { + type: at.COPY_DOWNLOAD_LINK, + data: { url: "foo.png" }, + _target: {}, + }; + feed.onAction(action); + + Assert.ok( + feed.downloadsManager.onAction.calledOnce, + "HighlightsFeed.downloadManager.onAction called" + ); + Assert.ok(feed.downloadsManager.onAction.calledWith(action)); + sandbox.restore(); +}); + +add_task(async function test_onAction_fetch_highlights_on_SYSTEM_TICK() { + info("HighlightsFeed.onAction should fetch highlights on SYSTEM_TICK"); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + await feed.fetchHighlights(); + + sandbox.spy(feed, "fetchHighlights"); + feed.onAction({ type: at.SYSTEM_TICK }); + + Assert.ok( + feed.fetchHighlights.calledOnce, + "HighlightsFeed.fetchHighlights called" + ); + Assert.ok( + feed.fetchHighlights.calledWithExactly({ + broadcast: false, + isStartup: false, + }) + ); + sandbox.restore(); +}); + +add_task( + async function test_onAction_fetch_highlights_on_PREF_CHANGED_for_include() { + info( + "HighlightsFeed.onAction should fetch highlights on PREF_CHANGED " + + "for include prefs" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.spy(feed, "fetchHighlights"); + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: "section.highlights.includeBookmarks" }, + }); + + Assert.ok( + feed.fetchHighlights.calledOnce, + "HighlightsFeed.fetchHighlights called" + ); + Assert.ok(feed.fetchHighlights.calledWithExactly({ broadcast: true })); + sandbox.restore(); + } +); + +add_task( + async function test_onAction_no_fetch_highlights_on_PREF_CHANGED_for_other() { + info( + "HighlightsFeed.onAction should not fetch highlights on PREF_CHANGED " + + "for other prefs" + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + sandbox.spy(feed, "fetchHighlights"); + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: "section.topstories.pocketCta" }, + }); + + Assert.ok( + feed.fetchHighlights.notCalled, + "HighlightsFeed.fetchHighlights not called" + ); + + sandbox.restore(); + } +); + +add_task(async function test_onAction_fetch_highlights_on_actions() { + info("HighlightsFeed.onAction should fetch highlights for various actions"); + + let actions = [ + { + actionType: "PLACES_HISTORY_CLEARED", + expectsExpire: false, + expectsBroadcast: true, + }, + { + actionType: "DOWNLOAD_CHANGED", + expectsExpire: false, + expectsBroadcast: true, + }, + { + actionType: "PLACES_LINKS_CHANGED", + expectsExpire: true, + expectsBroadcast: false, + }, + { + actionType: "PLACES_LINK_BLOCKED", + expectsExpire: false, + expectsBroadcast: true, + }, + { + actionType: "PLACES_SAVED_TO_POCKET", + expectsExpire: true, + expectsBroadcast: false, + }, + ]; + for (let action of actions) { + info( + `HighlightsFeed.onAction should fetch highlights on ${action.actionType}` + ); + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + await feed.fetchHighlights(); + sandbox.spy(feed, "fetchHighlights"); + sandbox.stub(feed.linksCache, "expire"); + + feed.onAction({ type: at[action.actionType] }); + Assert.ok( + feed.fetchHighlights.calledOnce, + "HighlightsFeed.fetchHighlights called" + ); + Assert.ok( + feed.fetchHighlights.calledWith({ broadcast: action.expectsBroadcast }) + ); + + if (action.expectsExpire) { + Assert.ok( + feed.linksCache.expire.calledOnce, + "HighlightsFeed.linksCache.expire called" + ); + } + + sandbox.restore(); + } +}); + +add_task( + async function test_onAction_fetch_highlights_no_broadcast_on_TOP_SITES_UPDATED() { + info( + "HighlightsFeed.onAction should fetch highlights with broadcast " + + "false on TOP_SITES_UPDATED" + ); + + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + await feed.fetchHighlights(); + sandbox.spy(feed, "fetchHighlights"); + + feed.onAction({ type: at.TOP_SITES_UPDATED }); + Assert.ok( + feed.fetchHighlights.calledOnce, + "HighlightsFeed.fetchHighlights called" + ); + Assert.ok( + feed.fetchHighlights.calledWithExactly({ + broadcast: false, + isStartup: false, + }) + ); + + sandbox.restore(); + } +); + +add_task( + async function test_onAction_fetch_highlights_on_deleting_archiving_pocket() { + info( + "HighlightsFeed.onAction should call fetchHighlights when deleting " + + "or archiving from Pocket" + ); + + let sandbox = sinon.createSandbox(); + let feed = getHighlightsFeedForTest(sandbox); + + await feed.fetchHighlights(); + sandbox.spy(feed, "fetchHighlights"); + + feed.onAction({ + type: at.POCKET_LINK_DELETED_OR_ARCHIVED, + data: { pocket_id: 12345 }, + }); + Assert.ok( + feed.fetchHighlights.calledOnce, + "HighlightsFeed.fetchHighlights called" + ); + Assert.ok(feed.fetchHighlights.calledWithExactly({ broadcast: true })); + + sandbox.restore(); + } +); diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml index ad5f46b122f24..007d8ef2799d3 100644 --- a/browser/components/newtab/test/xpcshell/xpcshell.toml +++ b/browser/components/newtab/test/xpcshell/xpcshell.toml @@ -30,6 +30,8 @@ skip-if = ["socketprocess_networking"] # Bug 1759035 ["test_CFRMessageProvider.js"] +["test_HighlightsFeed.js"] + ["test_InflightAssetsMessageProvider.js"] ["test_NimbusRolloutMessageProvider.js"]