1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/test/addons/places/tests/test-places-bookmarks.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,965 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +'use strict'; 1.8 + 1.9 +module.metadata = { 1.10 + 'engines': { 1.11 + 'Firefox': '*' 1.12 + } 1.13 +}; 1.14 + 1.15 +const { Cc, Ci } = require('chrome'); 1.16 +const { request } = require('sdk/addon/host'); 1.17 +const { filter } = require('sdk/event/utils'); 1.18 +const { on, off } = require('sdk/event/core'); 1.19 +const { setTimeout } = require('sdk/timers'); 1.20 +const { newURI } = require('sdk/url/utils'); 1.21 +const { defer, all } = require('sdk/core/promise'); 1.22 +const { defer: async } = require('sdk/lang/functional'); 1.23 +const { before, after } = require('sdk/test/utils'); 1.24 + 1.25 +const { 1.26 + Bookmark, Group, Separator, 1.27 + save, search, remove, 1.28 + MENU, TOOLBAR, UNSORTED 1.29 +} = require('sdk/places/bookmarks'); 1.30 +const { 1.31 + invalidResolve, invalidReject, createTree, 1.32 + compareWithHost, createBookmark, createBookmarkItem, 1.33 + createBookmarkTree, addVisits, resetPlaces 1.34 +} = require('../places-helper'); 1.35 +const { promisedEmitter } = require('sdk/places/utils'); 1.36 +const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1']. 1.37 + getService(Ci.nsINavBookmarksService); 1.38 +const tagsrv = Cc['@mozilla.org/browser/tagging-service;1']. 1.39 + getService(Ci.nsITaggingService); 1.40 + 1.41 +exports.testDefaultFolders = function (assert) { 1.42 + var ids = [ 1.43 + bmsrv.bookmarksMenuFolder, 1.44 + bmsrv.toolbarFolder, 1.45 + bmsrv.unfiledBookmarksFolder 1.46 + ]; 1.47 + [MENU, TOOLBAR, UNSORTED].forEach(function (g, i) { 1.48 + assert.ok(g.id === ids[i], ' default group matches id'); 1.49 + }); 1.50 +}; 1.51 + 1.52 +exports.testValidation = function (assert) { 1.53 + assert.throws(() => { 1.54 + Bookmark({ title: 'a title' }); 1.55 + }, /The `url` property must be a valid URL/, 'throws empty URL error'); 1.56 + 1.57 + assert.throws(() => { 1.58 + Bookmark({ title: 'a title', url: 'not.a.url' }); 1.59 + }, /The `url` property must be a valid URL/, 'throws invalid URL error'); 1.60 + 1.61 + assert.throws(() => { 1.62 + Bookmark({ url: 'http://foo.com' }); 1.63 + }, /The `title` property must be defined/, 'throws title error'); 1.64 + 1.65 + assert.throws(() => { 1.66 + Bookmark(); 1.67 + }, /./, 'throws any error'); 1.68 + 1.69 + assert.throws(() => { 1.70 + Group(); 1.71 + }, /The `title` property must be defined/, 'throws title error for group'); 1.72 + 1.73 + assert.throws(() => { 1.74 + Bookmark({ url: 'http://foo.com', title: 'my title', tags: 'a tag' }); 1.75 + }, /The `tags` property must be a Set, or an array/, 'throws error for non set/array tag'); 1.76 +}; 1.77 + 1.78 +exports.testCreateBookmarks = function (assert, done) { 1.79 + var bm = Bookmark({ 1.80 + title: 'moz', 1.81 + url: 'http://mozilla.org', 1.82 + tags: ['moz1', 'moz2', 'moz3'] 1.83 + }); 1.84 + 1.85 + save(bm).on('data', (bookmark, input) => { 1.86 + assert.equal(input, bm, 'input is original input item'); 1.87 + assert.ok(bookmark.id, 'Bookmark has ID'); 1.88 + assert.equal(bookmark.title, 'moz'); 1.89 + assert.equal(bookmark.url, 'http://mozilla.org'); 1.90 + assert.equal(bookmark.group, UNSORTED, 'Unsorted folder is default parent'); 1.91 + assert.ok(bookmark !== bm, 'bookmark should be a new instance'); 1.92 + compareWithHost(assert, bookmark); 1.93 + }).on('end', bookmarks => { 1.94 + assert.equal(bookmarks.length, 1, 'returned bookmarks in end'); 1.95 + assert.equal(bookmarks[0].url, 'http://mozilla.org'); 1.96 + assert.equal(bookmarks[0].tags.has('moz1'), true, 'has first tag'); 1.97 + assert.equal(bookmarks[0].tags.has('moz2'), true, 'has second tag'); 1.98 + assert.equal(bookmarks[0].tags.has('moz3'), true, 'has third tag'); 1.99 + assert.pass('end event is called'); 1.100 + done(); 1.101 + }); 1.102 +}; 1.103 + 1.104 +exports.testCreateGroup = function (assert, done) { 1.105 + save(Group({ title: 'mygroup', group: MENU })).on('data', g => { 1.106 + assert.ok(g.id, 'Bookmark has ID'); 1.107 + assert.equal(g.title, 'mygroup', 'matches title'); 1.108 + assert.equal(g.group, MENU, 'Menu folder matches'); 1.109 + compareWithHost(assert, g); 1.110 + }).on('end', results => { 1.111 + assert.equal(results.length, 1); 1.112 + assert.pass('end event is called'); 1.113 + done(); 1.114 + }); 1.115 +}; 1.116 + 1.117 +exports.testCreateSeparator = function (assert, done) { 1.118 + save(Separator({ group: MENU })).on('data', function (s) { 1.119 + assert.ok(s.id, 'Separator has id'); 1.120 + assert.equal(s.group, MENU, 'Parent group matches'); 1.121 + compareWithHost(assert, s); 1.122 + }).on('end', function (results) { 1.123 + assert.equal(results.length, 1); 1.124 + assert.pass('end event is called'); 1.125 + done(); 1.126 + }); 1.127 +}; 1.128 + 1.129 +exports.testCreateError = function (assert, done) { 1.130 + let bookmarks = [ 1.131 + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark'}, 1.132 + { title: 'moz2', url: 'invalidurl', type: 'bookmark'}, 1.133 + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark'} 1.134 + ]; 1.135 + 1.136 + let dataCount = 0, errorCount = 0; 1.137 + save(bookmarks).on('data', bookmark => { 1.138 + assert.ok(/moz[1|3]/.test(bookmark.title), 'valid bookmarks complete'); 1.139 + dataCount++; 1.140 + }).on('error', (reason, item) => { 1.141 + assert.ok( 1.142 + /The `url` property must be a valid URL/.test(reason), 1.143 + 'Error event called with correct reason'); 1.144 + assert.equal(item, bookmarks[1], 'returns input that failed in event'); 1.145 + errorCount++; 1.146 + }).on('end', items => { 1.147 + assert.equal(dataCount, 2, 'data event called twice'); 1.148 + assert.equal(errorCount, 1, 'error event called once'); 1.149 + assert.equal(items.length, bookmarks.length, 'all items should be in result'); 1.150 + assert.equal(items[0].toString(), '[object Bookmark]', 1.151 + 'should be a saved instance'); 1.152 + assert.equal(items[2].toString(), '[object Bookmark]', 1.153 + 'should be a saved instance'); 1.154 + assert.equal(items[1], bookmarks[1], 'should be original, unsaved object'); 1.155 + 1.156 + search({ query: 'moz' }).on('end', items => { 1.157 + assert.equal(items.length, 2, 'only two items were successfully saved'); 1.158 + bookmarks[1].url = 'http://moz2.com/'; 1.159 + dataCount = errorCount = 0; 1.160 + save(bookmarks).on('data', bookmark => { 1.161 + dataCount++; 1.162 + }).on('error', reason => errorCount++) 1.163 + .on('end', items => { 1.164 + assert.equal(items.length, 3, 'all 3 items saved'); 1.165 + assert.equal(dataCount, 3, '3 data events called'); 1.166 + assert.equal(errorCount, 0, 'no error events called'); 1.167 + search({ query: 'moz' }).on('end', items => { 1.168 + assert.equal(items.length, 3, 'only 3 items saved'); 1.169 + items.map(item => 1.170 + assert.ok(/moz\d\.com/.test(item.url), 'correct item')) 1.171 + done(); 1.172 + }); 1.173 + }); 1.174 + }); 1.175 + }); 1.176 +}; 1.177 + 1.178 +exports.testSaveDucktypes = function (assert, done) { 1.179 + save({ 1.180 + title: 'moz', 1.181 + url: 'http://mozilla.org', 1.182 + type: 'bookmark' 1.183 + }).on('data', (bookmark) => { 1.184 + compareWithHost(assert, bookmark); 1.185 + done(); 1.186 + }); 1.187 +}; 1.188 + 1.189 +exports.testSaveDucktypesParent = function (assert, done) { 1.190 + let folder = { title: 'myfolder', type: 'group' }; 1.191 + let bookmark = { title: 'mozzie', url: 'http://moz.com', group: folder, type: 'bookmark' }; 1.192 + let sep = { type: 'separator', group: folder }; 1.193 + save([sep, bookmark]).on('end', (res) => { 1.194 + compareWithHost(assert, res[0]); 1.195 + compareWithHost(assert, res[1]); 1.196 + assert.equal(res[0].group.title, 'myfolder', 'parent is ducktyped group'); 1.197 + assert.equal(res[1].group.title, 'myfolder', 'parent is ducktyped group'); 1.198 + done(); 1.199 + }); 1.200 +}; 1.201 + 1.202 +/* 1.203 + * Tests the scenario where the original bookmark item is resaved 1.204 + * and does not have an ID or an updated date, but should still be 1.205 + * mapped to the item it created previously 1.206 + */ 1.207 +exports.testResaveOriginalItemMapping = function (assert, done) { 1.208 + let bookmark = Bookmark({ title: 'moz', url: 'http://moz.org' }); 1.209 + save(bookmark).on('data', newBookmark => { 1.210 + bookmark.title = 'new moz'; 1.211 + save(bookmark).on('data', newNewBookmark => { 1.212 + assert.equal(newBookmark.id, newNewBookmark.id, 'should be the same bookmark item'); 1.213 + assert.equal(bmsrv.getItemTitle(newBookmark.id), 'new moz', 'should have updated title'); 1.214 + done(); 1.215 + }); 1.216 + }); 1.217 +}; 1.218 + 1.219 +exports.testCreateMultipleBookmarks = function (assert, done) { 1.220 + let data = [ 1.221 + Bookmark({title: 'bm1', url: 'http://bm1.com'}), 1.222 + Bookmark({title: 'bm2', url: 'http://bm2.com'}), 1.223 + Bookmark({title: 'bm3', url: 'http://bm3.com'}), 1.224 + ]; 1.225 + save(data).on('data', function (bookmark, input) { 1.226 + let stored = data.filter(({title}) => title === bookmark.title)[0]; 1.227 + assert.equal(input, stored, 'input is original input item'); 1.228 + assert.equal(bookmark.title, stored.title, 'titles match'); 1.229 + assert.equal(bookmark.url, stored.url, 'urls match'); 1.230 + compareWithHost(assert, bookmark); 1.231 + }).on('end', function (bookmarks) { 1.232 + assert.equal(bookmarks.length, 3, 'all bookmarks returned'); 1.233 + done(); 1.234 + }); 1.235 +}; 1.236 + 1.237 +exports.testCreateImplicitParent = function (assert, done) { 1.238 + let folder = Group({ title: 'my parent' }); 1.239 + let bookmarks = [ 1.240 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }), 1.241 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }), 1.242 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder }) 1.243 + ]; 1.244 + save(bookmarks).on('data', function (bookmark) { 1.245 + if (bookmark.type === 'bookmark') { 1.246 + assert.equal(bookmark.group.title, folder.title, 'parent is linked'); 1.247 + compareWithHost(assert, bookmark); 1.248 + } else if (bookmark.type === 'group') { 1.249 + assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct'); 1.250 + compareWithHost(assert, bookmark); 1.251 + } 1.252 + }).on('end', function (results) { 1.253 + assert.equal(results.length, 3, 'results should only hold explicit saves'); 1.254 + done(); 1.255 + }); 1.256 +}; 1.257 + 1.258 +exports.testCreateExplicitParent = function (assert, done) { 1.259 + let folder = Group({ title: 'my parent' }); 1.260 + let bookmarks = [ 1.261 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }), 1.262 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }), 1.263 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder }) 1.264 + ]; 1.265 + save(bookmarks.concat(folder)).on('data', function (bookmark) { 1.266 + if (bookmark.type === 'bookmark') { 1.267 + assert.equal(bookmark.group.title, folder.title, 'parent is linked'); 1.268 + compareWithHost(assert, bookmark); 1.269 + } else if (bookmark.type === 'group') { 1.270 + assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct'); 1.271 + compareWithHost(assert, bookmark); 1.272 + } 1.273 + }).on('end', function () { 1.274 + done(); 1.275 + }); 1.276 +}; 1.277 + 1.278 +exports.testCreateNested = function (assert, done) { 1.279 + let topFolder = Group({ title: 'top', group: MENU }); 1.280 + let midFolder = Group({ title: 'middle', group: topFolder }); 1.281 + let bookmarks = [ 1.282 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder }), 1.283 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder }), 1.284 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder }) 1.285 + ]; 1.286 + let dataEventCount = 0; 1.287 + save(bookmarks).on('data', function (bookmark) { 1.288 + if (bookmark.type === 'bookmark') { 1.289 + assert.equal(bookmark.group.title, midFolder.title, 'parent is linked'); 1.290 + } else if (bookmark.title === 'top') { 1.291 + assert.equal(bookmark.group.id, MENU.id, 'parent ID of top group is correct'); 1.292 + } else { 1.293 + assert.equal(bookmark.group.title, topFolder.title, 'parent title of middle group is correct'); 1.294 + } 1.295 + dataEventCount++; 1.296 + compareWithHost(assert, bookmark); 1.297 + }).on('end', () => { 1.298 + assert.equal(dataEventCount, 5, 'data events for all saves have occurred'); 1.299 + assert.ok('end event called'); 1.300 + done(); 1.301 + }); 1.302 +}; 1.303 + 1.304 +/* 1.305 + * Was a scenario when implicitly saving a bookmark that was already created, 1.306 + * it was not being properly fetched and attempted to recreate 1.307 + */ 1.308 +exports.testAddingToExistingParent = function (assert, done) { 1.309 + let group = { type: 'group', title: 'mozgroup' }; 1.310 + let bookmarks = [ 1.311 + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group }, 1.312 + { title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group }, 1.313 + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group } 1.314 + ], 1.315 + firstBatch, secondBatch; 1.316 + 1.317 + saveP(bookmarks).then(data => { 1.318 + firstBatch = data; 1.319 + return saveP([ 1.320 + { title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group }, 1.321 + { title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group } 1.322 + ]); 1.323 + }, assert.fail).then(data => { 1.324 + secondBatch = data; 1.325 + assert.equal(firstBatch[0].group.id, secondBatch[0].group.id, 1.326 + 'successfully saved to the same parent'); 1.327 + done(); 1.328 + }, assert.fail); 1.329 +}; 1.330 + 1.331 +exports.testUpdateParent = function (assert, done) { 1.332 + let group = { type: 'group', title: 'mozgroup' }; 1.333 + saveP(group).then(item => { 1.334 + item[0].title = 'mozgroup-resave'; 1.335 + return saveP(item[0]); 1.336 + }).then(item => { 1.337 + assert.equal(item[0].title, 'mozgroup-resave', 'group saved successfully'); 1.338 + done(); 1.339 + }); 1.340 +}; 1.341 + 1.342 +exports.testUpdateSeparator = function (assert, done) { 1.343 + let sep = [Separator(), Separator(), Separator()]; 1.344 + saveP(sep).then(item => { 1.345 + item[0].index = 2; 1.346 + return saveP(item[0]); 1.347 + }).then(item => { 1.348 + assert.equal(item[0].index, 2, 'updated index of separator'); 1.349 + done(); 1.350 + }); 1.351 +}; 1.352 + 1.353 +exports.testPromisedSave = function (assert, done) { 1.354 + let topFolder = Group({ title: 'top', group: MENU }); 1.355 + let midFolder = Group({ title: 'middle', group: topFolder }); 1.356 + let bookmarks = [ 1.357 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), 1.358 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), 1.359 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) 1.360 + ]; 1.361 + let first, second, third; 1.362 + saveP(bookmarks).then(bms => { 1.363 + first = bms.filter(b => b.title === 'moz1')[0]; 1.364 + second = bms.filter(b => b.title === 'moz2')[0]; 1.365 + third = bms.filter(b => b.title === 'moz3')[0]; 1.366 + assert.equal(first.index, 0); 1.367 + assert.equal(second.index, 1); 1.368 + assert.equal(third.index, 2); 1.369 + first.index = 3; 1.370 + return saveP(first); 1.371 + }).then(() => { 1.372 + assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark'); 1.373 + assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted'); 1.374 + assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted'); 1.375 + done(); 1.376 + }); 1.377 +}; 1.378 + 1.379 +exports.testPromisedErrorSave = function (assert, done) { 1.380 + let bookmarks = [ 1.381 + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark'}, 1.382 + { title: 'moz2', url: 'invalidurl', type: 'bookmark'}, 1.383 + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark'} 1.384 + ]; 1.385 + saveP(bookmarks).then(invalidResolve, reason => { 1.386 + assert.ok( 1.387 + /The `url` property must be a valid URL/.test(reason), 1.388 + 'Error event called with correct reason'); 1.389 + 1.390 + bookmarks[1].url = 'http://moz2.com'; 1.391 + return saveP(bookmarks); 1.392 + }).then(res => { 1.393 + return searchP({ query: 'moz' }); 1.394 + }).then(res => { 1.395 + assert.equal(res.length, 3, 'all 3 should be saved upon retry'); 1.396 + res.map(item => assert.ok(/moz\d\.com/.test(item.url), 'correct item')); 1.397 + done(); 1.398 + }, invalidReject); 1.399 +}; 1.400 + 1.401 +exports.testMovingChildren = function (assert, done) { 1.402 + let topFolder = Group({ title: 'top', group: MENU }); 1.403 + let midFolder = Group({ title: 'middle', group: topFolder }); 1.404 + let bookmarks = [ 1.405 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), 1.406 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), 1.407 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) 1.408 + ]; 1.409 + save(bookmarks).on('end', bms => { 1.410 + let first = bms.filter(b => b.title === 'moz1')[0]; 1.411 + let second = bms.filter(b => b.title === 'moz2')[0]; 1.412 + let third = bms.filter(b => b.title === 'moz3')[0]; 1.413 + assert.equal(first.index, 0); 1.414 + assert.equal(second.index, 1); 1.415 + assert.equal(third.index, 2); 1.416 + /* When moving down in the same container we take 1.417 + * into account the removal of the original item. If you want 1.418 + * to move from index X to index Y > X you must use 1.419 + * moveItem(id, folder, Y + 1) 1.420 + */ 1.421 + first.index = 3; 1.422 + save(first).on('end', () => { 1.423 + assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark'); 1.424 + assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted'); 1.425 + assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted'); 1.426 + done(); 1.427 + }); 1.428 + }); 1.429 +}; 1.430 + 1.431 +exports.testMovingChildrenNewFolder = function (assert, done) { 1.432 + let topFolder = Group({ title: 'top', group: MENU }); 1.433 + let midFolder = Group({ title: 'middle', group: topFolder }); 1.434 + let newFolder = Group({ title: 'new', group: MENU }); 1.435 + let bookmarks = [ 1.436 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), 1.437 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), 1.438 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) 1.439 + ]; 1.440 + save(bookmarks).on('end', bms => { 1.441 + let first = bms.filter(b => b.title === 'moz1')[0]; 1.442 + let second = bms.filter(b => b.title === 'moz2')[0]; 1.443 + let third = bms.filter(b => b.title === 'moz3')[0]; 1.444 + let definedMidFolder = first.group; 1.445 + let definedNewFolder; 1.446 + first.group = newFolder; 1.447 + assert.equal(first.index, 0); 1.448 + assert.equal(second.index, 1); 1.449 + assert.equal(third.index, 2); 1.450 + save(first).on('data', (data) => { 1.451 + if (data.type === 'group') definedNewFolder = data; 1.452 + }).on('end', (moved) => { 1.453 + assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted'); 1.454 + assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted'); 1.455 + assert.equal(bmsrv.getItemIndex(first.id), 0, 'properly moved bookmark'); 1.456 + assert.equal(bmsrv.getFolderIdForItem(first.id), definedNewFolder.id, 1.457 + 'bookmark has new parent'); 1.458 + assert.equal(bmsrv.getFolderIdForItem(second.id), definedMidFolder.id, 1.459 + 'sibling bookmarks did not move'); 1.460 + assert.equal(bmsrv.getFolderIdForItem(third.id), definedMidFolder.id, 1.461 + 'sibling bookmarks did not move'); 1.462 + done(); 1.463 + }); 1.464 + }); 1.465 +}; 1.466 + 1.467 +exports.testRemoveFunction = function (assert) { 1.468 + let topFolder = Group({ title: 'new', group: MENU }); 1.469 + let midFolder = Group({ title: 'middle', group: topFolder }); 1.470 + let bookmarks = [ 1.471 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), 1.472 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), 1.473 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) 1.474 + ]; 1.475 + remove([midFolder, topFolder].concat(bookmarks)).map(item => { 1.476 + assert.equal(item.remove, true, 'remove toggled `remove` property to true'); 1.477 + }); 1.478 +}; 1.479 + 1.480 +exports.testRemove = function (assert, done) { 1.481 + let id; 1.482 + createBookmarkItem().then(data => { 1.483 + id = data.id; 1.484 + compareWithHost(assert, data); // ensure bookmark exists 1.485 + save(remove(data)).on('data', (res) => { 1.486 + assert.pass('data event should be called'); 1.487 + assert.ok(!res, 'response should be empty'); 1.488 + }).on('end', () => { 1.489 + assert.throws(function () { 1.490 + bmsrv.getItemTitle(id); 1.491 + }, 'item should no longer exist'); 1.492 + done(); 1.493 + }); 1.494 + }); 1.495 +}; 1.496 + 1.497 +/* 1.498 + * Tests recursively removing children when removing a group 1.499 + */ 1.500 +exports.testRemoveAllChildren = function (assert, done) { 1.501 + let topFolder = Group({ title: 'new', group: MENU }); 1.502 + let midFolder = Group({ title: 'middle', group: topFolder }); 1.503 + let bookmarks = [ 1.504 + Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}), 1.505 + Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}), 1.506 + Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder}) 1.507 + ]; 1.508 + 1.509 + let saved = []; 1.510 + save(bookmarks).on('data', (data) => saved.push(data)).on('end', () => { 1.511 + save(remove(topFolder)).on('end', () => { 1.512 + assert.equal(saved.length, 5, 'all items should have been saved'); 1.513 + saved.map((item) => { 1.514 + assert.throws(function () { 1.515 + bmsrv.getItemTitle(item.id); 1.516 + }, 'item should no longer exist'); 1.517 + }); 1.518 + done(); 1.519 + }); 1.520 + }); 1.521 +}; 1.522 + 1.523 +exports.testResolution = function (assert, done) { 1.524 + let firstSave, secondSave; 1.525 + createBookmarkItem().then((item) => { 1.526 + firstSave = item; 1.527 + assert.ok(item.updated, 'bookmark has updated time'); 1.528 + item.title = 'my title'; 1.529 + // Ensure delay so a different save time is set 1.530 + return delayed(item); 1.531 + }).then(saveP) 1.532 + .then(items => { 1.533 + let item = items[0]; 1.534 + secondSave = item; 1.535 + assert.ok(firstSave.updated < secondSave.updated, 'snapshots have different update times'); 1.536 + firstSave.title = 'updated title'; 1.537 + return saveP(firstSave, { resolve: (mine, theirs) => { 1.538 + assert.equal(mine.title, 'updated title', 'correct data for my object'); 1.539 + assert.equal(theirs.title, 'my title', 'correct data for their object'); 1.540 + assert.equal(mine.url, theirs.url, 'other data is equal'); 1.541 + assert.equal(mine.group, theirs.group, 'other data is equal'); 1.542 + assert.ok(mine !== firstSave, 'instance is not passed in'); 1.543 + assert.ok(theirs !== secondSave, 'instance is not passed in'); 1.544 + assert.equal(mine.toString(), '[object Object]', 'serialized objects'); 1.545 + assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); 1.546 + mine.title = 'a new title'; 1.547 + return mine; 1.548 + }}); 1.549 + }).then((results) => { 1.550 + let result = results[0]; 1.551 + assert.equal(result.title, 'a new title', 'resolve handles results'); 1.552 + done(); 1.553 + }); 1.554 +}; 1.555 + 1.556 +/* 1.557 + * Same as the resolution test, but with the 'unsaved' snapshot 1.558 + */ 1.559 +exports.testResolutionMapping = function (assert, done) { 1.560 + let bookmark = Bookmark({ title: 'moz', url: 'http://bookmarks4life.com/' }); 1.561 + let saved; 1.562 + saveP(bookmark).then(data => { 1.563 + saved = data[0]; 1.564 + saved.title = 'updated title'; 1.565 + // Ensure a delay for different updated times 1.566 + return delayed(saved); 1.567 + }).then(saveP) 1.568 + .then(() => { 1.569 + bookmark.title = 'conflicting title'; 1.570 + return saveP(bookmark, { resolve: (mine, theirs) => { 1.571 + assert.equal(mine.title, 'conflicting title', 'correct data for my object'); 1.572 + assert.equal(theirs.title, 'updated title', 'correct data for their object'); 1.573 + assert.equal(mine.url, theirs.url, 'other data is equal'); 1.574 + assert.equal(mine.group, theirs.group, 'other data is equal'); 1.575 + assert.ok(mine !== bookmark, 'instance is not passed in'); 1.576 + assert.ok(theirs !== saved, 'instance is not passed in'); 1.577 + assert.equal(mine.toString(), '[object Object]', 'serialized objects'); 1.578 + assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); 1.579 + mine.title = 'a new title'; 1.580 + return mine; 1.581 + }}); 1.582 + }).then((results) => { 1.583 + let result = results[0]; 1.584 + assert.equal(result.title, 'a new title', 'resolve handles results'); 1.585 + done(); 1.586 + }); 1.587 +}; 1.588 + 1.589 +exports.testUpdateTags = function (assert, done) { 1.590 + createBookmarkItem({ tags: ['spidermonkey'] }).then(bookmark => { 1.591 + bookmark.tags.add('jagermonkey'); 1.592 + bookmark.tags.add('ionmonkey'); 1.593 + bookmark.tags.delete('spidermonkey'); 1.594 + save(bookmark).on('data', saved => { 1.595 + assert.equal(saved.tags.size, 2, 'should have 2 tags'); 1.596 + assert.ok(saved.tags.has('jagermonkey'), 'should have added tag'); 1.597 + assert.ok(saved.tags.has('ionmonkey'), 'should have added tag'); 1.598 + assert.ok(!saved.tags.has('spidermonkey'), 'should not have removed tag'); 1.599 + done(); 1.600 + }); 1.601 + }); 1.602 +}; 1.603 + 1.604 +/* 1.605 + * View `createBookmarkTree` in `./places-helper.js` to see 1.606 + * expected tree construction 1.607 + */ 1.608 + 1.609 +exports.testSearchByGroupSimple = function (assert, done) { 1.610 + createBookmarkTree().then(() => { 1.611 + // In initial release of Places API, groups can only be queried 1.612 + // via a 'simple query', which is one folder set, and no other 1.613 + // parameters 1.614 + return searchP({ group: UNSORTED }); 1.615 + }).then(results => { 1.616 + let groups = results.filter(({type}) => type === 'group'); 1.617 + assert.equal(groups.length, 2, 'returns folders'); 1.618 + assert.equal(results.length, 7, 1.619 + 'should return all bookmarks and folders under UNSORTED'); 1.620 + assert.equal(groups[0].toString(), '[object Group]', 'returns instance'); 1.621 + return searchP({ 1.622 + group: groups.filter(({title}) => title === 'mozgroup')[0] 1.623 + }); 1.624 + }).then(results => { 1.625 + let groups = results.filter(({type}) => type === 'group'); 1.626 + assert.equal(groups.length, 1, 'returns one subfolder'); 1.627 + assert.equal(results.length, 6, 1.628 + 'returns all children bookmarks/folders'); 1.629 + assert.ok(results.filter(({url}) => url === 'http://w3schools.com/'), 1.630 + 'returns nested children'); 1.631 + done(); 1.632 + }).then(null, assert.fail); 1.633 +}; 1.634 + 1.635 +exports.testSearchByGroupComplex = function (assert, done) { 1.636 + let mozgroup; 1.637 + createBookmarkTree().then(results => { 1.638 + mozgroup = results.filter(({title}) => title === 'mozgroup')[0]; 1.639 + return searchP({ group: mozgroup, query: 'javascript' }); 1.640 + }).then(results => { 1.641 + assert.equal(results.length, 1, 'only one javascript result under mozgroup'); 1.642 + assert.equal(results[0].url, 'http://w3schools.com/', 'correct result'); 1.643 + return searchP({ group: mozgroup, url: '*.mozilla.org' }); 1.644 + }).then(results => { 1.645 + assert.equal(results.length, 2, 'expected results'); 1.646 + assert.ok( 1.647 + !results.filter(({url}) => /developer.mozilla/.test(url)).length, 1.648 + 'does not find results from other folders'); 1.649 + done(); 1.650 + }, assert.fail); 1.651 +}; 1.652 + 1.653 +exports.testSearchEmitters = function (assert, done) { 1.654 + createBookmarkTree().then(() => { 1.655 + let count = 0; 1.656 + search({ tags: ['mozilla', 'firefox'] }).on('data', data => { 1.657 + assert.ok(/mozilla|firefox/.test(data.title), 'one of the correct items'); 1.658 + assert.ok(data.tags.has('firefox'), 'has firefox tag'); 1.659 + assert.ok(data.tags.has('mozilla'), 'has mozilla tag'); 1.660 + assert.equal(data + '', '[object Bookmark]', 'returns bookmark'); 1.661 + count++; 1.662 + }).on('end', data => { 1.663 + assert.equal(count, 3, 'data event was called for each item'); 1.664 + assert.equal(data.length, 3, 1.665 + 'should return two bookmarks that have both mozilla AND firefox'); 1.666 + assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark'); 1.667 + assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark'); 1.668 + assert.equal(data[2].title, 'firefox', 'returns correct bookmark'); 1.669 + assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks'); 1.670 + done(); 1.671 + }); 1.672 + }); 1.673 +}; 1.674 + 1.675 +exports.testSearchTags = function (assert, done) { 1.676 + createBookmarkTree().then(() => { 1.677 + // AND tags 1.678 + return searchP({ tags: ['mozilla', 'firefox'] }); 1.679 + }).then(data => { 1.680 + assert.equal(data.length, 3, 1.681 + 'should return two bookmarks that have both mozilla AND firefox'); 1.682 + assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark'); 1.683 + assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark'); 1.684 + assert.equal(data[2].title, 'firefox', 'returns correct bookmark'); 1.685 + assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks'); 1.686 + return searchP([{tags: ['firefox']}, {tags: ['javascript']}]); 1.687 + }).then(data => { 1.688 + // OR tags 1.689 + assert.equal(data.length, 6, 1.690 + 'should return all bookmarks with firefox OR javascript tag'); 1.691 + done(); 1.692 + }); 1.693 +}; 1.694 + 1.695 +/* 1.696 + * Tests 4 scenarios 1.697 + * '*.mozilla.com' 1.698 + * 'mozilla.com' 1.699 + * 'http://mozilla.com/' 1.700 + * 'http://mozilla.com/*' 1.701 + */ 1.702 +exports.testSearchURL = function (assert, done) { 1.703 + createBookmarkTree().then(() => { 1.704 + return searchP({ url: 'mozilla.org' }); 1.705 + }).then(data => { 1.706 + assert.equal(data.length, 2, 'only URLs with host domain'); 1.707 + assert.equal(data[0].url, 'http://mozilla.org/'); 1.708 + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); 1.709 + return searchP({ url: '*.mozilla.org' }); 1.710 + }).then(data => { 1.711 + assert.equal(data.length, 3, 'returns domain and when host is other than domain'); 1.712 + assert.equal(data[0].url, 'http://mozilla.org/'); 1.713 + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); 1.714 + assert.equal(data[2].url, 'http://developer.mozilla.org/en-US/'); 1.715 + return searchP({ url: 'http://mozilla.org' }); 1.716 + }).then(data => { 1.717 + assert.equal(data.length, 1, 'only exact URL match'); 1.718 + assert.equal(data[0].url, 'http://mozilla.org/'); 1.719 + return searchP({ url: 'http://mozilla.org/*' }); 1.720 + }).then(data => { 1.721 + assert.equal(data.length, 2, 'only URLs that begin with query'); 1.722 + assert.equal(data[0].url, 'http://mozilla.org/'); 1.723 + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); 1.724 + return searchP([{ url: 'mozilla.org' }, { url: 'component.fm' }]); 1.725 + }).then(data => { 1.726 + assert.equal(data.length, 3, 'returns URLs that match EITHER query'); 1.727 + assert.equal(data[0].url, 'http://mozilla.org/'); 1.728 + assert.equal(data[1].url, 'http://mozilla.org/thunderbird/'); 1.729 + assert.equal(data[2].url, 'http://component.fm/'); 1.730 + }).then(() => { 1.731 + done(); 1.732 + }); 1.733 +}; 1.734 + 1.735 +/* 1.736 + * Searches url, title, tags 1.737 + */ 1.738 +exports.testSearchQuery = function (assert, done) { 1.739 + createBookmarkTree().then(() => { 1.740 + return searchP({ query: 'thunder' }); 1.741 + }).then(data => { 1.742 + assert.equal(data.length, 3); 1.743 + assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title'); 1.744 + assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title'); 1.745 + assert.equal(data[2].title, 'thunderbird', 'query matches tag, url, or title'); 1.746 + return searchP([{ query: 'rust' }, { query: 'component' }]); 1.747 + }).then(data => { 1.748 + // rust OR component 1.749 + assert.equal(data.length, 3); 1.750 + assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title'); 1.751 + assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title'); 1.752 + assert.equal(data[2].title, 'web audio components', 'query matches tag, url, or title'); 1.753 + return searchP([{ query: 'moz', tags: ['javascript']}]); 1.754 + }).then(data => { 1.755 + assert.equal(data.length, 1); 1.756 + assert.equal(data[0].title, 'mdn', 1.757 + 'only one item matches moz query AND has a javascript tag'); 1.758 + }).then(() => { 1.759 + done(); 1.760 + }); 1.761 +}; 1.762 + 1.763 +/* 1.764 + * Test caching on bulk calls. 1.765 + * Each construction of a bookmark item snapshot results in 1.766 + * the recursive lookup of parent groups up to the root groups -- 1.767 + * ensure that the appropriate instances equal each other, and no duplicate 1.768 + * fetches are called 1.769 + * 1.770 + * Implementation-dependent, this checks the host event `sdk-places-bookmarks-get`, 1.771 + * and if implementation changes, this could increase or decrease 1.772 + */ 1.773 + 1.774 +exports.testCaching = function (assert, done) { 1.775 + let count = 0; 1.776 + let stream = filter(request, ({event}) => 1.777 + /sdk-places-bookmarks-get/.test(event)); 1.778 + on(stream, 'data', handle); 1.779 + 1.780 + let group = { type: 'group', title: 'mozgroup' }; 1.781 + let bookmarks = [ 1.782 + { title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group }, 1.783 + { title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group }, 1.784 + { title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group } 1.785 + ]; 1.786 + 1.787 + /* 1.788 + * Use timeout in tests since the platform calls are synchronous 1.789 + * and the counting event shim may not have occurred yet 1.790 + */ 1.791 + 1.792 + saveP(bookmarks).then(() => { 1.793 + assert.equal(count, 0, 'all new items and root group, no fetches should occur'); 1.794 + count = 0; 1.795 + return saveP([ 1.796 + { title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group }, 1.797 + { title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group } 1.798 + ]); 1.799 + // Test `save` look-up 1.800 + }).then(() => { 1.801 + assert.equal(count, 1, 'should only look up parent once'); 1.802 + count = 0; 1.803 + return searchP({ query: 'moz' }); 1.804 + }).then(results => { 1.805 + // Should query for each bookmark (5) from the query (id -> data), 1.806 + // their parent during `construct` (1) and the root shouldn't 1.807 + // require a lookup 1.808 + assert.equal(count, 6, 'lookup occurs once for each item and parent'); 1.809 + off(stream, 'data', handle); 1.810 + done(); 1.811 + }); 1.812 + 1.813 + function handle ({data}) count++ 1.814 +}; 1.815 + 1.816 +/* 1.817 + * Search Query Options 1.818 + */ 1.819 + 1.820 +exports.testSearchCount = function (assert, done) { 1.821 + let max = 8; 1.822 + createBookmarkTree() 1.823 + .then(testCount(1)) 1.824 + .then(testCount(2)) 1.825 + .then(testCount(3)) 1.826 + .then(testCount(5)) 1.827 + .then(testCount(10)) 1.828 + .then(() => { 1.829 + done(); 1.830 + }); 1.831 + 1.832 + function testCount (n) { 1.833 + return function () { 1.834 + return searchP({}, { count: n }).then(results => { 1.835 + if (n > max) n = max; 1.836 + assert.equal(results.length, n, 1.837 + 'count ' + n + ' returns ' + n + ' results'); 1.838 + }); 1.839 + }; 1.840 + } 1.841 +}; 1.842 + 1.843 +exports.testSearchSort = function (assert, done) { 1.844 + let urls = [ 1.845 + 'http://mozilla.com/', 'http://webaud.io/', 'http://mozilla.com/webfwd/', 1.846 + 'http://developer.mozilla.com/', 'http://bandcamp.com/' 1.847 + ]; 1.848 + 1.849 + saveP( 1.850 + urls.map(url => 1.851 + Bookmark({ url: url, title: url.replace(/http:\/\/|\//g,'')})) 1.852 + ).then(() => { 1.853 + return searchP({}, { sort: 'title' }); 1.854 + }).then(results => { 1.855 + checkOrder(results, [4,3,0,2,1]); 1.856 + return searchP({}, { sort: 'title', descending: true }); 1.857 + }).then(results => { 1.858 + checkOrder(results, [1,2,0,3,4]); 1.859 + return searchP({}, { sort: 'url' }); 1.860 + }).then(results => { 1.861 + checkOrder(results, [4,3,0,2,1]); 1.862 + return searchP({}, { sort: 'url', descending: true }); 1.863 + }).then(results => { 1.864 + checkOrder(results, [1,2,0,3,4]); 1.865 + return addVisits(['http://mozilla.com/', 'http://mozilla.com']); 1.866 + }).then(() => 1.867 + saveP(Bookmark({ url: 'http://github.com', title: 'github.com' })) 1.868 + ).then(() => addVisits('http://bandcamp.com/')) 1.869 + .then(() => searchP({ query: 'webfwd' })) 1.870 + .then(results => { 1.871 + results[0].title = 'new title for webfwd'; 1.872 + return saveP(results[0]); 1.873 + }) 1.874 + .then(() => 1.875 + searchP({}, { sort: 'visitCount' }) 1.876 + ).then(results => { 1.877 + assert.equal(results[5].url, 'http://mozilla.com/', 1.878 + 'last entry is the highest visit count'); 1.879 + return searchP({}, { sort: 'visitCount', descending: true }); 1.880 + }).then(results => { 1.881 + assert.equal(results[0].url, 'http://mozilla.com/', 1.882 + 'first entry is the highest visit count'); 1.883 + return searchP({}, { sort: 'date' }); 1.884 + }).then(results => { 1.885 + assert.equal(results[5].url, 'http://bandcamp.com/', 1.886 + 'latest visited should be first'); 1.887 + return searchP({}, { sort: 'date', descending: true }); 1.888 + }).then(results => { 1.889 + assert.equal(results[0].url, 'http://bandcamp.com/', 1.890 + 'latest visited should be at the end'); 1.891 + return searchP({}, { sort: 'dateAdded' }); 1.892 + }).then(results => { 1.893 + assert.equal(results[5].url, 'http://github.com/', 1.894 + 'last added should be at the end'); 1.895 + return searchP({}, { sort: 'dateAdded', descending: true }); 1.896 + }).then(results => { 1.897 + assert.equal(results[0].url, 'http://github.com/', 1.898 + 'last added should be first'); 1.899 + return searchP({}, { sort: 'lastModified' }); 1.900 + }).then(results => { 1.901 + assert.equal(results[5].url, 'http://mozilla.com/webfwd/', 1.902 + 'last modified should be last'); 1.903 + return searchP({}, { sort: 'lastModified', descending: true }); 1.904 + }).then(results => { 1.905 + assert.equal(results[0].url, 'http://mozilla.com/webfwd/', 1.906 + 'last modified should be first'); 1.907 + }).then(() => { 1.908 + done(); 1.909 + }); 1.910 + 1.911 + function checkOrder (results, nums) { 1.912 + assert.equal(results.length, nums.length, 'expected return count'); 1.913 + for (let i = 0; i < nums.length; i++) { 1.914 + assert.equal(results[i].url, urls[nums[i]], 'successful order'); 1.915 + } 1.916 + } 1.917 +}; 1.918 + 1.919 +exports.testSearchComplexQueryWithOptions = function (assert, done) { 1.920 + createBookmarkTree().then(() => { 1.921 + return searchP([ 1.922 + { tags: ['rust'], url: '*.mozilla.org' }, 1.923 + { tags: ['javascript'], query: 'mozilla' } 1.924 + ], { sort: 'title' }); 1.925 + }).then(results => { 1.926 + let expected = [ 1.927 + 'http://developer.mozilla.org/en-US/', 1.928 + 'http://mozilla.org/' 1.929 + ]; 1.930 + for (let i = 0; i < expected.length; i++) 1.931 + assert.equal(results[i].url, expected[i], 'correct ordering and item'); 1.932 + done(); 1.933 + }); 1.934 +}; 1.935 + 1.936 +exports.testCheckSaveOrder = function (assert, done) { 1.937 + let group = Group({ title: 'mygroup' }); 1.938 + let bookmarks = [ 1.939 + Bookmark({ url: 'http://url1.com', title: 'url1', group: group }), 1.940 + Bookmark({ url: 'http://url2.com', title: 'url2', group: group }), 1.941 + Bookmark({ url: 'http://url3.com', title: 'url3', group: group }), 1.942 + Bookmark({ url: 'http://url4.com', title: 'url4', group: group }), 1.943 + Bookmark({ url: 'http://url5.com', title: 'url5', group: group }) 1.944 + ]; 1.945 + saveP(bookmarks).then(results => { 1.946 + for (let i = 0; i < bookmarks.length; i++) 1.947 + assert.equal(results[i].url, bookmarks[i].url, 1.948 + 'correct ordering of bookmark results'); 1.949 + done(); 1.950 + }); 1.951 +}; 1.952 + 1.953 +before(exports, (name, assert, done) => resetPlaces(done)); 1.954 +after(exports, (name, assert, done) => resetPlaces(done)); 1.955 + 1.956 +function saveP () { 1.957 + return promisedEmitter(save.apply(null, Array.slice(arguments))); 1.958 +} 1.959 + 1.960 +function searchP () { 1.961 + return promisedEmitter(search.apply(null, Array.slice(arguments))); 1.962 +} 1.963 + 1.964 +function delayed (value, ms) { 1.965 + let { promise, resolve } = defer(); 1.966 + setTimeout(() => resolve(value), ms || 10); 1.967 + return promise; 1.968 +}