diff options
Diffstat (limited to 'addon-sdk/source/test/addons/places/lib/test-places-bookmarks.js')
-rw-r--r-- | addon-sdk/source/test/addons/places/lib/test-places-bookmarks.js | 948 |
1 files changed, 948 insertions, 0 deletions
diff --git a/addon-sdk/source/test/addons/places/lib/test-places-bookmarks.js b/addon-sdk/source/test/addons/places/lib/test-places-bookmarks.js new file mode 100644 index 000000000..ff490f6a4 --- /dev/null +++ b/addon-sdk/source/test/addons/places/lib/test-places-bookmarks.js @@ -0,0 +1,948 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.metadata = { + 'engines': { + 'Firefox': '*' + } +}; + +const { Cc, Ci } = require('chrome'); +const { request } = require('sdk/addon/host'); +const { filter } = require('sdk/event/utils'); +const { on, off } = require('sdk/event/core'); +const { setTimeout } = require('sdk/timers'); +const { newURI } = require('sdk/url/utils'); +const { defer, all, resolve } = require('sdk/core/promise'); +const { before, after } = require('sdk/test/utils'); + +const { + Bookmark, Group, Separator, + save, search, remove, + MENU, TOOLBAR, UNSORTED +} = require('sdk/places/bookmarks'); +const { + invalidResolve, createTree, + compareWithHost, createBookmark, createBookmarkItem, + createBookmarkTree, addVisits, resetPlaces +} = require('./places-helper'); +const { promisedEmitter } = require('sdk/places/utils'); +const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1']. + getService(Ci.nsINavBookmarksService); +const tagsrv = Cc['@mozilla.org/browser/tagging-service;1']. + getService(Ci.nsITaggingService); + +exports.testDefaultFolders = function (assert) { + var ids = [ + bmsrv.bookmarksMenuFolder, + bmsrv.toolbarFolder, + bmsrv.unfiledBookmarksFolder + ]; + + [MENU, TOOLBAR, UNSORTED].forEach(function (g, i) { + assert.ok(g.id === ids[i], ' default group matches id'); + }); +}; + +exports.testValidation = function (assert) { + assert.throws(() => { + Bookmark({ title: 'a title' }); + }, /The `url` property must be a valid URL/, 'throws empty URL error'); + + assert.throws(() => { + Bookmark({ title: 'a title', url: 'not.a.url' }); + }, /The `url` property must be a valid URL/, 'throws invalid URL error'); + + assert.throws(() => { + Bookmark({ url: 'http://foo.com' }); + }, /The `title` property must be defined/, 'throws title error'); + + assert.throws(() => { + Bookmark(); + }, /./, 'throws any error'); + + assert.throws(() => { + Group(); + }, /The `title` property must be defined/, 'throws title error for group'); + + assert.throws(() => { + Bookmark({ url: 'http://foo.com', title: 'my title', tags: 'a tag' }); + }, /The `tags` property must be a Set, or an array/, 'throws error for non set/array tag'); +}; + +exports.testCreateBookmarks = function (assert, done) { + var bm = Bookmark({ + title: 'moz', + url: 'http://mozilla.org', + tags: ['moz1', 'moz2', 'moz3'] + }); + + save(bm).on('data', (bookmark, input) => { + assert.equal(input, bm, 'input is original input item'); + assert.ok(bookmark.id, 'Bookmark has ID'); + assert.equal(bookmark.title, 'moz'); + assert.equal(bookmark.url, 'http://mozilla.org'); + assert.equal(bookmark.group, UNSORTED, 'Unsorted folder is default parent'); + assert.ok(bookmark !== bm, 'bookmark should be a new instance'); + compareWithHost(assert, bookmark); + }).on('end', bookmarks => { + assert.equal(bookmarks.length, 1, 'returned bookmarks in end'); + assert.equal(bookmarks[0].url, 'http://mozilla.org'); + assert.equal(bookmarks[0].tags.has('moz1'), true, 'has first tag'); + assert.equal(bookmarks[0].tags.has('moz2'), true, 'has second tag'); + assert.equal(bookmarks[0].tags.has('moz3'), true, 'has third tag'); + assert.pass('end event is called'); + done(); + }); +}; + +exports.testCreateGroup = function (assert, done) { + save(Group({ title: 'mygroup', group: MENU })).on('data', g => { + assert.ok(g.id, 'Bookmark has ID'); + assert.equal(g.title, 'mygroup', 'matches title'); + assert.equal(g.group, MENU, 'Menu folder matches'); + compareWithHost(assert, g); + }).on('end', results => { + assert.equal(results.length, 1); + assert.pass('end event is called'); + done(); + }); +}; + +exports.testCreateSeparator = function (assert, done) { + save(Separator({ group: MENU })).on('data', function (s) { + assert.ok(s.id, 'Separator has id'); + assert.equal(s.group, MENU, 'Parent group matches'); + compareWithHost(assert, s); + }).on('end', function (results) { + assert.equal(results.length, 1); + assert.pass('end event is called'); + done(); + }); +}; + +exports.testCreateError = function (assert, done) { + let bookmarks = [ + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark'}, + { title: 'moz2', url: 'invalidurl', type: 'bookmark'}, + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark'} + ]; + + let dataCount = 0, errorCount = 0; + save(bookmarks).on('data', bookmark => { + assert.ok(/moz[1|3]/.test(bookmark.title), 'valid bookmarks complete'); + dataCount++; + }).on('error', (reason, item) => { + assert.ok( + /The `url` property must be a valid URL/.test(reason), + 'Error event called with correct reason'); + assert.equal(item, bookmarks[1], 'returns input that failed in event'); + errorCount++; + }).on('end', items => { + assert.equal(dataCount, 2, 'data event called twice'); + assert.equal(errorCount, 1, 'error event called once'); + assert.equal(items.length, bookmarks.length, 'all items should be in result'); + assert.equal(items[0].toString(), '[object Bookmark]', + 'should be a saved instance'); + assert.equal(items[2].toString(), '[object Bookmark]', + 'should be a saved instance'); + assert.equal(items[1], bookmarks[1], 'should be original, unsaved object'); + + search({ query: 'moz' }).on('end', items => { + assert.equal(items.length, 2, 'only two items were successfully saved'); + bookmarks[1].url = 'http://moz2.com/'; + dataCount = errorCount = 0; + save(bookmarks).on('data', bookmark => { + dataCount++; + }).on('error', reason => errorCount++) + .on('end', items => { + assert.equal(items.length, 3, 'all 3 items saved'); + assert.equal(dataCount, 3, '3 data events called'); + assert.equal(errorCount, 0, 'no error events called'); + search({ query: 'moz' }).on('end', items => { + assert.equal(items.length, 3, 'only 3 items saved'); + items.map(item => + assert.ok(/moz\d\.com/.test(item.url), 'correct item')) + done(); + }); + }); + }); + }); +}; + +exports.testSaveDucktypes = function (assert, done) { + save({ + title: 'moz', + url: 'http://mozilla.org', + type: 'bookmark' + }).on('data', (bookmark) => { + compareWithHost(assert, bookmark); + done(); + }); +}; + +exports.testSaveDucktypesParent = function (assert, done) { + let folder = { title: 'myfolder', type: 'group' }; + let bookmark = { title: 'mozzie', url: 'http://moz.com', group: folder, type: 'bookmark' }; + let sep = { type: 'separator', group: folder }; + save([sep, bookmark]).on('end', (res) => { + compareWithHost(assert, res[0]); + compareWithHost(assert, res[1]); + assert.equal(res[0].group.title, 'myfolder', 'parent is ducktyped group'); + assert.equal(res[1].group.title, 'myfolder', 'parent is ducktyped group'); + done(); + }); +}; + +/* + * Tests the scenario where the original bookmark item is resaved + * and does not have an ID or an updated date, but should still be + * mapped to the item it created previously + */ +exports.testResaveOriginalItemMapping = function (assert, done) { + let bookmark = Bookmark({ title: 'moz', url: 'http://moz.org' }); + save(bookmark).on('data', newBookmark => { + bookmark.title = 'new moz'; + save(bookmark).on('data', newNewBookmark => { + assert.equal(newBookmark.id, newNewBookmark.id, 'should be the same bookmark item'); + assert.equal(bmsrv.getItemTitle(newBookmark.id), 'new moz', 'should have updated title'); + done(); + }); + }); +}; + +exports.testCreateMultipleBookmarks = function (assert, done) { + let data = [ + Bookmark({title: 'bm1', url: 'http://bm1.com'}), + Bookmark({title: 'bm2', url: 'http://bm2.com'}), + Bookmark({title: 'bm3', url: 'http://bm3.com'}), + ]; + save(data).on('data', function (bookmark, input) { + let stored = data.filter(({title}) => title === bookmark.title)[0]; + assert.equal(input, stored, 'input is original input item'); + assert.equal(bookmark.title, stored.title, 'titles match'); + assert.equal(bookmark.url, stored.url, 'urls match'); + compareWithHost(assert, bookmark); + }).on('end', function (bookmarks) { + assert.equal(bookmarks.length, 3, 'all bookmarks returned'); + done(); + }); +}; + +exports.testCreateImplicitParent = function (assert, done) { + let folder = Group({ title: 'my parent' }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder }) + ]; + save(bookmarks).on('data', function (bookmark) { + if (bookmark.type === 'bookmark') { + assert.equal(bookmark.group.title, folder.title, 'parent is linked'); + compareWithHost(assert, bookmark); + } else if (bookmark.type === 'group') { + assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct'); + compareWithHost(assert, bookmark); + } + }).on('end', function (results) { + assert.equal(results.length, 3, 'results should only hold explicit saves'); + done(); + }); +}; + +exports.testCreateExplicitParent = function (assert, done) { + let folder = Group({ title: 'my parent' }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder }) + ]; + save(bookmarks.concat(folder)).on('data', function (bookmark) { + if (bookmark.type === 'bookmark') { + assert.equal(bookmark.group.title, folder.title, 'parent is linked'); + compareWithHost(assert, bookmark); + } else if (bookmark.type === 'group') { + assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct'); + compareWithHost(assert, bookmark); + } + }).on('end', function () { + done(); + }); +}; + +exports.testCreateNested = function (assert, done) { + let topFolder = Group({ title: 'top', group: MENU }); + let midFolder = Group({ title: 'middle', group: topFolder }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder }), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder }), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder }) + ]; + let dataEventCount = 0; + save(bookmarks).on('data', function (bookmark) { + if (bookmark.type === 'bookmark') { + assert.equal(bookmark.group.title, midFolder.title, 'parent is linked'); + } else if (bookmark.title === 'top') { + assert.equal(bookmark.group.id, MENU.id, 'parent ID of top group is correct'); + } else { + assert.equal(bookmark.group.title, topFolder.title, 'parent title of middle group is correct'); + } + dataEventCount++; + compareWithHost(assert, bookmark); + }).on('end', () => { + assert.equal(dataEventCount, 5, 'data events for all saves have occurred'); + assert.ok('end event called'); + done(); + }); +}; + +/* + * Was a scenario when implicitly saving a bookmark that was already created, + * it was not being properly fetched and attempted to recreate + */ +exports.testAddingToExistingParent = function (assert, done) { + let group = { type: 'group', title: 'mozgroup' }; + let bookmarks = [ + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group }, + { title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group }, + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group } + ], + firstBatch, secondBatch; + + saveP(bookmarks).then(data => { + firstBatch = data; + return saveP([ + { title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group }, + { title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group } + ]); + }, assert.fail).then(data => { + secondBatch = data; + assert.equal(firstBatch[0].group.id, secondBatch[0].group.id, + 'successfully saved to the same parent'); + }).then(done).catch(assert.fail); +}; + +exports.testUpdateParent = function (assert, done) { + let group = { type: 'group', title: 'mozgroup' }; + saveP(group).then(item => { + item[0].title = 'mozgroup-resave'; + return saveP(item[0]); + }).then(item => { + assert.equal(item[0].title, 'mozgroup-resave', 'group saved successfully'); + }).then(done).catch(assert.fail); +}; + +exports.testUpdateSeparator = function (assert, done) { + let sep = [Separator(), Separator(), Separator()]; + saveP(sep).then(item => { + item[0].index = 2; + return saveP(item[0]); + }).then(item => { + assert.equal(item[0].index, 2, 'updated index of separator'); + }).then(done).catch(assert.fail); +}; + +exports.testPromisedSave = function (assert, done) { + let topFolder = Group({ title: 'top', group: MENU }); + let midFolder = Group({ title: 'middle', group: topFolder }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) + ]; + let first, second, third; + saveP(bookmarks).then(bms => { + first = bms.filter(b => b.title === 'moz1')[0]; + second = bms.filter(b => b.title === 'moz2')[0]; + third = bms.filter(b => b.title === 'moz3')[0]; + assert.equal(first.index, 0); + assert.equal(second.index, 1); + assert.equal(third.index, 2); + first.index = 3; + return saveP(first); + }).then(() => { + assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark'); + assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted'); + assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted'); + }).then(done).catch(assert.fail); +}; + +exports.testPromisedErrorSave = function*(assert) { + let bookmarks = [ + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark'}, + { title: 'moz2', url: 'invalidurl', type: 'bookmark'}, + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark'} + ]; + + yield saveP(bookmarks).then(() => { + assert.fail("should not resolve"); + }, reason => { + assert.ok( + /The `url` property must be a valid URL/.test(reason), + 'Error event called with correct reason'); + }); + + bookmarks[1].url = 'http://moz2.com'; + yield saveP(bookmarks); + + let res = yield searchP({ query: 'moz' }); + assert.equal(res.length, 3, 'all 3 should be saved upon retry'); + res.map(item => assert.ok(/moz\d\.com/.test(item.url), 'correct item')); +}; + +exports.testMovingChildren = function (assert, done) { + let topFolder = Group({ title: 'top', group: MENU }); + let midFolder = Group({ title: 'middle', group: topFolder }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) + ]; + + save(bookmarks).on('end', bms => { + let first = bms.filter(b => b.title === 'moz1')[0]; + let second = bms.filter(b => b.title === 'moz2')[0]; + let third = bms.filter(b => b.title === 'moz3')[0]; + assert.equal(first.index, 0); + assert.equal(second.index, 1); + assert.equal(third.index, 2); + /* When moving down in the same container we take + * into account the removal of the original item. If you want + * to move from index X to index Y > X you must use + * moveItem(id, folder, Y + 1) + */ + first.index = 3; + save(first).on('end', () => { + assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark'); + assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted'); + assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted'); + done(); + }); + }); +}; + +exports.testMovingChildrenNewFolder = function (assert, done) { + let topFolder = Group({ title: 'top', group: MENU }); + let midFolder = Group({ title: 'middle', group: topFolder }); + let newFolder = Group({ title: 'new', group: MENU }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) + ]; + save(bookmarks).on('end', bms => { + let first = bms.filter(b => b.title === 'moz1')[0]; + let second = bms.filter(b => b.title === 'moz2')[0]; + let third = bms.filter(b => b.title === 'moz3')[0]; + let definedMidFolder = first.group; + let definedNewFolder; + first.group = newFolder; + assert.equal(first.index, 0); + assert.equal(second.index, 1); + assert.equal(third.index, 2); + save(first).on('data', (data) => { + if (data.type === 'group') definedNewFolder = data; + }).on('end', (moved) => { + assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted'); + assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted'); + assert.equal(bmsrv.getItemIndex(first.id), 0, 'properly moved bookmark'); + assert.equal(bmsrv.getFolderIdForItem(first.id), definedNewFolder.id, + 'bookmark has new parent'); + assert.equal(bmsrv.getFolderIdForItem(second.id), definedMidFolder.id, + 'sibling bookmarks did not move'); + assert.equal(bmsrv.getFolderIdForItem(third.id), definedMidFolder.id, + 'sibling bookmarks did not move'); + done(); + }); + }); +}; + +exports.testRemoveFunction = function (assert) { + let topFolder = Group({ title: 'new', group: MENU }); + let midFolder = Group({ title: 'middle', group: topFolder }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) + ]; + remove([midFolder, topFolder].concat(bookmarks)).map(item => { + assert.equal(item.remove, true, 'remove toggled `remove` property to true'); + }); +}; + +exports.testRemove = function (assert, done) { + let id; + createBookmarkItem().then(data => { + id = data.id; + compareWithHost(assert, data); // ensure bookmark exists + save(remove(data)).on('data', (res) => { + assert.pass('data event should be called'); + assert.ok(!res, 'response should be empty'); + }).on('end', () => { + assert.throws(function () { + bmsrv.getItemTitle(id); + }, 'item should no longer exist'); + done(); + }); + }).catch(assert.fail); +}; + +/* + * Tests recursively removing children when removing a group + */ +exports.testRemoveAllChildren = function (assert, done) { + let topFolder = Group({ title: 'new', group: MENU }); + let midFolder = Group({ title: 'middle', group: topFolder }); + let bookmarks = [ + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) + ]; + + let saved = []; + save(bookmarks).on('data', (data) => saved.push(data)).on('end', () => { + save(remove(topFolder)).on('end', () => { + assert.equal(saved.length, 5, 'all items should have been saved'); + saved.map((item) => { + assert.throws(function () { + bmsrv.getItemTitle(item.id); + }, 'item should no longer exist'); + }); + done(); + }); + }); +}; + +exports.testResolution = function (assert, done) { + let firstSave, secondSave; + createBookmarkItem().then((item) => { + firstSave = item; + assert.ok(item.updated, 'bookmark has updated time'); + item.title = 'my title'; + // Ensure delay so a different save time is set + return resolve(item); + }).then(saveP) + .then(items => { + let item = items[0]; + secondSave = item; + assert.ok(firstSave.updated < secondSave.updated, 'snapshots have different update times'); + firstSave.title = 'updated title'; + return saveP(firstSave, { resolve: (mine, theirs) => { + assert.equal(mine.title, 'updated title', 'correct data for my object'); + assert.equal(theirs.title, 'my title', 'correct data for their object'); + assert.equal(mine.url, theirs.url, 'other data is equal'); + assert.equal(mine.group, theirs.group, 'other data is equal'); + assert.ok(mine !== firstSave, 'instance is not passed in'); + assert.ok(theirs !== secondSave, 'instance is not passed in'); + assert.equal(mine.toString(), '[object Object]', 'serialized objects'); + assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); + mine.title = 'a new title'; + return mine; + }}); + }).then((results) => { + let result = results[0]; + assert.equal(result.title, 'a new title', 'resolve handles results'); + }).then(done).catch(assert.fail); +}; + +/* + * Same as the resolution test, but with the 'unsaved' snapshot + */ +exports.testResolutionMapping = function (assert, done) { + let bookmark = Bookmark({ title: 'moz', url: 'http://bookmarks4life.com/' }); + let saved; + + saveP(bookmark).then(data => { + saved = data[0]; + saved.title = 'updated title'; + // Ensure a delay for different updated times + return resolve(saved); + }). + then(saveP). + then(() => { + bookmark.title = 'conflicting title'; + return saveP(bookmark, { resolve: (mine, theirs) => { + assert.equal(mine.title, 'conflicting title', 'correct data for my object'); + assert.equal(theirs.title, 'updated title', 'correct data for their object'); + assert.equal(mine.url, theirs.url, 'other data is equal'); + assert.equal(mine.group, theirs.group, 'other data is equal'); + assert.ok(mine !== bookmark, 'instance is not passed in'); + assert.ok(theirs !== saved, 'instance is not passed in'); + assert.equal(mine.toString(), '[object Object]', 'serialized objects'); + assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); + mine.title = 'a new title'; + return mine; + }}); + }).then((results) => { + let result = results[0]; + assert.equal(result.title, 'a new title', 'resolve handles results'); + }).then(done).catch(assert.fail); +}; + +exports.testUpdateTags = function (assert, done) { + createBookmarkItem({ tags: ['spidermonkey'] }).then(bookmark => { + bookmark.tags.add('jagermonkey'); + bookmark.tags.add('ionmonkey'); + bookmark.tags.delete('spidermonkey'); + save(bookmark).on('data', saved => { + assert.equal(saved.tags.size, 2, 'should have 2 tags'); + assert.ok(saved.tags.has('jagermonkey'), 'should have added tag'); + assert.ok(saved.tags.has('ionmonkey'), 'should have added tag'); + assert.ok(!saved.tags.has('spidermonkey'), 'should not have removed tag'); + done(); + }); + }).catch(assert.fail); +}; + +/* + * View `createBookmarkTree` in `./places-helper.js` to see + * expected tree construction + */ + +exports.testSearchByGroupSimple = function (assert, done) { + createBookmarkTree().then(() => { + // In initial release of Places API, groups can only be queried + // via a 'simple query', which is one folder set, and no other + // parameters + return searchP({ group: UNSORTED }); + }).then(results => { + let groups = results.filter(({type}) => type === 'group'); + assert.equal(groups.length, 2, 'returns folders'); + assert.equal(results.length, 7, + 'should return all bookmarks and folders under UNSORTED'); + assert.equal(groups[0].toString(), '[object Group]', 'returns instance'); + return searchP({ + group: groups.filter(({title}) => title === 'mozgroup')[0] + }); + }).then(results => { + let groups = results.filter(({type}) => type === 'group'); + assert.equal(groups.length, 1, 'returns one subfolder'); + assert.equal(results.length, 6, + 'returns all children bookmarks/folders'); + assert.ok(results.filter(({url}) => url === 'http://w3schools.com/'), + 'returns nested children'); + }).then(done).catch(assert.fail); +}; + +exports.testSearchByGroupComplex = function (assert, done) { + let mozgroup; + createBookmarkTree().then(results => { + mozgroup = results.filter(({title}) => title === 'mozgroup')[0]; + return searchP({ group: mozgroup, query: 'javascript' }); + }).then(results => { + assert.equal(results.length, 1, 'only one javascript result under mozgroup'); + assert.equal(results[0].url, 'http://w3schools.com/', 'correct result'); + return searchP({ group: mozgroup, url: '*.mozilla.org' }); + }).then(results => { + assert.equal(results.length, 2, 'expected results'); + assert.ok( + !results.filter(({url}) => /developer.mozilla/.test(url)).length, + 'does not find results from other folders'); + }).then(done).catch(assert.fail); +}; + +exports.testSearchEmitters = function (assert, done) { + createBookmarkTree().then(() => { + let count = 0; + search({ tags: ['mozilla', 'firefox'] }).on('data', data => { + assert.ok(/mozilla|firefox/.test(data.title), 'one of the correct items'); + assert.ok(data.tags.has('firefox'), 'has firefox tag'); + assert.ok(data.tags.has('mozilla'), 'has mozilla tag'); + assert.equal(data + '', '[object Bookmark]', 'returns bookmark'); + count++; + }).on('end', data => { + assert.equal(count, 3, 'data event was called for each item'); + assert.equal(data.length, 3, + 'should return two bookmarks that have both mozilla AND firefox'); + assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark'); + assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark'); + assert.equal(data[2].title, 'firefox', 'returns correct bookmark'); + assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks'); + done(); + }); + }).catch(assert.fail); +}; + +exports.testSearchTags = function (assert, done) { + createBookmarkTree().then(() => { + // AND tags + return searchP({ tags: ['mozilla', 'firefox'] }); + }).then(data => { + assert.equal(data.length, 3, + 'should return two bookmarks that have both mozilla AND firefox'); + assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark'); + assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark'); + assert.equal(data[2].title, 'firefox', 'returns correct bookmark'); + assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks'); + return searchP([{tags: ['firefox']}, {tags: ['javascript']}]); + }).then(data => { + // OR tags + assert.equal(data.length, 6, + 'should return all bookmarks with firefox OR javascript tag'); + }).then(done).catch(assert.fail); +}; + +/* + * Tests 4 scenarios + * '*.mozilla.com' + * 'mozilla.com' + * 'http://mozilla.com/' + * 'http://mozilla.com/*' + */ +exports.testSearchURLForBookmarks = function*(assert) { + yield createBookmarkTree() + let data = yield searchP({ url: 'mozilla.org' }); + + assert.equal(data.length, 2, 'only URLs with host domain'); + assert.equal(data[0].url, 'http://mozilla.org/'); + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); + + data = yield searchP({ url: '*.mozilla.org' }); + + assert.equal(data.length, 3, 'returns domain and when host is other than domain'); + assert.equal(data[0].url, 'http://mozilla.org/'); + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); + assert.equal(data[2].url, 'http://developer.mozilla.org/en-US/'); + + data = yield searchP({ url: 'http://mozilla.org' }); + + assert.equal(data.length, 1, 'only exact URL match'); + assert.equal(data[0].url, 'http://mozilla.org/'); + + data = yield searchP({ url: 'http://mozilla.org/*' }); + + assert.equal(data.length, 2, 'only URLs that begin with query'); + assert.equal(data[0].url, 'http://mozilla.org/'); + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); + + data = yield searchP([{ url: 'mozilla.org' }, { url: 'component.fm' }]); + + assert.equal(data.length, 3, 'returns URLs that match EITHER query'); + assert.equal(data[0].url, 'http://mozilla.org/'); + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); + assert.equal(data[2].url, 'http://component.fm/'); +}; + +/* + * Searches url, title, tags + */ +exports.testSearchQueryForBookmarks = function*(assert) { + yield createBookmarkTree(); + + let data = yield searchP({ query: 'thunder' }); + assert.equal(data.length, 3); + assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title'); + assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title'); + assert.equal(data[2].title, 'thunderbird', 'query matches tag, url, or title'); + + data = yield searchP([{ query: 'rust' }, { query: 'component' }]); + // rust OR component + assert.equal(data.length, 3); + assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title'); + assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title'); + assert.equal(data[2].title, 'web audio components', 'query matches tag, url, or title'); + + data = yield searchP([{ query: 'moz', tags: ['javascript']}]); + assert.equal(data.length, 1); + assert.equal(data[0].title, 'mdn', + 'only one item matches moz query AND has a javascript tag'); +}; + +/* + * Test caching on bulk calls. + * Each construction of a bookmark item snapshot results in + * the recursive lookup of parent groups up to the root groups -- + * ensure that the appropriate instances equal each other, and no duplicate + * fetches are called + * + * Implementation-dependent, this checks the host event `sdk-places-bookmarks-get`, + * and if implementation changes, this could increase or decrease + */ + +exports.testCaching = function (assert, done) { + let count = 0; + let stream = filter(request, ({event}) => + /sdk-places-bookmarks-get/.test(event)); + on(stream, 'data', handle); + + let group = { type: 'group', title: 'mozgroup' }; + let bookmarks = [ + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group }, + { title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group }, + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group } + ]; + + /* + * Use timeout in tests since the platform calls are synchronous + * and the counting event shim may not have occurred yet + */ + + saveP(bookmarks).then(() => { + assert.equal(count, 0, 'all new items and root group, no fetches should occur'); + count = 0; + return saveP([ + { title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group }, + { title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group } + ]); + // Test `save` look-up + }).then(() => { + assert.equal(count, 1, 'should only look up parent once'); + count = 0; + return searchP({ query: 'moz' }); + }).then(results => { + // Should query for each bookmark (5) from the query (id -> data), + // their parent during `construct` (1) and the root shouldn't + // require a lookup + assert.equal(count, 6, 'lookup occurs once for each item and parent'); + off(stream, 'data', handle); + }).then(done).catch(assert.fail); + + function handle ({data}) { + return count++; + } +}; + +/* + * Search Query Options + */ + +exports.testSearchCount = function (assert, done) { + let max = 8; + createBookmarkTree() + .then(testCount(1)) + .then(testCount(2)) + .then(testCount(3)) + .then(testCount(5)) + .then(testCount(10)) + .then(done) + .catch(assert.fail); + + function testCount (n) { + return function () { + return searchP({}, { count: n }).then(results => { + if (n > max) n = max; + assert.equal(results.length, n, + 'count ' + n + ' returns ' + n + ' results'); + }); + }; + } +}; + +exports.testSearchSortForBookmarks = function (assert, done) { + let urls = [ + 'http://mozilla.com/', 'http://webaud.io/', 'http://mozilla.com/webfwd/', + 'http://developer.mozilla.com/', 'http://bandcamp.com/' + ]; + + saveP( + urls.map(url => + Bookmark({ url: url, title: url.replace(/http:\/\/|\//g,'')})) + ).then(() => { + return searchP({}, { sort: 'title' }); + }).then(results => { + checkOrder(results, [4,3,0,2,1]); + return searchP({}, { sort: 'title', descending: true }); + }).then(results => { + checkOrder(results, [1,2,0,3,4]); + return searchP({}, { sort: 'url' }); + }).then(results => { + checkOrder(results, [4,3,0,2,1]); + return searchP({}, { sort: 'url', descending: true }); + }).then(results => { + checkOrder(results, [1,2,0,3,4]); + return addVisits(['http://mozilla.com/', 'http://mozilla.com']); + }).then(() => + saveP(Bookmark({ url: 'http://github.com', title: 'github.com' })) + ).then(() => addVisits('http://bandcamp.com/')) + .then(() => searchP({ query: 'webfwd' })) + .then(results => { + results[0].title = 'new title for webfwd'; + return saveP(results[0]); + }) + .then(() => + searchP({}, { sort: 'visitCount' }) + ).then(results => { + assert.equal(results[5].url, 'http://mozilla.com/', + 'last entry is the highest visit count'); + return searchP({}, { sort: 'visitCount', descending: true }); + }).then(results => { + assert.equal(results[0].url, 'http://mozilla.com/', + 'first entry is the highest visit count'); + return searchP({}, { sort: 'date' }); + }).then(results => { + assert.equal(results[5].url, 'http://bandcamp.com/', + 'latest visited should be first'); + return searchP({}, { sort: 'date', descending: true }); + }).then(results => { + assert.equal(results[0].url, 'http://bandcamp.com/', + 'latest visited should be at the end'); + return searchP({}, { sort: 'dateAdded' }); + }).then(results => { + assert.equal(results[5].url, 'http://github.com/', + 'last added should be at the end'); + return searchP({}, { sort: 'dateAdded', descending: true }); + }).then(results => { + assert.equal(results[0].url, 'http://github.com/', + 'last added should be first'); + return searchP({}, { sort: 'lastModified' }); + }).then(results => { + assert.equal(results[5].url, 'http://mozilla.com/webfwd/', + 'last modified should be last'); + return searchP({}, { sort: 'lastModified', descending: true }); + }).then(results => { + assert.equal(results[0].url, 'http://mozilla.com/webfwd/', + 'last modified should be first'); + }).then(done).catch(assert.fail); + + function checkOrder (results, nums) { + assert.equal(results.length, nums.length, 'expected return count'); + for (let i = 0; i < nums.length; i++) { + assert.equal(results[i].url, urls[nums[i]], 'successful order'); + } + } +}; + +exports.testSearchComplexQueryWithOptions = function (assert, done) { + createBookmarkTree().then(() => { + return searchP([ + { tags: ['rust'], url: '*.mozilla.org' }, + { tags: ['javascript'], query: 'mozilla' } + ], { sort: 'title' }); + }).then(results => { + let expected = [ + 'http://developer.mozilla.org/en-US/', + 'http://mozilla.org/' + ]; + for (let i = 0; i < expected.length; i++) + assert.equal(results[i].url, expected[i], 'correct ordering and item'); + }).then(done).catch(assert.fail); +}; + +exports.testCheckSaveOrder = function (assert, done) { + let group = Group({ title: 'mygroup' }); + let bookmarks = [ + Bookmark({ url: 'http://url1.com', title: 'url1', group: group }), + Bookmark({ url: 'http://url2.com', title: 'url2', group: group }), + Bookmark({ url: 'http://url3.com', title: 'url3', group: group }), + Bookmark({ url: 'http://url4.com', title: 'url4', group: group }), + Bookmark({ url: 'http://url5.com', title: 'url5', group: group }) + ]; + saveP(bookmarks).then(results => { + for (let i = 0; i < bookmarks.length; i++) + assert.equal(results[i].url, bookmarks[i].url, + 'correct ordering of bookmark results'); + }).then(done).catch(assert.fail); +}; + +before(exports, (name, assert, done) => resetPlaces(done)); +after(exports, (name, assert, done) => resetPlaces(done)); + +function saveP () { + return promisedEmitter(save.apply(null, Array.prototype.slice.call(arguments))); +} + +function searchP () { + return promisedEmitter(search.apply(null, Array.prototype.slice.call(arguments))); +} |