Bug 1221764 - Implement simple chrome.bookmarks events draft
authorTom Schuster <evilpies@gmail.com>
Wed, 17 Aug 2016 10:22:10 -0400
changeset 420294 2c8a03a333d9e28809f0d5b127ecfc42f689bc55
parent 420017 955840bfd3c20eb24dd5a01be27bdc55c489a285
child 532786 e6344484b8679183a013b99d5c85dc8c4c754531
push id31169
push usermixedpuppy@gmail.com
push dateTue, 04 Oct 2016 01:15:33 +0000
bugs1221764
milestone52.0a1
Bug 1221764 - Implement simple chrome.bookmarks events MozReview-Commit-ID: InuMF38ZMbr
toolkit/components/places/nsINavBookmarksService.idl
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavHistoryResult.cpp
toolkit/components/places/nsPlacesMacros.h
toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -8,19 +8,30 @@
 interface nsIFile;
 interface nsIURI;
 interface nsITransaction;
 interface nsINavHistoryBatchCallback;
 
 /**
  * Observer for bookmarks changes.
  */
-[scriptable, uuid(cff3efcc-e144-490d-9f23-8b6f6dd09e7f)]
+[scriptable, uuid(c06b4e7d-15b1-4d4f-bdf7-147d2be9084a)]
 interface nsINavBookmarkObserver : nsISupports
 {
+  /*
+   * This observer should not be called for items that are tags.
+   */
+  readonly attribute boolean skipTags;
+
+  /*
+   * This observer should not be called for descendants when the parent is removed.
+   * For example when revmoing a folder containing bookmarks.
+   */
+  readonly attribute boolean skipDescendantsOnItemRemoval;
+
   /**
    * Notifies that a batch transaction has started.
    * Other notifications will be sent during the batch, but the observer is
    * guaranteed that onEndUpdateBatch() will be called at its completion.
    * During a batch the observer should do its best to reduce the work done to
    * handle notifications, since multiple changes are going to happen in a short
    * timeframe.
    */
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -457,16 +457,30 @@ nsNavBookmarks::InsertBookmarkInDB(int64
   }
   bookmark.parentGuid = aParentGuid;
   bookmark.grandParentId = aGrandParentId;
 
   return NS_OK;
 }
 
 
+#define SKIP_TAGS(condition) ((condition) ? SkipTags : DontSkip)
+
+bool DontSkip(nsCOMPtr<nsINavBookmarkObserver> obs) { return false; }
+bool SkipTags(nsCOMPtr<nsINavBookmarkObserver> obs) {
+  bool skipTags = false;
+  (void) obs->GetSkipTags(&skipTags);
+  return skipTags;
+}
+bool SkipDescendants(nsCOMPtr<nsINavBookmarkObserver> obs) {
+  bool skipDescendantsOnItemRemoval = false;
+  (void) obs->GetSkipTags(&skipDescendantsOnItemRemoval);
+  return skipDescendantsOnItemRemoval;
+}
+
 NS_IMETHODIMP
 nsNavBookmarks::InsertBookmark(int64_t aFolder,
                                nsIURI* aURI,
                                int32_t aIndex,
                                const nsACString& aTitle,
                                const nsACString& aGUID,
                                uint16_t aSource,
                                int64_t* aNewBookmarkId)
