Bug 1103622 - PlacesTransactions.Annotate for multiple items. r=mak.
authorAsaf Romano <mano@mozilla.com>
Tue, 25 Nov 2014 09:20:00 +0200
changeset 241723 1ad2becc23b3d64df6efad22021c77da217270fd
parent 241722 6db486ed2de1dad2999c791f53da136ab9ae874d
child 241724 03d7928d03dfc21c0d442f870e7ea0a18fc436d6
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1103622
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1103622 - PlacesTransactions.Annotate for multiple items. r=mak.
toolkit/components/places/PlacesTransactions.jsm
toolkit/components/places/tests/unit/test_async_transactions.js
--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -62,17 +62,18 @@ this.EXPORTED_SYMBOLS = ["PlacesTransact
  * values:
  *  - url: a URL object, an nsIURI object, or a href.
  *  - urls: an array of urls, as above.
  *  - feedUrl: an url (as above), holding the url for a live bookmark.
  *  - siteUrl an url (as above), holding the url for the site with which
  *            a live bookmark is associated.
  *  - tag - a string.
  *  - tags: an array of strings.
- *  - guid, parentGuid, newParentGuid: a valid places GUID string.
+ *  - guid, parentGuid, newParentGuid: a valid Places GUID string.
+ *  - guids: an array of valid Places GUID strings.
  *  - title: a string
  *  - index, newIndex: the position of an item in its containing folder,
  *    starting from 0.
  *    integer and PlacesUtils.bookmarks.DEFAULT_INDEX
  *  - annotation: see PlacesUtils.setAnnotationsForItem
  *  - annotations: an array of annotation objects as above.
  *  - excludingAnnotation: a string (annotation name).
  *  - excludingAnnotations: an array of string (annotation names).
@@ -458,17 +459,16 @@ Enqueuer.prototype = {
   },
 
   /**
    * The promise for this queue.
    */
   get promise() this._promise
 };
 
