test-places-bookmarks.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. 'use strict';
  5. module.metadata = {
  6. 'engines': {
  7. 'Firefox': '*'
  8. }
  9. };
  10. const { Cc, Ci } = require('chrome');
  11. const { request } = require('sdk/addon/host');
  12. const { filter } = require('sdk/event/utils');
  13. const { on, off } = require('sdk/event/core');
  14. const { setTimeout } = require('sdk/timers');
  15. const { newURI } = require('sdk/url/utils');
  16. const { defer, all } = require('sdk/core/promise');
  17. const { defer: async } = require('sdk/lang/functional');
  18. const { before, after } = require('sdk/test/utils');
  19. const {
  20. Bookmark, Group, Separator,
  21. save, search, remove,
  22. MENU, TOOLBAR, UNSORTED
  23. } = require('sdk/places/bookmarks');
  24. const {
  25. invalidResolve, invalidReject, createTree,
  26. compareWithHost, createBookmark, createBookmarkItem,
  27. createBookmarkTree, addVisits, resetPlaces
  28. } = require('../places-helper');
  29. const { promisedEmitter } = require('sdk/places/utils');
  30. const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1'].
  31. getService(Ci.nsINavBookmarksService);
  32. const tagsrv = Cc['@mozilla.org/browser/tagging-service;1'].
  33. getService(Ci.nsITaggingService);
  34. exports.testDefaultFolders = function (assert) {
  35. var ids = [
  36. bmsrv.bookmarksMenuFolder,
  37. bmsrv.toolbarFolder,
  38. bmsrv.unfiledBookmarksFolder
  39. ];
  40. [MENU, TOOLBAR, UNSORTED].forEach(function (g, i) {
  41. assert.ok(g.id === ids[i], ' default group matches id');
  42. });
  43. };
  44. exports.testValidation = function (assert) {
  45. assert.throws(() => {
  46. Bookmark({ title: 'a title' });
  47. }, /The `url` property must be a valid URL/, 'throws empty URL error');
  48. assert.throws(() => {
  49. Bookmark({ title: 'a title', url: 'not.a.url' });
  50. }, /The `url` property must be a valid URL/, 'throws invalid URL error');
  51. assert.throws(() => {
  52. Bookmark({ url: 'http://foo.com' });
  53. }, /The `title` property must be defined/, 'throws title error');
  54. assert.throws(() => {
  55. Bookmark();
  56. }, /./, 'throws any error');
  57. assert.throws(() => {
  58. Group();
  59. }, /The `title` property must be defined/, 'throws title error for group');
  60. assert.throws(() => {
  61. Bookmark({ url: 'http://foo.com', title: 'my title', tags: 'a tag' });
  62. }, /The `tags` property must be a Set, or an array/, 'throws error for non set/array tag');
  63. };
  64. exports.testCreateBookmarks = function (assert, done) {
  65. var bm = Bookmark({
  66. title: 'moz',
  67. url: 'http://mozilla.org',
  68. tags: ['moz1', 'moz2', 'moz3']
  69. });
  70. save(bm).on('data', (bookmark, input) => {
  71. assert.equal(input, bm, 'input is original input item');
  72. assert.ok(bookmark.id, 'Bookmark has ID');
  73. assert.equal(bookmark.title, 'moz');
  74. assert.equal(bookmark.url, 'http://mozilla.org');
  75. assert.equal(bookmark.group, UNSORTED, 'Unsorted folder is default parent');
  76. assert.ok(bookmark !== bm, 'bookmark should be a new instance');
  77. compareWithHost(assert, bookmark);
  78. }).on('end', bookmarks => {
  79. assert.equal(bookmarks.length, 1, 'returned bookmarks in end');
  80. assert.equal(bookmarks[0].url, 'http://mozilla.org');
  81. assert.equal(bookmarks[0].tags.has('moz1'), true, 'has first tag');
  82. assert.equal(bookmarks[0].tags.has('moz2'), true, 'has second tag');
  83. assert.equal(bookmarks[0].tags.has('moz3'), true, 'has third tag');
  84. assert.pass('end event is called');
  85. done();
  86. });
  87. };
  88. exports.testCreateGroup = function (assert, done) {
  89. save(Group({ title: 'mygroup', group: MENU })).on('data', g => {
  90. assert.ok(g.id, 'Bookmark has ID');
  91. assert.equal(g.title, 'mygroup', 'matches title');
  92. assert.equal(g.group, MENU, 'Menu folder matches');
  93. compareWithHost(assert, g);
  94. }).on('end', results => {
  95. assert.equal(results.length, 1);
  96. assert.pass('end event is called');
  97. done();
  98. });
  99. };
  100. exports.testCreateSeparator = function (assert, done) {
  101. save(Separator({ group: MENU })).on('data', function (s) {
  102. assert.ok(s.id, 'Separator has id');
  103. assert.equal(s.group, MENU, 'Parent group matches');
  104. compareWithHost(assert, s);
  105. }).on('end', function (results) {
  106. assert.equal(results.length, 1);
  107. assert.pass('end event is called');
  108. done();
  109. });
  110. };
  111. exports.testCreateError = function (assert, done) {
  112. let bookmarks = [
  113. { title: 'moz1', url: 'http://moz1.com', type: 'bookmark'},
  114. { title: 'moz2', url: 'invalidurl', type: 'bookmark'},
  115. { title: 'moz3', url: 'http://moz3.com', type: 'bookmark'}
  116. ];
  117. let dataCount = 0, errorCount = 0;
  118. save(bookmarks).on('data', bookmark => {
  119. assert.ok(/moz[1|3]/.test(bookmark.title), 'valid bookmarks complete');
  120. dataCount++;
  121. }).on('error', (reason, item) => {
  122. assert.ok(
  123. /The `url` property must be a valid URL/.test(reason),
  124. 'Error event called with correct reason');
  125. assert.equal(item, bookmarks[1], 'returns input that failed in event');
  126. errorCount++;
  127. }).on('end', items => {
  128. assert.equal(dataCount, 2, 'data event called twice');
  129. assert.equal(errorCount, 1, 'error event called once');
  130. assert.equal(items.length, bookmarks.length, 'all items should be in result');
  131. assert.equal(items[0].toString(), '[object Bookmark]',
  132. 'should be a saved instance');
  133. assert.equal(items[2].toString(), '[object Bookmark]',
  134. 'should be a saved instance');
  135. assert.equal(items[1], bookmarks[1], 'should be original, unsaved object');
  136. search({ query: 'moz' }).on('end', items => {
  137. assert.equal(items.length, 2, 'only two items were successfully saved');
  138. bookmarks[1].url = 'http://moz2.com/';
  139. dataCount = errorCount = 0;
  140. save(bookmarks).on('data', bookmark => {
  141. dataCount++;
  142. }).on('error', reason => errorCount++)
  143. .on('end', items => {
  144. assert.equal(items.length, 3, 'all 3 items saved');
  145. assert.equal(dataCount, 3, '3 data events called');
  146. assert.equal(errorCount, 0, 'no error events called');
  147. search({ query: 'moz' }).on('end', items => {
  148. assert.equal(items.length, 3, 'only 3 items saved');
  149. items.map(item =>
  150. assert.ok(/moz\d\.com/.test(item.url), 'correct item'))
  151. done();
  152. });
  153. });
  154. });
  155. });
  156. };
  157. exports.testSaveDucktypes = function (assert, done) {
  158. save({
  159. title: 'moz',
  160. url: 'http://mozilla.org',
  161. type: 'bookmark'
  162. }).on('data', (bookmark) => {
  163. compareWithHost(assert, bookmark);
  164. done();
  165. });
  166. };
  167. exports.testSaveDucktypesParent = function (assert, done) {
  168. let folder = { title: 'myfolder', type: 'group' };
  169. let bookmark = { title: 'mozzie', url: 'http://moz.com', group: folder, type: 'bookmark' };
  170. let sep = { type: 'separator', group: folder };
  171. save([sep, bookmark]).on('end', (res) => {
  172. compareWithHost(assert, res[0]);
  173. compareWithHost(assert, res[1]);
  174. assert.equal(res[0].group.title, 'myfolder', 'parent is ducktyped group');
  175. assert.equal(res[1].group.title, 'myfolder', 'parent is ducktyped group');
  176. done();
  177. });
  178. };
  179. /*
  180. * Tests the scenario where the original bookmark item is resaved
  181. * and does not have an ID or an updated date, but should still be
  182. * mapped to the item it created previously
  183. */
  184. exports.testResaveOriginalItemMapping = function (assert, done) {
  185. let bookmark = Bookmark({ title: 'moz', url: 'http://moz.org' });
  186. save(bookmark).on('data', newBookmark => {
  187. bookmark.title = 'new moz';
  188. save(bookmark).on('data', newNewBookmark => {
  189. assert.equal(newBookmark.id, newNewBookmark.id, 'should be the same bookmark item');
  190. assert.equal(bmsrv.getItemTitle(newBookmark.id), 'new moz', 'should have updated title');
  191. done();
  192. });
  193. });
  194. };
  195. exports.testCreateMultipleBookmarks = function (assert, done) {
  196. let data = [
  197. Bookmark({title: 'bm1', url: 'http://bm1.com'}),
  198. Bookmark({title: 'bm2', url: 'http://bm2.com'}),
  199. Bookmark({title: 'bm3', url: 'http://bm3.com'}),
  200. ];
  201. save(data).on('data', function (bookmark, input) {
  202. let stored = data.filter(({title}) => title === bookmark.title)[0];
  203. assert.equal(input, stored, 'input is original input item');
  204. assert.equal(bookmark.title, stored.title, 'titles match');
  205. assert.equal(bookmark.url, stored.url, 'urls match');
  206. compareWithHost(assert, bookmark);
  207. }).on('end', function (bookmarks) {
  208. assert.equal(bookmarks.length, 3, 'all bookmarks returned');
  209. done();
  210. });
  211. };
  212. exports.testCreateImplicitParent = function (assert, done) {
  213. let folder = Group({ title: 'my parent' });
  214. let bookmarks = [
  215. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }),
  216. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }),
  217. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder })
  218. ];
  219. save(bookmarks).on('data', function (bookmark) {
  220. if (bookmark.type === 'bookmark') {
  221. assert.equal(bookmark.group.title, folder.title, 'parent is linked');
  222. compareWithHost(assert, bookmark);
  223. } else if (bookmark.type === 'group') {
  224. assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct');
  225. compareWithHost(assert, bookmark);
  226. }
  227. }).on('end', function (results) {
  228. assert.equal(results.length, 3, 'results should only hold explicit saves');
  229. done();
  230. });
  231. };
  232. exports.testCreateExplicitParent = function (assert, done) {
  233. let folder = Group({ title: 'my parent' });
  234. let bookmarks = [
  235. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }),
  236. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }),
  237. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder })
  238. ];
  239. save(bookmarks.concat(folder)).on('data', function (bookmark) {
  240. if (bookmark.type === 'bookmark') {
  241. assert.equal(bookmark.group.title, folder.title, 'parent is linked');
  242. compareWithHost(assert, bookmark);
  243. } else if (bookmark.type === 'group') {
  244. assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct');
  245. compareWithHost(assert, bookmark);
  246. }
  247. }).on('end', function () {
  248. done();
  249. });
  250. };
  251. exports.testCreateNested = function (assert, done) {
  252. let topFolder = Group({ title: 'top', group: MENU });
  253. let midFolder = Group({ title: 'middle', group: topFolder });
  254. let bookmarks = [
  255. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder }),
  256. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder }),
  257. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder })
  258. ];
  259. let dataEventCount = 0;
  260. save(bookmarks).on('data', function (bookmark) {
  261. if (bookmark.type === 'bookmark') {
  262. assert.equal(bookmark.group.title, midFolder.title, 'parent is linked');
  263. } else if (bookmark.title === 'top') {
  264. assert.equal(bookmark.group.id, MENU.id, 'parent ID of top group is correct');
  265. } else {
  266. assert.equal(bookmark.group.title, topFolder.title, 'parent title of middle group is correct');
  267. }
  268. dataEventCount++;
  269. compareWithHost(assert, bookmark);
  270. }).on('end', () => {
  271. assert.equal(dataEventCount, 5, 'data events for all saves have occurred');
  272. assert.ok('end event called');
  273. done();
  274. });
  275. };
  276. /*
  277. * Was a scenario when implicitly saving a bookmark that was already created,
  278. * it was not being properly fetched and attempted to recreate
  279. */
  280. exports.testAddingToExistingParent = function (assert, done) {
  281. let group = { type: 'group', title: 'mozgroup' };
  282. let bookmarks = [
  283. { title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group },
  284. { title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group },
  285. { title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group }
  286. ],
  287. firstBatch, secondBatch;
  288. saveP(bookmarks).then(data => {
  289. firstBatch = data;
  290. return saveP([
  291. { title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group },
  292. { title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group }
  293. ]);
  294. }, assert.fail).then(data => {
  295. secondBatch = data;
  296. assert.equal(firstBatch[0].group.id, secondBatch[0].group.id,
  297. 'successfully saved to the same parent');
  298. done();
  299. }, assert.fail);
  300. };
  301. exports.testUpdateParent = function (assert, done) {
  302. let group = { type: 'group', title: 'mozgroup' };
  303. saveP(group).then(item => {
  304. item[0].title = 'mozgroup-resave';
  305. return saveP(item[0]);
  306. }).then(item => {
  307. assert.equal(item[0].title, 'mozgroup-resave', 'group saved successfully');
  308. done();
  309. });
  310. };
  311. exports.testUpdateSeparator = function (assert, done) {
  312. let sep = [Separator(), Separator(), Separator()];
  313. saveP(sep).then(item => {
  314. item[0].index = 2;
  315. return saveP(item[0]);
  316. }).then(item => {
  317. assert.equal(item[0].index, 2, 'updated index of separator');
  318. done();
  319. });
  320. };
  321. exports.testPromisedSave = function (assert, done) {
  322. let topFolder = Group({ title: 'top', group: MENU });
  323. let midFolder = Group({ title: 'middle', group: topFolder });
  324. let bookmarks = [
  325. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
  326. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
  327. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
  328. ];
  329. let first, second, third;
  330. saveP(bookmarks).then(bms => {
  331. first = bms.filter(b => b.title === 'moz1')[0];
  332. second = bms.filter(b => b.title === 'moz2')[0];
  333. third = bms.filter(b => b.title === 'moz3')[0];
  334. assert.equal(first.index, 0);
  335. assert.equal(second.index, 1);
  336. assert.equal(third.index, 2);
  337. first.index = 3;
  338. return saveP(first);
  339. }).then(() => {
  340. assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark');
  341. assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted');
  342. assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted');
  343. done();
  344. });
  345. };
  346. exports.testPromisedErrorSave = function (assert, done) {
  347. let bookmarks = [
  348. { title: 'moz1', url: 'http://moz1.com', type: 'bookmark'},
  349. { title: 'moz2', url: 'invalidurl', type: 'bookmark'},
  350. { title: 'moz3', url: 'http://moz3.com', type: 'bookmark'}
  351. ];
  352. saveP(bookmarks).then(invalidResolve, reason => {
  353. assert.ok(
  354. /The `url` property must be a valid URL/.test(reason),
  355. 'Error event called with correct reason');
  356. bookmarks[1].url = 'http://moz2.com';
  357. return saveP(bookmarks);
  358. }).then(res => {
  359. return searchP({ query: 'moz' });
  360. }).then(res => {
  361. assert.equal(res.length, 3, 'all 3 should be saved upon retry');
  362. res.map(item => assert.ok(/moz\d\.com/.test(item.url), 'correct item'));
  363. done();
  364. }, invalidReject);
  365. };
  366. exports.testMovingChildren = function (assert, done) {
  367. let topFolder = Group({ title: 'top', group: MENU });
  368. let midFolder = Group({ title: 'middle', group: topFolder });
  369. let bookmarks = [
  370. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
  371. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
  372. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
  373. ];
  374. save(bookmarks).on('end', bms => {
  375. let first = bms.filter(b => b.title === 'moz1')[0];
  376. let second = bms.filter(b => b.title === 'moz2')[0];
  377. let third = bms.filter(b => b.title === 'moz3')[0];
  378. assert.equal(first.index, 0);
  379. assert.equal(second.index, 1);
  380. assert.equal(third.index, 2);
  381. /* When moving down in the same container we take
  382. * into account the removal of the original item. If you want
  383. * to move from index X to index Y > X you must use
  384. * moveItem(id, folder, Y + 1)
  385. */
  386. first.index = 3;
  387. save(first).on('end', () => {
  388. assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark');
  389. assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted');
  390. assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted');
  391. done();
  392. });
  393. });
  394. };
  395. exports.testMovingChildrenNewFolder = function (assert, done) {
  396. let topFolder = Group({ title: 'top', group: MENU });
  397. let midFolder = Group({ title: 'middle', group: topFolder });
  398. let newFolder = Group({ title: 'new', group: MENU });
  399. let bookmarks = [
  400. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
  401. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
  402. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
  403. ];
  404. save(bookmarks).on('end', bms => {
  405. let first = bms.filter(b => b.title === 'moz1')[0];
  406. let second = bms.filter(b => b.title === 'moz2')[0];
  407. let third = bms.filter(b => b.title === 'moz3')[0];
  408. let definedMidFolder = first.group;
  409. let definedNewFolder;
  410. first.group = newFolder;
  411. assert.equal(first.index, 0);
  412. assert.equal(second.index, 1);
  413. assert.equal(third.index, 2);
  414. save(first).on('data', (data) => {
  415. if (data.type === 'group') definedNewFolder = data;
  416. }).on('end', (moved) => {
  417. assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted');
  418. assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted');
  419. assert.equal(bmsrv.getItemIndex(first.id), 0, 'properly moved bookmark');
  420. assert.equal(bmsrv.getFolderIdForItem(first.id), definedNewFolder.id,
  421. 'bookmark has new parent');
  422. assert.equal(bmsrv.getFolderIdForItem(second.id), definedMidFolder.id,
  423. 'sibling bookmarks did not move');
  424. assert.equal(bmsrv.getFolderIdForItem(third.id), definedMidFolder.id,
  425. 'sibling bookmarks did not move');
  426. done();
  427. });
  428. });
  429. };
  430. exports.testRemoveFunction = function (assert) {
  431. let topFolder = Group({ title: 'new', group: MENU });
  432. let midFolder = Group({ title: 'middle', group: topFolder });
  433. let bookmarks = [
  434. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
  435. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
  436. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
  437. ];
  438. remove([midFolder, topFolder].concat(bookmarks)).map(item => {
  439. assert.equal(item.remove, true, 'remove toggled `remove` property to true');
  440. });
  441. };
  442. exports.testRemove = function (assert, done) {
  443. let id;
  444. createBookmarkItem().then(data => {
  445. id = data.id;
  446. compareWithHost(assert, data); // ensure bookmark exists
  447. save(remove(data)).on('data', (res) => {
  448. assert.pass('data event should be called');
  449. assert.ok(!res, 'response should be empty');
  450. }).on('end', () => {
  451. assert.throws(function () {
  452. bmsrv.getItemTitle(id);
  453. }, 'item should no longer exist');
  454. done();
  455. });
  456. });
  457. };
  458. /*
  459. * Tests recursively removing children when removing a group
  460. */
  461. exports.testRemoveAllChildren = function (assert, done) {
  462. let topFolder = Group({ title: 'new', group: MENU });
  463. let midFolder = Group({ title: 'middle', group: topFolder });
  464. let bookmarks = [
  465. Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
  466. Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
  467. Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
  468. ];
  469. let saved = [];
  470. save(bookmarks).on('data', (data) => saved.push(data)).on('end', () => {
  471. save(remove(topFolder)).on('end', () => {
  472. assert.equal(saved.length, 5, 'all items should have been saved');
  473. saved.map((item) => {
  474. assert.throws(function () {
  475. bmsrv.getItemTitle(item.id);
  476. }, 'item should no longer exist');
  477. });
  478. done();
  479. });
  480. });
  481. };
  482. exports.testResolution = function (assert, done) {
  483. let firstSave, secondSave;
  484. createBookmarkItem().then((item) => {
  485. firstSave = item;
  486. assert.ok(item.updated, 'bookmark has updated time');
  487. item.title = 'my title';
  488. // Ensure delay so a different save time is set
  489. return delayed(item);
  490. }).then(saveP)
  491. .then(items => {
  492. let item = items[0];
  493. secondSave = item;
  494. assert.ok(firstSave.updated < secondSave.updated, 'snapshots have different update times');
  495. firstSave.title = 'updated title';
  496. return saveP(firstSave, { resolve: (mine, theirs) => {
  497. assert.equal(mine.title, 'updated title', 'correct data for my object');
  498. assert.equal(theirs.title, 'my title', 'correct data for their object');
  499. assert.equal(mine.url, theirs.url, 'other data is equal');
  500. assert.equal(mine.group, theirs.group, 'other data is equal');
  501. assert.ok(mine !== firstSave, 'instance is not passed in');
  502. assert.ok(theirs !== secondSave, 'instance is not passed in');
  503. assert.equal(mine.toString(), '[object Object]', 'serialized objects');
  504. assert.equal(theirs.toString(), '[object Object]', 'serialized objects');
  505. mine.title = 'a new title';
  506. return mine;
  507. }});
  508. }).then((results) => {
  509. let result = results[0];
  510. assert.equal(result.title, 'a new title', 'resolve handles results');
  511. done();
  512. });
  513. };
  514. /*
  515. * Same as the resolution test, but with the 'unsaved' snapshot
  516. */
  517. exports.testResolutionMapping = function (assert, done) {
  518. let bookmark = Bookmark({ title: 'moz', url: 'http://bookmarks4life.com/' });
  519. let saved;
  520. saveP(bookmark).then(data => {
  521. saved = data[0];
  522. saved.title = 'updated title';
  523. // Ensure a delay for different updated times
  524. return delayed(saved);
  525. }).then(saveP)
  526. .then(() => {
  527. bookmark.title = 'conflicting title';
  528. return saveP(bookmark, { resolve: (mine, theirs) => {
  529. assert.equal(mine.title, 'conflicting title', 'correct data for my object');
  530. assert.equal(theirs.title, 'updated title', 'correct data for their object');
  531. assert.equal(mine.url, theirs.url, 'other data is equal');
  532. assert.equal(mine.group, theirs.group, 'other data is equal');
  533. assert.ok(mine !== bookmark, 'instance is not passed in');
  534. assert.ok(theirs !== saved, 'instance is not passed in');
  535. assert.equal(mine.toString(), '[object Object]', 'serialized objects');
  536. assert.equal(theirs.toString(), '[object Object]', 'serialized objects');
  537. mine.title = 'a new title';
  538. return mine;
  539. }});
  540. }).then((results) => {
  541. let result = results[0];
  542. assert.equal(result.title, 'a new title', 'resolve handles results');
  543. done();
  544. });
  545. };
  546. exports.testUpdateTags = function (assert, done) {
  547. createBookmarkItem({ tags: ['spidermonkey'] }).then(bookmark => {
  548. bookmark.tags.add('jagermonkey');
  549. bookmark.tags.add('ionmonkey');
  550. bookmark.tags.delete('spidermonkey');
  551. save(bookmark).on('data', saved => {
  552. assert.equal(saved.tags.size, 2, 'should have 2 tags');
  553. assert.ok(saved.tags.has('jagermonkey'), 'should have added tag');
  554. assert.ok(saved.tags.has('ionmonkey'), 'should have added tag');
  555. assert.ok(!saved.tags.has('spidermonkey'), 'should not have removed tag');
  556. done();
  557. });
  558. });
  559. };
  560. /*
  561. * View `createBookmarkTree` in `./places-helper.js` to see
  562. * expected tree construction
  563. */
  564. exports.testSearchByGroupSimple = function (assert, done) {
  565. createBookmarkTree().then(() => {
  566. // In initial release of Places API, groups can only be queried
  567. // via a 'simple query', which is one folder set, and no other
  568. // parameters
  569. return searchP({ group: UNSORTED });
  570. }).then(results => {
  571. let groups = results.filter(({type}) => type === 'group');
  572. assert.equal(groups.length, 2, 'returns folders');
  573. assert.equal(results.length, 7,
  574. 'should return all bookmarks and folders under UNSORTED');
  575. assert.equal(groups[0].toString(), '[object Group]', 'returns instance');
  576. return searchP({
  577. group: groups.filter(({title}) => title === 'mozgroup')[0]
  578. });
  579. }).then(results => {
  580. let groups = results.filter(({type}) => type === 'group');
  581. assert.equal(groups.length, 1, 'returns one subfolder');
  582. assert.equal(results.length, 6,
  583. 'returns all children bookmarks/folders');
  584. assert.ok(results.filter(({url}) => url === 'http://w3schools.com/'),
  585. 'returns nested children');
  586. done();
  587. }).then(null, assert.fail);
  588. };
  589. exports.testSearchByGroupComplex = function (assert, done) {
  590. let mozgroup;
  591. createBookmarkTree().then(results => {
  592. mozgroup = results.filter(({title}) => title === 'mozgroup')[0];
  593. return searchP({ group: mozgroup, query: 'javascript' });
  594. }).then(results => {
  595. assert.equal(results.length, 1, 'only one javascript result under mozgroup');
  596. assert.equal(results[0].url, 'http://w3schools.com/', 'correct result');
  597. return searchP({ group: mozgroup, url: '*.mozilla.org' });
  598. }).then(results => {
  599. assert.equal(results.length, 2, 'expected results');
  600. assert.ok(
  601. !results.filter(({url}) => /developer.mozilla/.test(url)).length,
  602. 'does not find results from other folders');
  603. done();
  604. }, assert.fail);
  605. };
  606. exports.testSearchEmitters = function (assert, done) {
  607. createBookmarkTree().then(() => {
  608. let count = 0;
  609. search({ tags: ['mozilla', 'firefox'] }).on('data', data => {
  610. assert.ok(/mozilla|firefox/.test(data.title), 'one of the correct items');
  611. assert.ok(data.tags.has('firefox'), 'has firefox tag');
  612. assert.ok(data.tags.has('mozilla'), 'has mozilla tag');
  613. assert.equal(data + '', '[object Bookmark]', 'returns bookmark');
  614. count++;
  615. }).on('end', data => {
  616. assert.equal(count, 3, 'data event was called for each item');
  617. assert.equal(data.length, 3,
  618. 'should return two bookmarks that have both mozilla AND firefox');
  619. assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark');
  620. assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark');
  621. assert.equal(data[2].title, 'firefox', 'returns correct bookmark');
  622. assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks');
  623. done();
  624. });
  625. });
  626. };
  627. exports.testSearchTags = function (assert, done) {
  628. createBookmarkTree().then(() => {
  629. // AND tags
  630. return searchP({ tags: ['mozilla', 'firefox'] });
  631. }).then(data => {
  632. assert.equal(data.length, 3,
  633. 'should return two bookmarks that have both mozilla AND firefox');
  634. assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark');
  635. assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark');
  636. assert.equal(data[2].title, 'firefox', 'returns correct bookmark');
  637. assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks');
  638. return searchP([{tags: ['firefox']}, {tags: ['javascript']}]);
  639. }).then(data => {
  640. // OR tags
  641. assert.equal(data.length, 6,
  642. 'should return all bookmarks with firefox OR javascript tag');
  643. done();
  644. });
  645. };
  646. /*
  647. * Tests 4 scenarios
  648. * '*.mozilla.com'
  649. * 'mozilla.com'
  650. * 'http://mozilla.com/'
  651. * 'http://mozilla.com/*'
  652. */
  653. exports.testSearchURL = function (assert, done) {
  654. createBookmarkTree().then(() => {
  655. return searchP({ url: 'mozilla.org' });
  656. }).then(data => {
  657. assert.equal(data.length, 2, 'only URLs with host domain');
  658. assert.equal(data[0].url, 'http://mozilla.org/');
  659. assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
  660. return searchP({ url: '*.mozilla.org' });
  661. }).then(data => {
  662. assert.equal(data.length, 3, 'returns domain and when host is other than domain');
  663. assert.equal(data[0].url, 'http://mozilla.org/');
  664. assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
  665. assert.equal(data[2].url, 'http://developer.mozilla.org/en-US/');
  666. return searchP({ url: 'http://mozilla.org' });
  667. }).then(data => {
  668. assert.equal(data.length, 1, 'only exact URL match');
  669. assert.equal(data[0].url, 'http://mozilla.org/');
  670. return searchP({ url: 'http://mozilla.org/*' });
  671. }).then(data => {
  672. assert.equal(data.length, 2, 'only URLs that begin with query');
  673. assert.equal(data[0].url, 'http://mozilla.org/');
  674. assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
  675. return searchP([{ url: 'mozilla.org' }, { url: 'component.fm' }]);
  676. }).then(data => {
  677. assert.equal(data.length, 3, 'returns URLs that match EITHER query');
  678. assert.equal(data[0].url, 'http://mozilla.org/');
  679. assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
  680. assert.equal(data[2].url, 'http://component.fm/');
  681. }).then(() => {
  682. done();
  683. });
  684. };
  685. /*
  686. * Searches url, title, tags
  687. */
  688. exports.testSearchQuery = function (assert, done) {
  689. createBookmarkTree().then(() => {
  690. return searchP({ query: 'thunder' });
  691. }).then(data => {
  692. assert.equal(data.length, 3);
  693. assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title');
  694. assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title');
  695. assert.equal(data[2].title, 'thunderbird', 'query matches tag, url, or title');
  696. return searchP([{ query: 'rust' }, { query: 'component' }]);
  697. }).then(data => {
  698. // rust OR component
  699. assert.equal(data.length, 3);
  700. assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title');
  701. assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title');
  702. assert.equal(data[2].title, 'web audio components', 'query matches tag, url, or title');
  703. return searchP([{ query: 'moz', tags: ['javascript']}]);
  704. }).then(data => {
  705. assert.equal(data.length, 1);
  706. assert.equal(data[0].title, 'mdn',
  707. 'only one item matches moz query AND has a javascript tag');
  708. }).then(() => {
  709. done();
  710. });
  711. };
  712. /*
  713. * Test caching on bulk calls.
  714. * Each construction of a bookmark item snapshot results in
  715. * the recursive lookup of parent groups up to the root groups --
  716. * ensure that the appropriate instances equal each other, and no duplicate
  717. * fetches are called
  718. *
  719. * Implementation-dependent, this checks the host event `sdk-places-bookmarks-get`,
  720. * and if implementation changes, this could increase or decrease
  721. */
  722. exports.testCaching = function (assert, done) {
  723. let count = 0;
  724. let stream = filter(request, ({event}) =>
  725. /sdk-places-bookmarks-get/.test(event));
  726. on(stream, 'data', handle);
  727. let group = { type: 'group', title: 'mozgroup' };
  728. let bookmarks = [
  729. { title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group },
  730. { title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group },
  731. { title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group }
  732. ];
  733. /*
  734. * Use timeout in tests since the platform calls are synchronous
  735. * and the counting event shim may not have occurred yet
  736. */
  737. saveP(bookmarks).then(() => {
  738. assert.equal(count, 0, 'all new items and root group, no fetches should occur');
  739. count = 0;
  740. return saveP([
  741. { title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group },
  742. { title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group }
  743. ]);
  744. // Test `save` look-up
  745. }).then(() => {
  746. assert.equal(count, 1, 'should only look up parent once');
  747. count = 0;
  748. return searchP({ query: 'moz' });
  749. }).then(results => {
  750. // Should query for each bookmark (5) from the query (id -> data),
  751. // their parent during `construct` (1) and the root shouldn't
  752. // require a lookup
  753. assert.equal(count, 6, 'lookup occurs once for each item and parent');
  754. off(stream, 'data', handle);
  755. done();
  756. });
  757. function handle ({data}) count++
  758. };
  759. /*
  760. * Search Query Options
  761. */
  762. exports.testSearchCount = function (assert, done) {
  763. let max = 8;
  764. createBookmarkTree()
  765. .then(testCount(1))
  766. .then(testCount(2))
  767. .then(testCount(3))
  768. .then(testCount(5))
  769. .then(testCount(10))
  770. .then(() => {
  771. done();
  772. });
  773. function testCount (n) {
  774. return function () {
  775. return searchP({}, { count: n }).then(results => {
  776. if (n > max) n = max;
  777. assert.equal(results.length, n,
  778. 'count ' + n + ' returns ' + n + ' results');
  779. });
  780. };
  781. }
  782. };
  783. exports.testSearchSort = function (assert, done) {
  784. let urls = [
  785. 'http://mozilla.com/', 'http://webaud.io/', 'http://mozilla.com/webfwd/',
  786. 'http://developer.mozilla.com/', 'http://bandcamp.com/'
  787. ];
  788. saveP(
  789. urls.map(url =>
  790. Bookmark({ url: url, title: url.replace(/http:\/\/|\//g,'')}))
  791. ).then(() => {
  792. return searchP({}, { sort: 'title' });
  793. }).then(results => {
  794. checkOrder(results, [4,3,0,2,1]);
  795. return searchP({}, { sort: 'title', descending: true });
  796. }).then(results => {
  797. checkOrder(results, [1,2,0,3,4]);
  798. return searchP({}, { sort: 'url' });
  799. }).then(results => {
  800. checkOrder(results, [4,3,0,2,1]);
  801. return searchP({}, { sort: 'url', descending: true });
  802. }).then(results => {
  803. checkOrder(results, [1,2,0,3,4]);
  804. return addVisits(['http://mozilla.com/', 'http://mozilla.com']);
  805. }).then(() =>
  806. saveP(Bookmark({ url: 'http://github.com', title: 'github.com' }))
  807. ).then(() => addVisits('http://bandcamp.com/'))
  808. .then(() => searchP({ query: 'webfwd' }))
  809. .then(results => {
  810. results[0].title = 'new title for webfwd';
  811. return saveP(results[0]);
  812. })
  813. .then(() =>
  814. searchP({}, { sort: 'visitCount' })
  815. ).then(results => {
  816. assert.equal(results[5].url, 'http://mozilla.com/',
  817. 'last entry is the highest visit count');
  818. return searchP({}, { sort: 'visitCount', descending: true });
  819. }).then(results => {
  820. assert.equal(results[0].url, 'http://mozilla.com/',
  821. 'first entry is the highest visit count');
  822. return searchP({}, { sort: 'date' });
  823. }).then(results => {
  824. assert.equal(results[5].url, 'http://bandcamp.com/',
  825. 'latest visited should be first');
  826. return searchP({}, { sort: 'date', descending: true });
  827. }).then(results => {
  828. assert.equal(results[0].url, 'http://bandcamp.com/',
  829. 'latest visited should be at the end');
  830. return searchP({}, { sort: 'dateAdded' });
  831. }).then(results => {
  832. assert.equal(results[5].url, 'http://github.com/',
  833. 'last added should be at the end');
  834. return searchP({}, { sort: 'dateAdded', descending: true });
  835. }).then(results => {
  836. assert.equal(results[0].url, 'http://github.com/',
  837. 'last added should be first');
  838. return searchP({}, { sort: 'lastModified' });
  839. }).then(results => {
  840. assert.equal(results[5].url, 'http://mozilla.com/webfwd/',
  841. 'last modified should be last');
  842. return searchP({}, { sort: 'lastModified', descending: true });
  843. }).then(results => {
  844. assert.equal(results[0].url, 'http://mozilla.com/webfwd/',
  845. 'last modified should be first');
  846. }).then(() => {
  847. done();
  848. });
  849. function checkOrder (results, nums) {
  850. assert.equal(results.length, nums.length, 'expected return count');
  851. for (let i = 0; i < nums.length; i++) {
  852. assert.equal(results[i].url, urls[nums[i]], 'successful order');
  853. }
  854. }
  855. };
  856. exports.testSearchComplexQueryWithOptions = function (assert, done) {
  857. createBookmarkTree().then(() => {
  858. return searchP([
  859. { tags: ['rust'], url: '*.mozilla.org' },
  860. { tags: ['javascript'], query: 'mozilla' }
  861. ], { sort: 'title' });
  862. }).then(results => {
  863. let expected = [
  864. 'http://developer.mozilla.org/en-US/',
  865. 'http://mozilla.org/'
  866. ];
  867. for (let i = 0; i < expected.length; i++)
  868. assert.equal(results[i].url, expected[i], 'correct ordering and item');
  869. done();
  870. });
  871. };
  872. exports.testCheckSaveOrder = function (assert, done) {
  873. let group = Group({ title: 'mygroup' });
  874. let bookmarks = [
  875. Bookmark({ url: 'http://url1.com', title: 'url1', group: group }),
  876. Bookmark({ url: 'http://url2.com', title: 'url2', group: group }),
  877. Bookmark({ url: 'http://url3.com', title: 'url3', group: group }),
  878. Bookmark({ url: 'http://url4.com', title: 'url4', group: group }),
  879. Bookmark({ url: 'http://url5.com', title: 'url5', group: group })
  880. ];
  881. saveP(bookmarks).then(results => {
  882. for (let i = 0; i < bookmarks.length; i++)
  883. assert.equal(results[i].url, bookmarks[i].url,
  884. 'correct ordering of bookmark results');
  885. done();
  886. });
  887. };
  888. before(exports, (name, assert, done) => resetPlaces(done));
  889. after(exports, (name, assert, done) => resetPlaces(done));
  890. function saveP () {
  891. return promisedEmitter(save.apply(null, Array.slice(arguments)));
  892. }
  893. function searchP () {
  894. return promisedEmitter(search.apply(null, Array.slice(arguments)));
  895. }
  896. function delayed (value, ms) {
  897. let { promise, resolve } = defer();
  898. setTimeout(() => resolve(value), ms || 10);
  899. return promise;
  900. }