@@ -519,47 +533,48 @@ nsNavBookmarks::InsertBookmark(int64_t a
   if (grandParentId != mTagsRoot) {
     rv = history->UpdateFrecency(placeId);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                   nsINavBookmarkObserver,
-                   OnItemAdded(*aNewBookmarkId, aFolder, index, TYPE_BOOKMARK,
-                               aURI, title, dateAdded, guid, folderGuid, aSource));
+  NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                             SKIP_TAGS(grandParentId == mTagsRoot),
+                             OnItemAdded(*aNewBookmarkId, aFolder, index,
+                                         TYPE_BOOKMARK, aURI, title, dateAdded,
+                                         guid, folderGuid, aSource));
 
   // If the bookmark has been added to a tag container, notify all
   // bookmark-folder result nodes which contain a bookmark for the new
   // bookmark's url.
   if (grandParentId == mTagsRoot) {
     // Notify a tags change to all bookmarks for this URI.
     nsTArray<BookmarkData> bookmarks;
     rv = GetBookmarksForURI(aURI, bookmarks);
     NS_ENSURE_SUCCESS(rv, rv);
 
     for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
       // Check that bookmarks doesn't include the current tag itemId.
       MOZ_ASSERT(bookmarks[i].id != *aNewBookmarkId);
 
-      NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                       nsINavBookmarkObserver,
-                       OnItemChanged(bookmarks[i].id,
-                                     NS_LITERAL_CSTRING("tags"),
-                                     false,
-                                     EmptyCString(),
-                                     bookmarks[i].lastModified,
-                                     TYPE_BOOKMARK,
-                                     bookmarks[i].parentId,
-                                     bookmarks[i].guid,
-                                     bookmarks[i].parentGuid,
-                                     EmptyCString(),
-                                     aSource));
+      NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                                 DontSkip,
+                                 OnItemChanged(bookmarks[i].id,
+                                               NS_LITERAL_CSTRING("tags"),
+                                               false,
+                                               EmptyCString(),
+                                               bookmarks[i].lastModified,
+                                               TYPE_BOOKMARK,
+                                               bookmarks[i].parentId,
+                                               bookmarks[i].guid,
+                                               bookmarks[i].parentGuid,
+                                               EmptyCString(),
+                                               aSource));
     }
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
@@ -627,49 +642,50 @@ nsNavBookmarks::RemoveItem(int64_t aItem
       NS_ENSURE_SUCCESS(rv, rv);
     }
     // A broken url should not interrupt the removal process.
     (void)NS_NewURI(getter_AddRefs(uri), bookmark.url);
     // We cannot assert since some automated tests are checking this path.
     NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveItem");
   }
 
-  NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                   nsINavBookmarkObserver,
-                   OnItemRemoved(bookmark.id,
-                                 bookmark.parentId,
-                                 bookmark.position,
-                                 bookmark.type,
-                                 uri,
-                                 bookmark.guid,
-                                 bookmark.parentGuid,
-                                 aSource));
+  NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                             SKIP_TAGS(bookmark.parentId == mTagsRoot ||
+                                       bookmark.grandParentId == mTagsRoot),
+                             OnItemRemoved(bookmark.id,
+                                           bookmark.parentId,
+                                           bookmark.position,
+                                           bookmark.type,
+                                           uri,
+                                           bookmark.guid,
+                                           bookmark.parentGuid,
+                                           aSource));
 
   if (bookmark.type == TYPE_BOOKMARK && bookmark.grandParentId == mTagsRoot &&
       uri) {
     // If the removed bookmark was child of a tag container, notify a tags
     // change to all bookmarks for this URI.
     nsTArray<BookmarkData> bookmarks;
     rv = GetBookmarksForURI(uri, bookmarks);
     NS_ENSURE_SUCCESS(rv, rv);
 
     for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
-      NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                       nsINavBookmarkObserver,
-                       OnItemChanged(bookmarks[i].id,
-                                     NS_LITERAL_CSTRING("tags"),
-                                     false,
-                                     EmptyCString(),
-                                     bookmarks[i].lastModified,
-                                     TYPE_BOOKMARK,
-                                     bookmarks[i].parentId,
-                                     bookmarks[i].guid,
-                                     bookmarks[i].parentGuid,
-                                     EmptyCString(),
-                                     aSource));
+      NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                                 DontSkip,
+                                 OnItemChanged(bookmarks[i].id,
+                                               NS_LITERAL_CSTRING("tags"),
+                                               false,
+                                               EmptyCString(),
+                                               bookmarks[i].lastModified,
+                                               TYPE_BOOKMARK,
+                                               bookmarks[i].parentId,
+                                               bookmarks[i].guid,
+                                               bookmarks[i].parentGuid,
+                                               EmptyCString(),
+                                               aSource));
     }
 
   }
 
   return NS_OK;
 }
 
 
