diff options
Diffstat (limited to 'toolkit/components/places/tests/unit/test_async_transactions.js')
-rw-r--r-- | toolkit/components/places/tests/unit/test_async_transactions.js | 1739 |
1 files changed, 1739 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/unit/test_async_transactions.js b/toolkit/components/places/tests/unit/test_async_transactions.js new file mode 100644 index 000000000..edc9abf87 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_async_transactions.js @@ -0,0 +1,1739 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +const bmsvc = PlacesUtils.bookmarks; +const tagssvc = PlacesUtils.tagging; +const annosvc = PlacesUtils.annotations; +const PT = PlacesTransactions; +const rootGuid = PlacesUtils.bookmarks.rootGuid; + +Components.utils.importGlobalProperties(["URL"]); + +// Create and add bookmarks observer. +var observer = { + __proto__: NavBookmarkObserver.prototype, + + tagRelatedGuids: new Set(), + + reset: function () { + this.itemsAdded = new Map(); + this.itemsRemoved = new Map(); + this.itemsChanged = new Map(); + this.itemsMoved = new Map(); + this.beginUpdateBatch = false; + this.endUpdateBatch = false; + }, + + onBeginUpdateBatch: function () { + this.beginUpdateBatch = true; + }, + + onEndUpdateBatch: function () { + this.endUpdateBatch = true; + }, + + onItemAdded: + function (aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, + aGuid, aParentGuid) { + // Ignore tag items. + if (aParentId == PlacesUtils.tagsFolderId || + (aParentId != PlacesUtils.placesRootId && + bmsvc.getFolderIdForItem(aParentId) == PlacesUtils.tagsFolderId)) { + this.tagRelatedGuids.add(aGuid); + return; + } + + this.itemsAdded.set(aGuid, { itemId: aItemId + , parentGuid: aParentGuid + , index: aIndex + , itemType: aItemType + , title: aTitle + , url: aURI }); + }, + + onItemRemoved: + function (aItemId, aParentId, aIndex, aItemType, aURI, aGuid, aParentGuid) { + if (this.tagRelatedGuids.has(aGuid)) + return; + + this.itemsRemoved.set(aGuid, { parentGuid: aParentGuid + , index: aIndex + , itemType: aItemType }); + }, + + onItemChanged: + function (aItemId, aProperty, aIsAnnoProperty, aNewValue, aLastModified, + aItemType, aParentId, aGuid, aParentGuid) { + if (this.tagRelatedGuids.has(aGuid)) + return; + + let changesForGuid = this.itemsChanged.get(aGuid); + if (changesForGuid === undefined) { + changesForGuid = new Map(); + this.itemsChanged.set(aGuid, changesForGuid); + } + + let newValue = aNewValue; + if (aIsAnnoProperty) { + if (annosvc.itemHasAnnotation(aItemId, aProperty)) + newValue = annosvc.getItemAnnotation(aItemId, aProperty); + else + newValue = null; + } + let change = { isAnnoProperty: aIsAnnoProperty + , newValue: newValue + , lastModified: aLastModified + , itemType: aItemType }; + changesForGuid.set(aProperty, change); + }, + + onItemVisited: () => {}, + + onItemMoved: + function (aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex, aItemType, + aGuid, aOldParentGuid, aNewParentGuid) { + this.itemsMoved.set(aGuid, { oldParentGuid: aOldParentGuid + , oldIndex: aOldIndex + , newParentGuid: aNewParentGuid + , newIndex: aNewIndex + , itemType: aItemType }); + } +}; +observer.reset(); + +// index at which items should begin +var bmStartIndex = 0; + +function run_test() { + bmsvc.addObserver(observer, false); + do_register_cleanup(function () { + bmsvc.removeObserver(observer); + }); + + run_next_test(); +} + +function sanityCheckTransactionHistory() { + do_check_true(PT.undoPosition <= PT.length); + + let check_entry_throws = f => { + try { + f(); + do_throw("PT.entry should throw for invalid input"); + } catch (ex) {} + }; + check_entry_throws( () => PT.entry(-1) ); + check_entry_throws( () => PT.entry({}) ); + check_entry_throws( () => PT.entry(PT.length) ); + + if (PT.undoPosition < PT.length) + do_check_eq(PT.topUndoEntry, PT.entry(PT.undoPosition)); + else + do_check_null(PT.topUndoEntry); + if (PT.undoPosition > 0) + do_check_eq(PT.topRedoEntry, PT.entry(PT.undoPosition - 1)); + else + do_check_null(PT.topRedoEntry); +} + +function getTransactionsHistoryState() { + let history = []; + for (let i = 0; i < PT.length; i++) { + history.push(PT.entry(i)); + } + return [history, PT.undoPosition]; +} + +function ensureUndoState(aExpectedEntries = [], aExpectedUndoPosition = 0) { + // ensureUndoState is called in various places during this test, so it's + // a good places to sanity-check the transaction-history APIs in all + // cases. + sanityCheckTransactionHistory(); + + let [actualEntries, actualUndoPosition] = getTransactionsHistoryState(); + do_check_eq(actualEntries.length, aExpectedEntries.length); + do_check_eq(actualUndoPosition, aExpectedUndoPosition); + + function checkEqualEntries(aExpectedEntry, aActualEntry) { + do_check_eq(aExpectedEntry.length, aActualEntry.length); + aExpectedEntry.forEach( (t, i) => do_check_eq(t, aActualEntry[i]) ); + } + aExpectedEntries.forEach( (e, i) => checkEqualEntries(e, actualEntries[i]) ); +} + +function ensureItemsAdded(...items) { + Assert.equal(observer.itemsAdded.size, items.length); + for (let item of items) { + Assert.ok(observer.itemsAdded.has(item.guid)); + let info = observer.itemsAdded.get(item.guid); + Assert.equal(info.parentGuid, item.parentGuid); + for (let propName of ["title", "index", "itemType"]) { + if (propName in item) + Assert.equal(info[propName], item[propName]); + } + if ("url" in item) + Assert.ok(info.url.equals(item.url)); + } +} + +function ensureItemsRemoved(...items) { + Assert.equal(observer.itemsRemoved.size, items.length); + for (let item of items) { + // We accept both guids and full info object here. + if (typeof(item) == "string") { + Assert.ok(observer.itemsRemoved.has(item)); + } + else { + Assert.ok(observer.itemsRemoved.has(item.guid)); + let info = observer.itemsRemoved.get(item.guid); + Assert.equal(info.parentGuid, item.parentGuid); + if ("index" in item) + Assert.equal(info.index, item.index); + } + } +} + +function ensureItemsChanged(...items) { + for (let item of items) { + do_check_true(observer.itemsChanged.has(item.guid)); + let changes = observer.itemsChanged.get(item.guid); + do_check_true(changes.has(item.property)); + let info = changes.get(item.property); + do_check_eq(info.isAnnoProperty, Boolean(item.isAnnoProperty)); + do_check_eq(info.newValue, item.newValue); + if ("url" in item) + do_check_true(item.url.equals(info.url)); + } +} + +function ensureAnnotationsSet(aGuid, aAnnos) { + do_check_true(observer.itemsChanged.has(aGuid)); + let changes = observer.itemsChanged.get(aGuid); + for (let anno of aAnnos) { + do_check_true(changes.has(anno.name)); + let changeInfo = changes.get(anno.name); + do_check_true(changeInfo.isAnnoProperty); + do_check_eq(changeInfo.newValue, anno.value); + } +} + +function ensureItemsMoved(...items) { + do_check_true(observer.itemsMoved.size, items.length); + for (let item of items) { + do_check_true(observer.itemsMoved.has(item.guid)); + let info = observer.itemsMoved.get(item.guid); + do_check_eq(info.oldParentGuid, item.oldParentGuid); + do_check_eq(info.oldIndex, item.oldIndex); + do_check_eq(info.newParentGuid, item.newParentGuid); + do_check_eq(info.newIndex, item.newIndex); + } +} + +function ensureTimestampsUpdated(aGuid, aCheckDateAdded = false) { + do_check_true(observer.itemsChanged.has(aGuid)); + let changes = observer.itemsChanged.get(aGuid); + if (aCheckDateAdded) + do_check_true(changes.has("dateAdded")) + do_check_true(changes.has("lastModified")); +} + +function ensureTagsForURI(aURI, aTags) { + let tagsSet = tagssvc.getTagsForURI(aURI); + do_check_eq(tagsSet.length, aTags.length); + do_check_true(aTags.every( t => tagsSet.includes(t))); +} + +function createTestFolderInfo(aTitle = "Test Folder") { + return { parentGuid: rootGuid, title: "Test Folder" }; +} + +function isLivemarkTree(aTree) { + return !!aTree.annos && + aTree.annos.some( a => a.name == PlacesUtils.LMANNO_FEEDURI ); +} + +function* ensureLivemarkCreatedByAddLivemark(aLivemarkGuid) { + // This throws otherwise. + yield PlacesUtils.livemarks.getLivemark({ guid: aLivemarkGuid }); +} + +// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the +// same. +// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set. +function* ensureEqualBookmarksTrees(aOriginal, + aNew, + aIsRestoredItem = true, + aCheckParentAndPosition = false) { + // Note "id" is not-enumerable, and is therefore skipped by Object.keys (both + // ours and the one at deepEqual). This is fine for us because ids are not + // restored by Redo. + if (aIsRestoredItem) { + Assert.deepEqual(aOriginal, aNew); + if (isLivemarkTree(aNew)) + yield ensureLivemarkCreatedByAddLivemark(aNew.guid); + return; + } + + for (let property of Object.keys(aOriginal)) { + if (property == "children") { + Assert.equal(aOriginal.children.length, aNew.children.length); + for (let i = 0; i < aOriginal.children.length; i++) { + yield ensureEqualBookmarksTrees(aOriginal.children[i], + aNew.children[i], + false, + true); + } + } + else if (property == "guid") { + // guid shouldn't be copied if the item was not restored. + Assert.notEqual(aOriginal.guid, aNew.guid); + } + else if (property == "dateAdded") { + // dateAdded shouldn't be copied if the item was not restored. + Assert.ok(is_time_ordered(aOriginal.dateAdded, aNew.dateAdded)); + } + else if (property == "lastModified") { + // same same, except for the never-changed case + if (!aOriginal.lastModified) + Assert.ok(!aNew.lastModified); + else + Assert.ok(is_time_ordered(aOriginal.lastModified, aNew.lastModified)); + } + else if (aCheckParentAndPosition || + (property != "parentGuid" && property != "index")) { + Assert.deepEqual(aOriginal[property], aNew[property]); + } + } + + if (isLivemarkTree(aNew)) + yield ensureLivemarkCreatedByAddLivemark(aNew.guid); +} + +function* ensureBookmarksTreeRestoredCorrectly(...aOriginalBookmarksTrees) { + for (let originalTree of aOriginalBookmarksTrees) { + let restoredTree = + yield PlacesUtils.promiseBookmarksTree(originalTree.guid); + yield ensureEqualBookmarksTrees(originalTree, restoredTree); + } +} + +function* ensureNonExistent(...aGuids) { + for (let guid of aGuids) { + Assert.strictEqual((yield PlacesUtils.promiseBookmarksTree(guid)), null); + } +} + +add_task(function* test_recycled_transactions() { + function* ensureTransactThrowsFor(aTransaction) { + let [txns, undoPosition] = getTransactionsHistoryState(); + try { + yield aTransaction.transact(); + do_throw("Shouldn't be able to use the same transaction twice"); + } + catch (ex) { } + ensureUndoState(txns, undoPosition); + } + + let txn_a = PT.NewFolder(createTestFolderInfo()); + yield txn_a.transact(); + ensureUndoState([[txn_a]], 0); + yield ensureTransactThrowsFor(txn_a); + + yield PT.undo(); + ensureUndoState([[txn_a]], 1); + ensureTransactThrowsFor(txn_a); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); + ensureTransactThrowsFor(txn_a); + + let txn_b = PT.NewFolder(createTestFolderInfo()); + yield PT.batch(function* () { + try { + yield txn_a.transact(); + do_throw("Shouldn't be able to use the same transaction twice"); + } + catch (ex) { } + ensureUndoState(); + yield txn_b.transact(); + }); + ensureUndoState([[txn_b]], 0); + + yield PT.undo(); + ensureUndoState([[txn_b]], 1); + ensureTransactThrowsFor(txn_a); + ensureTransactThrowsFor(txn_b); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); + observer.reset(); +}); + +add_task(function* test_new_folder_with_annotation() { + const ANNO = { name: "TestAnno", value: "TestValue" }; + let folder_info = createTestFolderInfo(); + folder_info.index = bmStartIndex; + folder_info.annotations = [ANNO]; + ensureUndoState(); + let txn = PT.NewFolder(folder_info); + folder_info.guid = yield txn.transact(); + let ensureDo = function* (aRedo = false) { + ensureUndoState([[txn]], 0); + yield ensureItemsAdded(folder_info); + ensureAnnotationsSet(folder_info.guid, [ANNO]); + if (aRedo) + ensureTimestampsUpdated(folder_info.guid, true); + observer.reset(); + }; + + let ensureUndo = () => { + ensureUndoState([[txn]], 1); + ensureItemsRemoved({ guid: folder_info.guid + , parentGuid: folder_info.parentGuid + , index: bmStartIndex }); + observer.reset(); + }; + + yield ensureDo(); + yield PT.undo(); + yield ensureUndo(); + yield PT.redo(); + yield ensureDo(true); + yield PT.undo(); + ensureUndo(); + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_new_bookmark() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test_create_item.com") + , index: bmStartIndex + , title: "Test creating an item" }; + + ensureUndoState(); + let txn = PT.NewBookmark(bm_info); + bm_info.guid = yield txn.transact(); + + let ensureDo = function* (aRedo = false) { + ensureUndoState([[txn]], 0); + yield ensureItemsAdded(bm_info); + if (aRedo) + ensureTimestampsUpdated(bm_info.guid, true); + observer.reset(); + }; + let ensureUndo = () => { + ensureUndoState([[txn]], 1); + ensureItemsRemoved({ guid: bm_info.guid + , parentGuid: bm_info.parentGuid + , index: bmStartIndex }); + observer.reset(); + }; + + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(true); + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_merge_create_folder_and_item() { + let folder_info = createTestFolderInfo(); + let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com") + , title: "Test Bookmark" + , index: bmStartIndex }; + + let [folderTxnResult, bkmTxnResult] = yield PT.batch(function* () { + let folderTxn = PT.NewFolder(folder_info); + folder_info.guid = bm_info.parentGuid = yield folderTxn.transact(); + let bkmTxn = PT.NewBookmark(bm_info); + bm_info.guid = yield bkmTxn.transact(); + return [folderTxn, bkmTxn]; + }); + + let ensureDo = function* () { + ensureUndoState([[bkmTxnResult, folderTxnResult]], 0); + yield ensureItemsAdded(folder_info, bm_info); + observer.reset(); + }; + + let ensureUndo = () => { + ensureUndoState([[bkmTxnResult, folderTxnResult]], 1); + ensureItemsRemoved(folder_info, bm_info); + observer.reset(); + }; + + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(); + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_move_items_to_folder() { + let folder_a_info = createTestFolderInfo("Folder A"); + let bkm_a_info = { url: new URL("http://test_move_items.com") + , title: "Bookmark A" }; + let bkm_b_info = { url: NetUtil.newURI("http://test_move_items.com") + , title: "Bookmark B" }; + + // Test moving items within the same folder. + let [folder_a_txn_result, bkm_a_txn_result, bkm_b_txn_result] = yield PT.batch(function* () { + let folder_a_txn = PT.NewFolder(folder_a_info); + + folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid = + yield folder_a_txn.transact(); + let bkm_a_txn = PT.NewBookmark(bkm_a_info); + bkm_a_info.guid = yield bkm_a_txn.transact(); + let bkm_b_txn = PT.NewBookmark(bkm_b_info); + bkm_b_info.guid = yield bkm_b_txn.transact(); + return [folder_a_txn, bkm_a_txn, bkm_b_txn]; + }); + + ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0); + + let moveTxn = PT.Move({ guid: bkm_a_info.guid + , newParentGuid: folder_a_info.guid }); + yield moveTxn.transact(); + + let ensureDo = () => { + ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_a_info.guid + , newParentGuid: folder_a_info.guid + , oldIndex: 0 + , newIndex: 1 }); + observer.reset(); + }; + let ensureUndo = () => { + ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 1); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_a_info.guid + , newParentGuid: folder_a_info.guid + , oldIndex: 1 + , newIndex: 0 }); + observer.reset(); + }; + + ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(); + ensureDo(); + yield PT.undo(); + ensureUndo(); + + yield PT.clearTransactionsHistory(false, true); + ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0); + + // Test moving items between folders. + let folder_b_info = createTestFolderInfo("Folder B"); + let folder_b_txn = PT.NewFolder(folder_b_info); + folder_b_info.guid = yield folder_b_txn.transact(); + ensureUndoState([ [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0); + + moveTxn = PT.Move({ guid: bkm_a_info.guid + , newParentGuid: folder_b_info.guid + , newIndex: bmsvc.DEFAULT_INDEX }); + yield moveTxn.transact(); + + ensureDo = () => { + ensureUndoState([ [moveTxn] + , [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_a_info.guid + , newParentGuid: folder_b_info.guid + , oldIndex: 0 + , newIndex: 0 }); + observer.reset(); + }; + ensureUndo = () => { + ensureUndoState([ [moveTxn] + , [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 1); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_b_info.guid + , newParentGuid: folder_a_info.guid + , oldIndex: 0 + , newIndex: 0 }); + observer.reset(); + }; + + ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(); + ensureDo(); + yield PT.undo(); + ensureUndo(); + + // Clean up + yield PT.undo(); // folder_b_txn + yield PT.undo(); // folder_a_txn + the bookmarks; + do_check_eq(observer.itemsRemoved.size, 4); + ensureUndoState([ [moveTxn] + , [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 3); + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_remove_folder() { + let folder_level_1_info = createTestFolderInfo("Folder Level 1"); + let folder_level_2_info = { title: "Folder Level 2" }; + let [folder_level_1_txn_result, + folder_level_2_txn_result] = yield PT.batch(function* () { + let folder_level_1_txn = PT.NewFolder(folder_level_1_info); + folder_level_1_info.guid = yield folder_level_1_txn.transact(); + folder_level_2_info.parentGuid = folder_level_1_info.guid; + let folder_level_2_txn = PT.NewFolder(folder_level_2_info); + folder_level_2_info.guid = yield folder_level_2_txn.transact(); + return [folder_level_1_txn, folder_level_2_txn]; + }); + + ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]); + yield ensureItemsAdded(folder_level_1_info, folder_level_2_info); + observer.reset(); + + let remove_folder_2_txn = PT.Remove(folder_level_2_info); + yield remove_folder_2_txn.transact(); + + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ]); + yield ensureItemsRemoved(folder_level_2_info); + + // Undo Remove "Folder Level 2" + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_2_info); + ensureTimestampsUpdated(folder_level_2_info.guid, true); + observer.reset(); + + // Redo Remove "Folder Level 2" + yield PT.redo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ]); + yield ensureItemsRemoved(folder_level_2_info); + observer.reset(); + + // Undo it again + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_2_info); + ensureTimestampsUpdated(folder_level_2_info.guid, true); + observer.reset(); + + // Undo the creation of both folders + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2); + yield ensureItemsRemoved(folder_level_2_info, folder_level_1_info); + observer.reset(); + + // Redo the creation of both folders + yield PT.redo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_1_info, folder_level_2_info); + ensureTimestampsUpdated(folder_level_1_info.guid, true); + ensureTimestampsUpdated(folder_level_2_info.guid, true); + observer.reset(); + + // Redo Remove "Folder Level 2" + yield PT.redo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ]); + yield ensureItemsRemoved(folder_level_2_info); + observer.reset(); + + // Undo everything one last time + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_2_info); + observer.reset(); + + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2); + yield ensureItemsRemoved(folder_level_2_info, folder_level_2_info); + observer.reset(); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_add_and_remove_bookmarks_with_additional_info() { + const testURI = NetUtil.newURI("http://add.remove.tag"); + const TAG_1 = "TestTag1"; + const TAG_2 = "TestTag2"; + const KEYWORD = "test_keyword"; + const POST_DATA = "post_data"; + const ANNO = { name: "TestAnno", value: "TestAnnoValue" }; + + let folder_info = createTestFolderInfo(); + folder_info.guid = yield PT.NewFolder(folder_info).transact(); + let ensureTags = ensureTagsForURI.bind(null, testURI); + + // Check that the NewBookmark transaction preserves tags. + observer.reset(); + let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] }; + b1_info.guid = yield PT.NewBookmark(b1_info).transact(); + ensureTags([TAG_1]); + yield PT.undo(); + ensureTags([]); + + observer.reset(); + yield PT.redo(); + ensureTimestampsUpdated(b1_info.guid, true); + ensureTags([TAG_1]); + + // Check if the Remove transaction removes and restores tags of children + // correctly. + yield PT.Remove(folder_info.guid).transact(); + ensureTags([]); + + observer.reset(); + yield PT.undo(); + ensureTimestampsUpdated(b1_info.guid, true); + ensureTags([TAG_1]); + + yield PT.redo(); + ensureTags([]); + + observer.reset(); + yield PT.undo(); + ensureTimestampsUpdated(b1_info.guid, true); + ensureTags([TAG_1]); + + // * Check that no-op tagging (the uri is already tagged with TAG_1) is + // also a no-op on undo. + // * Test the "keyword" property of the NewBookmark transaction. + observer.reset(); + let b2_info = { parentGuid: folder_info.guid + , url: testURI, tags: [TAG_1, TAG_2] + , keyword: KEYWORD + , postData: POST_DATA + , annotations: [ANNO] }; + b2_info.guid = yield PT.NewBookmark(b2_info).transact(); + let b2_post_creation_changes = [ + { guid: b2_info.guid + , isAnnoProperty: true + , property: ANNO.name + , newValue: ANNO.value }, + { guid: b2_info.guid + , property: "keyword" + , newValue: KEYWORD } ]; + ensureItemsChanged(...b2_post_creation_changes); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + yield PT.undo(); + yield ensureItemsRemoved(b2_info); + ensureTags([TAG_1]); + + // Check if Remove correctly restores keywords, tags and annotations. + // Since both bookmarks share the same uri, they also share the keyword that + // is not removed along with one of the bookmarks. + observer.reset(); + yield PT.redo(); + ensureItemsChanged({ guid: b2_info.guid + , isAnnoProperty: true + , property: ANNO.name + , newValue: ANNO.value }); + ensureTags([TAG_1, TAG_2]); + + // Test Remove for multiple items. + observer.reset(); + yield PT.Remove(b1_info.guid).transact(); + yield PT.Remove(b2_info.guid).transact(); + yield PT.Remove(folder_info.guid).transact(); + yield ensureItemsRemoved(b1_info, b2_info, folder_info); + ensureTags([]); + // There is no keyword removal notification cause all bookmarks are removed + // before the keyword itself, so there's no one to notify. + let entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry, null, "keyword has been removed"); + + observer.reset(); + yield PT.undo(); + yield ensureItemsAdded(folder_info); + ensureTags([]); + + observer.reset(); + yield PT.undo(); + ensureItemsChanged(...b2_post_creation_changes); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + yield PT.undo(); + yield ensureItemsAdded(b1_info); + ensureTags([TAG_1, TAG_2]); + + // The redo calls below cleanup everything we did. + observer.reset(); + yield PT.redo(); + yield ensureItemsRemoved(b1_info); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + yield PT.redo(); + yield ensureItemsRemoved(b2_info); + ensureTags([]); + + observer.reset(); + yield PT.redo(); + yield ensureItemsRemoved(folder_info); + ensureTags([]); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_creating_and_removing_a_separator() { + let folder_info = createTestFolderInfo(); + let separator_info = {}; + let undoEntries = []; + + observer.reset(); + let create_txns = yield PT.batch(function* () { + let folder_txn = PT.NewFolder(folder_info); + folder_info.guid = separator_info.parentGuid = yield folder_txn.transact(); + let separator_txn = PT.NewSeparator(separator_info); + separator_info.guid = yield separator_txn.transact(); + return [separator_txn, folder_txn]; + }); + undoEntries.unshift(create_txns); + ensureUndoState(undoEntries, 0); + ensureItemsAdded(folder_info, separator_info); + + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsRemoved(folder_info, separator_info); + + observer.reset(); + yield PT.redo(); + ensureUndoState(undoEntries, 0); + ensureItemsAdded(folder_info, separator_info); + + observer.reset(); + let remove_sep_txn = PT.Remove(separator_info); + yield remove_sep_txn.transact(); + undoEntries.unshift([remove_sep_txn]); + ensureUndoState(undoEntries, 0); + ensureItemsRemoved(separator_info); + + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsAdded(separator_info); + + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 2); + ensureItemsRemoved(folder_info, separator_info); + + observer.reset(); + yield PT.redo(); + ensureUndoState(undoEntries, 1); + ensureItemsAdded(folder_info, separator_info); + + // Clear redo entries and check that |redo| does nothing + observer.reset(); + yield PT.clearTransactionsHistory(false, true); + undoEntries.shift(); + ensureUndoState(undoEntries, 0); + yield PT.redo(); + ensureItemsAdded(); + ensureItemsRemoved(); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsRemoved(folder_info, separator_info); + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_add_and_remove_livemark() { + let createLivemarkTxn = PT.NewLivemark( + { feedUrl: NetUtil.newURI("http://test.remove.livemark") + , parentGuid: rootGuid + , title: "Test Remove Livemark" }); + let guid = yield createLivemarkTxn.transact(); + let originalInfo = yield PlacesUtils.promiseBookmarksTree(guid); + Assert.ok(originalInfo); + yield ensureLivemarkCreatedByAddLivemark(guid); + + let removeTxn = PT.Remove(guid); + yield removeTxn.transact(); + yield ensureNonExistent(guid); + function* undo() { + ensureUndoState([[removeTxn], [createLivemarkTxn]], 0); + yield PT.undo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 1); + yield ensureBookmarksTreeRestoredCorrectly(originalInfo); + yield PT.undo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 2); + yield ensureNonExistent(guid); + } + function* redo() { + ensureUndoState([[removeTxn], [createLivemarkTxn]], 2); + yield PT.redo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 1); + yield ensureBookmarksTreeRestoredCorrectly(originalInfo); + yield PT.redo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 0); + yield ensureNonExistent(guid); + } + + yield undo(); + yield redo(); + yield undo(); + yield redo(); + + // Cleanup + yield undo(); + observer.reset(); + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_edit_title() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test_create_item.com") + , title: "Original Title" }; + + function ensureTitleChange(aCurrentTitle) { + ensureItemsChanged({ guid: bm_info.guid + , property: "title" + , newValue: aCurrentTitle}); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact(); + ensureTitleChange("New Title"); + + observer.reset(); + yield PT.undo(); + ensureTitleChange("Original Title"); + + observer.reset(); + yield PT.redo(); + ensureTitleChange("New Title"); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureTitleChange("Original Title"); + yield PT.undo(); + ensureItemsRemoved(bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_edit_url() { + let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/"); + let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/"); + let bm_info = { parentGuid: rootGuid, url: oldURI, tags: ["TestTag"] }; + function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) { + ensureItemsChanged({ guid: bm_info.guid + , property: "uri" + , newValue: aPostChangeURI.spec }); + ensureTagsForURI(aPostChangeURI, bm_info.tags); + ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + ensureTagsForURI(oldURI, bm_info.tags); + + // When there's a single bookmark for the same url, tags should be moved. + observer.reset(); + yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact(); + ensureURIAndTags(oldURI, newURI, false); + + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, false); + + observer.reset(); + yield PT.redo(); + ensureURIAndTags(oldURI, newURI, false); + + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, false); + + // When there're multiple bookmarks for the same url, tags should be copied. + let bm2_info = Object.create(bm_info); + bm2_info.guid = yield PT.NewBookmark(bm2_info).transact(); + let bm3_info = Object.create(bm_info); + bm3_info.url = newURI; + bm3_info.guid = yield PT.NewBookmark(bm3_info).transact(); + + observer.reset(); + yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact(); + ensureURIAndTags(oldURI, newURI, true); + + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, true); + + observer.reset(); + yield PT.redo(); + ensureURIAndTags(oldURI, newURI, true); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, true); + yield PT.undo(); + yield PT.undo(); + yield PT.undo(); + ensureItemsRemoved(bm3_info, bm2_info, bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_edit_keyword() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test.edit.keyword") }; + const KEYWORD = "test_keyword"; + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + function ensureKeywordChange(aCurrentKeyword = "") { + ensureItemsChanged({ guid: bm_info.guid + , property: "keyword" + , newValue: aCurrentKeyword }); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD, postData: "postData" }).transact(); + ensureKeywordChange(KEYWORD); + let entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData"); + + observer.reset(); + yield PT.undo(); + ensureKeywordChange(); + entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry, null); + + observer.reset(); + yield PT.redo(); + ensureKeywordChange(KEYWORD); + entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData"); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureKeywordChange(); + yield PT.undo(); + ensureItemsRemoved(bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_edit_specific_keyword() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test.edit.keyword") }; + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") { + ensureItemsChanged({ guid: bm_info.guid + , property: "keyword" + , newValue: aCurrentKeyword + }); + } + + yield PlacesUtils.keywords.insert({ keyword: "kw1", url: bm_info.url.spec, postData: "postData1" }); + yield PlacesUtils.keywords.insert({ keyword: "kw2", url: bm_info.url.spec, postData: "postData2" }); + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.EditKeyword({ guid: bm_info.guid, keyword: "keyword", oldKeyword: "kw2" }).transact(); + ensureKeywordChange("keyword", "kw2"); + let entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData1"); + entry = yield PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData2"); + entry = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry, null); + + observer.reset(); + yield PT.undo(); + ensureKeywordChange("kw2", "keyword"); + entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData1"); + entry = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData2"); + entry = yield PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry, null); + + observer.reset(); + yield PT.redo(); + ensureKeywordChange("keyword", "kw2"); + entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData1"); + entry = yield PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData2"); + entry = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry, null); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureKeywordChange("kw2"); + yield PT.undo(); + ensureItemsRemoved(bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_tag_uri() { + // This also tests passing uri specs. + let bm_info_a = { url: "http://bookmarked.uri" + , parentGuid: rootGuid }; + let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri") + , parentGuid: rootGuid }; + let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri"); + + function* promiseIsBookmarked(aURI) { + let deferred = Promise.defer(); + PlacesUtils.asyncGetBookmarkIds(aURI, ids => { + deferred.resolve(ids.length > 0); + }); + return deferred.promise; + } + + yield PT.batch(function* () { + bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact(); + bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact(); + }); + + function* doTest(aInfo) { + let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls; + let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags; + + let ensureURI = url => typeof(url) == "string" ? NetUtil.newURI(url) : url; + urls = urls.map(ensureURI); + + let tagWillAlsoBookmark = new Set(); + for (let url of urls) { + if (!(yield promiseIsBookmarked(url))) { + tagWillAlsoBookmark.add(url); + } + } + + function* ensureTagsSet() { + for (let url of urls) { + ensureTagsForURI(url, tags); + Assert.ok(yield promiseIsBookmarked(url)); + } + } + function* ensureTagsUnset() { + for (let url of urls) { + ensureTagsForURI(url, []); + if (tagWillAlsoBookmark.has(url)) + Assert.ok(!(yield promiseIsBookmarked(url))); + else + Assert.ok(yield promiseIsBookmarked(url)); + } + } + + yield PT.Tag(aInfo).transact(); + yield ensureTagsSet(); + yield PT.undo(); + yield ensureTagsUnset(); + yield PT.redo(); + yield ensureTagsSet(); + yield PT.undo(); + yield ensureTagsUnset(); + } + + yield doTest({ url: bm_info_a.url, tags: ["MyTag"] }); + yield doTest({ urls: [bm_info_a.url], tag: "MyTag" }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] }); + yield doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" }); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureItemsRemoved(bm_info_a, bm_info_b); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_untag_uri() { + let bm_info_a = { url: NetUtil.newURI("http://bookmarked.uri") + , parentGuid: rootGuid + , tags: ["A", "B"] }; + let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri") + , parentGuid: rootGuid + , tag: "B" }; + + yield PT.batch(function* () { + bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact(); + ensureTagsForURI(bm_info_a.url, bm_info_a.tags); + bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact(); + ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]); + }); + + function* doTest(aInfo) { + let urls, tagsRemoved; + if (aInfo instanceof Ci.nsIURI) { + urls = [aInfo]; + tagsRemoved = []; + } + else if (Array.isArray(aInfo)) { + urls = aInfo; + tagsRemoved = []; + } + else { + urls = "url" in aInfo ? [aInfo.url] : aInfo.urls; + tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags; + } + + let preRemovalTags = new Map(); + for (let url of urls) { + preRemovalTags.set(url, tagssvc.getTagsForURI(url)); + } + + function ensureTagsSet() { + for (let url of urls) { + ensureTagsForURI(url, preRemovalTags.get(url)); + } + } + function ensureTagsUnset() { + for (let url of urls) { + let expectedTags = tagsRemoved.length == 0 ? + [] : preRemovalTags.get(url).filter(tag => !tagsRemoved.includes(tag)); + ensureTagsForURI(url, expectedTags); + } + } + + yield PT.Untag(aInfo).transact(); + yield ensureTagsUnset(); + yield PT.undo(); + yield ensureTagsSet(); + yield PT.redo(); + yield ensureTagsUnset(); + yield PT.undo(); + yield ensureTagsSet(); + } + + yield doTest(bm_info_a); + yield doTest(bm_info_b); + yield doTest(bm_info_a.url); + yield doTest(bm_info_b.url); + yield doTest([bm_info_a.url, bm_info_b.url]); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] }); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureItemsRemoved(bm_info_a, bm_info_b); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_annotate() { + let bm_info = { url: NetUtil.newURI("http://test.item.annotation") + , parentGuid: rootGuid }; + let anno_info = { name: "TestAnno", value: "TestValue" }; + function ensureAnnoState(aSet) { + ensureAnnotationsSet(bm_info.guid, + [{ name: anno_info.name + , value: aSet ? anno_info.value : null }]); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.Annotate({ guid: bm_info.guid, annotation: anno_info }).transact(); + ensureAnnoState(true); + + observer.reset(); + yield PT.undo(); + ensureAnnoState(false); + + observer.reset(); + yield PT.redo(); + ensureAnnoState(true); + + // Test removing the annotation by not passing the |value| property. + observer.reset(); + yield PT.Annotate({ guid: bm_info.guid, + annotation: { name: anno_info.name }}).transact(); + ensureAnnoState(false); + + observer.reset(); + yield PT.undo(); + ensureAnnoState(true); + + observer.reset(); + yield PT.redo(); + ensureAnnoState(false); + + // Cleanup + yield PT.undo(); + observer.reset(); +}); + +add_task(function* test_annotate_multiple() { + let guid = yield PT.NewFolder(createTestFolderInfo()).transact(); + let itemId = yield PlacesUtils.promiseItemId(guid); + + function AnnoObj(aName, aValue) { + this.name = aName; + this.value = aValue; + this.flags = 0; + this.expires = Ci.nsIAnnotationService.EXPIRE_NEVER; + } + + function annos(a = null, b = null) { + return [new AnnoObj("A", a), new AnnoObj("B", b)]; + } + + function verifyAnnoValues(a = null, b = null) { + let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId); + let expectedAnnos = []; + if (a !== null) + expectedAnnos.push(new AnnoObj("A", a)); + if (b !== null) + expectedAnnos.push(new AnnoObj("B", b)); + + Assert.deepEqual(currentAnnos, expectedAnnos); + } + + yield PT.Annotate({ guid: guid, annotations: annos(1, 2) }).transact(); + verifyAnnoValues(1, 2); + yield PT.undo(); + verifyAnnoValues(); + yield PT.redo(); + verifyAnnoValues(1, 2); + + yield PT.Annotate({ guid: guid + , annotation: { name: "A" } }).transact(); + verifyAnnoValues(null, 2); + + yield PT.Annotate({ guid: guid + , annotation: { name: "B", value: 0 } }).transact(); + verifyAnnoValues(null, 0); + yield PT.undo(); + verifyAnnoValues(null, 2); + yield PT.redo(); + verifyAnnoValues(null, 0); + yield PT.undo(); + verifyAnnoValues(null, 2); + yield PT.undo(); + verifyAnnoValues(1, 2); + yield PT.undo(); + verifyAnnoValues(); + + // Cleanup + yield PT.undo(); + observer.reset(); +}); + +add_task(function* test_sort_folder_by_name() { + let folder_info = createTestFolderInfo(); + + let url = NetUtil.newURI("http://sort.by.name/"); + let preSep = ["3", "2", "1"].map(i => ({ title: i, url })); + let sep = {}; + let postSep = ["c", "b", "a"].map(l => ({ title: l, url })); + let originalOrder = [...preSep, sep, ...postSep]; + let sortedOrder = [...preSep.slice(0).reverse(), + sep, + ...postSep.slice(0).reverse()]; + yield PT.batch(function* () { + folder_info.guid = yield PT.NewFolder(folder_info).transact(); + for (let info of originalOrder) { + info.parentGuid = folder_info.guid; + info.guid = yield info == sep ? + PT.NewSeparator(info).transact() : + PT.NewBookmark(info).transact(); + } + }); + + let folderId = yield PlacesUtils.promiseItemId(folder_info.guid); + let folderContainer = PlacesUtils.getFolderContents(folderId).root; + function ensureOrder(aOrder) { + for (let i = 0; i < folderContainer.childCount; i++) { + do_check_eq(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid); + } + } + + ensureOrder(originalOrder); + yield PT.SortByName(folder_info.guid).transact(); + ensureOrder(sortedOrder); + yield PT.undo(); + ensureOrder(originalOrder); + yield PT.redo(); + ensureOrder(sortedOrder); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureOrder(originalOrder); + yield PT.undo(); + ensureItemsRemoved(...originalOrder, folder_info); +}); + +add_task(function* test_livemark_txns() { + let livemark_info = + { feedUrl: NetUtil.newURI("http://test.feed.uri") + , parentGuid: rootGuid + , title: "Test Livemark" }; + function ensureLivemarkAdded() { + ensureItemsAdded({ guid: livemark_info.guid + , title: livemark_info.title + , parentGuid: livemark_info.parentGuid + , itemType: bmsvc.TYPE_FOLDER }); + let annos = [{ name: PlacesUtils.LMANNO_FEEDURI + , value: livemark_info.feedUrl.spec }]; + if ("siteUrl" in livemark_info) { + annos.push({ name: PlacesUtils.LMANNO_SITEURI + , value: livemark_info.siteUrl.spec }); + } + ensureAnnotationsSet(livemark_info.guid, annos); + } + function ensureLivemarkRemoved() { + ensureItemsRemoved({ guid: livemark_info.guid + , parentGuid: livemark_info.parentGuid }); + } + + function* _testDoUndoRedoUndo() { + observer.reset(); + livemark_info.guid = yield PT.NewLivemark(livemark_info).transact(); + ensureLivemarkAdded(); + + observer.reset(); + yield PT.undo(); + ensureLivemarkRemoved(); + + observer.reset(); + yield PT.redo(); + ensureLivemarkAdded(); + + yield PT.undo(); + ensureLivemarkRemoved(); + } + + yield* _testDoUndoRedoUndo() + livemark_info.siteUrl = NetUtil.newURI("http://feed.site.uri"); + yield* _testDoUndoRedoUndo(); + + // Cleanup + observer.reset(); + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_copy() { + function* duplicate_and_test(aOriginalGuid) { + let txn = PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }); + yield duplicateGuid = yield txn.transact(); + let originalInfo = yield PlacesUtils.promiseBookmarksTree(aOriginalGuid); + let duplicateInfo = yield PlacesUtils.promiseBookmarksTree(duplicateGuid); + yield ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false); + + function* redo() { + yield PT.redo(); + yield ensureBookmarksTreeRestoredCorrectly(originalInfo); + yield PT.redo(); + yield ensureBookmarksTreeRestoredCorrectly(duplicateInfo); + } + function* undo() { + yield PT.undo(); + // also undo the original item addition. + yield PT.undo(); + yield ensureNonExistent(aOriginalGuid, duplicateGuid); + } + + yield undo(); + yield redo(); + yield undo(); + yield redo(); + + // Cleanup. This also remove the original item. + yield PT.undo(); + observer.reset(); + yield PT.clearTransactionsHistory(); + } + + // Test duplicating leafs (bookmark, separator, empty folder) + PT.NewBookmark({ url: new URL("http://test.item.duplicate") + , parentGuid: rootGuid + , annos: [{ name: "Anno", value: "AnnoValue"}] }); + let sepTxn = PT.NewSeparator({ parentGuid: rootGuid, index: 1 }); + let livemarkTxn = PT.NewLivemark( + { feedUrl: new URL("http://test.feed.uri") + , parentGuid: rootGuid + , title: "Test Livemark", index: 1 }); + let emptyFolderTxn = PT.NewFolder(createTestFolderInfo()); + for (let txn of [livemarkTxn, sepTxn, emptyFolderTxn]) { + let guid = yield txn.transact(); + yield duplicate_and_test(guid); + } + + // Test duplicating a folder having some contents. + let filledFolderGuid = yield PT.batch(function *() { + let folderGuid = yield PT.NewFolder(createTestFolderInfo()).transact(); + let nestedFolderGuid = + yield PT.NewFolder({ parentGuid: folderGuid + , title: "Nested Folder" }).transact(); + // Insert a bookmark under the nested folder. + yield PT.NewBookmark({ url: new URL("http://nested.nested.bookmark") + , parentGuid: nestedFolderGuid }).transact(); + // Insert a separator below the nested folder + yield PT.NewSeparator({ parentGuid: folderGuid }).transact(); + // And another bookmark. + yield PT.NewBookmark({ url: new URL("http://nested.bookmark") + , parentGuid: folderGuid }).transact(); + return folderGuid; + }); + + yield duplicate_and_test(filledFolderGuid); + + // Cleanup + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_array_input_for_batch() { + let folderTxn = PT.NewFolder(createTestFolderInfo()); + let folderGuid = yield folderTxn.transact(); + + let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid }); + let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid }); + yield PT.batch([sep1_txn, sep2_txn]); + ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0); + + let ensureChildCount = function* (count) { + let tree = yield PlacesUtils.promiseBookmarksTree(folderGuid); + if (count == 0) + Assert.ok(!("children" in tree)); + else + Assert.equal(tree.children.length, count); + }; + + yield ensureChildCount(2); + yield PT.undo(); + yield ensureChildCount(0); + yield PT.redo() + yield ensureChildCount(2); + yield PT.undo(); + yield ensureChildCount(0); + + yield PT.undo(); + Assert.equal((yield PlacesUtils.promiseBookmarksTree(folderGuid)), null); + + // Cleanup + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_copy_excluding_annotations() { + let folderInfo = createTestFolderInfo(); + let anno = n => { return { name: n, value: 1 } }; + folderInfo.annotations = [anno("a"), anno("b"), anno("c")]; + let folderGuid = yield PT.NewFolder(folderInfo).transact(); + + let ensureAnnosSet = function* (guid, ...expectedAnnoNames) { + let tree = yield PlacesUtils.promiseBookmarksTree(guid); + let annoNames = "annos" in tree ? + tree.annos.map(a => a.name).sort() : []; + Assert.deepEqual(annoNames, expectedAnnoNames); + }; + + yield ensureAnnosSet(folderGuid, "a", "b", "c"); + + let excluding_a_dupeGuid = + yield PT.Copy({ guid: folderGuid + , newParentGuid: rootGuid + , excludingAnnotation: "a" }).transact(); + yield ensureAnnosSet(excluding_a_dupeGuid, "b", "c"); + + let excluding_ac_dupeGuid = + yield PT.Copy({ guid: folderGuid + , newParentGuid: rootGuid + , excludingAnnotations: ["a", "c"] }).transact(); + yield ensureAnnosSet(excluding_ac_dupeGuid, "b"); + + // Cleanup + yield PT.undo(); + yield PT.undo(); + yield PT.undo(); + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_invalid_uri_spec_throws() { + Assert.throws(() => + PT.NewBookmark({ parentGuid: rootGuid + , url: "invalid uri spec" + , title: "test bookmark" })); + Assert.throws(() => + PT.Tag({ tag: "TheTag" + , urls: ["invalid uri spec"] })); + Assert.throws(() => + PT.Tag({ tag: "TheTag" + , urls: ["about:blank", "invalid uri spec"] })); +}); + +add_task(function* test_annotate_multiple_items() { + let parentGuid = rootGuid; + let guids = [ + yield PT.NewBookmark({ url: "about:blank", parentGuid }).transact(), + yield PT.NewFolder({ title: "Test Folder", parentGuid }).transact()]; + + let annotation = { name: "TestAnno", value: "TestValue" }; + yield PT.Annotate({ guids, annotation }).transact(); + + function *ensureAnnoSet() { + for (let guid of guids) { + let itemId = yield PlacesUtils.promiseItemId(guid); + Assert.equal(annosvc.getItemAnnotation(itemId, annotation.name), + annotation.value); + } + } + function *ensureAnnoUnset() { + for (let guid of guids) { + let itemId = yield PlacesUtils.promiseItemId(guid); + Assert.ok(!annosvc.itemHasAnnotation(itemId, annotation.name)); + } + } + + yield ensureAnnoSet(); + yield PT.undo(); + yield ensureAnnoUnset(); + yield PT.redo(); + yield ensureAnnoSet(); + yield PT.undo(); + yield ensureAnnoUnset(); + + // Cleanup + yield PT.undo(); + yield PT.undo(); + yield ensureNonExistent(...guids); + yield PT.clearTransactionsHistory(); + observer.reset(); +}); + +add_task(function* test_remove_multiple() { + let guids = []; + yield PT.batch(function* () { + let folderGuid = yield PT.NewFolder({ title: "Test Folder" + , parentGuid: rootGuid }).transact(); + let nestedFolderGuid = + yield PT.NewFolder({ title: "Nested Test Folder" + , parentGuid: folderGuid }).transact(); + yield PT.NewSeparator(nestedFolderGuid).transact(); + + guids.push(folderGuid); + + let bmGuid = + yield PT.NewBookmark({ url: new URL("http://test.bookmark.removed") + , parentGuid: rootGuid }).transact(); + guids.push(bmGuid); + }); + + let originalInfos = []; + for (let guid of guids) { + originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid)); + } + + yield PT.Remove(guids).transact(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + yield PT.redo(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + + // Undo the New* transactions batch. + yield PT.undo(); + yield ensureNonExistent(...guids); + + // Redo it. + yield PT.redo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + + // Redo remove. + yield PT.redo(); + yield ensureNonExistent(...guids); + + // Cleanup + yield PT.clearTransactionsHistory(); + observer.reset(); +}); + +add_task(function* test_remove_bookmarks_for_urls() { + let urls = [new URL("http://test.url.1"), new URL("http://test.url.2")]; + let guids = []; + yield PT.batch(function* () { + for (let url of urls) { + for (let title of ["test title a", "test title b"]) { + let txn = PT.NewBookmark({ url, title, parentGuid: rootGuid }); + guids.push(yield txn.transact()); + } + } + }); + + let originalInfos = []; + for (let guid of guids) { + originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid)); + } + + yield PT.RemoveBookmarksForUrls(urls).transact(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + yield PT.redo(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + + // Cleanup. + yield PT.redo(); + yield ensureNonExistent(...guids); + yield PT.clearTransactionsHistory(); + observer.reset(); +}); |