/* -*- 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(); });