import { actionCreators as ac, actionTypes as at, } from "common/Actions.sys.mjs"; import { GlobalOverrider } from "test/unit/utils"; import injector from "inject!lib/PlacesFeed.jsm"; const FAKE_BOOKMARK = { bookmarkGuid: "xi31", bookmarkTitle: "Foo", dateAdded: 123214232, url: "foo.com", }; const TYPE_BOOKMARK = 0; // This is fake, for testing const SOURCES = { DEFAULT: 0, SYNC: 1, IMPORT: 2, RESTORE: 5, RESTORE_ON_STARTUP: 6, }; const BLOCKED_EVENT = "newtab-linkBlocked"; // The event dispatched in NewTabUtils when a link is blocked; const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors"; const POCKET_SITE_PREF = "extensions.pocket.site"; describe("PlacesFeed", () => { let PlacesFeed; let PlacesObserver; let globals; let sandbox; let feed; let shortURLStub; beforeEach(() => { globals = new GlobalOverrider(); sandbox = globals.sandbox; globals.set("NewTabUtils", { activityStreamProvider: { getBookmark() {} }, activityStreamLinks: { addBookmark: sandbox.spy(), deleteBookmark: sandbox.spy(), deleteHistoryEntry: sandbox.spy(), blockURL: sandbox.spy(), addPocketEntry: sandbox.spy(() => Promise.resolve()), deletePocketEntry: sandbox.spy(() => Promise.resolve()), archivePocketEntry: sandbox.spy(() => Promise.resolve()), }, }); globals.set("pktApi", { isUserLoggedIn: sandbox.spy(), }); globals.set("ExperimentAPI", { getExperiment: sandbox.spy(), }); globals.set("NimbusFeatures", { pocketNewtab: { getVariable: sandbox.spy(), }, }); globals.set("PartnerLinkAttribution", { makeRequest: sandbox.spy(), }); sandbox .stub(global.PlacesUtils.bookmarks, "TYPE_BOOKMARK") .value(TYPE_BOOKMARK); sandbox.stub(global.PlacesUtils.bookmarks, "SOURCES").value(SOURCES); sandbox.spy(global.PlacesUtils.history, "addObserver"); sandbox.spy(global.PlacesUtils.history, "removeObserver"); sandbox.spy(global.PlacesUtils.observers, "addListener"); sandbox.spy(global.PlacesUtils.observers, "removeListener"); sandbox.spy(global.Services.obs, "addObserver"); sandbox.spy(global.Services.obs, "removeObserver"); sandbox.spy(global.console, "error"); shortURLStub = sandbox .stub() .callsFake(site => site.url.replace(/(.com|.ca)/, "").replace("https://", "") ); global.Services.io.newURI = spec => ({ mutate: () => ({ setRef: ref => ({ finalize: () => ({ ref, spec, }), }), }), spec, scheme: "https", }); global.Cc["@mozilla.org/timer;1"] = { createInstance() { return { initWithCallback: sinon.stub().callsFake(callback => callback()), cancel: sinon.spy(), }; }, }; ({ PlacesFeed } = injector({ "lib/ShortURL.jsm": { shortURL: shortURLStub }, })); PlacesObserver = PlacesFeed.PlacesObserver; feed = new PlacesFeed(); feed.store = { dispatch: sinon.spy() }; globals.set("AboutNewTab", { activityStream: { store: { feeds: { get() {} } } }, }); }); afterEach(() => { globals.restore(); sandbox.restore(); }); it("should have a PlacesObserver that dispatches to the store", () => { assert.instanceOf(feed.placesObserver, PlacesObserver); const action = { type: "FOO" }; feed.placesObserver.dispatch(action); assert.calledOnce(feed.store.dispatch); assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type); }); describe("#addToBlockedTopSitesSponsors", () => { let spy; beforeEach(() => { sandbox .stub(global.Services.prefs, "getStringPref") .withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF) .returns(`["foo","bar"]`); spy = sandbox.spy(global.Services.prefs, "setStringPref"); }); it("should add the blocked sponsors to the blocklist", () => { feed.addToBlockedTopSitesSponsors([ { url: "test.com" }, { url: "test1.com" }, ]); assert.calledOnce(spy); const [, sponsors] = spy.firstCall.args; assert.deepEqual( new Set(["foo", "bar", "test", "test1"]), new Set(JSON.parse(sponsors)) ); }); it("should not add duplicate sponsors to the blocklist", () => { feed.addToBlockedTopSitesSponsors([ { url: "foo.com" }, { url: "bar.com" }, { url: "test.com" }, ]); assert.calledOnce(spy); const [, sponsors] = spy.firstCall.args; assert.deepEqual( new Set(["foo", "bar", "test"]), new Set(JSON.parse(sponsors)) ); }); }); describe("#onAction", () => { it("should add bookmark, history, places, blocked observers on INIT", () => { feed.onAction({ type: at.INIT }); assert.calledWith( global.PlacesUtils.observers.addListener, [ "bookmark-added", "bookmark-removed", "history-cleared", "page-removed", ], feed.placesObserver.handlePlacesEvent ); assert.calledWith(global.Services.obs.addObserver, feed, BLOCKED_EVENT); }); it("should remove bookmark, history, places, blocked observers, and timers on UNINIT", () => { feed.placesChangedTimer = global.Cc["@mozilla.org/timer;1"].createInstance(); let spy = feed.placesChangedTimer.cancel; feed.onAction({ type: at.UNINIT }); assert.calledWith( global.PlacesUtils.observers.removeListener, [ "bookmark-added", "bookmark-removed", "history-cleared", "page-removed", ], feed.placesObserver.handlePlacesEvent ); assert.calledWith( global.Services.obs.removeObserver, feed, BLOCKED_EVENT ); assert.equal(feed.placesChangedTimer, null); assert.calledOnce(spy); }); it("should block a url on BLOCK_URL", () => { feed.onAction({ type: at.BLOCK_URL, data: [{ url: "apple.com", pocket_id: 1234 }], }); assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, { url: "apple.com", pocket_id: 1234, }); }); it("should update the blocked top sites sponsors", () => { sandbox.stub(feed, "addToBlockedTopSitesSponsors"); feed.onAction({ type: at.BLOCK_URL, data: [{ url: "foo.com", pocket_id: 1234, isSponsoredTopSite: 1 }], }); assert.calledWith(feed.addToBlockedTopSitesSponsors, [ { url: "foo.com" }, ]); }); it("should bookmark a url on BOOKMARK_URL", () => { const data = { url: "pear.com", title: "A pear" }; const _target = { browser: { ownerGlobal() {} } }; feed.onAction({ type: at.BOOKMARK_URL, data, _target }); assert.calledWith( global.NewTabUtils.activityStreamLinks.addBookmark, data, _target.browser.ownerGlobal ); }); it("should delete a bookmark on DELETE_BOOKMARK_BY_ID", () => { feed.onAction({ type: at.DELETE_BOOKMARK_BY_ID, data: "g123kd" }); assert.calledWith( global.NewTabUtils.activityStreamLinks.deleteBookmark, "g123kd" ); }); it("should delete a history entry on DELETE_HISTORY_URL", () => { feed.onAction({ type: at.DELETE_HISTORY_URL, data: { url: "guava.com", forceBlock: null }, }); assert.calledWith( global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com" ); assert.notCalled(global.NewTabUtils.activityStreamLinks.blockURL); }); it("should delete a history entry on DELETE_HISTORY_URL and force a site to be blocked if specified", () => { feed.onAction({ type: at.DELETE_HISTORY_URL, data: { url: "guava.com", forceBlock: "g123kd" }, }); assert.calledWith( global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com" ); assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, { url: "guava.com", pocket_id: undefined, }); }); it("should call openTrustedLinkIn with the correct url, where and params on OPEN_NEW_WINDOW", () => { const openTrustedLinkIn = sinon.stub(); const openWindowAction = { type: at.OPEN_NEW_WINDOW, data: { url: "https://foo.com" }, _target: { browser: { ownerGlobal: { openTrustedLinkIn } } }, }; feed.onAction(openWindowAction); assert.calledOnce(openTrustedLinkIn); const [url, where, params] = openTrustedLinkIn.firstCall.args; assert.equal(url, "https://foo.com"); assert.equal(where, "window"); assert.propertyVal(params, "private", false); assert.propertyVal(params, "forceForeground", false); }); it("should call openTrustedLinkIn with the correct url, where, params and privacy args on OPEN_PRIVATE_WINDOW", () => { const openTrustedLinkIn = sinon.stub(); const openWindowAction = { type: at.OPEN_PRIVATE_WINDOW, data: { url: "https://foo.com" }, _target: { browser: { ownerGlobal: { openTrustedLinkIn } } }, }; feed.onAction(openWindowAction); assert.calledOnce(openTrustedLinkIn); const [url, where, params] = openTrustedLinkIn.firstCall.args; assert.equal(url, "https://foo.com"); assert.equal(where, "window"); assert.propertyVal(params, "private", true); assert.propertyVal(params, "forceForeground", false); }); it("should call openTrustedLinkIn with the correct url, where and params on OPEN_LINK", () => { const openTrustedLinkIn = sinon.stub(); const openLinkAction = { type: at.OPEN_LINK, data: { url: "https://foo.com" }, _target: { browser: { ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "current" }, }, }, }; feed.onAction(openLinkAction); assert.calledOnce(openTrustedLinkIn); const [url, where, params] = openTrustedLinkIn.firstCall.args; assert.equal(url, "https://foo.com"); assert.equal(where, "current"); assert.propertyVal(params, "private", false); assert.propertyVal(params, "forceForeground", false); }); it("should open link with referrer on OPEN_LINK", () => { const openTrustedLinkIn = sinon.stub(); const openLinkAction = { type: at.OPEN_LINK, data: { url: "https://foo.com", referrer: "https://foo.com/ref" }, _target: { browser: { ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "tab" }, }, }, }; feed.onAction(openLinkAction); const [, , params] = openTrustedLinkIn.firstCall.args; assert.nestedPropertyVal(params, "referrerInfo.referrerPolicy", 5); assert.nestedPropertyVal( params, "referrerInfo.originalReferrer.spec", "https://foo.com/ref" ); }); it("should mark link with typed bonus as typed before opening OPEN_LINK", () => { const callOrder = []; sinon .stub(global.PlacesUtils.history, "markPageAsTyped") .callsFake(() => { callOrder.push("markPageAsTyped"); }); const openTrustedLinkIn = sinon.stub().callsFake(() => { callOrder.push("openTrustedLinkIn"); }); const openLinkAction = { type: at.OPEN_LINK, data: { typedBonus: true, url: "https://foo.com", }, _target: { browser: { ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "tab" }, }, }, }; feed.onAction(openLinkAction); assert.sameOrderedMembers(callOrder, [ "markPageAsTyped", "openTrustedLinkIn", ]); }); it("should open the pocket link if it's a pocket story on OPEN_LINK", () => { const openTrustedLinkIn = sinon.stub(); const openLinkAction = { type: at.OPEN_LINK, data: { url: "https://foo.com", open_url: "getpocket.com/foo", type: "pocket", }, _target: { browser: { ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "current" }, }, }, }; feed.onAction(openLinkAction); assert.calledOnce(openTrustedLinkIn); const [url, where, params] = openTrustedLinkIn.firstCall.args; assert.equal(url, "getpocket.com/foo"); assert.equal(where, "current"); assert.propertyVal(params, "private", false); }); it("should not open link if not http", () => { const openTrustedLinkIn = sinon.stub(); global.Services.io.newURI = spec => ({ mutate: () => ({ setRef: ref => ({ finalize: () => ({ ref, spec, }), }), }), spec, scheme: "file", }); const openLinkAction = { type: at.OPEN_LINK, data: { url: "file:///foo.com" }, _target: { browser: { ownerGlobal: { openTrustedLinkIn, whereToOpenLink: e => "current" }, }, }, }; feed.onAction(openLinkAction); const [e] = global.console.error.firstCall.args; assert.equal( e.message, "Can't open link using file protocol from the new tab page." ); }); it("should call fillSearchTopSiteTerm on FILL_SEARCH_TERM", () => { sinon.stub(feed, "fillSearchTopSiteTerm"); feed.onAction({ type: at.FILL_SEARCH_TERM }); assert.calledOnce(feed.fillSearchTopSiteTerm); }); it("should call openTrustedLinkIn with the correct SUMO url on ABOUT_SPONSORED_TOP_SITES", () => { const openTrustedLinkIn = sinon.stub(); const openLinkAction = { type: at.ABOUT_SPONSORED_TOP_SITES, _target: { browser: { ownerGlobal: { openTrustedLinkIn }, }, }, }; feed.onAction(openLinkAction); assert.calledOnce(openTrustedLinkIn); const [url, where] = openTrustedLinkIn.firstCall.args; assert.equal(url.endsWith("sponsor-privacy"), true); assert.equal(where, "tab"); }); it("should set the URL bar value to the label value", async () => { const locationBar = { search: sandbox.stub() }; const action = { type: at.FILL_SEARCH_TERM, data: { label: "@Foo" }, _target: { browser: { ownerGlobal: { gURLBar: locationBar } } }, }; await feed.fillSearchTopSiteTerm(action); assert.calledOnce(locationBar.search); assert.calledWithExactly(locationBar.search, "@Foo", { searchEngine: null, searchModeEntry: "topsites_newtab", }); }); it("should call saveToPocket on SAVE_TO_POCKET", () => { const action = { type: at.SAVE_TO_POCKET, data: { site: { url: "raspberry.com", title: "raspberry" } }, _target: { browser: {} }, }; sinon.stub(feed, "saveToPocket"); feed.onAction(action); assert.calledWithExactly( feed.saveToPocket, action.data.site, action._target.browser ); }); it("should openTrustedLinkIn with sendToPocket if not logged in", () => { const openTrustedLinkIn = sinon.stub(); global.NimbusFeatures.pocketNewtab.getVariable = sandbox .stub() .returns(true); global.pktApi.isUserLoggedIn = sandbox.stub().returns(false); global.ExperimentAPI.getExperiment = sandbox.stub().returns({ slug: "slug", branch: { slug: "branch-slug" }, }); sandbox .stub(global.Services.prefs, "getStringPref") .withArgs(POCKET_SITE_PREF) .returns("getpocket.com"); const action = { type: at.SAVE_TO_POCKET, data: { site: { url: "raspberry.com", title: "raspberry" } }, _target: { browser: { ownerGlobal: { openTrustedLinkIn, }, }, }, }; feed.onAction(action); assert.calledOnce(openTrustedLinkIn); const [url, where] = openTrustedLinkIn.firstCall.args; assert.equal( url, "https://getpocket.com/signup?utm_source=firefox_newtab_save_button&utm_campaign=slug&utm_content=branch-slug" ); assert.equal(where, "tab"); }); it("should call NewTabUtils.activityStreamLinks.addPocketEntry if we are saving a pocket story", async () => { const action = { data: { site: { url: "raspberry.com", title: "raspberry" } }, _target: { browser: {} }, }; await feed.saveToPocket(action.data.site, action._target.browser); assert.calledOnce(global.NewTabUtils.activityStreamLinks.addPocketEntry); assert.calledWithExactly( global.NewTabUtils.activityStreamLinks.addPocketEntry, action.data.site.url, action.data.site.title, action._target.browser ); }); it("should reject the promise if NewTabUtils.activityStreamLinks.addPocketEntry rejects", async () => { const e = new Error("Error"); const action = { data: { site: { url: "raspberry.com", title: "raspberry" } }, _target: { browser: {} }, }; global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox .stub() .rejects(e); await feed.saveToPocket(action.data.site, action._target.browser); assert.calledWith(global.console.error, e); }); it("should broadcast to content if we successfully added a link to Pocket", async () => { // test in the form that the API returns data based on: https://getpocket.com/developer/docs/v3/add global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox .stub() .resolves({ item: { open_url: "pocket.com/itemID", item_id: 1234 } }); const action = { data: { site: { url: "raspberry.com", title: "raspberry" } }, _target: { browser: {} }, }; await feed.saveToPocket(action.data.site, action._target.browser); assert.equal( feed.store.dispatch.firstCall.args[0].type, at.PLACES_SAVED_TO_POCKET ); assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, { url: "raspberry.com", title: "raspberry", pocket_id: 1234, open_url: "pocket.com/itemID", }); }); it("should only broadcast if we got some data back from addPocketEntry", async () => { global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox .stub() .resolves(null); const action = { data: { site: { url: "raspberry.com", title: "raspberry" } }, _target: { browser: {} }, }; await feed.saveToPocket(action.data.site, action._target.browser); assert.notCalled(feed.store.dispatch); }); it("should call deleteFromPocket on DELETE_FROM_POCKET", () => { sandbox.stub(feed, "deleteFromPocket"); feed.onAction({ type: at.DELETE_FROM_POCKET, data: { pocket_id: 12345 }, }); assert.calledOnce(feed.deleteFromPocket); assert.calledWithExactly(feed.deleteFromPocket, 12345); }); it("should catch if deletePocketEntry throws", async () => { const e = new Error("Error"); global.NewTabUtils.activityStreamLinks.deletePocketEntry = sandbox .stub() .rejects(e); await feed.deleteFromPocket(12345); assert.calledWith(global.console.error, e); }); it("should call NewTabUtils.deletePocketEntry and dispatch POCKET_LINK_DELETED_OR_ARCHIVED when deleting from Pocket", async () => { await feed.deleteFromPocket(12345); assert.calledOnce( global.NewTabUtils.activityStreamLinks.deletePocketEntry ); assert.calledWith( global.NewTabUtils.activityStreamLinks.deletePocketEntry, 12345 ); assert.calledOnce(feed.store.dispatch); assert.calledWith(feed.store.dispatch, { type: at.POCKET_LINK_DELETED_OR_ARCHIVED, }); }); it("should call archiveFromPocket on ARCHIVE_FROM_POCKET", async () => { sandbox.stub(feed, "archiveFromPocket"); await feed.onAction({ type: at.ARCHIVE_FROM_POCKET, data: { pocket_id: 12345 }, }); assert.calledOnce(feed.archiveFromPocket); assert.calledWithExactly(feed.archiveFromPocket, 12345); }); it("should catch if archiveFromPocket throws", async () => { const e = new Error("Error"); global.NewTabUtils.activityStreamLinks.archivePocketEntry = sandbox .stub() .rejects(e); await feed.archiveFromPocket(12345); assert.calledWith(global.console.error, e); }); it("should call NewTabUtils.archivePocketEntry and dispatch POCKET_LINK_DELETED_OR_ARCHIVED when archiving from Pocket", async () => { await feed.archiveFromPocket(12345); assert.calledOnce( global.NewTabUtils.activityStreamLinks.archivePocketEntry ); assert.calledWith( global.NewTabUtils.activityStreamLinks.archivePocketEntry, 12345 ); assert.calledOnce(feed.store.dispatch); assert.calledWith(feed.store.dispatch, { type: at.POCKET_LINK_DELETED_OR_ARCHIVED, }); }); it("should call handoffSearchToAwesomebar on HANDOFF_SEARCH_TO_AWESOMEBAR", () => { const action = { type: at.HANDOFF_SEARCH_TO_AWESOMEBAR, data: { text: "f" }, meta: { fromTarget: {} }, _target: { browser: { ownerGlobal: { gURLBar: { focus: () => {} } } } }, }; sinon.stub(feed, "handoffSearchToAwesomebar"); feed.onAction(action); assert.calledWith(feed.handoffSearchToAwesomebar, action); }); it("should call makeAttributionRequest on PARTNER_LINK_ATTRIBUTION", () => { sinon.stub(feed, "makeAttributionRequest"); let data = { targetURL: "https://partnersite.com", source: "topsites" }; feed.onAction({ type: at.PARTNER_LINK_ATTRIBUTION, data, }); assert.calledOnce(feed.makeAttributionRequest); assert.calledWithExactly(feed.makeAttributionRequest, data); }); it("should call PartnerLinkAttribution.makeRequest when calling makeAttributionRequest", () => { let data = { targetURL: "https://partnersite.com", source: "topsites" }; feed.makeAttributionRequest(data); assert.calledOnce(global.PartnerLinkAttribution.makeRequest); }); }); describe("handoffSearchToAwesomebar", () => { let fakeUrlBar; let listeners; beforeEach(() => { fakeUrlBar = { focus: sinon.spy(), handoff: sinon.spy(), setHiddenFocus: sinon.spy(), removeHiddenFocus: sinon.spy(), addEventListener: (ev, cb) => { listeners[ev] = cb; }, removeEventListener: sinon.spy(), }; listeners = {}; }); it("should properly handle handoff with no text passed in", () => { feed.handoffSearchToAwesomebar({ _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } }, data: {}, meta: { fromTarget: {} }, }); assert.calledOnce(fakeUrlBar.setHiddenFocus); assert.notCalled(fakeUrlBar.handoff); assert.notCalled(feed.store.dispatch); // Now type a character. listeners.keydown({ key: "f" }); assert.calledOnce(fakeUrlBar.handoff); assert.calledOnce(fakeUrlBar.removeHiddenFocus); assert.calledOnce(feed.store.dispatch); assert.calledWith(feed.store.dispatch, { meta: { from: "ActivityStream:Main", skipMain: true, to: "ActivityStream:Content", toTarget: {}, }, type: "DISABLE_SEARCH", }); }); it("should properly handle handoff with text data passed in", () => { const sessionId = "decafc0ffee"; sandbox .stub(global.AboutNewTab.activityStream.store.feeds, "get") .returns({ sessions: { get: () => { return { session_id: sessionId }; }, }, }); feed.handoffSearchToAwesomebar({ _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } }, data: { text: "foo" }, meta: { fromTarget: {} }, }); assert.calledOnce(fakeUrlBar.handoff); assert.calledWithExactly( fakeUrlBar.handoff, "foo", global.Services.search.defaultEngine, sessionId ); assert.notCalled(fakeUrlBar.focus); assert.notCalled(fakeUrlBar.setHiddenFocus); // Now call blur listener. listeners.blur(); assert.calledOnce(feed.store.dispatch); assert.calledWith(feed.store.dispatch, { meta: { from: "ActivityStream:Main", skipMain: true, to: "ActivityStream:Content", toTarget: {}, }, type: "SHOW_SEARCH", }); }); it("should properly handle handoff with text data passed in, in private browsing mode", () => { global.PrivateBrowsingUtils.isBrowserPrivate = () => true; feed.handoffSearchToAwesomebar({ _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } }, data: { text: "foo" }, meta: { fromTarget: {} }, }); assert.calledOnce(fakeUrlBar.handoff); assert.calledWithExactly( fakeUrlBar.handoff, "foo", global.Services.search.defaultPrivateEngine, undefined ); assert.notCalled(fakeUrlBar.focus); assert.notCalled(fakeUrlBar.setHiddenFocus); // Now call blur listener. listeners.blur(); assert.calledOnce(feed.store.dispatch); assert.calledWith(feed.store.dispatch, { meta: { from: "ActivityStream:Main", skipMain: true, to: "ActivityStream:Content", toTarget: {}, }, type: "SHOW_SEARCH", }); global.PrivateBrowsingUtils.isBrowserPrivate = () => false; }); it("should SHOW_SEARCH on ESC keydown", () => { feed.handoffSearchToAwesomebar({ _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } }, data: { text: "foo" }, meta: { fromTarget: {} }, }); assert.calledOnce(fakeUrlBar.handoff); assert.calledWithExactly( fakeUrlBar.handoff, "foo", global.Services.search.defaultEngine, undefined ); assert.notCalled(fakeUrlBar.focus); // Now call ESC keydown. listeners.keydown({ key: "Escape" }); assert.calledOnce(feed.store.dispatch); assert.calledWith(feed.store.dispatch, { meta: { from: "ActivityStream:Main", skipMain: true, to: "ActivityStream:Content", toTarget: {}, }, type: "SHOW_SEARCH", }); }); it("should properly handoff a newtab session id with no text passed in", () => { const sessionId = "decafc0ffee"; sandbox .stub(global.AboutNewTab.activityStream.store.feeds, "get") .returns({ sessions: { get: () => { return { session_id: sessionId }; }, }, }); feed.handoffSearchToAwesomebar({ _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } }, data: {}, meta: { fromTarget: {} }, }); assert.calledOnce(fakeUrlBar.setHiddenFocus); assert.notCalled(fakeUrlBar.handoff); assert.notCalled(feed.store.dispatch); // Now type a character. listeners.keydown({ key: "f" }); assert.calledOnce(fakeUrlBar.handoff); assert.calledWithExactly( fakeUrlBar.handoff, "", global.Services.search.defaultEngine, sessionId ); assert.calledOnce(fakeUrlBar.removeHiddenFocus); assert.calledOnce(feed.store.dispatch); assert.calledWith(feed.store.dispatch, { meta: { from: "ActivityStream:Main", skipMain: true, to: "ActivityStream:Content", toTarget: {}, }, type: "DISABLE_SEARCH", }); }); }); describe("#observe", () => { it("should dispatch a PLACES_LINK_BLOCKED action with the url of the blocked link", () => { feed.observe(null, BLOCKED_EVENT, "foo123.com"); assert.equal( feed.store.dispatch.firstCall.args[0].type, at.PLACES_LINK_BLOCKED ); assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, { url: "foo123.com", }); }); it("should not call dispatch if the topic is something other than BLOCKED_EVENT", () => { feed.observe(null, "someotherevent"); assert.notCalled(feed.store.dispatch); }); }); describe("Custom dispatch", () => { it("should only dispatch 1 PLACES_LINKS_CHANGED action if many bookmark-added notifications happened at once", async () => { // Yes, onItemAdded has at least 8 arguments. See function definition for docs. const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.DEFAULT, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "https://www.foo.com", isTagging: false, type: "bookmark-added", }, ]; await feed.placesObserver.handlePlacesEvent(args); await feed.placesObserver.handlePlacesEvent(args); await feed.placesObserver.handlePlacesEvent(args); await feed.placesObserver.handlePlacesEvent(args); assert.calledOnce( feed.store.dispatch.withArgs( ac.OnlyToMain({ type: at.PLACES_LINKS_CHANGED }) ) ); }); it("should only dispatch 1 PLACES_LINKS_CHANGED action if many onItemRemoved notifications happened at once", async () => { const args = [ { id: null, parentId: null, index: null, itemType: TYPE_BOOKMARK, url: "foo.com", guid: "123foo", parentGuid: "", source: SOURCES.DEFAULT, type: "bookmark-removed", }, ]; await feed.placesObserver.handlePlacesEvent(args); await feed.placesObserver.handlePlacesEvent(args); await feed.placesObserver.handlePlacesEvent(args); await feed.placesObserver.handlePlacesEvent(args); assert.calledOnce( feed.store.dispatch.withArgs( ac.OnlyToMain({ type: at.PLACES_LINKS_CHANGED }) ) ); }); it("should only dispatch 1 PLACES_LINKS_CHANGED action if any page-removed notifications happened at once", async () => { await feed.placesObserver.handlePlacesEvent([ { type: "page-removed", url: "foo.com", isRemovedFromStore: true }, ]); await feed.placesObserver.handlePlacesEvent([ { type: "page-removed", url: "foo1.com", isRemovedFromStore: true }, ]); await feed.placesObserver.handlePlacesEvent([ { type: "page-removed", url: "foo2.com", isRemovedFromStore: true }, ]); assert.calledOnce( feed.store.dispatch.withArgs( ac.OnlyToMain({ type: at.PLACES_LINKS_CHANGED }) ) ); }); }); describe("PlacesObserver", () => { let dispatch; let observer; beforeEach(() => { dispatch = sandbox.spy(); observer = new PlacesObserver(dispatch); }); describe("#history-cleared", () => { it("should dispatch a PLACES_HISTORY_CLEARED action", async () => { const args = [{ type: "history-cleared" }]; await observer.handlePlacesEvent(args); assert.calledWith(dispatch, { type: at.PLACES_HISTORY_CLEARED }); }); }); describe("#page-removed", () => { it("should dispatch a PLACES_LINKS_DELETED action with the right url", async () => { const args = [ { type: "page-removed", url: "foo.com", isRemovedFromStore: true, }, ]; await observer.handlePlacesEvent(args); assert.calledWith(dispatch, { type: at.PLACES_LINKS_DELETED, data: { urls: ["foo.com"] }, }); }); }); describe("#bookmark-added", () => { it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - http", async () => { const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.DEFAULT, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "http://www.foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.calledWith(dispatch.secondCall, { type: at.PLACES_BOOKMARK_ADDED, data: { bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid, bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle, dateAdded: FAKE_BOOKMARK.dateAdded * 1000, url: "http://www.foo.com", }, }); }); it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - https", async () => { const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.DEFAULT, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "https://www.foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.calledWith(dispatch.secondCall, { type: at.PLACES_BOOKMARK_ADDED, data: { bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid, bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle, dateAdded: FAKE_BOOKMARK.dateAdded * 1000, url: "https://www.foo.com", }, }); }); it("should not dispatch a PLACES_BOOKMARK_ADDED action - not http/https", async () => { const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.DEFAULT, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARK_ADDED action - has IMPORT source", async () => { const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.IMPORT, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE source", async () => { const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.RESTORE, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE_ON_STARTUP source", async () => { const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.RESTORE_ON_STARTUP, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARK_ADDED action - has SYNC source", async () => { const args = [ { itemType: TYPE_BOOKMARK, source: SOURCES.SYNC, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should ignore events that are not of TYPE_BOOKMARK", async () => { const args = [ { itemType: "nottypebookmark", source: SOURCES.DEFAULT, dateAdded: FAKE_BOOKMARK.dateAdded, guid: FAKE_BOOKMARK.bookmarkGuid, title: FAKE_BOOKMARK.bookmarkTitle, url: "https://www.foo.com", isTagging: false, type: "bookmark-added", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); }); describe("#bookmark-removed", () => { it("should ignore events that are not of TYPE_BOOKMARK", async () => { const args = [ { id: null, parentId: null, index: null, itemType: "nottypebookmark", url: null, guid: "123foo", parentGuid: "", source: SOURCES.DEFAULT, type: "bookmark-removed", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARKS_REMOVED action - has SYNC source", async () => { const args = [ { id: null, parentId: null, index: null, itemType: TYPE_BOOKMARK, url: "foo.com", guid: "123foo", parentGuid: "", source: SOURCES.SYNC, type: "bookmark-removed", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARKS_REMOVED action - has IMPORT source", async () => { const args = [ { id: null, parentId: null, index: null, itemType: TYPE_BOOKMARK, url: "foo.com", guid: "123foo", parentGuid: "", source: SOURCES.IMPORT, type: "bookmark-removed", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARKS_REMOVED action - has RESTORE source", async () => { const args = [ { id: null, parentId: null, index: null, itemType: TYPE_BOOKMARK, url: "foo.com", guid: "123foo", parentGuid: "", source: SOURCES.RESTORE, type: "bookmark-removed", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should not dispatch a PLACES_BOOKMARKS_REMOVED action - has RESTORE_ON_STARTUP source", async () => { const args = [ { id: null, parentId: null, index: null, itemType: TYPE_BOOKMARK, url: "foo.com", guid: "123foo", parentGuid: "", source: SOURCES.RESTORE_ON_STARTUP, type: "bookmark-removed", }, ]; await observer.handlePlacesEvent(args); assert.notCalled(dispatch); }); it("should dispatch a PLACES_BOOKMARKS_REMOVED action with the right URL and bookmarkGuid", async () => { const args = [ { id: null, parentId: null, index: null, itemType: TYPE_BOOKMARK, url: "foo.com", guid: "123foo", parentGuid: "", source: SOURCES.DEFAULT, type: "bookmark-removed", }, ]; await observer.handlePlacesEvent(args); assert.calledWith(dispatch, { type: at.PLACES_BOOKMARKS_REMOVED, data: { urls: ["foo.com"] }, }); }); }); }); });