@@ -747,21 +763,21 @@ nsNavBookmarks::CreateContainerWithID(in
   rv = InsertBookmarkInDB(-1, FOLDER, aParent, index,
                           title, dateAdded, 0, folderGuid, grandParentId,
                           nullptr, aSource, aNewFolder, guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                   nsINavBookmarkObserver,
-                   OnItemAdded(*aNewFolder, aParent, index, FOLDER,
-                               nullptr, title, dateAdded, guid, folderGuid,
-                               aSource));
+  NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                             SKIP_TAGS(aParent == mTagsRoot),
+                             OnItemAdded(*aNewFolder, aParent, index, FOLDER,
+                                         nullptr, title, dateAdded, guid,
+                                         folderGuid, aSource));
 
   *aIndex = index;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::InsertSeparator(int64_t aParent,
@@ -804,21 +820,21 @@ nsNavBookmarks::InsertSeparator(int64_t 
   rv = InsertBookmarkInDB(-1, SEPARATOR, aParent, index, NullCString(), dateAdded,
                           0, folderGuid, grandParentId, nullptr, aSource,
                           aNewItemId, guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                   nsINavBookmarkObserver,
-                   OnItemAdded(*aNewItemId, aParent, index, TYPE_SEPARATOR,
-                               nullptr, NullCString(), dateAdded, guid, folderGuid,
-                               aSource));
+  NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                             DontSkip,
+                             OnItemAdded(*aNewItemId, aParent, index, TYPE_SEPARATOR,
+                                         nullptr, NullCString(), dateAdded, guid,
+                                         folderGuid, aSource));
 
   return NS_OK;
 }
 
 
 nsresult
 nsNavBookmarks::GetLastChildId(int64_t aFolderId, int64_t* aItemId)
 {
@@ -1110,50 +1126,50 @@ nsNavBookmarks::RemoveFolderChildren(int
         NS_ENSURE_SUCCESS(rv, rv);
       }
       // A broken url should not interrupt the removal process.
       (void)NS_NewURI(getter_AddRefs(uri), child.url);
       // We cannot assert since some automated tests are checking this path.
       NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveFolderChildren");
     }
 