-
 let TransactionsManager = {
   // See the documentation at the top of this file. |transact| calls are not
   // serialized with |batch| calls.
   _mainEnqueuer: new Enqueuer(),
   _transactEnqueuer: new Enqueuer(),
 
   // Is a batch in progress? set when we enter a batch function and unset when
   // it's execution is done.
@@ -864,16 +864,17 @@ DefineTransaction.defineInputProps(["tit
 DefineTransaction.defineInputProps(["keyword", "postData", "tag",
                                     "excludingAnnotation"],
                                    DefineTransaction.strValidate, "");
 DefineTransaction.defineInputProps(["index", "newIndex"],
                                    DefineTransaction.indexValidate,
                                    PlacesUtils.bookmarks.DEFAULT_INDEX);
 DefineTransaction.defineInputProps(["annotation"],
                                    DefineTransaction.annotationObjectValidate);
+DefineTransaction.defineArrayInputProp("guids", "guid");
 DefineTransaction.defineArrayInputProp("urls", "url");
 DefineTransaction.defineArrayInputProp("tags", "tag");
 DefineTransaction.defineArrayInputProp("annotations", "annotation");
 DefineTransaction.defineArrayInputProp("excludingAnnotations",
                                        "excludingAnnotation");
 
 /**
  * Internal helper for implementing the execute method of NewBookmark, NewFolder
@@ -1264,39 +1265,50 @@ PT.EditUrl.prototype = Object.seal({
   }
 });
 
 /**
  * Transaction for setting annotations for an item.
  *
  * Required Input Properties: guid, annotationObject
  */
-PT.Annotate = DefineTransaction(["guid", "annotations"]);
+PT.Annotate = DefineTransaction(["guids", "annotations"]);
 PT.Annotate.prototype = {
-  execute: function* (aGuid, aNewAnnos) {
-    let itemId = yield PlacesUtils.promiseItemId(aGuid);
-    let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
-    let undoAnnos = [];
-    for (let newAnno of aNewAnnos) {
-      let currentAnno = currentAnnos.find( a => a.name == newAnno.name );
-      if (!!currentAnno) {
-        undoAnnos.push(currentAnno);
+  *execute(aGuids, aNewAnnos) {
+    let undoAnnosForItem = new Map(); // itemId => undoAnnos;
+    for (let guid of aGuids) {
+      let itemId = yield PlacesUtils.promiseItemId(guid);
+      let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
+
+      let undoAnnos = [];
+      for (let newAnno of aNewAnnos) {
+        let currentAnno = currentAnnos.find(a => a.name == newAnno.name);
+        if (!!currentAnno) {
+          undoAnnos.push(currentAnno);
+        }
+        else {
+          // An unset value removes the annotation.
+          undoAnnos.push({ name: newAnno.name });
+        }
       }
-      else {
-        // An unset value removes the annotation.
-        undoAnnos.push({ name: newAnno.name });
-      }
+      undoAnnosForItem.set(itemId, undoAnnos);
+
+      PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
     }
 
-    PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
-    this.undo = () => {
-      PlacesUtils.setAnnotationsForItem(itemId, undoAnnos);
+    this.undo = function() {
+      for (let [itemId, undoAnnos] of undoAnnosForItem) {
+        PlacesUtils.setAnnotationsForItem(itemId, undoAnnos);
+      }
     };
-    this.redo = () => {
-      PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+    this.redo = function* () {
+      for (let guid of aGuids) {
+        let itemId = yield PlacesUtils.promiseItemId(guid);
+        PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+      }
     };
   }
 };
 
 /**
  * Transaction for setting the keyword for a bookmark.
  *
  * Required Input Properties: guid, keyword.
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -1,18 +1,19 @@
 /* -*- 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 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.
 let observer = {
   __proto__: NavBookmarkObserver.prototype,
 
   tagRelatedGuids: new Set(),
@@ -101,19 +102,16 @@ let observer = {
                                , itemType:      aItemType });
   }
 };
 observer.reset();
 
 // index at which items should begin
 let bmStartIndex = 0;
 
-// get bookmarks root id
-let root = PlacesUtils.bookmarksMenuFolderId;
-
 function run_test() {
   bmsvc.addObserver(observer, false);
   do_register_cleanup(function () {
     bmsvc.removeObserver(observer);
   });
 
   run_next_test();
 }
@@ -243,19 +241,18 @@ function ensureTimestampsUpdated(aGuid, 
 }
 
 function ensureTagsForURI(aURI, aTags) {
   let tagsSet = tagssvc.getTagsForURI(aURI);
   do_check_eq(tagsSet.length, aTags.length);
   do_check_true(aTags.every( t => tagsSet.indexOf(t) != -1 ));
 }
 
-function* createTestFolderInfo(aTitle = "Test Folder") {
-  return { parentGuid: yield PlacesUtils.promiseItemGuid(root)
-         , title: "Test Folder" };
+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) {
@@ -333,30 +330,30 @@ add_task(function* test_recycled_transac
     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(yield createTestFolderInfo());
+  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(yield createTestFolderInfo());
+  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();
@@ -370,17 +367,17 @@ add_task(function* test_recycled_transac
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
   observer.reset();
 });
 
 add_task(function* test_new_folder_with_annotation() {
   const ANNO = { name: "TestAnno", value: "TestValue" };
-  let folder_info = yield createTestFolderInfo();
+  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);
@@ -405,17 +402,17 @@ add_task(function* test_new_folder_with_
   yield ensureDo(true);
   yield PT.undo();
   ensureUndo();
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_new_bookmark() {
-  let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
+  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();
 
@@ -442,17 +439,17 @@ add_task(function* test_new_bookmark() {
   yield PT.undo();
   ensureUndo();
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_merge_create_folder_and_item() {
-  let folder_info = yield createTestFolderInfo();
+  let folder_info = createTestFolderInfo();
   let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com")
                 , title: "Test Bookmark"
                 , index: bmStartIndex };
 
   let { folderTxn, bkmTxn } = 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);
@@ -480,17 +477,17 @@ add_task(function* test_merge_create_fol
   yield PT.undo();
   ensureUndo();
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_move_items_to_folder() {
-  let folder_a_info = yield createTestFolderInfo("Folder A");
+  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, bkm_a_txn, bkm_b_txn] = yield PT.batch(function* () {
     let folder_a_txn = PT.NewFolder(folder_a_info);
@@ -536,17 +533,17 @@ add_task(function* test_move_items_to_fo
   ensureDo();
   yield PT.undo();
   ensureUndo();
 
   yield PT.clearTransactionsHistory(false, true);
   ensureUndoState([[bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
 
   // Test moving items between folders.
-  let folder_b_info = yield createTestFolderInfo("Folder B");
+  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, bkm_a_txn, folder_a_txn] ], 0);
 
   moveTxn = PT.Move({ guid:          bkm_a_info.guid
                     , newParentGuid: folder_b_info.guid
                     , newIndex:      bmsvc.DEFAULT_INDEX });
@@ -590,17 +587,17 @@ add_task(function* test_move_items_to_fo
   ensureUndoState([ [moveTxn]
                   , [folder_b_txn]
                   , [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 3);
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_remove_folder() {
-  let folder_level_1_info = yield createTestFolderInfo("Folder Level 1");
+  let folder_level_1_info = createTestFolderInfo("Folder Level 1");
   let folder_level_2_info = { title: "Folder Level 2" };
   let [folder_level_1_txn,
        folder_level_2_txn] = 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();
@@ -683,17 +680,17 @@ add_task(function* test_remove_folder() 
 
 add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
   const testURI = NetUtil.newURI("http://add.remove.tag")
       , TAG_1 = "TestTag1", TAG_2 = "TestTag2"
       , KEYWORD = "test_keyword"
       , POST_DATA = "post_data"
       , ANNO = { name: "TestAnno", value: "TestAnnoValue" };
 
-  let folder_info = yield createTestFolderInfo();
+  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]);
@@ -798,17 +795,17 @@ add_task(function* test_add_and_remove_b
   yield ensureItemsRemoved(folder_info);
   ensureTags([]);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_creating_and_removing_a_separator() {
-  let folder_info = yield createTestFolderInfo();
+  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);
@@ -867,17 +864,17 @@ add_task(function* test_creating_and_rem
   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: yield PlacesUtils.promiseItemGuid(root)
+    , 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();
@@ -908,17 +905,17 @@ add_task(function* test_add_and_remove_l
 
   // Cleanup
   yield undo();
   observer.reset();
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_edit_title() {
-  let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
+  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});
   }
@@ -946,20 +943,17 @@ add_task(function* test_edit_title() {
 
   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: yield PlacesUtils.promiseItemGuid(root)
-                , url:        oldURI
-                , tags:       ["TestTag"]};
-
+  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 : []);
   }
 
@@ -1011,18 +1005,18 @@ add_task(function* test_edit_url() {
   yield PT.undo();
   ensureItemsRemoved(bm3_info, bm2_info, bm_info);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_edit_keyword() {
-  let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
-                , url:        NetUtil.newURI("http://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 });
   }
 
@@ -1049,19 +1043,19 @@ add_task(function* test_edit_keyword() {
 
   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: yield PlacesUtils.promiseItemGuid(root) };
+                  , parentGuid: rootGuid };
   let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
-                  , parentGuid: yield PlacesUtils.promiseItemGuid(root) };
+                  , 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;
@@ -1123,20 +1117,20 @@ add_task(function* test_tag_uri() {
   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: yield PlacesUtils.promiseItemGuid(root)
+                  , parentGuid: rootGuid
                   , tags: ["A", "B"] };
   let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
-                  , parentGuid: yield PlacesUtils.promiseItemGuid(root)
+                  , 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]);
   });
@@ -1201,17 +1195,17 @@ add_task(function* test_untag_uri() {
   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: yield PlacesUtils.promiseItemGuid(root) };
+                , 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();
@@ -1243,17 +1237,17 @@ add_task(function* test_annotate() {
   ensureAnnoState(false);
 
   // Cleanup
   yield PT.undo();
   observer.reset();
 });
 
 add_task(function* test_annotate_multiple() {
-  let guid = yield PT.NewFolder(yield createTestFolderInfo()).transact();
+  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;
   }
@@ -1297,17 +1291,17 @@ add_task(function* test_annotate_multipl
   verifyAnnoValues();
 
   // Cleanup
   yield PT.undo();
   observer.reset();
 });
 
 add_task(function* test_sort_folder_by_name() {
-  let folder_info = yield createTestFolderInfo();
+  let folder_info = createTestFolderInfo();
 
   let url = NetUtil.newURI("http://sort.by.name/");
   let preSep =  [{ title: i, url } for (i of ["3","2","1"])];
   let sep = {};
   let postSep = [{ title: l, url } for (l of ["c","b","a"])];
   let originalOrder = [...preSep, sep, ...postSep];
   let sortedOrder = [...preSep.slice(0).reverse(),
                      sep,
@@ -1344,17 +1338,17 @@ add_task(function* test_sort_folder_by_n
   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: yield PlacesUtils.promiseItemGuid(root)
+    , 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 }];
@@ -1391,21 +1385,19 @@ add_task(function* test_livemark_txns() 
   yield* _testDoUndoRedoUndo();
 
   // Cleanup
   observer.reset();
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_copy() {
-  let rootGuid = yield PlacesUtils.promiseItemGuid(root);
-
   function* duplicate_and_test(aOriginalGuid) {
-    yield duplicateGuid =
-      yield PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }).transact();
+    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();
@@ -1431,28 +1423,27 @@ add_task(function* test_copy() {
 
   // Test duplicating leafs (bookmark, separator, empty folder)
   let bmTxn = 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: yield PlacesUtils.promiseItemGuid(root)
+    , parentGuid: rootGuid
     , title: "Test Livemark", index: 1 });
-  let emptyFolderTxn = PT.NewFolder(yield createTestFolderInfo());
+  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(yield createTestFolderInfo()).transact();
+    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();
@@ -1464,19 +1455,17 @@ add_task(function* test_copy() {
 
   yield duplicate_and_test(filledFolderGuid);
 
   // Cleanup
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_array_input_for_batch() {
-  let rootGuid = yield PlacesUtils.promiseItemGuid(root);
-
-  let folderTxn = PT.NewFolder(yield createTestFolderInfo());
+  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) {
@@ -1498,19 +1487,17 @@ add_task(function* test_array_input_for_
   yield PT.undo();
   Assert.equal((yield PlacesUtils.promiseBookmarksTree(folderGuid)), null);
 
   // Cleanup
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_copy_excluding_annotations() {
-  let rootGuid = yield PlacesUtils.promiseItemGuid(root);
-
-  let folderInfo = yield createTestFolderInfo();
+  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 ?
                       [for (a of tree.annos) a.name].sort() : [];
@@ -1534,20 +1521,58 @@ add_task(function* test_copy_excluding_a
   // Cleanup
   yield PT.undo();
   yield PT.undo();
   yield PT.undo();
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_invalid_uri_spec_throws() {
-  let rootGuid = yield PlacesUtils.promiseItemGuid(root);
   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);
+  PT.clearTransactionsHistory();
+  observer.reset();
+});