PlacesFeed.test.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
  2. import {GlobalOverrider} from "test/unit/utils";
  3. import {PlacesFeed} from "lib/PlacesFeed.jsm";
  4. const {HistoryObserver, BookmarksObserver, PlacesObserver} = PlacesFeed;
  5. const FAKE_BOOKMARK = {bookmarkGuid: "xi31", bookmarkTitle: "Foo", dateAdded: 123214232, url: "foo.com"};
  6. const TYPE_BOOKMARK = 0; // This is fake, for testing
  7. const SOURCES = {
  8. DEFAULT: 0,
  9. SYNC: 1,
  10. IMPORT: 2,
  11. RESTORE: 5,
  12. RESTORE_ON_STARTUP: 6,
  13. };
  14. const BLOCKED_EVENT = "newtab-linkBlocked"; // The event dispatched in NewTabUtils when a link is blocked;
  15. describe("PlacesFeed", () => {
  16. let globals;
  17. let sandbox;
  18. let feed;
  19. beforeEach(() => {
  20. globals = new GlobalOverrider();
  21. sandbox = globals.sandbox;
  22. globals.set("NewTabUtils", {
  23. activityStreamProvider: {getBookmark() {}},
  24. activityStreamLinks: {
  25. addBookmark: sandbox.spy(),
  26. deleteBookmark: sandbox.spy(),
  27. deleteHistoryEntry: sandbox.spy(),
  28. blockURL: sandbox.spy(),
  29. addPocketEntry: sandbox.spy(() => Promise.resolve()),
  30. deletePocketEntry: sandbox.spy(() => Promise.resolve()),
  31. archivePocketEntry: sandbox.spy(() => Promise.resolve()),
  32. },
  33. });
  34. sandbox.stub(global.PlacesUtils.bookmarks, "TYPE_BOOKMARK").value(TYPE_BOOKMARK);
  35. sandbox.stub(global.PlacesUtils.bookmarks, "SOURCES").value(SOURCES);
  36. sandbox.spy(global.PlacesUtils.bookmarks, "addObserver");
  37. sandbox.spy(global.PlacesUtils.bookmarks, "removeObserver");
  38. sandbox.spy(global.PlacesUtils.history, "addObserver");
  39. sandbox.spy(global.PlacesUtils.history, "removeObserver");
  40. sandbox.spy(global.PlacesUtils.observers, "addListener");
  41. sandbox.spy(global.PlacesUtils.observers, "removeListener");
  42. sandbox.spy(global.Services.obs, "addObserver");
  43. sandbox.spy(global.Services.obs, "removeObserver");
  44. sandbox.spy(global.Cu, "reportError");
  45. global.Cc["@mozilla.org/timer;1"] = {
  46. createInstance() {
  47. return {
  48. initWithCallback: sinon.stub().callsFake(callback => callback()),
  49. cancel: sinon.spy(),
  50. };
  51. },
  52. };
  53. feed = new PlacesFeed();
  54. feed.store = {dispatch: sinon.spy()};
  55. });
  56. afterEach(() => globals.restore());
  57. it("should have a HistoryObserver that dispatches to the store", () => {
  58. assert.instanceOf(feed.historyObserver, HistoryObserver);
  59. const action = {type: "FOO"};
  60. feed.historyObserver.dispatch(action);
  61. assert.calledOnce(feed.store.dispatch);
  62. assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
  63. });
  64. it("should have a BookmarksObserver that dispatch to the store", () => {
  65. assert.instanceOf(feed.bookmarksObserver, BookmarksObserver);
  66. const action = {type: "FOO"};
  67. feed.bookmarksObserver.dispatch(action);
  68. assert.calledOnce(feed.store.dispatch);
  69. assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
  70. });
  71. it("should have a PlacesObserver that dispatches to the store", () => {
  72. assert.instanceOf(feed.placesObserver, PlacesObserver);
  73. const action = {type: "FOO"};
  74. feed.placesObserver.dispatch(action);
  75. assert.calledOnce(feed.store.dispatch);
  76. assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
  77. });
  78. describe("#onAction", () => {
  79. it("should add bookmark, history, places, blocked observers on INIT", () => {
  80. feed.onAction({type: at.INIT});
  81. assert.calledWith(global.PlacesUtils.history.addObserver, feed.historyObserver, true);
  82. assert.calledWith(global.PlacesUtils.bookmarks.addObserver, feed.bookmarksObserver, true);
  83. assert.calledWith(global.PlacesUtils.observers.addListener, ["bookmark-added"], feed.placesObserver.handlePlacesEvent);
  84. assert.calledWith(global.Services.obs.addObserver, feed, BLOCKED_EVENT);
  85. });
  86. it("should remove bookmark, history, places, blocked observers, and timers on UNINIT", () => {
  87. feed.placesChangedTimer = global.Cc["@mozilla.org/timer;1"].createInstance();
  88. let spy = feed.placesChangedTimer.cancel;
  89. feed.onAction({type: at.UNINIT});
  90. assert.calledWith(global.PlacesUtils.history.removeObserver, feed.historyObserver);
  91. assert.calledWith(global.PlacesUtils.bookmarks.removeObserver, feed.bookmarksObserver);
  92. assert.calledWith(global.PlacesUtils.observers.removeListener, ["bookmark-added"], feed.placesObserver.handlePlacesEvent);
  93. assert.calledWith(global.Services.obs.removeObserver, feed, BLOCKED_EVENT);
  94. assert.equal(feed.placesChangedTimer, null);
  95. assert.calledOnce(spy);
  96. });
  97. it("should block a url on BLOCK_URL", () => {
  98. feed.onAction({type: at.BLOCK_URL, data: {url: "apple.com", pocket_id: 1234}});
  99. assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "apple.com", pocket_id: 1234});
  100. });
  101. it("should bookmark a url on BOOKMARK_URL", () => {
  102. const data = {url: "pear.com", title: "A pear"};
  103. const _target = {browser: {ownerGlobal() {}}};
  104. feed.onAction({type: at.BOOKMARK_URL, data, _target});
  105. assert.calledWith(global.NewTabUtils.activityStreamLinks.addBookmark, data, _target.browser.ownerGlobal);
  106. });
  107. it("should delete a bookmark on DELETE_BOOKMARK_BY_ID", () => {
  108. feed.onAction({type: at.DELETE_BOOKMARK_BY_ID, data: "g123kd"});
  109. assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteBookmark, "g123kd");
  110. });
  111. it("should delete a history entry on DELETE_HISTORY_URL", () => {
  112. feed.onAction({type: at.DELETE_HISTORY_URL, data: {url: "guava.com", forceBlock: null}});
  113. assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
  114. assert.notCalled(global.NewTabUtils.activityStreamLinks.blockURL);
  115. });
  116. it("should delete a history entry on DELETE_HISTORY_URL and force a site to be blocked if specified", () => {
  117. feed.onAction({type: at.DELETE_HISTORY_URL, data: {url: "guava.com", forceBlock: "g123kd"}});
  118. assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
  119. assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "guava.com", pocket_id: undefined});
  120. });
  121. it("should call openLinkIn with the correct url and where on OPEN_NEW_WINDOW", () => {
  122. const openLinkIn = sinon.stub();
  123. const openWindowAction = {
  124. type: at.OPEN_NEW_WINDOW,
  125. data: {url: "foo.com"},
  126. _target: {browser: {ownerGlobal: {openLinkIn}}},
  127. };
  128. feed.onAction(openWindowAction);
  129. assert.calledOnce(openLinkIn);
  130. const [url, where, params] = openLinkIn.firstCall.args;
  131. assert.equal(url, "foo.com");
  132. assert.equal(where, "window");
  133. assert.propertyVal(params, "private", false);
  134. });
  135. it("should call openLinkIn with the correct url, where and privacy args on OPEN_PRIVATE_WINDOW", () => {
  136. const openLinkIn = sinon.stub();
  137. const openWindowAction = {
  138. type: at.OPEN_PRIVATE_WINDOW,
  139. data: {url: "foo.com"},
  140. _target: {browser: {ownerGlobal: {openLinkIn}}},
  141. };
  142. feed.onAction(openWindowAction);
  143. assert.calledOnce(openLinkIn);
  144. const [url, where, params] = openLinkIn.firstCall.args;
  145. assert.equal(url, "foo.com");
  146. assert.equal(where, "window");
  147. assert.propertyVal(params, "private", true);
  148. });
  149. it("should open link on OPEN_LINK", () => {
  150. const openLinkIn = sinon.stub();
  151. const openLinkAction = {
  152. type: at.OPEN_LINK,
  153. data: {url: "foo.com"},
  154. _target: {browser: {ownerGlobal: {openLinkIn, whereToOpenLink: e => "current"}}},
  155. };
  156. feed.onAction(openLinkAction);
  157. assert.calledOnce(openLinkIn);
  158. const [url, where, params] = openLinkIn.firstCall.args;
  159. assert.equal(url, "foo.com");
  160. assert.equal(where, "current");
  161. assert.propertyVal(params, "private", false);
  162. assert.propertyVal(params, "triggeringPrincipal", undefined);
  163. });
  164. it("should open link with referrer on OPEN_LINK", () => {
  165. const openLinkIn = sinon.stub();
  166. const openLinkAction = {
  167. type: at.OPEN_LINK,
  168. data: {url: "foo.com", referrer: "foo.com/ref"},
  169. _target: {browser: {ownerGlobal: {openLinkIn, whereToOpenLink: e => "tab"}}},
  170. };
  171. feed.onAction(openLinkAction);
  172. const [, , params] = openLinkIn.firstCall.args;
  173. assert.propertyVal(params, "referrerPolicy", 5);
  174. assert.propertyVal(params.referrerURI, "spec", "foo.com/ref");
  175. });
  176. it("should mark link with typed bonus as typed before opening OPEN_LINK", () => {
  177. const callOrder = [];
  178. sinon.stub(global.PlacesUtils.history, "markPageAsTyped").callsFake(() => {
  179. callOrder.push("markPageAsTyped");
  180. });
  181. const openLinkIn = sinon.stub().callsFake(() => {
  182. callOrder.push("openLinkIn");
  183. });
  184. const openLinkAction = {
  185. type: at.OPEN_LINK,
  186. data: {
  187. typedBonus: true,
  188. url: "foo.com",
  189. },
  190. _target: {browser: {ownerGlobal: {openLinkIn, whereToOpenLink: e => "tab"}}},
  191. };
  192. feed.onAction(openLinkAction);
  193. assert.sameOrderedMembers(callOrder, ["markPageAsTyped", "openLinkIn"]);
  194. });
  195. it("should open the pocket link if it's a pocket story on OPEN_LINK", () => {
  196. const openLinkIn = sinon.stub();
  197. const openLinkAction = {
  198. type: at.OPEN_LINK,
  199. data: {url: "foo.com", open_url: "getpocket.com/foo", type: "pocket"},
  200. _target: {browser: {ownerGlobal: {openLinkIn, whereToOpenLink: e => "current"}}},
  201. };
  202. feed.onAction(openLinkAction);
  203. assert.calledOnce(openLinkIn);
  204. const [url, where, params] = openLinkIn.firstCall.args;
  205. assert.equal(url, "getpocket.com/foo");
  206. assert.equal(where, "current");
  207. assert.propertyVal(params, "private", false);
  208. assert.propertyVal(params, "triggeringPrincipal", undefined);
  209. });
  210. it("should call fillSearchTopSiteTerm on FILL_SEARCH_TERM", () => {
  211. sinon.stub(feed, "fillSearchTopSiteTerm");
  212. feed.onAction({type: at.FILL_SEARCH_TERM});
  213. assert.calledOnce(feed.fillSearchTopSiteTerm);
  214. });
  215. it("should set the URL bar value to the label value", () => {
  216. const locationBar = {search: sandbox.stub()};
  217. const action = {
  218. type: at.FILL_SEARCH_TERM,
  219. data: {label: "@Foo"},
  220. _target: {browser: {ownerGlobal: {gURLBar: locationBar}}},
  221. };
  222. feed.fillSearchTopSiteTerm(action);
  223. assert.calledOnce(locationBar.search);
  224. assert.calledWithExactly(locationBar.search, "@Foo ");
  225. });
  226. it("should call saveToPocket on SAVE_TO_POCKET", () => {
  227. const action = {
  228. type: at.SAVE_TO_POCKET,
  229. data: {site: {url: "raspberry.com", title: "raspberry"}},
  230. _target: {browser: {}},
  231. };
  232. sinon.stub(feed, "saveToPocket");
  233. feed.onAction(action);
  234. assert.calledWithExactly(feed.saveToPocket, action.data.site, action._target.browser);
  235. });
  236. it("should call NewTabUtils.activityStreamLinks.addPocketEntry if we are saving a pocket story", async () => {
  237. const action = {
  238. data: {site: {url: "raspberry.com", title: "raspberry"}},
  239. _target: {browser: {}},
  240. };
  241. await feed.saveToPocket(action.data.site, action._target.browser);
  242. assert.calledOnce(global.NewTabUtils.activityStreamLinks.addPocketEntry);
  243. assert.calledWithExactly(global.NewTabUtils.activityStreamLinks.addPocketEntry, action.data.site.url, action.data.site.title, action._target.browser);
  244. });
  245. it("should reject the promise if NewTabUtils.activityStreamLinks.addPocketEntry rejects", async () => {
  246. const e = new Error("Error");
  247. const action = {
  248. data: {site: {url: "raspberry.com", title: "raspberry"}},
  249. _target: {browser: {}},
  250. };
  251. global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox.stub().rejects(e);
  252. await feed.saveToPocket(action.data.site, action._target.browser);
  253. assert.calledWith(global.Cu.reportError, e);
  254. });
  255. it("should broadcast to content if we successfully added a link to Pocket", async () => {
  256. // test in the form that the API returns data based on: https://getpocket.com/developer/docs/v3/add
  257. global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox.stub().resolves({item: {open_url: "pocket.com/itemID", item_id: 1234}});
  258. const action = {
  259. data: {site: {url: "raspberry.com", title: "raspberry"}},
  260. _target: {browser: {}},
  261. };
  262. await feed.saveToPocket(action.data.site, action._target.browser);
  263. assert.equal(feed.store.dispatch.firstCall.args[0].type, at.PLACES_SAVED_TO_POCKET);
  264. assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
  265. url: "raspberry.com",
  266. title: "raspberry",
  267. pocket_id: 1234,
  268. open_url: "pocket.com/itemID",
  269. });
  270. });
  271. it("should only broadcast if we got some data back from addPocketEntry", async () => {
  272. global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox.stub().resolves(null);
  273. const action = {
  274. data: {site: {url: "raspberry.com", title: "raspberry"}},
  275. _target: {browser: {}},
  276. };
  277. await feed.saveToPocket(action.data.site, action._target.browser);
  278. assert.notCalled(feed.store.dispatch);
  279. });
  280. it("should call deleteFromPocket on DELETE_FROM_POCKET", () => {
  281. sandbox.stub(feed, "deleteFromPocket");
  282. feed.onAction({type: at.DELETE_FROM_POCKET, data: {pocket_id: 12345}});
  283. assert.calledOnce(feed.deleteFromPocket);
  284. assert.calledWithExactly(feed.deleteFromPocket, 12345);
  285. });
  286. it("should catch if deletePocketEntry throws", async () => {
  287. const e = new Error("Error");
  288. global.NewTabUtils.activityStreamLinks.deletePocketEntry = sandbox.stub().rejects(e);
  289. await feed.deleteFromPocket(12345);
  290. assert.calledWith(global.Cu.reportError, e);
  291. });
  292. it("should call NewTabUtils.deletePocketEntry and dispatch POCKET_LINK_DELETED_OR_ARCHIVED when deleting from Pocket", async () => {
  293. await feed.deleteFromPocket(12345);
  294. assert.calledOnce(global.NewTabUtils.activityStreamLinks.deletePocketEntry);
  295. assert.calledWith(global.NewTabUtils.activityStreamLinks.deletePocketEntry, 12345);
  296. assert.calledOnce(feed.store.dispatch);
  297. assert.calledWith(feed.store.dispatch, {type: at.POCKET_LINK_DELETED_OR_ARCHIVED});
  298. });
  299. it("should call archiveFromPocket on ARCHIVE_FROM_POCKET", async () => {
  300. sandbox.stub(feed, "archiveFromPocket");
  301. await feed.onAction({type: at.ARCHIVE_FROM_POCKET, data: {pocket_id: 12345}});
  302. assert.calledOnce(feed.archiveFromPocket);
  303. assert.calledWithExactly(feed.archiveFromPocket, 12345);
  304. });
  305. it("should catch if archiveFromPocket throws", async () => {
  306. const e = new Error("Error");
  307. global.NewTabUtils.activityStreamLinks.archivePocketEntry = sandbox.stub().rejects(e);
  308. await feed.archiveFromPocket(12345);
  309. assert.calledWith(global.Cu.reportError, e);
  310. });
  311. it("should call NewTabUtils.archivePocketEntry and dispatch POCKET_LINK_DELETED_OR_ARCHIVED when archiving from Pocket", async () => {
  312. await feed.archiveFromPocket(12345);
  313. assert.calledOnce(global.NewTabUtils.activityStreamLinks.archivePocketEntry);
  314. assert.calledWith(global.NewTabUtils.activityStreamLinks.archivePocketEntry, 12345);
  315. assert.calledOnce(feed.store.dispatch);
  316. assert.calledWith(feed.store.dispatch, {type: at.POCKET_LINK_DELETED_OR_ARCHIVED});
  317. });
  318. it("should call handoffSearchToAwesomebar on HANDOFF_SEARCH_TO_AWESOMEBAR", () => {
  319. const action = {
  320. type: at.HANDOFF_SEARCH_TO_AWESOMEBAR,
  321. data: {text: "f"},
  322. meta: {fromTarget: {}},
  323. _target: {browser: {ownerGlobal: {gURLBar: {focus: () => {}}}}},
  324. };
  325. sinon.stub(feed, "handoffSearchToAwesomebar");
  326. feed.onAction(action);
  327. assert.calledWith(feed.handoffSearchToAwesomebar, action);
  328. });
  329. });
  330. describe("handoffSearchToAwesomebar", () => {
  331. let fakeUrlBar;
  332. let listeners;
  333. beforeEach(() => {
  334. fakeUrlBar = {
  335. focus: sinon.spy(),
  336. search: sinon.spy(),
  337. setHiddenFocus: sinon.spy(),
  338. removeHiddenFocus: sinon.spy(),
  339. addEventListener: (ev, cb) => {
  340. listeners[ev] = cb;
  341. },
  342. removeEventListener: sinon.spy(),
  343. };
  344. listeners = {};
  345. });
  346. it("should properly handle handoff with no text passed in", () => {
  347. feed.handoffSearchToAwesomebar({
  348. _target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
  349. data: {},
  350. meta: {fromTarget: {}},
  351. });
  352. assert.calledOnce(fakeUrlBar.setHiddenFocus);
  353. assert.notCalled(fakeUrlBar.search);
  354. assert.notCalled(feed.store.dispatch);
  355. // Now type a character.
  356. listeners.keydown({key: "f"});
  357. assert.calledOnce(fakeUrlBar.search);
  358. assert.calledOnce(fakeUrlBar.removeHiddenFocus);
  359. assert.calledOnce(feed.store.dispatch);
  360. assert.calledWith(feed.store.dispatch, {
  361. meta: {
  362. from: "ActivityStream:Main",
  363. skipMain: true,
  364. to: "ActivityStream:Content",
  365. toTarget: {},
  366. },
  367. type: "HIDE_SEARCH",
  368. });
  369. });
  370. it("should properly handle handoff with text data passed in", () => {
  371. feed.handoffSearchToAwesomebar({
  372. _target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
  373. data: {text: "foo"},
  374. meta: {fromTarget: {}},
  375. });
  376. assert.calledOnce(fakeUrlBar.search);
  377. assert.calledWith(fakeUrlBar.search, "@google foo");
  378. assert.notCalled(fakeUrlBar.focus);
  379. assert.notCalled(fakeUrlBar.setHiddenFocus);
  380. // Now call blur listener.
  381. listeners.blur();
  382. assert.calledOnce(feed.store.dispatch);
  383. assert.calledWith(feed.store.dispatch, {
  384. meta: {
  385. from: "ActivityStream:Main",
  386. skipMain: true,
  387. to: "ActivityStream:Content",
  388. toTarget: {},
  389. },
  390. type: "SHOW_SEARCH",
  391. });
  392. });
  393. it("should SHOW_SEARCH on ESC keydown", () => {
  394. feed.handoffSearchToAwesomebar({
  395. _target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
  396. data: {text: "foo"},
  397. meta: {fromTarget: {}},
  398. });
  399. assert.calledOnce(fakeUrlBar.search);
  400. assert.calledWith(fakeUrlBar.search, "@google foo");
  401. assert.notCalled(fakeUrlBar.focus);
  402. // Now call ESC keydown.
  403. listeners.keydown({key: "Escape"});
  404. assert.calledOnce(feed.store.dispatch);
  405. assert.calledWith(feed.store.dispatch, {
  406. meta: {
  407. from: "ActivityStream:Main",
  408. skipMain: true,
  409. to: "ActivityStream:Content",
  410. toTarget: {},
  411. },
  412. type: "SHOW_SEARCH",
  413. });
  414. });
  415. it("should properly handle no defined search alias", () => {
  416. global.Services.search.defaultEngine.wrappedJSObject.__internalAliases = [];
  417. feed.handoffSearchToAwesomebar({
  418. _target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
  419. data: {text: "foo"},
  420. meta: {fromTarget: {}},
  421. });
  422. assert.calledOnce(fakeUrlBar.search);
  423. assert.calledWith(fakeUrlBar.search, "foo");
  424. });
  425. });
  426. describe("#observe", () => {
  427. it("should dispatch a PLACES_LINK_BLOCKED action with the url of the blocked link", () => {
  428. feed.observe(null, BLOCKED_EVENT, "foo123.com");
  429. assert.equal(feed.store.dispatch.firstCall.args[0].type, at.PLACES_LINK_BLOCKED);
  430. assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {url: "foo123.com"});
  431. });
  432. it("should not call dispatch if the topic is something other than BLOCKED_EVENT", () => {
  433. feed.observe(null, "someotherevent");
  434. assert.notCalled(feed.store.dispatch);
  435. });
  436. });
  437. describe("HistoryObserver", () => {
  438. let dispatch;
  439. let observer;
  440. beforeEach(() => {
  441. dispatch = sandbox.spy();
  442. observer = new HistoryObserver(dispatch);
  443. });
  444. it("should have a QueryInterface property", () => {
  445. assert.property(observer, "QueryInterface");
  446. });
  447. describe("#onDeleteURI", () => {
  448. it("should dispatch a PLACES_LINK_DELETED action with the right url", async () => {
  449. await observer.onDeleteURI({spec: "foo.com"});
  450. assert.calledWith(dispatch, {type: at.PLACES_LINK_DELETED, data: {url: "foo.com"}});
  451. });
  452. });
  453. describe("#onClearHistory", () => {
  454. it("should dispatch a PLACES_HISTORY_CLEARED action", () => {
  455. observer.onClearHistory();
  456. assert.calledWith(dispatch, {type: at.PLACES_HISTORY_CLEARED});
  457. });
  458. });
  459. describe("Other empty methods (to keep code coverage happy)", () => {
  460. it("should have a various empty functions for xpconnect happiness", () => {
  461. observer.onBeginUpdateBatch();
  462. observer.onEndUpdateBatch();
  463. observer.onTitleChanged();
  464. observer.onFrecencyChanged();
  465. observer.onManyFrecenciesChanged();
  466. observer.onPageChanged();
  467. observer.onDeleteVisits();
  468. });
  469. });
  470. });
  471. describe("Custom dispatch", () => {
  472. it("should only dispatch 1 PLACES_LINKS_CHANGED action if many bookmark-added notifications happened at once", async () => {
  473. // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
  474. const args = [{
  475. itemType: TYPE_BOOKMARK,
  476. source: SOURCES.DEFAULT,
  477. dateAdded: FAKE_BOOKMARK.dateAdded,
  478. guid: FAKE_BOOKMARK.bookmarkGuid,
  479. title: FAKE_BOOKMARK.bookmarkTitle,
  480. url: "https://www.foo.com",
  481. isTagging: false,
  482. }];
  483. await feed.placesObserver.handlePlacesEvent(args);
  484. await feed.placesObserver.handlePlacesEvent(args);
  485. await feed.placesObserver.handlePlacesEvent(args);
  486. await feed.placesObserver.handlePlacesEvent(args);
  487. assert.calledOnce(feed.store.dispatch.withArgs(ac.OnlyToMain({type: at.PLACES_LINKS_CHANGED})));
  488. });
  489. it("should only dispatch 1 PLACES_LINKS_CHANGED action if many onItemRemoved notifications happened at once", async () => {
  490. const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.DEFAULT];
  491. await feed.bookmarksObserver.onItemRemoved(...args);
  492. await feed.bookmarksObserver.onItemRemoved(...args);
  493. await feed.bookmarksObserver.onItemRemoved(...args);
  494. await feed.bookmarksObserver.onItemRemoved(...args);
  495. assert.calledOnce(feed.store.dispatch.withArgs(ac.OnlyToMain({type: at.PLACES_LINKS_CHANGED})));
  496. });
  497. it("should only dispatch 1 PLACES_LINKS_CHANGED action if any onDeleteURI notifications happened at once", async () => {
  498. await feed.historyObserver.onDeleteURI({spec: "foo.com"});
  499. await feed.historyObserver.onDeleteURI({spec: "foo1.com"});
  500. await feed.historyObserver.onDeleteURI({spec: "foo2.com"});
  501. assert.calledOnce(feed.store.dispatch.withArgs(ac.OnlyToMain({type: at.PLACES_LINKS_CHANGED})));
  502. });
  503. });
  504. describe("PlacesObserver", () => {
  505. describe("#bookmark-added", () => {
  506. let dispatch;
  507. let observer;
  508. beforeEach(() => {
  509. dispatch = sandbox.spy();
  510. observer = new PlacesObserver(dispatch);
  511. });
  512. it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - http", async () => {
  513. const args = [{
  514. itemType: TYPE_BOOKMARK,
  515. source: SOURCES.DEFAULT,
  516. dateAdded: FAKE_BOOKMARK.dateAdded,
  517. guid: FAKE_BOOKMARK.bookmarkGuid,
  518. title: FAKE_BOOKMARK.bookmarkTitle,
  519. url: "http://www.foo.com",
  520. isTagging: false,
  521. }];
  522. await observer.handlePlacesEvent(args);
  523. assert.calledWith(dispatch.secondCall, {
  524. type: at.PLACES_BOOKMARK_ADDED,
  525. data: {
  526. bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,
  527. bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,
  528. dateAdded: FAKE_BOOKMARK.dateAdded * 1000,
  529. url: "http://www.foo.com",
  530. },
  531. });
  532. });
  533. it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - https", async () => {
  534. const args = [{
  535. itemType: TYPE_BOOKMARK,
  536. source: SOURCES.DEFAULT,
  537. dateAdded: FAKE_BOOKMARK.dateAdded,
  538. guid: FAKE_BOOKMARK.bookmarkGuid,
  539. title: FAKE_BOOKMARK.bookmarkTitle,
  540. url: "https://www.foo.com",
  541. isTagging: false,
  542. }];
  543. await observer.handlePlacesEvent(args);
  544. assert.calledWith(dispatch.secondCall, {
  545. type: at.PLACES_BOOKMARK_ADDED,
  546. data: {
  547. bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,
  548. bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,
  549. dateAdded: FAKE_BOOKMARK.dateAdded * 1000,
  550. url: "https://www.foo.com",
  551. },
  552. });
  553. });
  554. it("should not dispatch a PLACES_BOOKMARK_ADDED action - not http/https", async () => {
  555. const args = [{
  556. itemType: TYPE_BOOKMARK,
  557. source: SOURCES.DEFAULT,
  558. dateAdded: FAKE_BOOKMARK.dateAdded,
  559. guid: FAKE_BOOKMARK.bookmarkGuid,
  560. title: FAKE_BOOKMARK.bookmarkTitle,
  561. url: "foo.com",
  562. isTagging: false,
  563. }];
  564. await observer.handlePlacesEvent(args);
  565. assert.notCalled(dispatch);
  566. });
  567. it("should not dispatch a PLACES_BOOKMARK_ADDED action - has IMPORT source", async () => {
  568. const args = [{
  569. itemType: TYPE_BOOKMARK,
  570. source: SOURCES.IMPORT,
  571. dateAdded: FAKE_BOOKMARK.dateAdded,
  572. guid: FAKE_BOOKMARK.bookmarkGuid,
  573. title: FAKE_BOOKMARK.bookmarkTitle,
  574. url: "foo.com",
  575. isTagging: false,
  576. }];
  577. await observer.handlePlacesEvent(args);
  578. assert.notCalled(dispatch);
  579. });
  580. it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE source", async () => {
  581. const args = [{
  582. itemType: TYPE_BOOKMARK,
  583. source: SOURCES.RESTORE,
  584. dateAdded: FAKE_BOOKMARK.dateAdded,
  585. guid: FAKE_BOOKMARK.bookmarkGuid,
  586. title: FAKE_BOOKMARK.bookmarkTitle,
  587. url: "foo.com",
  588. isTagging: false,
  589. }];
  590. await observer.handlePlacesEvent(args);
  591. assert.notCalled(dispatch);
  592. });
  593. it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE_ON_STARTUP source", async () => {
  594. const args = [{
  595. itemType: TYPE_BOOKMARK,
  596. source: SOURCES.RESTORE_ON_STARTUP,
  597. dateAdded: FAKE_BOOKMARK.dateAdded,
  598. guid: FAKE_BOOKMARK.bookmarkGuid,
  599. title: FAKE_BOOKMARK.bookmarkTitle,
  600. url: "foo.com",
  601. isTagging: false,
  602. }];
  603. await observer.handlePlacesEvent(args);
  604. assert.notCalled(dispatch);
  605. });
  606. it("should not dispatch a PLACES_BOOKMARK_ADDED action - has SYNC source", async () => {
  607. const args = [{
  608. itemType: TYPE_BOOKMARK,
  609. source: SOURCES.SYNC,
  610. dateAdded: FAKE_BOOKMARK.dateAdded,
  611. guid: FAKE_BOOKMARK.bookmarkGuid,
  612. title: FAKE_BOOKMARK.bookmarkTitle,
  613. url: "foo.com",
  614. isTagging: false,
  615. }];
  616. await observer.handlePlacesEvent(args);
  617. assert.notCalled(dispatch);
  618. });
  619. it("should ignore events that are not of TYPE_BOOKMARK", async () => {
  620. const args = [{
  621. itemType: "nottypebookmark",
  622. source: SOURCES.DEFAULT,
  623. dateAdded: FAKE_BOOKMARK.dateAdded,
  624. guid: FAKE_BOOKMARK.bookmarkGuid,
  625. title: FAKE_BOOKMARK.bookmarkTitle,
  626. url: "https://www.foo.com",
  627. isTagging: false,
  628. }];
  629. await observer.handlePlacesEvent(args);
  630. assert.notCalled(dispatch);
  631. });
  632. });
  633. });
  634. describe("BookmarksObserver", () => {
  635. let dispatch;
  636. let observer;
  637. beforeEach(() => {
  638. dispatch = sandbox.spy();
  639. observer = new BookmarksObserver(dispatch);
  640. });
  641. it("should have a QueryInterface property", () => {
  642. assert.property(observer, "QueryInterface");
  643. });
  644. describe("#onItemRemoved", () => {
  645. it("should ignore events that are not of TYPE_BOOKMARK", async () => {
  646. await observer.onItemRemoved(null, null, null, "nottypebookmark", null, "123foo", "", SOURCES.DEFAULT);
  647. assert.notCalled(dispatch);
  648. });
  649. it("should not dispatch a PLACES_BOOKMARK_REMOVED action - has SYNC source", async () => {
  650. const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.SYNC];
  651. await observer.onItemRemoved(...args);
  652. assert.notCalled(dispatch);
  653. });
  654. it("should not dispatch a PLACES_BOOKMARK_REMOVED action - has IMPORT source", async () => {
  655. const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.IMPORT];
  656. await observer.onItemRemoved(...args);
  657. assert.notCalled(dispatch);
  658. });
  659. it("should not dispatch a PLACES_BOOKMARK_REMOVED action - has RESTORE source", async () => {
  660. const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.RESTORE];
  661. await observer.onItemRemoved(...args);
  662. assert.notCalled(dispatch);
  663. });
  664. it("should not dispatch a PLACES_BOOKMARK_REMOVED action - has RESTORE_ON_STARTUP source", async () => {
  665. const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.RESTORE_ON_STARTUP];
  666. await observer.onItemRemoved(...args);
  667. assert.notCalled(dispatch);
  668. });
  669. it("should dispatch a PLACES_BOOKMARK_REMOVED action with the right URL and bookmarkGuid", () => {
  670. observer.onItemRemoved(null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.DEFAULT);
  671. assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_REMOVED, data: {bookmarkGuid: "123foo", url: "foo.com"}});
  672. });
  673. });
  674. describe("Other empty methods (to keep code coverage happy)", () => {
  675. it("should have a various empty functions for xpconnect happiness", () => {
  676. observer.onBeginUpdateBatch();
  677. observer.onEndUpdateBatch();
  678. observer.onItemVisited();
  679. observer.onItemMoved();
  680. observer.onItemChanged();
  681. });
  682. });
  683. });
  684. });