-    NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                     nsINavBookmarkObserver,
-                     OnItemRemoved(child.id,
-                                   child.parentId,
-                                   child.position,
-                                   child.type,
-                                   uri,
-                                   child.guid,
-                                   child.parentGuid,
-                                   aSource));
+    NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                               ((child.grandParentId == mTagsRoot) ? SkipTags : SkipDescendants),
+                               OnItemRemoved(child.id,
+                                             child.parentId,
+                                             child.position,
+                                             child.type,
+                                             uri,
+                                             child.guid,
+                                             child.parentGuid,
+                                             aSource));
 
     if (child.type == TYPE_BOOKMARK && child.grandParentId == mTagsRoot &&
         uri) {
       // If the removed bookmark was a child of a tag container, notify all
       // bookmark-folder result nodes which contain a bookmark for the removed
       // bookmark's url.
       nsTArray<BookmarkData> bookmarks;
       rv = GetBookmarksForURI(uri, bookmarks);
       NS_ENSURE_SUCCESS(rv, rv);
 
       for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
-        NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                         nsINavBookmarkObserver,
-                         OnItemChanged(bookmarks[i].id,
-                                       NS_LITERAL_CSTRING("tags"),
-                                       false,
-                                       EmptyCString(),
-                                       bookmarks[i].lastModified,
-                                       TYPE_BOOKMARK,
-                                       bookmarks[i].parentId,
-                                       bookmarks[i].guid,
-                                       bookmarks[i].parentGuid,
-                                       EmptyCString(),
-                                       aSource));
+        NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                                   DontSkip,
+                                   OnItemChanged(bookmarks[i].id,
+                                                 NS_LITERAL_CSTRING("tags"),
+                                                 false,
+                                                 EmptyCString(),
+                                                 bookmarks[i].lastModified,
+                                                 TYPE_BOOKMARK,
+                                                 bookmarks[i].parentId,
+                                                 bookmarks[i].guid,
+                                                 bookmarks[i].parentGuid,
+                                                 EmptyCString(),
+                                                 aSource));
       }
     }
   }
 
   return NS_OK;
 }
 
 
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -2346,24 +2346,36 @@ nsNavHistoryQueryResultNode::RecursiveSo
     mChildren.Sort(aComparator, data);
 
   for (int32_t i = 0; i < mChildren.Count(); ++i) {
     if (mChildren[i]->IsContainer())
       mChildren[i]->GetAsContainer()->RecursiveSort(aData, aComparator);
   }
 }
 
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetSkipTags(bool *aSkipTags)
+{
+  *aSkipTags = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetSkipDescendantsOnItemRemoval(bool *aSkipDescendantsOnItemRemoval)
+{
+  *aSkipDescendantsOnItemRemoval = false;
+  return NS_OK;
+}
 
 NS_IMETHODIMP
 nsNavHistoryQueryResultNode::OnBeginUpdateBatch()
 {
   return NS_OK;
 }
 
-
 NS_IMETHODIMP
 nsNavHistoryQueryResultNode::OnEndUpdateBatch()
 {
   // If the query has no children it's possible it's not yet listening to
   // bookmarks changes, in such a case it's safer to force a refresh to gather
   // eventual new nodes matching query options.
   if (mChildren.Count() == 0) {
     nsresult rv = Refresh();
@@ -3515,16 +3527,29 @@ nsNavHistoryFolderResultNode::FindChildB
 // If the container is notified of a bookmark event while asynchronous execution
 // is pending, this restarts it and returns.
 #define RESTART_AND_RETURN_IF_ASYNC_PENDING() \
   if (mAsyncPendingStmt) { \
     CancelAsyncOpen(true); \
     return NS_OK; \
   }
 
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetSkipTags(bool *aSkipTags)
+{
+  *aSkipTags = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetSkipDescendantsOnItemRemoval(bool *aSkipDescendantsOnItemRemoval)
+{
+  *aSkipDescendantsOnItemRemoval = false;
+  return NS_OK;
+}
 
 NS_IMETHODIMP
 nsNavHistoryFolderResultNode::OnBeginUpdateBatch()
 {
   return NS_OK;
 }
 
 
@@ -4396,16 +4421,30 @@ nsNavHistoryResult::requestRefresh(nsNav
 
 #define NOTIFY_REFRESH_PARTICIPANTS() \
   PR_BEGIN_MACRO \
   ENUMERATE_LIST_OBSERVERS(ContainerObserverList, Refresh(), mRefreshParticipants, IsContainer()); \
   mRefreshParticipants.Clear(); \
   PR_END_MACRO
 
 NS_IMETHODIMP
+nsNavHistoryResult::GetSkipTags(bool *aSkipTags)
+{
+  *aSkipTags = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryResult::GetSkipDescendantsOnItemRemoval(bool *aSkipDescendantsOnItemRemoval)
+{
+  *aSkipDescendantsOnItemRemoval = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsNavHistoryResult::OnBeginUpdateBatch()
 {
   // Since we could be observing both history and bookmarks, it's possible both
   // notify the batch.  We can safely ignore nested calls.
   if (!mBatchInProgress) {
     mBatchInProgress = true;
     ENUMERATE_HISTORY_OBSERVERS(OnBeginUpdateBatch());
     ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnBeginUpdateBatch());
--- a/toolkit/components/places/nsPlacesMacros.h
+++ b/toolkit/components/places/nsPlacesMacros.h
@@ -18,16 +18,37 @@
     nsCOMArray<type> entries;                                                  \
     cache.GetEntries(entries);                                                 \
     for (int32_t idx = 0; idx < entries.Count(); ++idx)                        \
         entries[idx]->method;                                                  \
     ENUMERATE_WEAKARRAY(array, type, method)                                   \
   }                                                                            \
   PR_END_MACRO;
 
+#define NOTIFY_BOOKMARKS_OBSERVERS(canFire, cache, array, skipIf, method)      \
+  PR_BEGIN_MACRO                                                               \
+  if (canFire) {                                                               \
+    nsCOMArray<nsINavBookmarkObserver> entries;                                \
+    cache.GetEntries(entries);                                                 \
+    for (int32_t idx = 0; idx < entries.Count(); ++idx) {                      \
+      if (skipIf(entries[idx]))                                                \
+        continue;                                                              \
+      entries[idx]->method;                                                    \
+    }                                                                          \
+    for (uint32_t idx = 0; idx < array.Length(); ++idx) {                      \
+      const nsCOMPtr<nsINavBookmarkObserver> &e = array.ElementAt(idx).GetValue(); \
+      if (e) {                                                                 \
+        if (skipIf(e))                                                         \
+            continue;                                                          \
+        e->method;                                                             \
+      }                                                                        \
+    }                                                                          \
+  }                                                                            \
+  PR_END_MACRO;
+
 #define PLACES_FACTORY_SINGLETON_IMPLEMENTATION(_className, _sInstance)        \
   _className * _className::_sInstance = nullptr;                                \
                                                                                \
   already_AddRefed<_className>                                                 \
   _className::GetSingleton()                                                   \
   {                                                                            \
     if (_sInstance) {                                                          \
       RefPtr<_className> ret = _sInstance;                                   \
--- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -1,27 +1,83 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that each nsINavBookmarksObserver method gets the correct input.
 
 var gBookmarksObserver = {
   expected: [],
+  setup(expected) {
+    this.expected = expected;
+		this.promise = new Promise(function(resolve, reject) {
+			this.resolve = resolve;
+			this.reject = reject;
+		}.bind(this));
+    return this.promise;
+  },
   validate: function (aMethodName, aArguments) {
     do_check_eq(this.expected[0].name, aMethodName);
 
     let args = this.expected.shift().args;
     do_check_eq(aArguments.length, args.length);
     for (let i = 0; i < aArguments.length; i++) {
-      do_print(aMethodName + "(args[" + i + "]: " + args[i].name + ")");
+
+      do_print(aMethodName + "(args[" + i + "]: " + args[i].name + ") ["+(aArguments[i] instanceof Ci.nsIURI && aArguments[i].spec || aArguments[i])+"]");
       do_check_true(args[i].check(aArguments[i]));
     }
 
-    if (this.expected.length == 0) {
-      run_next_test();
+    if (this.expected.length === 0) {
+      this.resolve();
+    }
+  },
+
+  // nsINavBookmarkObserver
+  onBeginUpdateBatch() {
+    return this.validate("onBeginUpdateBatch", arguments);
+  },
+  onEndUpdateBatch() {
+    return this.validate("onEndUpdateBatch", arguments);
+  },
+  onItemAdded() {
+    return this.validate("onItemAdded", arguments);
+  },
+  onItemRemoved() {
+    return this.validate("onItemRemoved", arguments);
+  },
+  onItemChanged() {
+    return this.validate("onItemChanged", arguments);
+  },
+  onItemVisited() {
+    return this.validate("onItemVisited", arguments);
+  },
+  onItemMoved() {
+    return this.validate("onItemMoved", arguments);
+  },
+
+  // nsISupports
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
+}
+
+var gBookmarkSkipObserver = {
+  skipTags: true,
+  skipDescendantsOnItemRemoval: true,
+
+  expected: null,
+  setup(expected) {
+    this.expected = expected;
+		this.promise = new Promise(function(resolve, reject) {
+			this.resolve = resolve;
+			this.reject = reject;
+		}.bind(this));
+    return this.promise;
+  },
+  validate: function (aMethodName) {
+    do_check_eq(this.expected.shift(), aMethodName);
+    if (this.expected.length === 0) {
+      this.resolve();
     }
   },
 
   // nsINavBookmarkObserver
   onBeginUpdateBatch() {
     return this.validate("onBeginUpdateBatch", arguments);
   },
   onEndUpdateBatch() {
@@ -42,357 +98,486 @@ var gBookmarksObserver = {
   onItemMoved() {
     return this.validate("onItemMoved", arguments);
   },
 
   // nsISupports
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
 }
 
+
 add_test(function batch() {
-  gBookmarksObserver.expected = [
-    { name: "onBeginUpdateBatch",
-     args: [] },
-    { name: "onEndUpdateBatch",
-     args: [] },
-  ];
+  Promise.all([
+    gBookmarksObserver.setup([
+      { name: "onBeginUpdateBatch",
+       args: [] },
+      { name: "onEndUpdateBatch",
+       args: [] },
+    ]),
+    gBookmarkSkipObserver.setup([
+      "onBeginUpdateBatch", "onEndUpdateBatch"
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.runInBatchMode({
     runBatched: function () {
       // Nothing.
     }
   }, null);
 });
 
 add_test(function onItemAdded_bookmark() {
   const TITLE = "Bookmark 1";
   let uri = NetUtil.newURI("http://1.mozilla.org/");
-  gBookmarksObserver.expected = [
-    { name: "onItemAdded",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
-        { name: "title", check: v => v === TITLE },
-        { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemAdded"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemAdded",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "title", check: v => v === TITLE },
+          { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                        uri, PlacesUtils.bookmarks.DEFAULT_INDEX,
                                        TITLE);
 });
 
 add_test(function onItemAdded_separator() {
-  gBookmarksObserver.expected = [
-    { name: "onItemAdded",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "index", check: v => v === 1 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
-        { name: "uri", check: v => v === null },
-        { name: "title", check: v => v === null },
-        { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemAdded"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemAdded",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "index", check: v => v === 1 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
+          { name: "uri", check: v => v === null },
+          { name: "title", check: v => v === null },
+          { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.insertSeparator(PlacesUtils.unfiledBookmarksFolderId,
                                         PlacesUtils.bookmarks.DEFAULT_INDEX);
 });
 
 add_test(function onItemAdded_folder() {
   const TITLE = "Folder 1";
-  gBookmarksObserver.expected = [
-    { name: "onItemAdded",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "index", check: v => v === 2 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-        { name: "uri", check: v => v === null },
-        { name: "title", check: v => v === TITLE },
-        { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemAdded"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemAdded",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "index", check: v => v === 2 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "uri", check: v => v === null },
+          { name: "title", check: v => v === TITLE },
+          { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
                                      TITLE,
                                      PlacesUtils.bookmarks.DEFAULT_INDEX);
 });
 
 add_test(function onItemChanged_title_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
   const TITLE = "New title";
-  gBookmarksObserver.expected = [
-    { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "property", check: v => v === "title" },
-        { name: "isAnno", check: v => v === false },
-        { name: "newValue", check: v => v === TITLE },
-        { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldValue", check: v => typeof(v) == "string" },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemChanged"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "property", check: v => v === "title" },
+          { name: "isAnno", check: v => v === false },
+          { name: "newValue", check: v => v === TITLE },
+          { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldValue", check: v => typeof(v) == "string" },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.setItemTitle(id, TITLE);
 });
 
 add_test(function onItemChanged_tags_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
   const TITLE = "New title";
-  const TAG = "tag"
-  gBookmarksObserver.expected = [
-    { name: "onItemAdded", // This is the tag folder.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-        { name: "uri", check: v => v === null },
-        { name: "title", check: v => v === TAG },
-        { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemAdded", // This is the tag.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
-        { name: "title", check: v => v === null },
-        { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemChanged",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "property", check: v => v === "tags" },
-        { name: "isAnno", check: v => v === false },
-        { name: "newValue", check: v => v === "" },
-        { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldValue", check: v => typeof(v) == "string" },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemRemoved", // This is the tag.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemRemoved", // This is the tag folder.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-        { name: "uri", check: v => v === null },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemChanged",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "property", check: v => v === "tags" },
-        { name: "isAnno", check: v => v === false },
-        { name: "newValue", check: v => v === "" },
-        { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldValue", check: v => typeof(v) == "string" },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  const TAG = "tag";
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemChanged", "onItemChanged"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemAdded", // This is the tag folder.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "uri", check: v => v === null },
+          { name: "title", check: v => v === TAG },
+          { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemAdded", // This is the tag.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "title", check: v => v === null },
+          { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemChanged",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "property", check: v => v === "tags" },
+          { name: "isAnno", check: v => v === false },
+          { name: "newValue", check: v => v === "" },
+          { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldValue", check: v => typeof(v) == "string" },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemRemoved", // This is the tag.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemRemoved", // This is the tag folder.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "uri", check: v => v === null },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemChanged",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "property", check: v => v === "tags" },
+          { name: "isAnno", check: v => v === false },
+          { name: "newValue", check: v => v === "" },
+          { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldValue", check: v => typeof(v) == "string" },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.tagging.tagURI(uri, [TAG]);
   PlacesUtils.tagging.untagURI(uri, [TAG]);
 });
 
 add_test(function onItemMoved_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
-  gBookmarksObserver.expected = [
-    { name: "onItemMoved",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "oldParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "oldIndex", check: v => v === 0 },
-        { name: "newParentId", check: v => v === PlacesUtils.toolbarFolderId },
-        { name: "newIndex", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "newParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemMoved",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "oldParentId", check: v => v === PlacesUtils.toolbarFolderId },
-        { name: "oldIndex", check: v => v === 0 },
-        { name: "newParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "newIndex", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "newParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemMoved", "onItemMoved"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemMoved",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "oldParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "oldIndex", check: v => v === 0 },
+          { name: "newParentId", check: v => v === PlacesUtils.toolbarFolderId },
+          { name: "newIndex", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "newParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemMoved",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "oldParentId", check: v => v === PlacesUtils.toolbarFolderId },
+          { name: "oldIndex", check: v => v === 0 },
+          { name: "newParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "newIndex", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "newParentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.moveItem(id, PlacesUtils.toolbarFolderId, 0);
   PlacesUtils.bookmarks.moveItem(id, PlacesUtils.unfiledBookmarksFolderId, 0);
 });
 
 add_test(function onItemMoved_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
-  gBookmarksObserver.expected = [
-    { name: "onItemVisited",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "visitId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "time", check: v => typeof(v) == "number" && v > 0 },
-        { name: "transitionType", check: v => v === PlacesUtils.history.TRANSITION_TYPED },
-        { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemVisited"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemVisited",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "visitId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "time", check: v => typeof(v) == "number" && v > 0 },
+          { name: "transitionType", check: v => v === PlacesUtils.history.TRANSITION_TYPED },
+          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesTestUtils.addVisits({ uri: uri, transition: TRANSITION_TYPED });
 });
 
 add_test(function onItemRemoved_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
-  gBookmarksObserver.expected = [
-    { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "property", check: v => v === "" },
-        { name: "isAnno", check: v => v === true },
-        { name: "newValue", check: v => v === "" },
-        { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldValue", check: v => typeof(v) == "string" },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemRemoved",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-        { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemChanged", "onItemRemoved"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "property", check: v => v === "" },
+          { name: "isAnno", check: v => v === true },
+          { name: "newValue", check: v => v === "" },
+          { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldValue", check: v => typeof(v) == "string" },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemRemoved",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.removeItem(id);
 });
 
 add_test(function onItemRemoved_separator() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
-  gBookmarksObserver.expected = [
-    { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "property", check: v => v === "" },
-        { name: "isAnno", check: v => v === true },
-        { name: "newValue", check: v => v === "" },
-        { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
-        { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldValue", check: v => typeof(v) == "string" },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemRemoved",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
-        { name: "uri", check: v => v === null },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemChanged", "onItemRemoved"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "property", check: v => v === "" },
+          { name: "isAnno", check: v => v === true },
+          { name: "newValue", check: v => v === "" },
+          { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldValue", check: v => typeof(v) == "string" },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemRemoved",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
+          { name: "uri", check: v => v === null },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.removeItem(id);
 });
 
 add_test(function onItemRemoved_folder() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   const TITLE = "Folder 2";
-  gBookmarksObserver.expected = [
-    { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "property", check: v => v === "" },
-        { name: "isAnno", check: v => v === true },
-        { name: "newValue", check: v => v === "" },
-        { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-        { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "oldValue", check: v => typeof(v) == "string" },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-    { name: "onItemRemoved",
-      args: [
-        { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
-        { name: "index", check: v => v === 0 },
-        { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-        { name: "uri", check: v => v === null },
-        { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
-        { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
-      ] },
-  ];
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemChanged", "onItemRemoved"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "property", check: v => v === "" },
+          { name: "isAnno", check: v => v === true },
+          { name: "newValue", check: v => v === "" },
+          { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldValue", check: v => typeof(v) == "string" },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemRemoved",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "uri", check: v => v === null },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
   PlacesUtils.bookmarks.removeItem(id);
 });
 
+add_test(function onItemRemoved_folder_recursive() {
+  const TITLE = "Folder 3";
+  const BMTITLE = "Bookmark 1";
+  let uri = NetUtil.newURI("http://1.mozilla.org/");
+  Promise.all([
+    gBookmarkSkipObserver.setup([
+      "onItemAdded", "onItemAdded", "onItemChanged", "onItemRemoved"
+    ]),
+    gBookmarksObserver.setup([
+      { name: "onItemAdded",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "uri", check: v => v === null },
+          { name: "title", check: v => v === TITLE },
+          { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemAdded",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => v === PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "title", check: v => v === BMTITLE },
+          { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "property", check: v => v === "" },
+          { name: "isAnno", check: v => v === true },
+          { name: "newValue", check: v => v === "" },
+          { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "oldValue", check: v => typeof(v) == "string" },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemRemoved",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+      { name: "onItemRemoved",
+        args: [
+          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "index", check: v => v === 0 },
+          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+          { name: "uri", check: v => v === null },
+          { name: "guid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "parentGuid", check: v => typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
+          { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+        ] },
+  ])]).then(run_next_test);
+  let folder = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+                                     TITLE,
+                                     PlacesUtils.bookmarks.DEFAULT_INDEX);
+  PlacesUtils.bookmarks.insertBookmark(folder,
+                                       uri, PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                       BMTITLE);
+
+  PlacesUtils.bookmarks.removeItem(folder);
+});
+
 function run_test() {
   PlacesUtils.bookmarks.addObserver(gBookmarksObserver, false);
+  PlacesUtils.bookmarks.addObserver(gBookmarkSkipObserver, false);
   run_next_test();
 }
 
 do_register_cleanup(function () {
   PlacesUtils.bookmarks.removeObserver(gBookmarksObserver);
+  PlacesUtils.bookmarks.removeObserver(gBookmarkSkipObserver);
 });