/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // See also browser/base/content/test/newtab/. const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); // A small 1x1 test png const image1x1 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="; function getBookmarksSize() { return NewTabUtils.activityStreamProvider.executePlacesQuery( "SELECT count(*) FROM moz_bookmarks WHERE type = :type", { params: { type: PlacesUtils.bookmarks.TYPE_BOOKMARK } } ); } function getHistorySize() { return NewTabUtils.activityStreamProvider.executePlacesQuery( "SELECT count(*) FROM moz_places WHERE hidden = 0 AND last_visit_date NOT NULL" ); } add_task(async function validCacheMidPopulation() { let expectedLinks = makeLinks(0, 3, 1); let provider = new TestProvider(done => done(expectedLinks)); provider.maxNumLinks = expectedLinks.length; NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider); let promise = new Promise(resolve => NewTabUtils.links.populateCache(resolve) ); // isTopSiteGivenProvider() and getProviderLinks() should still return results // even when cache is empty or being populated. Assert.ok(!NewTabUtils.isTopSiteGivenProvider("example1.com", provider)); do_check_links(NewTabUtils.getProviderLinks(provider), []); await promise; // Once the cache is populated, we get the expected results Assert.ok(NewTabUtils.isTopSiteGivenProvider("example1.com", provider)); do_check_links(NewTabUtils.getProviderLinks(provider), expectedLinks); NewTabUtils.links.removeProvider(provider); }); add_task(async function notifyLinkDelete() { let expectedLinks = makeLinks(0, 3, 1); let provider = new TestProvider(done => done(expectedLinks)); provider.maxNumLinks = expectedLinks.length; NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider); await new Promise(resolve => NewTabUtils.links.populateCache(resolve)); do_check_links(NewTabUtils.links.getLinks(), expectedLinks); // Remove a link. let removedLink = expectedLinks[2]; provider.notifyLinkChanged(removedLink, 2, true); let links = NewTabUtils.links._providers.get(provider); // Check that sortedLinks is correctly updated. do_check_links(NewTabUtils.links.getLinks(), expectedLinks.slice(0, 2)); // Check that linkMap is accurately updated. Assert.equal(links.linkMap.size, 2); Assert.ok(links.linkMap.get(expectedLinks[0].url)); Assert.ok(links.linkMap.get(expectedLinks[1].url)); Assert.ok(!links.linkMap.get(removedLink.url)); // Check that siteMap is correctly updated. Assert.equal(links.siteMap.size, 2); Assert.ok(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[0].url))); Assert.ok(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[1].url))); Assert.ok(!links.siteMap.has(NewTabUtils.extractSite(removedLink.url))); NewTabUtils.links.removeProvider(provider); }); add_task(async function populatePromise() { let count = 0; let expectedLinks = makeLinks(0, 10, 2); let getLinksFcn = async function (callback) { // Should not be calling getLinksFcn twice count++; Assert.equal(count, 1); await Promise.resolve(); callback(expectedLinks); }; let provider = new TestProvider(getLinksFcn); NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider); NewTabUtils.links.populateProviderCache(provider, () => {}); NewTabUtils.links.populateProviderCache(provider, () => { do_check_links(NewTabUtils.links.getLinks(), expectedLinks); NewTabUtils.links.removeProvider(provider); }); }); add_task(async function isTopSiteGivenProvider() { let expectedLinks = makeLinks(0, 10, 2); // The lowest 2 frecencies have the same base domain. expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test"; let provider = new TestProvider(done => done(expectedLinks)); provider.maxNumLinks = expectedLinks.length; NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider); await new Promise(resolve => NewTabUtils.links.populateCache(resolve)); Assert.equal( NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true ); Assert.equal( NewTabUtils.isTopSiteGivenProvider("example1.com", provider), false ); // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3 let newLink = makeLink(3); provider.notifyLinkChanged(newLink); // There is still a frecent url with example2 domain, so it's still frecent. Assert.equal( NewTabUtils.isTopSiteGivenProvider("example3.com", provider), true ); Assert.equal( NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true ); // Push out frecency 3 newLink = makeLink(5); provider.notifyLinkChanged(newLink); // Push out frecency 4 newLink = makeLink(9); provider.notifyLinkChanged(newLink); // Our count reached 0 for the example2.com domain so it's no longer a frecent site. Assert.equal( NewTabUtils.isTopSiteGivenProvider("example5.com", provider), true ); Assert.equal( NewTabUtils.isTopSiteGivenProvider("example2.com", provider), false ); NewTabUtils.links.removeProvider(provider); }); add_task(async function multipleProviders() { // Make each provider generate NewTabUtils.links.maxNumLinks links to check // that no more than maxNumLinks are actually returned in the merged list. let evenLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks, 2); let evenProvider = new TestProvider(done => done(evenLinks)); let oddLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks - 1, 2); let oddProvider = new TestProvider(done => done(oddLinks)); NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(evenProvider); NewTabUtils.links.addProvider(oddProvider); await new Promise(resolve => NewTabUtils.links.populateCache(resolve)); let links = NewTabUtils.links.getLinks(); let expectedLinks = makeLinks( NewTabUtils.links.maxNumLinks, 2 * NewTabUtils.links.maxNumLinks, 1 ); Assert.equal(links.length, NewTabUtils.links.maxNumLinks); do_check_links(links, expectedLinks); NewTabUtils.links.removeProvider(evenProvider); NewTabUtils.links.removeProvider(oddProvider); }); add_task(async function changeLinks() { let expectedLinks = makeLinks(0, 20, 2); let provider = new TestProvider(done => done(expectedLinks)); NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider); await new Promise(resolve => NewTabUtils.links.populateCache(resolve)); do_check_links(NewTabUtils.links.getLinks(), expectedLinks); // Notify of a new link. let newLink = makeLink(19); expectedLinks.splice(1, 0, newLink); provider.notifyLinkChanged(newLink); do_check_links(NewTabUtils.links.getLinks(), expectedLinks); // Notify of a link that's changed sort criteria. newLink.frecency = 17; expectedLinks.splice(1, 1); expectedLinks.splice(2, 0, newLink); provider.notifyLinkChanged({ url: newLink.url, frecency: 17, }); do_check_links(NewTabUtils.links.getLinks(), expectedLinks); // Notify of a link that's changed title. newLink.title = "My frecency is now 17"; provider.notifyLinkChanged({ url: newLink.url, title: newLink.title, }); do_check_links(NewTabUtils.links.getLinks(), expectedLinks); // Notify of a new link again, but this time make it overflow maxNumLinks. provider.maxNumLinks = expectedLinks.length; newLink = makeLink(21); expectedLinks.unshift(newLink); expectedLinks.pop(); Assert.equal(expectedLinks.length, provider.maxNumLinks); // Sanity check. provider.notifyLinkChanged(newLink); do_check_links(NewTabUtils.links.getLinks(), expectedLinks); // Notify of many links changed. expectedLinks = makeLinks(0, 3, 1); provider.notifyManyLinksChanged(); // Since _populateProviderCache() is async, we must wait until the provider's // populate promise has been resolved. await NewTabUtils.links._providers.get(provider).populatePromise; // NewTabUtils.links will now repopulate its cache do_check_links(NewTabUtils.links.getLinks(), expectedLinks); NewTabUtils.links.removeProvider(provider); }); add_task(async function oneProviderAlreadyCached() { let links1 = makeLinks(0, 10, 1); let provider1 = new TestProvider(done => done(links1)); NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider1); await new Promise(resolve => NewTabUtils.links.populateCache(resolve)); do_check_links(NewTabUtils.links.getLinks(), links1); let links2 = makeLinks(10, 20, 1); let provider2 = new TestProvider(done => done(links2)); NewTabUtils.links.addProvider(provider2); await new Promise(resolve => NewTabUtils.links.populateCache(resolve)); do_check_links(NewTabUtils.links.getLinks(), links2.concat(links1)); NewTabUtils.links.removeProvider(provider1); NewTabUtils.links.removeProvider(provider2); }); add_task(async function newLowRankedLink() { // Init a provider with 10 links and make its maximum number also 10. let links = makeLinks(0, 10, 1); let provider = new TestProvider(done => done(links)); provider.maxNumLinks = links.length; NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider); await new Promise(resolve => NewTabUtils.links.populateCache(resolve)); do_check_links(NewTabUtils.links.getLinks(), links); // Notify of a new link that's low-ranked enough not to make the list. let newLink = makeLink(0); provider.notifyLinkChanged(newLink); do_check_links(NewTabUtils.links.getLinks(), links); // Notify about the new link's title change. provider.notifyLinkChanged({ url: newLink.url, title: "a new title", }); do_check_links(NewTabUtils.links.getLinks(), links); NewTabUtils.links.removeProvider(provider); }); add_task(async function extractSite() { // All these should extract to the same site [ "mozilla.org", "m.mozilla.org", "mobile.mozilla.org", "www.mozilla.org", "www3.mozilla.org", ].forEach(host => { let url = "http://" + host; Assert.equal( NewTabUtils.extractSite(url), "mozilla.org", "extracted same " + host ); }); // All these should extract to the same subdomain ["bugzilla.mozilla.org", "www.bugzilla.mozilla.org"].forEach(host => { let url = "http://" + host; Assert.equal( NewTabUtils.extractSite(url), "bugzilla.mozilla.org", "extracted eTLD+2 " + host ); }); // All these should not extract to the same site [ "bugzilla.mozilla.org", "bug123.bugzilla.mozilla.org", "too.many.levels.bugzilla.mozilla.org", "m2.mozilla.org", "mobile30.mozilla.org", "ww.mozilla.org", "ww2.mozilla.org", "wwwww.mozilla.org", "wwwww50.mozilla.org", "wwws.mozilla.org", "secure.mozilla.org", "secure10.mozilla.org", "many.levels.deep.mozilla.org", "just.check.in", "192.168.0.1", "localhost", ].forEach(host => { let url = "http://" + host; Assert.notEqual( NewTabUtils.extractSite(url), "mozilla.org", "extracted diff " + host ); }); // All these should not extract to the same site [ "about:blank", "file:///Users/user/file", "chrome://browser/something", "ftp://ftp.mozilla.org/", ].forEach(url => { Assert.notEqual( NewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url ); }); }); add_task(async function faviconBytesToDataURI() { let tests = [ [{ favicon: "bar".split("").map(s => s.charCodeAt(0)), mimeType: "foo" }], [ { favicon: "bar".split("").map(s => s.charCodeAt(0)), mimeType: "foo", xxyy: "quz", }, ], ]; let provider = NewTabUtils.activityStreamProvider; for (let test of tests) { let clone = JSON.parse(JSON.stringify(test)); delete clone[0].mimeType; clone[0].favicon = `data:foo;base64,${btoa("bar")}`; let result = provider._faviconBytesToDataURI(test); Assert.deepEqual( JSON.stringify(clone), JSON.stringify(result), "favicon converted to data uri" ); } }); add_task(async function addFavicons() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamProvider; // start by passing in a bad uri and check that we get a null favicon back let links = [{ url: "mozilla.com" }]; await provider._addFavicons(links); Assert.equal( links[0].favicon, null, "Got a null favicon because we passed in a bad url" ); Assert.equal( links[0].mimeType, null, "Got a null mime type because we passed in a bad url" ); Assert.equal( links[0].faviconSize, null, "Got a null favicon size because we passed in a bad url" ); // now fix the url and try again - this time we get good favicon data back // a 1x1 favicon as a data URI of mime type image/png const base64URL = image1x1; links[0].url = "https://mozilla.com"; let visit = [ { uri: links[0].url, visitDate: timeDaysAgo(0), transition: PlacesUtils.history.TRANSITION_TYPED, }, ]; await PlacesTestUtils.addVisits(visit); let faviconData = new Map(); faviconData.set("https://mozilla.com", `${base64URL}#tippytop`); await PlacesTestUtils.addFavicons(faviconData); await provider._addFavicons(links); Assert.equal( links[0].mimeType, "image/png", "Got the right mime type before deleting it" ); Assert.equal( links[0].faviconLength, links[0].favicon.length, "Got the right length for the byte array" ); Assert.equal( provider._faviconBytesToDataURI(links)[0].favicon, base64URL, "Got the right favicon" ); Assert.equal( links[0].faviconSize, 1, "Got the right favicon size (width and height of favicon)" ); Assert.equal(links[0].faviconRef, "tippytop", "Got the favicon url ref"); // Check with http version of the link that doesn't have its own const nonHttps = [{ url: links[0].url.replace("https", "http") }]; await provider._addFavicons(nonHttps); Assert.equal( provider._faviconBytesToDataURI(nonHttps)[0].favicon, base64URL, "Got the same favicon" ); Assert.equal( nonHttps[0].faviconLength, links[0].faviconLength, "Got the same favicon length" ); Assert.equal( nonHttps[0].faviconSize, links[0].faviconSize, "Got the same favicon size" ); Assert.equal( nonHttps[0].mimeType, links[0].mimeType, "Got the same mime type" ); // Check that we do not collect favicons for pocket items const pocketItems = [ { url: links[0].url }, { url: "https://mozilla1.com", type: "pocket" }, ]; await provider._addFavicons(pocketItems); Assert.equal( provider._faviconBytesToDataURI(pocketItems)[0].favicon, base64URL, "Added favicon data only to the non-pocket item" ); Assert.equal( pocketItems[1].favicon, null, "Did not add a favicon to the pocket item" ); Assert.equal( pocketItems[1].mimeType, null, "Did not add mimeType to the pocket item" ); Assert.equal( pocketItems[1].faviconSize, null, "Did not add a faviconSize to the pocket item" ); }); add_task(async function getHighlightsWithoutPocket() { const addMetadata = url => PlacesUtils.history.update({ description: "desc", previewImageURL: "https://image/", url, }); await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; let links = await provider.getHighlights(); Assert.equal(links.length, 0, "empty history yields empty links"); // Add bookmarks const now = Date.now(); const oldSeconds = 24 * 60 * 60; // 1 day old let bookmarks = [ { dateAdded: new Date(now - oldSeconds * 1000), parentGuid: PlacesUtils.bookmarks.unfiledGuid, title: "foo", url: "https://mozilla1.com/dayOld", }, { parentGuid: PlacesUtils.bookmarks.unfiledGuid, title: "foo", url: "https://mozilla1.com/nowNew", }, ]; for (let placeInfo of bookmarks) { await PlacesUtils.bookmarks.insert(placeInfo); } links = await provider.getHighlights(); Assert.equal( links.length, 0, "adding bookmarks without visits doesn't yield more links" ); // Add a history visit let testURI = "http://mozilla.com/"; await PlacesTestUtils.addVisits(testURI); links = await provider.getHighlights(); Assert.equal( links.length, 0, "adding visits without metadata doesn't yield more links" ); // Add bookmark visits for (let placeInfo of bookmarks) { await PlacesTestUtils.addVisits(placeInfo.url); } links = await provider.getHighlights(); Assert.equal(links.length, 2, "adding visits to bookmarks yields more links"); Assert.equal( links[0].url, bookmarks[1].url, "first bookmark is younger bookmark" ); Assert.equal(links[0].type, "bookmark", "first bookmark is bookmark"); Assert.ok(links[0].date_added, "got a date_added for the bookmark"); Assert.equal( links[1].url, bookmarks[0].url, "second bookmark is older bookmark" ); Assert.equal(links[1].type, "bookmark", "second bookmark is bookmark"); Assert.ok(links[1].date_added, "got a date_added for the bookmark"); // Add metadata to history await addMetadata(testURI); links = await provider.getHighlights(); Assert.equal(links.length, 3, "adding metadata yield more links"); Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark"); Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark"); Assert.equal(links[2].url, testURI, "added visit corresponds to added url"); Assert.equal(links[2].type, "history", "added visit is history"); links = await provider.getHighlights({ numItems: 2 }); Assert.equal(links.length, 2, "limited to 2 items"); Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark"); Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark"); links = await provider.getHighlights({ excludeHistory: true }); Assert.equal(links.length, 2, "only have bookmarks"); Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark"); Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark"); links = await provider.getHighlights({ excludeBookmarks: true }); Assert.equal(links.length, 1, "only have history"); Assert.equal(links[0].url, testURI, "only have the history now"); links = await provider.getHighlights({ excludeBookmarks: true, excludeHistory: true, }); Assert.equal(links.length, 0, "requested nothing, get nothing"); links = await provider.getHighlights({ bookmarkSecondsAgo: oldSeconds / 2 }); Assert.equal(links.length, 2, "old bookmark filtered out with"); Assert.equal(links[0].url, bookmarks[1].url, "still have newer bookmark"); Assert.equal(links[1].url, testURI, "still have the history"); // Add a visit and metadata to the older bookmark await PlacesTestUtils.addVisits(bookmarks[0].url); await addMetadata(bookmarks[0].url); links = await provider.getHighlights({ bookmarkSecondsAgo: oldSeconds / 2 }); Assert.equal(links.length, 3, "old bookmark returns as history"); Assert.equal(links[0].url, bookmarks[1].url, "still have newer bookmark"); Assert.equal( links[1].url, bookmarks[0].url, "old bookmark now is newer history" ); Assert.equal(links[1].type, "history", "old bookmark now is history"); Assert.equal(links[2].url, testURI, "still have the history"); // Bookmark the history item await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, title: "now a bookmark", url: testURI, }); links = await provider.getHighlights(); Assert.equal( links.length, 3, "a visited bookmark doesn't appear as bookmark and history" ); Assert.equal( links[0].url, testURI, "history is now the first, i.e., most recent, bookmark" ); Assert.equal(links[0].type, "bookmark", "was history now bookmark"); Assert.ok(links[0].date_added, "got a date_added for the now bookmark"); Assert.equal( links[1].url, bookmarks[1].url, "still have younger bookmark now second" ); Assert.equal( links[2].url, bookmarks[0].url, "still have older bookmark now third" ); // Test the `withFavicons` option. await PlacesTestUtils.addFavicons(new Map([[testURI, image1x1]])); links = await provider.getHighlights({ withFavicons: true }); Assert.equal(links.length, 3, "We're not expecting a change in links"); Assert.equal(links[0].favicon, image1x1, "Link 1 should contain a favicon"); Assert.equal(links[1].favicon, null, "Link 2 has no favicon data"); Assert.equal(links[2].favicon, null, "Link 3 has no favicon data"); }); add_task(async function getHighlightsWithPocketSuccess() { await setUpActivityStreamTest(); // Add a bookmark let bookmark = { parentGuid: PlacesUtils.bookmarks.unfiledGuid, title: "foo", description: "desc", preview_image_url: "foo.com/img.png", url: "https://mozilla1.com/", }; const fakeResponse = { list: { 123: { time_added: "123", image: { src: "foo.com/img.png" }, excerpt: "A description for foo", resolved_title: "A title for foo", resolved_url: "http://www.foo.com", item_id: "123", open_url: "http://www.getpocket.com/itemID", status: "0", }, 456: { item_id: "456", status: "2", }, }, }; await PlacesUtils.bookmarks.insert(bookmark); await PlacesTestUtils.addVisits(bookmark.url); NewTabUtils.activityStreamProvider.fetchSavedPocketItems = () => fakeResponse; let provider = NewTabUtils.activityStreamLinks; // Force a cache invalidation NewTabUtils.activityStreamLinks._pocketLastUpdated = Date.now() - 70 * 60 * 1000; NewTabUtils.activityStreamLinks._pocketLastLatest = -1; let links = await provider.getHighlights(); // We should have 1 bookmark followed by 1 pocket story in highlights // We should not have stored the second pocket item since it was deleted Assert.equal(links.length, 2, "Should have 2 links in highlights"); // First highlight should be a bookmark Assert.equal(links[0].url, bookmark.url, "The first link is the bookmark"); // Second highlight should be a Pocket item with the correct fields to display let pocketItem = fakeResponse.list["123"]; let currentLink = links[1]; Assert.equal(currentLink.url, pocketItem.resolved_url, "Correct Pocket item"); Assert.equal(currentLink.type, "pocket", "Attached the correct type"); Assert.equal( currentLink.preview_image_url, pocketItem.image.src, "Correct preview image was added" ); Assert.equal( currentLink.title, pocketItem.resolved_title, "Correct title was added" ); Assert.equal( currentLink.description, pocketItem.excerpt, "Correct description was added" ); Assert.equal( currentLink.pocket_id, pocketItem.item_id, "item_id was preserved" ); Assert.equal( currentLink.open_url, `${pocketItem.open_url}?src=fx_new_tab`, "open_url was preserved" ); Assert.equal( currentLink.date_added, pocketItem.time_added * 1000, "date_added was added to pocket item" ); NewTabUtils.activityStreamLinks._savedPocketStories = null; }); add_task(async function getHighlightsWithPocketCached() { await setUpActivityStreamTest(); let fakeResponse = { list: { 123: { time_added: "123", image: { src: "foo.com/img.png" }, excerpt: "A description for foo", resolved_title: "A title for foo", resolved_url: "http://www.foo.com", item_id: "123", open_url: "http://www.getpocket.com/itemID", status: "0", }, 456: { item_id: "456", status: "2", }, }, }; NewTabUtils.activityStreamProvider.fetchSavedPocketItems = () => fakeResponse; let provider = NewTabUtils.activityStreamLinks; let links = await provider.getHighlights(); Assert.equal( links.length, 1, "Sanity check that we got 1 link back for highlights" ); Assert.equal( links[0].url, fakeResponse.list["123"].resolved_url, "Sanity check that it was the pocket story" ); // Update what the response would be fakeResponse.list["789"] = { time_added: "123", image: { src: "bar.com/img.png" }, excerpt: "A description for bar", resolved_title: "A title for bar", resolved_url: "http://www.bar.com", item_id: "789", open_url: "http://www.getpocket.com/itemID", status: "0", }; // Call getHighlights again - this time we should get the cached links since we just updated links = await provider.getHighlights(); Assert.equal(links.length, 1, "We still got 1 link back for highlights"); Assert.equal( links[0].url, fakeResponse.list["123"].resolved_url, "It was still the same pocket story" ); // Now force a cache invalidation and call getHighlights again NewTabUtils.activityStreamLinks._pocketLastUpdated = Date.now() - 70 * 60 * 1000; NewTabUtils.activityStreamLinks._pocketLastLatest = -1; links = await provider.getHighlights(); Assert.equal( links.length, 2, "This time we got fresh links with the new response" ); Assert.equal( links[0].url, fakeResponse.list["123"].resolved_url, "First link is unchanged" ); Assert.equal( links[1].url, fakeResponse.list["789"].resolved_url, "Second link is the new link" ); NewTabUtils.activityStreamLinks._savedPocketStories = null; }); add_task(async function getHighlightsWithPocketFailure() { await setUpActivityStreamTest(); NewTabUtils.activityStreamProvider.fetchSavedPocketItems = function () { throw new Error(); }; let provider = NewTabUtils.activityStreamLinks; // Force a cache invalidation NewTabUtils.activityStreamLinks._pocketLastUpdated = Date.now() - 70 * 60 * 1000; NewTabUtils.activityStreamLinks._pocketLastLatest = -1; let links = await provider.getHighlights(); Assert.equal(links.length, 0, "Return empty links if we reject the promise"); }); add_task(async function getHighlightsWithPocketNoData() { await setUpActivityStreamTest(); NewTabUtils.activityStreamProvider.fetchSavedPocketItems = () => {}; let provider = NewTabUtils.activityStreamLinks; // Force a cache invalidation NewTabUtils.activityStreamLinks._pocketLastUpdated = Date.now() - 70 * 60 * 1000; NewTabUtils.activityStreamLinks._pocketLastLatest = -1; let links = await provider.getHighlights(); Assert.equal( links.length, 0, "Return empty links if we got no data back from the response" ); }); add_task(async function getTopFrecentSites() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; let links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 0, "empty history yields empty links"); // add a visit let testURI = "http://mozilla.com/"; await PlacesTestUtils.addVisits(testURI); links = await provider.getTopSites(); Assert.equal( links.length, 0, "adding a single visit doesn't exceed default threshold" ); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 1, "adding a visit yields a link"); Assert.equal(links[0].url, testURI, "added visit corresponds to added url"); }); add_task( { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" || Services.prefs.getBoolPref( "browser.topsites.useRemoteSetting" ) /* see bug 1664502 */, }, async function getTopFrecentSites_improveSearch() { await setUpActivityStreamTest(); const SEARCH_SHORTCUTS_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts"; Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, true); let testURI = "https://www.amazon.com?search=tv"; await PlacesTestUtils.addVisits(testURI); let provider = NewTabUtils.activityStreamLinks; let links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal( links.length, 1, "sanity check that we got the link from top sites" ); Assert.equal( links[0].url, "https://amazon.com", "the amazon site was converted to generic search shortcut site" ); Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, false); } ); add_task(async function getTopFrecentSites_no_dedup() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; let links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 0, "empty history yields empty links"); // Add a visits in reverse order they will be returned in when not deduped. let testURIs = [ { uri: "http://www.mozilla.com/" }, { uri: "http://mozilla.com/" }, ]; await PlacesTestUtils.addVisits(testURIs); links = await provider.getTopSites(); Assert.equal( links.length, 0, "adding a single visit doesn't exceed default threshold" ); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 1, "adding a visit yields a link"); // Plain domain is returned when deduped. Assert.equal( links[0].url, testURIs[1].uri, "added visit corresponds to added url" ); links = await provider.getTopSites({ topsiteFrecency: 100, onePerDomain: false, }); Assert.equal(links.length, 2, "adding a visit yields a link"); Assert.equal( links[0].url, testURIs[1].uri, "added visit corresponds to added url" ); Assert.equal( links[1].url, testURIs[0].uri, "added visit corresponds to added url" ); }); add_task(async function getTopFrecentSites_dedupeWWW() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; let links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 0, "empty history yields empty links"); // add a visit without www let testURI = "http://mozilla.com"; await PlacesTestUtils.addVisits(testURI); // add a visit with www testURI = "http://www.mozilla.com"; await PlacesTestUtils.addVisits(testURI); // Test combined frecency score links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 1, "adding both www. and no-www. yields one link"); Assert.equal(links[0].frecency, 200, "frecency scores are combined"); // add another page visit with www and without www let noWWW = "http://mozilla.com/page"; await PlacesTestUtils.addVisits(noWWW); let withWWW = "http://www.mozilla.com/page"; await PlacesTestUtils.addVisits(withWWW); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 1, "adding both www. and no-www. yields one link"); Assert.equal( links[0].frecency, 200, "frecency scores are combined ignoring extra pages" ); // add another visit with www await PlacesTestUtils.addVisits(withWWW); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 1, "still yields one link"); Assert.equal(links[0].url, withWWW, "more frecent www link is used"); Assert.equal( links[0].frecency, 300, "frecency scores are combined ignoring extra pages" ); // add a couple more visits to the no-www page await PlacesTestUtils.addVisits(noWWW); await PlacesTestUtils.addVisits(noWWW); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 1, "still yields one link"); Assert.equal(links[0].url, noWWW, "now more frecent no-www link is used"); Assert.equal( links[0].frecency, 500, "frecency scores are combined ignoring extra pages" ); }); add_task(async function getTopFrencentSites_maxLimit() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; // add many visits const MANY_LINKS = 20; for (let i = 0; i < MANY_LINKS; i++) { let testURI = `http://mozilla${i}.com`; await PlacesTestUtils.addVisits(testURI); } let links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.ok( links.length < MANY_LINKS, "query default limited to less than many" ); Assert.greater(links.length, 6, "query default to more than visible count"); }); add_task(async function getTopFrencentSites_allowedProtocols() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; // add a visit from a file:// site let testURI = "file:///some/file/path.png"; await PlacesTestUtils.addVisits(testURI); let links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 0, "don't get sites with the file:// protocol"); // now add a site with an allowed protocol testURI = "http://www.mozilla.com"; await PlacesTestUtils.addVisits(testURI); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal(links.length, 1, "http:// is an allowed protocol"); // and just to be sure, add a visit to a site with ftp:// protocol testURI = "ftp://bad/example"; await PlacesTestUtils.addVisits(testURI); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal( links.length, 1, "we still only accept http:// and https:// for top sites" ); // add a different allowed protocol testURI = "https://https"; await PlacesTestUtils.addVisits(testURI); links = await provider.getTopSites({ topsiteFrecency: 100 }); Assert.equal( links.length, 2, "we now accept both http:// and https:// for top sites" ); }); add_task(async function getTopFrecentSites_order() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; let { TRANSITION_TYPED } = PlacesUtils.history; let timeEarlier = timeDaysAgo(0); let timeLater = timeDaysAgo(2); let visits = [ // frecency 200 { uri: "https://mozilla1.com/0", visitDate: timeEarlier, transition: TRANSITION_TYPED, }, // sort by url, frecency 200 { uri: "https://mozilla2.com/1", visitDate: timeEarlier, transition: TRANSITION_TYPED, }, // sort by last visit date, frecency 200 { uri: "https://mozilla3.com/2", visitDate: timeLater, transition: TRANSITION_TYPED, }, // sort by frecency, frecency 10 { uri: "https://mozilla0.com/", visitDate: timeLater }, ]; let links = await provider.getTopSites({ topsiteFrecency: 0 }); Assert.equal(links.length, 0, "empty history yields empty links"); // map of page url to favicon url let faviconData = new Map(); faviconData.set("https://mozilla3.com/2", image1x1); await PlacesTestUtils.addVisits(visits); await PlacesTestUtils.addFavicons(faviconData); links = await provider.getTopSites({ topsiteFrecency: 0 }); Assert.equal( links.length, visits.length, "number of links added is the same as obtain by getTopFrecentSites" ); // first link doesn't have a favicon Assert.equal( links[0].url, visits[0].uri, "links are obtained in the expected order" ); Assert.equal(null, links[0].favicon, "favicon data is stored as expected"); Assert.ok( isVisitDateOK(links[0].lastVisitDate), "visit date within expected range" ); // second link doesn't have a favicon Assert.equal( links[1].url, visits[1].uri, "links are obtained in the expected order" ); Assert.equal(null, links[1].favicon, "favicon data is stored as expected"); Assert.ok( isVisitDateOK(links[1].lastVisitDate), "visit date within expected range" ); // third link should have the favicon data that we added Assert.equal( links[2].url, visits[2].uri, "links are obtained in the expected order" ); Assert.equal( faviconData.get(links[2].url), links[2].favicon, "favicon data is stored as expected" ); Assert.ok( isVisitDateOK(links[2].lastVisitDate), "visit date within expected range" ); // fourth link doesn't have a favicon Assert.equal( links[3].url, visits[3].uri, "links are obtained in the expected order" ); Assert.equal(null, links[3].favicon, "favicon data is stored as expected"); Assert.ok( isVisitDateOK(links[3].lastVisitDate), "visit date within expected range" ); }); add_task(async function getTopFrecentSites_hideWithSearchParam() { await setUpActivityStreamTest(); let pref = "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam"; let provider = NewTabUtils.activityStreamLinks; // This maps URL search params to objects describing whether a URL with those // params is expected to be included in the returned links. Each object maps // from effective hide-with search params to whether the URL is expected to be // included. let tests = { "": { "": true, test: true, "test=": true, "test=hide": true, nomatch: true, "nomatch=": true, "nomatch=hide": true, }, test: { "": true, test: false, "test=": false, "test=hide": true, nomatch: true, "nomatch=": true, "nomatch=hide": true, }, "test=hide": { "": true, test: false, "test=": true, "test=hide": false, nomatch: true, "nomatch=": true, "nomatch=hide": true, }, "test=foo&test=hide": { "": true, test: false, "test=": true, "test=hide": false, nomatch: true, "nomatch=": true, "nomatch=hide": true, }, }; for (let [urlParams, expected] of Object.entries(tests)) { for (let prefValue of Object.keys(expected)) { info( "Running test: " + JSON.stringify({ urlParams, prefValue, expected }) ); // Add a visit to a URL with search params `urlParams`. let url = new URL("http://example.com/"); url.search = urlParams; await PlacesTestUtils.addVisits(url); // Set the pref to `prefValue`. Services.prefs.setCharPref(pref, prefValue); // Call `getTopSites()` with all the test values for `hideWithSearchParam` // plus undefined. When `hideWithSearchParam` is undefined, the pref value // should be used. Otherwise it should override the pref. for (let hideWithSearchParam of [undefined, ...Object.keys(expected)]) { info( "Calling getTopSites() with hideWithSearchParam: " + JSON.stringify(hideWithSearchParam) ); let options = { topsiteFrecency: 100 }; if (hideWithSearchParam !== undefined) { options = { ...options, hideWithSearchParam }; } let links = await provider.getTopSites(options); let effectiveHideWithParam = hideWithSearchParam === undefined ? prefValue : hideWithSearchParam; if (expected[effectiveHideWithParam]) { Assert.equal(links.length, 1, "One link returned"); Assert.equal(links[0].url, url.toString(), "Expected link returned"); } else { Assert.equal(links.length, 0, "No links returned"); } } await PlacesUtils.history.clear(); } } Services.prefs.clearUserPref(pref); }); add_task(async function activitySteamProvider_deleteHistoryLink() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; let { TRANSITION_TYPED } = PlacesUtils.history; let visits = [ // frecency 200 { uri: "https://mozilla1.com/0", visitDate: timeDaysAgo(1), transition: TRANSITION_TYPED, }, // sort by url, frecency 200 { uri: "https://mozilla2.com/1", visitDate: timeDaysAgo(0) }, ]; let size = await getHistorySize(); Assert.equal(size, 0, "empty history has size 0"); await PlacesTestUtils.addVisits(visits); size = await getHistorySize(); Assert.equal(size, 2, "expected history size"); // delete a link let deleted = await provider.deleteHistoryEntry("https://mozilla2.com/1"); Assert.equal(deleted, true, "link is deleted"); // ensure that there's only one link left size = await getHistorySize(); Assert.equal(size, 1, "expected history size"); // pin the link and delete it const linkToPin = { url: "https://mozilla1.com/0" }; NewTabUtils.pinnedLinks.pin(linkToPin, 0); // sanity check that the correct link was pinned Assert.equal( NewTabUtils.pinnedLinks.links.length, 1, "added a link to pinned sites" ); Assert.equal( NewTabUtils.pinnedLinks.isPinned(linkToPin), true, "pinned the correct link" ); // delete the pinned link and ensure it was both deleted from history and unpinned deleted = await provider.deleteHistoryEntry("https://mozilla1.com/0"); size = await getHistorySize(); Assert.equal(deleted, true, "link is deleted"); Assert.equal(size, 0, "expected history size"); Assert.equal( NewTabUtils.pinnedLinks.links.length, 0, "unpinned the deleted link" ); }); add_task(async function activityStream_deleteBookmark() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; let bookmarks = [ { url: "https://mozilla1.com/0", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }, { url: "https://mozilla1.com/1", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }, ]; let bookmarksSize = await getBookmarksSize(); Assert.equal(bookmarksSize, 0, "empty bookmarks yields 0 size"); for (let placeInfo of bookmarks) { await PlacesUtils.bookmarks.insert(placeInfo); } bookmarksSize = await getBookmarksSize(); Assert.equal(bookmarksSize, 2, "size 2 for 2 bookmarks added"); let bookmarkGuid = await new Promise(resolve => PlacesUtils.bookmarks.fetch({ url: bookmarks[0].url }, bookmark => resolve(bookmark.guid) ) ); await provider.deleteBookmark(bookmarkGuid); Assert.strictEqual( await PlacesUtils.bookmarks.fetch(bookmarkGuid), null, "the bookmark should no longer be found" ); bookmarksSize = await getBookmarksSize(); Assert.equal(bookmarksSize, 1, "size 1 after deleting"); }); add_task(async function activityStream_blockedURLs() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; NewTabUtils.blockedLinks.addObserver(provider); let { TRANSITION_TYPED } = PlacesUtils.history; let timeToday = timeDaysAgo(0); let timeEarlier = timeDaysAgo(2); let visits = [ { uri: "https://example1.com/", visitDate: timeToday, transition: TRANSITION_TYPED, }, { uri: "https://example2.com/", visitDate: timeToday, transition: TRANSITION_TYPED, }, { uri: "https://example3.com/", visitDate: timeEarlier, transition: TRANSITION_TYPED, }, { uri: "https://example4.com/", visitDate: timeEarlier, transition: TRANSITION_TYPED, }, ]; await PlacesTestUtils.addVisits(visits); await PlacesUtils.bookmarks.insert({ url: "https://example5.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); let sizeQueryResult; // bookmarks sizeQueryResult = await getBookmarksSize(); Assert.equal(sizeQueryResult, 1, "got the correct bookmark size"); }); add_task(async function activityStream_getTotalBookmarksCount() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamProvider; let bookmarks = [ { url: "https://mozilla1.com/0", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }, { url: "https://mozilla1.com/1", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }, ]; let bookmarksSize = await provider.getTotalBookmarksCount(); Assert.equal( bookmarksSize, 0, ".getTotalBookmarksCount() returns 0 for an empty bookmarks table" ); for (const bookmark of bookmarks) { await PlacesUtils.bookmarks.insert(bookmark); } bookmarksSize = await provider.getTotalBookmarksCount(); Assert.equal( bookmarksSize, 2, ".getTotalBookmarksCount() returns 2 after 2 bookmarks are inserted" ); }); function TestProvider(getLinksFn) { this.getLinks = getLinksFn; this._observers = new Set(); } TestProvider.prototype = { addObserver(observer) { this._observers.add(observer); }, notifyLinkChanged(link, index = -1, deleted = false) { this._notifyObservers("onLinkChanged", link, index, deleted); }, notifyManyLinksChanged() { this._notifyObservers("onManyLinksChanged"); }, _notifyObservers() { let observerMethodName = arguments[0]; let args = Array.prototype.slice.call(arguments, 1); args.unshift(this); for (let obs of this._observers) { if (obs[observerMethodName]) { obs[observerMethodName].apply(NewTabUtils.links, args); } } }, };