Bug 1426245 - Test changes r=mak
authorDoug Thayer <dothayer@mozilla.com>
Tue, 09 Oct 2018 14:47:31 +0000
changeset 488632 85a806b69f15dcf8d4ebf1a1a847be5641323013
parent 488631 50ca67245a715b0e37014f2066102ad9feec9f1f
child 488633 1961aeb46e981b607fa2330aa06ab056aeb7e859
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
reviewersmak
bugs1426245
milestone64.0a1
Bug 1426245 - Test changes r=mak MozReview-Commit-ID: 4fhhzspxLJZ Depends on D4606 Differential Revision: https://phabricator.services.mozilla.com/D5162
browser/base/content/browser-places.js
browser/base/content/test/general/browser_bookmark_titles.js
browser/base/content/test/general/head.js
browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js
browser/components/migration/tests/unit/test_360se_bookmarks.js
browser/components/migration/tests/unit/test_Chrome_bookmarks.js
browser/components/migration/tests/unit/test_Edge_db_migration.js
browser/components/migration/tests/unit/test_IE_bookmarks.js
browser/components/migration/tests/unit/test_Safari_bookmarks.js
browser/components/newtab/lib/PlacesFeed.jsm
browser/components/newtab/test/unit/lib/PlacesFeed.test.js
browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
browser/components/places/tests/browser/browser_bookmark_add_tags.js
browser/components/places/tests/browser/browser_bookmark_backup_export_import.js
browser/components/places/tests/browser/browser_bookmarksProperties.js
browser/components/places/tests/browser/browser_editBookmark_keywords.js
browser/components/places/tests/browser/browser_toolbar_drop_text.js
browser/components/places/tests/browser/browser_views_liveupdate.js
dom/base/PlacesObservers.cpp
services/sync/tests/unit/test_bookmark_tracker.js
toolkit/components/places/tests/PlacesTestUtils.jsm
toolkit/components/places/tests/bookmarks/head_bookmarks.js
toolkit/components/places/tests/bookmarks/test_393498.js
toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js
toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
toolkit/components/places/tests/bookmarks/test_insertTree_fixupOrSkipInvalidEntries.js
toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js
toolkit/components/places/tests/chrome/test_371798.xul
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/legacy/test_bookmarks.js
toolkit/components/places/tests/sync/head_sync.js
toolkit/components/places/tests/sync/test_bookmark_kinds.js
toolkit/components/places/tests/sync/test_bookmark_observer_recorder.js
toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
toolkit/components/places/tests/sync/test_bookmark_value_changes.js
toolkit/components/places/tests/unit/test_async_transactions.js
toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
toolkit/components/places/tests/unit/test_onItemChanged_tags.js
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1457,17 +1457,17 @@ var BookmarkingUI = {
 
     PlacesUtils.bookmarks.fetch({url: this._uri}, b => guids.add(b.guid), { concurrent: true })
       .catch(Cu.reportError)
       .then(() => {
          if (pendingUpdate != this._pendingUpdate) {
            return;
          }
 
-         // It's possible that onItemAdded gets called before the async statement
+         // It's possible that "bookmark-added" gets called before the async statement
          // calls back.  For such an edge case, retain all unique entries from the
          // array.
          if (this._itemGuids.size > 0) {
            this._itemGuids = new Set(...this._itemGuids, ...guids);
          } else {
            this._itemGuids = guids;
          }
 
--- a/browser/base/content/test/general/browser_bookmark_titles.js
+++ b/browser/base/content/test/general/browser_bookmark_titles.js
@@ -73,18 +73,19 @@ add_task(async function check_default_bo
 });
 
 // Bookmark the current page and confirm that the new bookmark has the expected
 // title. (Then delete the bookmark.)
 async function checkBookmark(url, expected_title) {
   Assert.equal(gBrowser.selectedBrowser.currentURI.spec, url,
     "Trying to bookmark the expected uri");
 
-  let promiseBookmark = PlacesTestUtils.waitForNotification("onItemAdded",
-    (id, parentId, index, type, itemUrl) => itemUrl.equals(gBrowser.selectedBrowser.currentURI));
+  let promiseBookmark = PlacesTestUtils.waitForNotification("bookmark-added",
+    (events) => events.some(({url: eventUrl}) => eventUrl == gBrowser.selectedBrowser.currentURI.spec),
+    "places");
   PlacesCommandHook.bookmarkPage();
   await promiseBookmark;
 
   let bookmark = await PlacesUtils.bookmarks.fetch({url});
 
   Assert.ok(bookmark, "Found the expected bookmark");
   Assert.equal(bookmark.title, expected_title, "Bookmark got a good default title.");
 
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -460,38 +460,28 @@ function promiseNotificationShown(notifi
   return panelPromise;
 }
 
 /**
  * Resolves when a bookmark with the given uri is added.
  */
 function promiseOnBookmarkItemAdded(aExpectedURI) {
   return new Promise((resolve, reject) => {
-    let bookmarksObserver = {
-      onItemAdded(aItemId, aFolderId, aIndex, aItemType, aURI) {
-        info("Added a bookmark to " + aURI.spec);
-        PlacesUtils.bookmarks.removeObserver(bookmarksObserver);
-        if (aURI.equals(aExpectedURI)) {
-          resolve();
-        } else {
-          reject(new Error("Added an unexpected bookmark"));
-        }
-      },
-      onBeginUpdateBatch() {},
-      onEndUpdateBatch() {},
-      onItemRemoved() {},
-      onItemChanged() {},
-      onItemVisited() {},
-      onItemMoved() {},
-      QueryInterface: ChromeUtils.generateQI([
-        Ci.nsINavBookmarkObserver,
-      ]),
+    let listener = events => {
+      is(events.length, 1, "Should only receive one event.");
+      info("Added a bookmark to " + events[0].url);
+      PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+      if (events[0].url == aExpectedURI.spec) {
+        resolve();
+      } else {
+        reject(new Error("Added an unexpected bookmark"));
+      }
     };
     info("Waiting for a bookmark to be added");
-    PlacesUtils.bookmarks.addObserver(bookmarksObserver);
+    PlacesUtils.observers.addListener(["bookmark-added"], listener);
   });
 }
 
 async function loadBadCertPage(url) {
   const EXCEPTION_DIALOG_URI = "chrome://pippki/content/exceptionDialog.xul";
   let exceptionDialogResolved = new Promise(function(resolve) {
     // When the certificate exception dialog has opened, click the button to add
     // an exception.
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js
@@ -64,40 +64,43 @@ function findBookmarkInPolicy(bookmark) 
       return entry;
     }
   }
   return null;
 }
 
 async function promiseAllChangesMade({itemsToAdd, itemsToRemove}) {
   return new Promise(resolve => {
+    let listener = events => {
+      is(events.length, 1, "Should only have 1 event.");
+      itemsToAdd--;
+      if (itemsToAdd == 0 && itemsToRemove == 0) {
+        PlacesUtils.bookmarks.removeObserver(bmObserver);
+        PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+        resolve();
+      }
+    };
     let bmObserver = {
-      onItemAdded() {
-        itemsToAdd--;
-        if (itemsToAdd == 0 && itemsToRemove == 0) {
-          PlacesUtils.bookmarks.removeObserver(bmObserver);
-          resolve();
-        }
-      },
-
       onItemRemoved() {
         itemsToRemove--;
         if (itemsToAdd == 0 && itemsToRemove == 0) {
           PlacesUtils.bookmarks.removeObserver(bmObserver);
+          PlacesUtils.observers.removeListener(["bookmark-added"], listener);
           resolve();
         }
       },
 
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
       onItemChanged() {},
       onItemVisited() {},
       onItemMoved() {},
     };
     PlacesUtils.bookmarks.addObserver(bmObserver);
+    PlacesUtils.observers.addListener(["bookmark-added"], listener);
   });
 }
 
 /*
  * ==================
  * = CHECK FUNCTION =
  * ==================
  *
--- a/browser/components/migration/tests/unit/test_360se_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_360se_bookmarks.js
@@ -16,43 +16,37 @@ add_task(async function() {
   // folders are created on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceName360se");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.toolbarFolderId ];
   let itemCount = 0;
 
   let gotFolder = false;
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (aTitle != label) {
+  let listener = events => {
+    for (let event of events) {
+      if (event.title != label) {
         itemCount++;
       }
-      if (aItemType == PlacesUtils.bookmarks.TYPE_FOLDER && aTitle == "360 \u76f8\u5173") {
+      if (event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER && event.title == "360 \u76f8\u5173") {
         gotFolder = true;
       }
-      if (expectedParents.length > 0 && aTitle == label) {
-        let index = expectedParents.indexOf(aParentId);
+      if (expectedParents.length > 0 && event.title == label) {
+        let index = expectedParents.indexOf(event.parentId);
         Assert.ok(index != -1, "Found expected parent");
         expectedParents.splice(index, 1);
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, {
     id: "default",
   });
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
 
   // Check the bookmarks have been imported to all the expected parents.
   Assert.ok(!expectedParents.length, "No more expected parents");
   Assert.ok(gotFolder, "Should have seen the folder get imported");
   Assert.equal(itemCount, 10, "Should import all 10 items.");
   // Check that the telemetry matches:
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, "Telemetry reporting correct.");
 });
--- a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
@@ -72,34 +72,28 @@ add_task(async function() {
 
   await OS.File.writeAtomic(target.path, JSON.stringify(bookmarksData), {encoding: "utf-8"});
 
   let migrator = await MigrationUtils.getMigrator("chrome");
   // Sanity check for the source.
   Assert.ok(await migrator.isSourceAvailable());
 
   let itemsSeen = {bookmarks: 0, folders: 0};
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (!aTitle.includes("Chrome")) {
-        itemsSeen[aItemType == PlacesUtils.bookmarks.TYPE_FOLDER ? "folders" : "bookmarks"]++;
+  let listener = events => {
+    for (let event of events) {
+      if (!event.title.includes("Chrome")) {
+        itemsSeen[event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER ? "folders" : "bookmarks"]++;
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
 
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
   const PROFILE = {
     id: "Default",
     name: "Default",
   };
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, PROFILE);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
 
   Assert.equal(itemsSeen.bookmarks, 200, "Should have seen 200 bookmarks.");
   Assert.equal(itemsSeen.folders, 10, "Should have seen 10 folders.");
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemsSeen.bookmarks + itemsSeen.folders, "Telemetry reporting correct.");
 });
--- a/browser/components/migration/tests/unit/test_Edge_db_migration.js
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -418,50 +418,55 @@ add_task(async function() {
                  .createInstance(Ci.nsIBrowserProfileMigrator);
   let bookmarksMigrator = migrator.wrappedJSObject.getBookmarksMigratorForTesting(db);
   Assert.ok(bookmarksMigrator.exists, "Should recognize db we just created");
 
   let source = MigrationUtils.getLocalizedString("sourceNameEdge");
   let sourceLabel = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let seenBookmarks = [];
-  let bookmarkObserver = {
-    onItemAdded(itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid) {
+  let listener = events => {
+    for (let event of events) {
+      let {
+        id,
+        itemType,
+        url,
+        title,
+        dateAdded,
+        guid,
+        index,
+        parentGuid,
+        parentId,
+      } = event;
       if (title.startsWith("Deleted")) {
         ok(false, "Should not see deleted items being bookmarked!");
       }
-      seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid});
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+      seenBookmarks.push({id, parentId, index, itemType, url, title, dateAdded, guid, parentGuid});
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bookmarkObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   let migrateResult = await new Promise(resolve => bookmarksMigrator.migrate(resolve)).catch(ex => {
     Cu.reportError(ex);
     Assert.ok(false, "Got an exception trying to migrate data! " + ex);
     return false;
   });
-  PlacesUtils.bookmarks.removeObserver(bookmarkObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   Assert.ok(migrateResult, "Migration should succeed");
   Assert.equal(seenBookmarks.length, 7, "Should have seen 7 items being bookmarked.");
   Assert.equal(seenBookmarks.filter(bm => bm.title != sourceLabel).length,
                MigrationUtils._importQuantities.bookmarks,
                "Telemetry should have items except for 'From Microsoft Edge' folders");
 
   let menuParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.menuGuid);
   Assert.equal(menuParents.length, 1, "Should have a single folder added to the menu");
   let toolbarParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid);
   Assert.equal(toolbarParents.length, 1, "Should have a single item added to the toolbar");
-  let menuParentGuid = menuParents[0].itemGuid;
-  let toolbarParentGuid = toolbarParents[0].itemGuid;
+  let menuParentGuid = menuParents[0].guid;
+  let toolbarParentGuid = toolbarParents[0].guid;
 
   let expectedTitlesInMenu = bookmarkReferenceItems.filter(item => item.ParentId == kEdgeMenuParent).map(item => item.Title);
   // Hacky, but seems like much the simplest way:
   expectedTitlesInMenu.push("Item in deleted folder (should be in root)");
   let expectedTitlesInToolbar = bookmarkReferenceItems.filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf").map(item => item.Title);
 
   let edgeNameStr = MigrationUtils.getLocalizedString("sourceNameEdge");
   let importParentFolderName = MigrationUtils.getLocalizedString("importedBookmarksFolder", [edgeNameStr]);
@@ -476,68 +481,73 @@ add_task(async function() {
       Assert.notEqual(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER,
           "Bookmark " + bookmark.title + " should not be a folder");
     }
 
     if (shouldBeInMenu) {
       Assert.equal(bookmark.parentGuid, menuParentGuid, "Item '" + bookmark.title + "' should be in menu");
     } else if (shouldBeInToolbar) {
       Assert.equal(bookmark.parentGuid, toolbarParentGuid, "Item '" + bookmark.title + "' should be in toolbar");
-    } else if (bookmark.itemGuid == menuParentGuid || bookmark.itemGuid == toolbarParentGuid) {
+    } else if (bookmark.guid == menuParentGuid || bookmark.guid == toolbarParentGuid) {
       Assert.ok(true, "Expect toolbar and menu folders to not be in menu or toolbar");
     } else {
       // Bit hacky, but we do need to check this.
       Assert.equal(bookmark.title, "Item in folder", "Subfoldered item shouldn't be in menu or toolbar");
-      let parent = seenBookmarks.find(maybeParent => maybeParent.itemGuid == bookmark.parentGuid);
+      let parent = seenBookmarks.find(maybeParent => maybeParent.guid == bookmark.parentGuid);
       Assert.equal(parent && parent.title, "Folder", "Subfoldered item should be in subfolder labeled 'Folder'");
     }
 
     let dbItem = bookmarkReferenceItems.find(someItem => bookmark.title == someItem.Title);
     if (!dbItem) {
       Assert.equal(bookmark.title, importParentFolderName, "Only the extra layer of folders isn't in the input we stuck in the DB.");
-      Assert.ok([menuParentGuid, toolbarParentGuid].includes(bookmark.itemGuid), "This item should be one of the containers");
+      Assert.ok([menuParentGuid, toolbarParentGuid].includes(bookmark.guid), "This item should be one of the containers");
     } else {
-      Assert.equal(dbItem.URL || null, bookmark.url && bookmark.url.spec, "URL is correct");
-      Assert.equal(dbItem.DateUpdated.valueOf(), (new Date(bookmark.dateAdded / 1000)).valueOf(), "Date added is correct");
+      Assert.equal(dbItem.URL || "", bookmark.url, "URL is correct");
+      Assert.equal(dbItem.DateUpdated.valueOf(), (new Date(bookmark.dateAdded)).valueOf(), "Date added is correct");
     }
   }
 
   MigrationUtils._importQuantities.bookmarks = 0;
   seenBookmarks = [];
-  bookmarkObserver = {
-    onItemAdded(itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid) {
-      seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid});
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+  listener = events => {
+    for (let event of events) {
+      let {
+        id,
+        itemType,
+        url,
+        title,
+        dateAdded,
+        guid,
+        index,
+        parentGuid,
+        parentId,
+      } = event;
+      seenBookmarks.push({id, parentId, index, itemType, url, title, dateAdded, guid, parentGuid});
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bookmarkObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   let readingListMigrator = migrator.wrappedJSObject.getReadingListMigratorForTesting(db);
   Assert.ok(readingListMigrator.exists, "Should recognize db we just created");
   migrateResult = await new Promise(resolve => readingListMigrator.migrate(resolve)).catch(ex => {
     Cu.reportError(ex);
     Assert.ok(false, "Got an exception trying to migrate data! " + ex);
     return false;
   });
-  PlacesUtils.bookmarks.removeObserver(bookmarkObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   Assert.ok(migrateResult, "Migration should succeed");
   Assert.equal(seenBookmarks.length, 3, "Should have seen 3 items being bookmarked (2 items + 1 folder).");
   Assert.equal(seenBookmarks.filter(bm => bm.title != sourceLabel).length,
                MigrationUtils._importQuantities.bookmarks,
                "Telemetry should have items except for 'From Microsoft Edge' folders");
   let readingListContainerLabel = MigrationUtils.getLocalizedString("importedEdgeReadingList");
 
   for (let bookmark of seenBookmarks) {
     if (readingListContainerLabel == bookmark.title) {
       continue;
     }
     let referenceItem = readingListReferenceItems.find(item => item.Title == bookmark.title);
     Assert.ok(referenceItem, "Should have imported what we expected");
-    Assert.equal(referenceItem.URL, bookmark.url.spec, "Should have the right URL");
+    Assert.equal(referenceItem.URL, bookmark.url, "Should have the right URL");
     readingListReferenceItems.splice(readingListReferenceItems.findIndex(item => item.Title == bookmark.title), 1);
   }
   Assert.ok(!readingListReferenceItems.length, "Should have seen all expected items.");
 });
--- a/browser/components/migration/tests/unit/test_IE_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -9,36 +9,30 @@ add_task(async function() {
   // folders are created in the menu and on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameIE");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.bookmarksMenuFolderId,
                           PlacesUtils.toolbarFolderId ];
 
   let itemCount = 0;
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (aTitle != label) {
+  let listener = events => {
+    for (let event of events) {
+      if (event.title != label) {
         itemCount++;
       }
-      if (expectedParents.length > 0 && aTitle == label) {
-        let index = expectedParents.indexOf(aParentId);
+      if (expectedParents.length > 0 && event.title == label) {
+        let index = expectedParents.indexOf(event.parentId);
         Assert.notEqual(index, -1);
         expectedParents.splice(index, 1);
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount,
                "Ensure telemetry matches actual number of imported items.");
 
   // Check the bookmarks have been imported to all the expected parents.
   Assert.equal(expectedParents.length, 0, "Got all the expected parents");
 });
--- a/browser/components/migration/tests/unit/test_Safari_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
@@ -11,41 +11,35 @@ add_task(async function() {
   // folders are created on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameSafari");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.toolbarFolderId ];
   let itemCount = 0;
 
   let gotFolder = false;
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (aTitle != label) {
+  let listener = events => {
+    for (let event of events) {
+      if (event.title != label) {
         itemCount++;
       }
-      if (aItemType == PlacesUtils.bookmarks.TYPE_FOLDER && aTitle == "Stuff") {
+      if (event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER && event.title == "Stuff") {
         gotFolder = true;
       }
-      if (expectedParents.length > 0 && aTitle == label) {
-        let index = expectedParents.indexOf(aParentId);
+      if (expectedParents.length > 0 && event.title == label) {
+        let index = expectedParents.indexOf(event.parentId);
         Assert.ok(index != -1, "Found expected parent");
         expectedParents.splice(index, 1);
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
 
   // Check the bookmarks have been imported to all the expected parents.
   Assert.ok(!expectedParents.length, "No more expected parents");
   Assert.ok(gotFolder, "Should have seen the folder get imported");
   Assert.equal(itemCount, 13, "Should import all 13 items.");
   // Check that the telemetry matches:
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, "Telemetry reporting correct.");
 });
--- a/browser/components/newtab/lib/PlacesFeed.jsm
+++ b/browser/components/newtab/lib/PlacesFeed.jsm
@@ -119,20 +119,20 @@ class BookmarksObserver extends Observer
 }
 
 /**
  * PlacesObserver - observes events from PlacesUtils.observers
  */
 class PlacesObserver extends Observer {
   constructor(dispatch) {
     super(dispatch, Ci.nsINavBookmarkObserver);
-    this.handlePlacesEvent = this.handlePlacesEvent.bind(this);
+    this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
   }
 
-  handlePlacesEvent(events) {
+  handlePlacesEvents(events) {
     for (let {itemType, source, dateAdded, guid, title, url, isTagging} of events) {
       // Skips items that are not bookmarks (like folders), about:* pages or
       // default bookmarks, added when the profile is created.
       if (isTagging ||
           itemType !== PlacesUtils.bookmarks.TYPE_BOOKMARK ||
           source === PlacesUtils.bookmarks.SOURCES.IMPORT ||
           source === PlacesUtils.bookmarks.SOURCES.RESTORE ||
           source === PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP ||
@@ -168,17 +168,17 @@ class PlacesFeed {
     // NB: Directly get services without importing the *BIG* PlacesUtils module
     Cc["@mozilla.org/browser/nav-history-service;1"]
       .getService(Ci.nsINavHistoryService)
       .addObserver(this.historyObserver, true);
     Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
       .getService(Ci.nsINavBookmarksService)
       .addObserver(this.bookmarksObserver, true);
     PlacesUtils.observers.addListener(["bookmark-added"],
-                                      this.placesObserver.handlePlacesEvent);
+                                      this.placesObserver.handlePlacesEvents);
 
     Services.obs.addObserver(this, LINK_BLOCKED_EVENT);
   }
 
   /**
    * setTimeout - A custom function that creates an nsITimer that can be cancelled
    *
    * @param {func} callback       A function to be executed after the timer expires
@@ -210,17 +210,17 @@ class PlacesFeed {
   removeObservers() {
     if (this.placesChangedTimer) {
       this.placesChangedTimer.cancel();
       this.placesChangedTimer = null;
     }
     PlacesUtils.history.removeObserver(this.historyObserver);
     PlacesUtils.bookmarks.removeObserver(this.bookmarksObserver);
     PlacesUtils.observers.removeListener(["bookmark-added"],
-                                         this.placesObserver.handlePlacesEvent);
+                                         this.placesObserver.handlePlacesEvents);
     Services.obs.removeObserver(this, LINK_BLOCKED_EVENT);
   }
 
   /**
    * observe - An observer for the LINK_BLOCKED_EVENT.
    *           Called when a link is blocked.
    *
    * @param  {null} subject
--- a/browser/components/newtab/test/unit/lib/PlacesFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/PlacesFeed.test.js
@@ -339,24 +339,28 @@ describe("PlacesFeed", () => {
         observer.onPageChanged();
         observer.onDeleteVisits();
       });
     });
   });
 
   describe("Custom dispatch", () => {
     it("should only dispatch 1 PLACES_LINKS_CHANGED action if many onItemAdded notifications happened at once", async () => {
-      // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-      const args = [null, null, null, TYPE_BOOKMARK,
-        {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-        FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-      await feed.bookmarksObserver.onItemAdded(...args);
-      await feed.bookmarksObserver.onItemAdded(...args);
-      await feed.bookmarksObserver.onItemAdded(...args);
-      await feed.bookmarksObserver.onItemAdded(...args);
+      const args = {
+        type: "bookmark-added",
+        itemType: TYPE_BOOKMARK,
+        url: "https://" + FAKE_BOOKMARK.url,
+        title: FAKE_BOOKMARK.bookmarkTitle,
+        dateAdded: FAKE_BOOKMARK.dateAdded,
+        guid: FAKE_BOOKMARK.bookmarkGuid,
+        source: SOURCES.DEFAULT,
+      };
+      await feed.placesObserver.handlePlacesEvents([args]);
+      await feed.placesObserver.handlePlacesEvents([args]);
+      await feed.placesObserver.handlePlacesEvents([args]);
       assert.calledOnce(feed.store.dispatch.withArgs(ac.OnlyToMain({type: at.PLACES_LINKS_CHANGED})));
     });
     it("should only dispatch 1 PLACES_LINKS_CHANGED action if many onItemRemoved notifications happened at once", async () => {
       const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.DEFAULT];
       await feed.bookmarksObserver.onItemRemoved(...args);
       await feed.bookmarksObserver.onItemRemoved(...args);
       await feed.bookmarksObserver.onItemRemoved(...args);
       await feed.bookmarksObserver.onItemRemoved(...args);
@@ -367,98 +371,152 @@ describe("PlacesFeed", () => {
       await feed.historyObserver.onDeleteURI({spec: "foo.com"});
       await feed.historyObserver.onDeleteURI({spec: "foo1.com"});
       await feed.historyObserver.onDeleteURI({spec: "foo2.com"});
 
       assert.calledOnce(feed.store.dispatch.withArgs(ac.OnlyToMain({type: at.PLACES_LINKS_CHANGED})));
     });
   });
 
+  describe("PlacesObserver", () => {
+    let dispatch;
+    let observer;
+    beforeEach(() => {
+      dispatch = sandbox.spy();
+      observer = new PlacesObserver(dispatch);
+    });
+
+    describe("#handlePlacesEvents", () => {
+      beforeEach(() => {
+      });
+
+      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - http", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.DEFAULT,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
+      });
+      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - https", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "https://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.DEFAULT,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - not http/https", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "places://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.DEFAULT,
+        };
+        await observer.handlePlacesEvents([args]);
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has IMPORT source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.IMPORT,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.RESTORE,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE_ON_STARTUP source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.RESTORE_ON_STARTUP,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has SYNC source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.SYNC,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should ignore events that are not of TYPE_BOOKMARK", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: "nottypebookmark",
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.SYNC,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+    });
+  });
+
   describe("BookmarksObserver", () => {
     let dispatch;
     let observer;
     beforeEach(() => {
       dispatch = sandbox.spy();
       observer = new BookmarksObserver(dispatch);
     });
     it("should have a QueryInterface property", () => {
       assert.property(observer, "QueryInterface");
     });
-    describe("#onItemAdded", () => {
-      beforeEach(() => {
-      });
-      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - http", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-        await observer.onItemAdded(...args);
-
-        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
-      });
-      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - https", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "https"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-        await observer.onItemAdded(...args);
-
-        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - not http/https", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "places"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has IMPORT source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.IMPORT];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.RESTORE];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE_ON_STARTUP source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.RESTORE_ON_STARTUP];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has SYNC source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.SYNC];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should ignore events that are not of TYPE_BOOKMARK", async () => {
-        const args = [null, null, null, "nottypebookmark"];
-        await observer.onItemAdded(...args);
-        assert.notCalled(dispatch);
-      });
-    });
     describe("#onItemRemoved", () => {
       it("should ignore events that are not of TYPE_BOOKMARK", async () => {
         await observer.onItemRemoved(null, null, null, "nottypebookmark", null, "123foo", "", SOURCES.DEFAULT);
         assert.notCalled(dispatch);
       });
       it("should not dispatch a PLACES_BOOKMARK_REMOVED action - has SYNC source", async () => {
         const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.SYNC];
         await observer.onItemRemoved(...args);
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
@@ -38,21 +38,27 @@ add_task(async function test_newFolder()
 
   let folderTree = document.getElementById("editBMPanel_folderTree");
 
   // Create new folder.
   let newFolderButton = document.getElementById("editBMPanel_newFolderButton");
   newFolderButton.click();
 
   let newFolderGuid;
-  let newFolderObserver = PlacesTestUtils.waitForNotification("onItemAdded",
-    (id, parentId, index, type, uri, title, dateAdded, guid) => {
-      newFolderGuid = guid;
-      return type == PlacesUtils.bookmarks.TYPE_FOLDER;
-  });
+  let newFolderObserver =
+    PlacesTestUtils.waitForNotification("bookmark-added",
+                                        events => {
+      for (let {guid, itemType} of events) {
+        newFolderGuid = guid;
+        if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER) {
+          return true;
+        }
+      }
+      return false;
+    }, "places");
 
   let menulist = document.getElementById("editBMPanel_folderMenuList");
 
   await newFolderObserver;
 
   // Wait for the folder to be created and for editing to start.
   await BrowserTestUtils.waitForCondition(() => folderTree.hasAttribute("editing"),
      "Should be in edit mode for the new folder");
--- a/browser/components/places/tests/browser/browser_bookmark_add_tags.js
+++ b/browser/components/places/tests/browser/browser_bookmark_add_tags.js
@@ -45,22 +45,20 @@ add_task(async function test_add_bookmar
     await clickBookmarkStar();
     Assert.equal(bookmarkPanelTitle.value, gNavigatorBundle.getString("editBookmarkPanel.newBookmarkTitle"), "Bookmark title is correct");
     Assert.equal(bookmarkStar.getAttribute("starred"), "true", "Page is starred");
   });
 
   // Click the bookmark star again to add tags.
   await clickBookmarkStar();
   Assert.equal(bookmarkPanelTitle.value, gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"), "Bookmark title is correct");
-  let promiseNotification = PlacesTestUtils.waitForNotification("onItemAdded", (id, parentId, index, type, itemUrl) => {
-    if (itemUrl !== null) {
-      return itemUrl.equals(Services.io.newURI(TEST_URL));
-    }
-    return true;
-  });
+  let promiseNotification =
+    PlacesTestUtils.waitForNotification("bookmark-added",
+                                        events => events.some(({url}) => !url || url == TEST_URL),
+                                        "places");
   await fillBookmarkTextField("editBMPanel_tagsField", "tag1", window);
   await promiseNotification;
   let bookmarks = [];
   await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm => bookmarks.push(bm));
   Assert.equal(PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)).length, 1, "Found the right number of tags");
   Assert.deepEqual(PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)), ["tag1"]);
   let doneButton = document.getElementById("editBookmarkPanelDoneButton");
   await hideBookmarksPanel(() => doneButton.click());
--- a/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js
+++ b/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js
@@ -120,20 +120,20 @@ add_task(async function test_export_json
 add_task(async function test_import_json() {
   let libraryWindow = await promiseLibrary();
   libraryWindow.document.querySelector("#maintenanceButtonPopup #restoreFromFile").click();
 
   await promiseImportExport();
   await BrowserTestUtils.promiseAlertDialogOpen("accept");
 
   let restored = 0;
-  let promiseBookmarksRestored = PlacesTestUtils.waitForNotification("onItemAdded", () => {
-    restored++;
-    return restored === actualBookmarks.length;
-  });
+  let promiseBookmarksRestored =
+    PlacesTestUtils.waitForNotification("bookmark-added",
+                                        events => events.some(() => ++restored == actualBookmarks.length),
+                                        "places");
 
   await promiseBookmarksRestored;
   await validateImportedBookmarks(PLACES);
   await promiseLibraryClosed(libraryWindow);
 
   registerCleanupFunction(async () => {
     if (saveDir) {
       saveDir.remove(true);
--- a/browser/components/places/tests/browser/browser_bookmarksProperties.js
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -264,17 +264,17 @@ gTests.push({
   action: ACTION_ADD,
   historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW,
   window: null,
 
   async setup() {
     // Add a visit.
     await PlacesTestUtils.addVisits(TEST_URL);
 
-    this._addObserver = PlacesTestUtils.waitForNotification("onItemAdded");
+    this._addObserver = PlacesTestUtils.waitForNotification("bookmark-added", null, "places");
   },
 
   selectNode(tree) {
     var visitNode = tree.view.nodeForTreeIndex(0);
     tree.selectNode(visitNode);
     Assert.equal(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
     Assert.equal(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
   },
--- a/browser/components/places/tests/browser/browser_editBookmark_keywords.js
+++ b/browser/components/places/tests/browser/browser_editBookmark_keywords.js
@@ -3,17 +3,16 @@
 const TEST_URL = "about:blank";
 
 add_task(async function() {
   function promiseOnItemChanged() {
     return new Promise(resolve => {
       PlacesUtils.bookmarks.addObserver({
         onBeginUpdateBatch() {},
         onEndUpdateBatch() {},
-        onItemAdded() {},
         onItemRemoved() {},
         onItemVisited() {},
         onItemMoved() {},
         onItemChanged(id, property, isAnno, value) {
           PlacesUtils.bookmarks.removeObserver(this);
           resolve({ property, value });
         },
         QueryInterface: ChromeUtils.generateQI([Ci.nsINavBookmarkObserver]),
--- a/browser/components/places/tests/browser/browser_toolbar_drop_text.js
+++ b/browser/components/places/tests/browser/browser_toolbar_drop_text.js
@@ -28,18 +28,20 @@ add_task(async function test() {
    *
    * @param aEffect
    *        The effect to use for the drop operation: move, copy, or link.
    * @param aMimeType
    *        The mime type to use for the drop operation.
    */
   let simulateDragDrop = async function(aEffect, aMimeType) {
     const url = "http://www.mozilla.org/D1995729-A152-4e30-8329-469B01F30AA7";
-    let promiseItemAddedNotification = PlacesTestUtils.waitForNotification(
-      "onItemAdded", (itemId, parentId, index, type, uri, guid) => uri.spec == url);
+    let promiseItemAddedNotification =
+      PlacesTestUtils.waitForNotification("bookmark-added",
+                                          events => events.some(({url: eventUrl}) => eventUrl == url),
+                                          "places");
 
     // We use the toolbar as the drag source, as we just need almost any node
     // to simulate the drag. The actual data for the drop is passed via the
     // drag data. Note: The toolbar is used rather than another bookmark node,
     // as we need something that is immovable from a places perspective, as this
     // forces the move into a copy.
     EventUtils.synthesizeDrop(toolbar,
                               placesItems,
@@ -74,18 +76,20 @@ add_task(async function test() {
       "http://www.mozilla.org/091A88BD-5743-4C16-A005-3D2EA3A3B71E",
     ];
     let data;
     if (aMimeType == "text/x-moz-url")
       data = urls.map(spec => spec + "\n" + spec).join("\n");
     else
       data = urls.join("\n");
 
-    let promiseItemAddedNotification = PlacesTestUtils.waitForNotification(
-      "onItemAdded", (itemId, parentId, index, type, uri, guid) => uri.spec == urls[2]);
+    let promiseItemAddedNotification =
+      PlacesTestUtils.waitForNotification("bookmark-added",
+                                          events => events.some(({url}) => url == urls[2]),
+                                          "places");
 
     // See notes for EventUtils.synthesizeDrop in simulateDragDrop().
     EventUtils.synthesizeDrop(toolbar,
                               placesItems,
                               [[{type: aMimeType,
                                  data}]],
                               aEffect, window);
 
--- a/browser/components/places/tests/browser/browser_views_liveupdate.js
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -92,17 +92,19 @@ add_task(async function test() {
   // Open bookmarks menu.
   var popup = document.getElementById("bookmarksMenuPopup");
   ok(popup, "Menu popup element exists");
   fakeOpenPopup(popup);
 
   // Open bookmarks sidebar.
   await withSidebarTree("bookmarks", async () => {
     // Add observers.
+    bookmarksObserver.handlePlacesEvents = bookmarksObserver.handlePlacesEvents.bind(bookmarksObserver);
     PlacesUtils.bookmarks.addObserver(bookmarksObserver);
+    PlacesUtils.observers.addListener(["bookmark-added"], bookmarksObserver.handlePlacesEvents);
     var addedBookmarks = [];
 
     // MENU
     info("*** Acting on menu bookmarks");
     addedBookmarks = addedBookmarks.concat(await testInFolder(PlacesUtils.bookmarks.menuGuid, "bm"));
 
     // TOOLBAR
     info("*** Acting on toolbar bookmarks");
@@ -119,16 +121,17 @@ add_task(async function test() {
       try {
         await PlacesUtils.bookmarks.remove(bm);
       } catch (ex) {}
       await bookmarksObserver.assertViewsUpdatedCorrectly();
     }
 
     // Remove observers.
     PlacesUtils.bookmarks.removeObserver(bookmarksObserver);
+    PlacesUtils.observers.removeListener(["bookmark-added"], bookmarksObserver.handlePlacesEvents);
   });
 
   // Collapse the personal toolbar if needed.
   if (wasCollapsed) {
     await promiseSetToolbarVisibility(toolbar, false);
   }
 });
 
@@ -138,20 +141,20 @@ add_task(async function test() {
  */
 var bookmarksObserver = {
   _notifications: [],
 
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsINavBookmarkObserver,
   ]),
 
-  // nsINavBookmarkObserver
-  onItemAdded(itemId, folderId, index, itemType, uri, title, dataAdded, guid,
-              parentGuid) {
-    this._notifications.push(["assertItemAdded", parentGuid, guid, index]);
+  handlePlacesEvents(events) {
+    for (let {parentGuid, guid, index} of events) {
+      this._notifications.push(["assertItemAdded", parentGuid, guid, index]);
+    }
   },
 
   onItemRemoved(itemId, folderId, index, itemType, uri, guid, parentGuid) {
     this._notifications.push(["assertItemRemoved", parentGuid, guid]);
   },
 
   onItemMoved(itemId, oldFolderId, oldIndex, newFolderId, newIndex, itemType, guid,
               oldParentGuid, newParentGuid) {
--- a/dom/base/PlacesObservers.cpp
+++ b/dom/base/PlacesObservers.cpp
@@ -332,28 +332,31 @@ PlacesObservers::NotifyListeners(const S
     });
 
   auto& listenersToRemove = *JSListeners::GetListenersToRemove();
   if (listenersToRemove.Length() > 0) {
     for (auto& listener : listenersToRemove) {
       RemoveListener(listener.flags, *listener.value);
     }
   }
+  listenersToRemove.Clear();
 
   auto& weakListenersToRemove = *WeakJSListeners::GetListenersToRemove();
   if (weakListenersToRemove.Length() > 0) {
     for (auto& listener : weakListenersToRemove) {
       RemoveListener(listener.flags, *listener.value.get());
     }
   }
+  weakListenersToRemove.Clear();
 
   auto& nativeListenersToRemove = *WeakNativeListeners::GetListenersToRemove();
   if (nativeListenersToRemove.Length() > 0) {
     for (auto& listener : nativeListenersToRemove) {
       RemoveListener(listener.flags, listener.value.get());
     }
   }
+  nativeListenersToRemove.Clear();
 
   gCallingListeners = false;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -272,17 +272,17 @@ add_task(async function test_tracker_sql
   await resetTracker();
 
   await PlacesUtils.bookmarks.remove(inserted[0]);
   await verifyTrackedCount(numItems + 2);
 
   await cleanup();
 });
 
-add_task(async function test_onItemAdded() {
+add_task(async function test_bookmarkAdded() {
   _("Items inserted via the synchronous bookmarks API should be tracked");
 
   try {
     await startTracking();
 
     _("Insert a folder using the sync API");
     let totalSyncChanges = PlacesUtils.bookmarks.totalSyncChanges;
     let syncFolderID = PlacesUtils.bookmarks.createFolder(
@@ -307,17 +307,17 @@ add_task(async function test_onItemAdded
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
     Assert.equal(PlacesUtils.bookmarks.totalSyncChanges, totalSyncChanges + 2);
   } finally {
     _("Clean up.");
     await cleanup();
   }
 });
 
-add_task(async function test_async_onItemAdded() {
+add_task(async function test_async_bookmarkAdded() {
   _("Items inserted via the asynchronous bookmarks API should be tracked");
 
   try {
     await startTracking();
 
     _("Insert a folder using the async API");
     let totalSyncChanges = PlacesUtils.bookmarks.totalSyncChanges;
     let asyncFolder = await PlacesUtils.bookmarks.insert({
@@ -708,17 +708,17 @@ add_task(async function test_async_onIte
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
     Assert.equal(PlacesUtils.bookmarks.totalSyncChanges, totalSyncChanges + 2);
   } finally {
     _("Clean up.");
     await cleanup();
   }
 });
 
-add_task(async function test_onItemAdded_filtered_root() {
+add_task(async function test_bookmarkAdded_filtered_root() {
   _("Items outside the change roots should not be tracked");
 
   try {
     await startTracking();
 
     _("Create a new root");
     let rootID = PlacesUtils.bookmarks.createFolder(
       PlacesUtils.bookmarks.placesRoot,
--- a/toolkit/components/places/tests/PlacesTestUtils.jsm
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -311,21 +311,21 @@ var PlacesTestUtils = Object.freeze({
       FROM moz_bookmarks_deleted
       ORDER BY guid`);
     return rows.map(row => ({
       guid: row.getResultByName("guid"),
       dateRemoved: PlacesUtils.toDate(row.getResultByName("dateRemoved")),
     }));
   },
 
-  waitForNotification(notification, conditionFn = () => true, type = "bookmarks") {
+  waitForNotification(notification, conditionFn, type = "bookmarks") {
     if (type == "places") {
       return new Promise(resolve => {
         function listener(events) {
-          if (conditionFn(events)) {
+          if (!conditionFn || conditionFn(events)) {
             PlacesObservers.removeListener([notification], listener);
             resolve();
           }
         }
         PlacesObservers.addListener([notification], listener);
       });
     }
 
@@ -333,17 +333,17 @@ var PlacesTestUtils = Object.freeze({
                                     : Ci.nsINavHistoryObserver;
     return new Promise(resolve => {
       let proxifiedObserver = new Proxy({}, {
         get: (target, name) => {
           if (name == "QueryInterface")
             return ChromeUtils.generateQI([iface]);
           if (name == notification)
             return (...args) => {
-              if (conditionFn.apply(this, args)) {
+              if (!conditionFn || conditionFn.apply(this, args)) {
                 PlacesUtils[type].removeObserver(proxifiedObserver);
                 resolve();
               }
             };
           if (name == "skipTags" || name == "skipDescendantsOnItemRemoval") {
             return false;
           }
           return () => false;
--- a/toolkit/components/places/tests/bookmarks/head_bookmarks.js
+++ b/toolkit/components/places/tests/bookmarks/head_bookmarks.js
@@ -49,8 +49,37 @@ function expectNotifications(skipDescend
       if (name in target)
         return target[name];
       return undefined;
     },
   });
   PlacesUtils.bookmarks.addObserver(observer);
   return observer;
 }
+
+function expectPlacesObserverNotifications(types, checkAllArgs) {
+  let notifications = [];
+  let listener = (events) => {
+    for (let event of events) {
+      notifications.push({
+        type: event.type,
+        id: event.id,
+        itemType: event.itemType,
+        parentId: event.parentId,
+        index: event.index,
+        url: event.url || undefined,
+        title: event.title,
+        dateAdded: new Date(event.dateAdded),
+        guid: event.guid,
+        parentGuid: event.parentGuid,
+        source: event.source,
+        isTagging: event.isTagging,
+      });
+    }
+  };
+  PlacesUtils.observers.addListener(types, listener);
+  return {
+    check(expectedNotifications) {
+      PlacesUtils.observers.removeListener(types, listener);
+      Assert.deepEqual(notifications, expectedNotifications);
+    },
+  };
+}
--- a/toolkit/components/places/tests/bookmarks/test_393498.js
+++ b/toolkit/components/places/tests/bookmarks/test_393498.js
@@ -2,32 +2,36 @@
 /* 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/. */
 
 var observer = {
   __proto__: NavBookmarkObserver.prototype,
 
-  onItemAdded(id, folder, index) {
-    this._itemAddedId = id;
-    this._itemAddedParent = folder;
-    this._itemAddedIndex = index;
+  handlePlacesEvents(events) {
+    Assert.equal(events.length, 1, "Should only be 1 event.");
+    this._itemAddedId = events[0].id;
+    this._itemAddedParent = events[0].parentId;
+    this._itemAddedIndex = events[0].index;
   },
   onItemChanged(id, property, isAnnotationProperty, value) {
     this._itemChangedId = id;
     this._itemChangedProperty = property;
     this._itemChanged_isAnnotationProperty = isAnnotationProperty;
     this._itemChangedValue = value;
   },
 };
 PlacesUtils.bookmarks.addObserver(observer);
+observer.handlePlacesEvents = observer.handlePlacesEvents.bind(observer);
+PlacesUtils.observers.addListener(["bookmark-added"], observer.handlePlacesEvents);
 
 registerCleanupFunction(function() {
   PlacesUtils.bookmarks.removeObserver(observer);
+PlacesUtils.observers.removeListener(["bookmark-added"], observer.handlePlacesEvents);
 });
 
 // Returns do_check_eq with .getTime() added onto parameters
 function do_check_date_eq( t1, t2) {
   return Assert.equal(t1.getTime(), t2.getTime()) ;
 }
 
 add_task(async function test_bookmark_update_notifications() {
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js
@@ -205,23 +205,23 @@ add_task(async function tree_where_separ
       url: "http://www.example.com/",
       title: "Test inserting into separator",
     }],
   }], guid: PlacesUtils.bookmarks.unfiledGuid}), /Invalid value for property 'children'/);
 });
 
 add_task(async function create_hierarchy() {
   let obsInvoked = 0;
-  let obs = {
-    onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, guid, parentGuid) {
+  let listener = events => {
+    for (let event of events) {
       obsInvoked++;
-      Assert.greater(itemId, 0, "Should have a valid itemId");
-    },
+      Assert.greater(event.id, 0, "Should have a valid itemId");
+    }
   };
-  PlacesUtils.bookmarks.addObserver(obs);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
   let bms = await PlacesUtils.bookmarks.insertTree({children: [{
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     title: "Root item",
     children: [
       {
         url: "http://www.example.com/1",
         title: "BM 1",
       },
@@ -241,17 +241,17 @@ add_task(async function create_hierarchy
             title: "Sub BM 2",
             url: "http://www.example.com/sub/2",
           },
         ],
       },
     ],
   }], guid: PlacesUtils.bookmarks.unfiledGuid});
   await PlacesTestUtils.promiseAsyncUpdates();
-  PlacesUtils.bookmarks.removeObserver(obs);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   let parentFolder = null, subFolder = null;
   let prevBM = null;
   for (let bm of bms) {
     checkBookmarkObject(bm);
     if (prevBM && prevBM.parentGuid == bm.parentGuid) {
       Assert.equal(prevBM.index + 1, bm.index, "Indices should be subsequent");
       Assert.equal((await PlacesUtils.bookmarks.fetch(bm.guid)).index, bm.index, "Index reflects inserted index");
     }
@@ -272,23 +272,23 @@ add_task(async function create_hierarchy
     }
   }
   Assert.equal(obsInvoked, bms.length);
   Assert.equal(obsInvoked, 6);
 });
 
 add_task(async function insert_many_non_nested() {
   let obsInvoked = 0;
-  let obs = {
-    onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, guid, parentGuid) {
+  let listener = events => {
+    for (let event of events) {
       obsInvoked++;
-      Assert.greater(itemId, 0, "Should have a valid itemId");
-    },
+      Assert.greater(event.id, 0, "Should have a valid itemId");
+    }
   };
-  PlacesUtils.bookmarks.addObserver(obs);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
   let bms = await PlacesUtils.bookmarks.insertTree({children: [{
       url: "http://www.example.com/1",
       title: "Item 1",
     },
     {
       url: "http://www.example.com/2",
       title: "Item 2",
     },
@@ -304,17 +304,17 @@ add_task(async function insert_many_non_
       url: "http://www.example.com/4",
     },
     {
       title: "Item 5",
       url: "http://www.example.com/5",
     },
   ], guid: PlacesUtils.bookmarks.unfiledGuid});
   await PlacesTestUtils.promiseAsyncUpdates();
-  PlacesUtils.bookmarks.removeObserver(obs);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   let startIndex = -1;
   for (let bm of bms) {
     checkBookmarkObject(bm);
     if (startIndex == -1) {
       startIndex = bm.index;
     } else {
       Assert.equal(++startIndex, bm.index, "Indices should be subsequent");
     }
@@ -331,22 +331,29 @@ add_task(async function insert_many_non_
 add_task(async function create_in_folder() {
   let mozFolder = await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     title: "Mozilla",
   });
 
   let notifications = [];
-  let obs = {
-    onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, guid, parentGuid) {
-      notifications.push({ itemId, parentId, index, title, guid, parentGuid });
-    },
+  let listener = events => {
+    for (let event of events) {
+      notifications.push({
+        itemId: event.id,
+        parentId: event.parentId,
+        index: event.index,
+        title: event.title,
+        guid: event.guid,
+        parentGuid: event.parentGuid,
+      });
+    }
   };
-  PlacesUtils.bookmarks.addObserver(obs);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   let bms = await PlacesUtils.bookmarks.insertTree({children: [{
     url: "http://getfirefox.com",
     title: "Get Firefox!",
   }, {
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     title: "Community",
     children: [
@@ -357,17 +364,17 @@ add_task(async function create_in_folder
       {
         url: "https://www.seamonkey-project.org",
         title: "SeaMonkey",
       },
     ],
   }], guid: mozFolder.guid});
   await PlacesTestUtils.promiseAsyncUpdates();
 
-  PlacesUtils.bookmarks.removeObserver(obs);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
 
   let mozFolderId = await PlacesUtils.promiseItemId(mozFolder.guid);
   let commFolderId = await PlacesUtils.promiseItemId(bms[1].guid);
   deepEqual(notifications, [{
     itemId: await PlacesUtils.promiseItemId(bms[0].guid),
     parentId: mozFolderId,
     index: 0,
     title: "Get Firefox!",
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -1,118 +1,169 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ChromeUtils.defineModuleGetter(this, "Preferences",
                                "resource://gre/modules/Preferences.jsm");
 
 add_task(async function insert_separator_notification() {
-  let observer = expectNotifications();
+  let observer = expectPlacesObserverNotifications(["bookmark-added"]);
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid});
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
-  observer.check([ { name: "onItemAdded",
-                     arguments: [ itemId, parentId, bm.index, bm.type,
-                                  null, "", PlacesUtils.toPRTime(bm.dateAdded),
-                                  bm.guid, bm.parentGuid,
-                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
-                 ]);
+  observer.check([{
+    type: "bookmark-added",
+    id: itemId,
+    itemType: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+    parentId,
+    index: bm.index,
+    url: bm.url,
+    title: bm.title,
+    dateAdded: bm.dateAdded,
+    guid: bm.guid,
+    parentGuid: bm.parentGuid,
+    source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+    isTagging: false,
+  }]);
 });
 
 add_task(async function insert_folder_notification() {
-  let observer = expectNotifications();
+  let observer = expectPlacesObserverNotifications(["bookmark-added"]);
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 title: "a folder" });
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
-  observer.check([ { name: "onItemAdded",
-                     arguments: [ itemId, parentId, bm.index, bm.type,
-                                  null, bm.title, PlacesUtils.toPRTime(bm.dateAdded),
-                                  bm.guid, bm.parentGuid,
-                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
-                 ]);
+  observer.check([{
+    type: "bookmark-added",
+    id: itemId,
+    itemType: PlacesUtils.bookmarks.TYPE_FOLDER,
+    parentId,
+    index: bm.index,
+    url: bm.url,
+    title: bm.title,
+    dateAdded: bm.dateAdded,
+    guid: bm.guid,
+    parentGuid: bm.parentGuid,
+    source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+    isTagging: false,
+  }]);
 });
 
 add_task(async function insert_folder_notitle_notification() {
-  let observer = expectNotifications();
+  let observer = expectPlacesObserverNotifications(["bookmark-added"]);
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   strictEqual(bm.title, "", "Should return empty string for untitled folder");
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
-  observer.check([ { name: "onItemAdded",
-                     arguments: [ itemId, parentId, bm.index, bm.type,
-                                  null, "", PlacesUtils.toPRTime(bm.dateAdded),
-                                  bm.guid, bm.parentGuid,
-                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
-                 ]);
+  observer.check([{
+    type: "bookmark-added",
+    id: itemId,
+    itemType: PlacesUtils.bookmarks.TYPE_FOLDER,
+    parentId,
+    index: bm.index,
+    url: bm.url,
+    title: bm.title,
+    dateAdded: bm.dateAdded,
+    guid: bm.guid,
+    parentGuid: bm.parentGuid,
+    source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+    isTagging: false,
+  }]);
 });
 
 add_task(async function insert_bookmark_notification() {
-  let observer = expectNotifications();
+  let observer = expectPlacesObserverNotifications(["bookmark-added"]);
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://example.com/"),
                                                 title: "a bookmark" });
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
-  observer.check([ { name: "onItemAdded",
-                     arguments: [ itemId, parentId, bm.index, bm.type,
-                                  bm.url, bm.title, PlacesUtils.toPRTime(bm.dateAdded),
-                                  bm.guid, bm.parentGuid,
-                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
-                 ]);
+  observer.check([{
+    type: "bookmark-added",
+    id: itemId,
+    itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+    parentId,
+    index: bm.index,
+    url: bm.url,
+    title: bm.title,
+    dateAdded: bm.dateAdded,
+    guid: bm.guid,
+    parentGuid: bm.parentGuid,
+    source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+    isTagging: false,
+  }]);
 });
 
 add_task(async function insert_bookmark_notitle_notification() {
-  let observer = expectNotifications();
+  let observer = expectPlacesObserverNotifications(["bookmark-added"]);
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://example.com/") });
   strictEqual(bm.title, "", "Should return empty string for untitled bookmark");
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
-  observer.check([ { name: "onItemAdded",
-                     arguments: [ itemId, parentId, bm.index, bm.type,
-                                  bm.url, "", PlacesUtils.toPRTime(bm.dateAdded),
-                                  bm.guid, bm.parentGuid,
-                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
-                 ]);
+  observer.check([{
+    type: "bookmark-added",
+    id: itemId,
+    itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+    parentId,
+    index: bm.index,
+    url: bm.url,
+    title: bm.title,
+    dateAdded: bm.dateAdded,
+    guid: bm.guid,
+    parentGuid: bm.parentGuid,
+    source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+    isTagging: false,
+  }]);
 });
 
 add_task(async function insert_bookmark_tag_notification() {
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://tag.example.com/") });
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
 
   let tagFolder = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                        parentGuid: PlacesUtils.bookmarks.tagsGuid,
                                                        title: "tag" });
-  let observer = expectNotifications();
+  let placesObserver = expectPlacesObserverNotifications(["bookmark-added"]);
+  let bookmarksObserver = expectNotifications();
   let tag = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  parentGuid: tagFolder.guid,
                                                  url: new URL("http://tag.example.com/") });
   let tagId = await PlacesUtils.promiseItemId(tag.guid);
   let tagParentId = await PlacesUtils.promiseItemId(tag.parentGuid);
 
-  observer.check([ { name: "onItemAdded",
-                     arguments: [ tagId, tagParentId, tag.index, tag.type,
-                                  tag.url, "", PlacesUtils.toPRTime(tag.dateAdded),
-                                  tag.guid, tag.parentGuid,
-                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
-                   { name: "onItemChanged",
-                     arguments: [ itemId, "tags", false, "",
-                                  PlacesUtils.toPRTime(bm.lastModified),
-                                  bm.type, parentId, bm.guid, bm.parentGuid, "",
-                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
-                 ]);
+  placesObserver.check([{
+    type: "bookmark-added",
+    id: tagId,
+    parentId: tagParentId,
+    index: tag.index,
+    itemType: tag.type,
+    url: tag.url,
+    title: "",
+    dateAdded: tag.dateAdded,
+    guid: tag.guid,
+    parentGuid: tag.parentGuid,
+    source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+    isTagging: true,
+  }]);
+
+  bookmarksObserver.check([ { name: "onItemChanged",
+                              arguments: [ itemId, "tags", false, "",
+                                            PlacesUtils.toPRTime(bm.lastModified),
+                                            bm.type, parentId, bm.guid, bm.parentGuid, "",
+                                            Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+                          ]);
 });
 
 add_task(async function update_bookmark_lastModified() {
   let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
   Preferences.set("privacy.reduceTimerPrecision", false);
 
   registerCleanupFunction(function() {
     Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
--- a/toolkit/components/places/tests/bookmarks/test_insertTree_fixupOrSkipInvalidEntries.js
+++ b/toolkit/components/places/tests/bookmarks/test_insertTree_fixupOrSkipInvalidEntries.js
@@ -9,29 +9,31 @@ add_task(async function() {
   await Assert.throws(() => insertTree({guid: "invalid", children: [{}]}),
                       /The parent guid is not valid/);
 
   let now = new Date();
   let url = "http://mozilla.com/";
   let obs = {
     count: 0,
     lastIndex: 0,
-    onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, itemGuid, parentGuid) {
-      this.count++;
-      let lastIndex = this.lastIndex;
-      this.lastIndex = index;
-      if (type == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
-        Assert.equal(uri.spec, url, "Found the expected url");
+    handlePlacesEvent(events) {
+      for (let event of events) {
+        obs.count++;
+        let lastIndex = obs.lastIndex;
+        obs.lastIndex = event.index;
+        if (event.itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+          Assert.equal(event.url, url, "Found the expected url");
+        }
+        Assert.ok(event.index == 0 || event.index == lastIndex + 1, "Consecutive indices");
+        Assert.ok(event.dateAdded >= now, "Found a valid dateAdded");
+        Assert.ok(PlacesUtils.isValidGuid(event.guid), "guid is valid");
       }
-      Assert.ok(index == 0 || index == lastIndex + 1, "Consecutive indices");
-      Assert.ok(dateAdded >= PlacesUtils.toPRTime(now), "Found a valid dateAdded");
-      Assert.ok(PlacesUtils.isValidGuid(itemGuid), "guid is valid");
     },
   };
-  PlacesUtils.bookmarks.addObserver(obs);
+  PlacesUtils.observers.addListener(["bookmark-added"], obs.handlePlacesEvent);
 
   let tree = {
     guid,
     children: [
       { // Should be inserted, and the invalid guid should be replaced.
         guid: "test",
         url,
       },
@@ -81,10 +83,12 @@ add_task(async function() {
   };
 
   let bms = await insertTree(tree);
   for (let bm of bms) {
     checkBookmarkObject(bm);
   }
   Assert.equal(bms.length, 5);
   Assert.equal(obs.count, bms.length);
+
+  PlacesUtils.observers.removeListener(["bookmark-added"], obs.handlePlacesEvent);
 });
 
--- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -7,40 +7,61 @@ var gUnfiledFolderId;
 
 var gBookmarksObserver = {
   expected: [],
   setup(expected) {
     this.expected = expected;
     this.deferred = PromiseUtils.defer();
     return this.deferred.promise;
   },
+
+  // Even though this isn't technically testing nsINavBookmarkObserver,
+  // this is the simplest place to keep this. Once all of the notifications
+  // are converted, we can just rename the file.
+  validateEvents(events) {
+    Assert.greaterOrEqual(this.expected.length, events.length);
+    for (let event of events) {
+      let expected = this.expected.shift();
+      Assert.equal(expected.eventType, event.type);
+      let args = expected.args;
+      for (let i = 0; i < args.length; i++) {
+        Assert.ok(args[i].check(event[args[i].name]), event.type + "(args[" + i + "]: " + args[i].name + ")");
+      }
+    }
+
+    if (this.expected.length === 0) {
+      this.deferred.resolve();
+    }
+  },
+
   validate(aMethodName, aArguments) {
     Assert.equal(this.expected[0].name, aMethodName);
 
     let args = this.expected.shift().args;
     Assert.equal(aArguments.length, args.length);
     for (let i = 0; i < aArguments.length; i++) {
       Assert.ok(args[i].check(aArguments[i]), aMethodName + "(args[" + i + "]: " + args[i].name + ")");
     }
 
     if (this.expected.length === 0) {
       this.deferred.resolve();
     }
   },
 
+  handlePlacesEvents(events) {
+    this.validateEvents(events);
+  },
+
   // 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);
@@ -58,33 +79,48 @@ var gBookmarkSkipObserver = {
   skipDescendantsOnItemRemoval: true,
 
   expected: null,
   setup(expected) {
     this.expected = expected;
     this.deferred = PromiseUtils.defer();
     return this.deferred.promise;
   },
+
+  validateEvents(events) {
+    events = events.filter(e => !e.isTagging);
+    Assert.greaterOrEqual(this.expected.length, events.length);
+    for (let event of events) {
+      let expectedEventType = this.expected.shift();
+      Assert.equal(expectedEventType, event.type);
+    }
+
+    if (this.expected.length === 0) {
+      this.deferred.resolve();
+    }
+  },
+
   validate(aMethodName) {
     Assert.equal(this.expected.shift(), aMethodName);
     if (this.expected.length === 0) {
       this.deferred.resolve();
     }
   },
 
+  handlePlacesEvents(events) {
+    this.validateEvents(events);
+  },
+
   // 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);
@@ -96,91 +132,96 @@ var gBookmarkSkipObserver = {
   // nsISupports
   QueryInterface: ChromeUtils.generateQI([Ci.nsINavBookmarkObserver]),
 };
 
 
 add_task(async function setup() {
   PlacesUtils.bookmarks.addObserver(gBookmarksObserver);
   PlacesUtils.bookmarks.addObserver(gBookmarkSkipObserver);
-
   gUnfiledFolderId = await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid);
+  gBookmarksObserver.handlePlacesEvents =
+    gBookmarksObserver.handlePlacesEvents.bind(gBookmarksObserver);
+  gBookmarkSkipObserver.handlePlacesEvents =
+    gBookmarkSkipObserver.handlePlacesEvents.bind(gBookmarkSkipObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], gBookmarksObserver.handlePlacesEvents);
+  PlacesUtils.observers.addListener(["bookmark-added"], gBookmarkSkipObserver.handlePlacesEvents);
 });
 
-add_task(async function onItemAdded_bookmark() {
+add_task(async function bookmarkItemAdded_bookmark() {
   const title = "Bookmark 1";
   let uri = Services.io.newURI("http://1.mozilla.org/");
   let promise = Promise.all([
     gBookmarkSkipObserver.setup([
-      "onItemAdded",
+      "bookmark-added",
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-added",
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => v === gUnfiledFolderId },
           { 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: "url", check: v => v == uri.spec },
           { name: "title", check: v => v === title },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
   ])]);
   await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     url: uri,
     title,
   });
   await promise;
 });
 
-add_task(async function onItemAdded_separator() {
+add_task(async function bookmarkItemAdded_separator() {
   let promise = Promise.all([
     gBookmarkSkipObserver.setup([
-      "onItemAdded",
+      "bookmark-added",
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-added",
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => v === gUnfiledFolderId },
           { name: "index", check: v => v === 1 },
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
-          { name: "uri", check: v => v === null },
+          { name: "url", check: v => v === "" },
           { name: "title", check: v => v === "" },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
   ])]);
   await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
   });
   await promise;
 });
 
-add_task(async function onItemAdded_folder() {
+add_task(async function bookmarkItemAdded_folder() {
   const title = "Folder 1";
   let promise = Promise.all([
     gBookmarkSkipObserver.setup([
-      "onItemAdded",
+      "bookmark-added",
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-added",
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => v === gUnfiledFolderId },
           { name: "index", check: v => v === 2 },
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-          { name: "uri", check: v => v === null },
+          { name: "url", check: v => v === "" },
           { name: "title", check: v => v === title },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
   ])]);
   await PlacesUtils.bookmarks.insert({
@@ -228,36 +269,36 @@ add_task(async function onItemChanged_ta
   });
   let uri = Services.io.newURI(bm.url.href);
   const TAG = "tag";
   let promise = Promise.all([
     gBookmarkSkipObserver.setup([
       "onItemChanged", "onItemChanged",
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded", // This is the tag folder.
+      { eventType: "bookmark-added", // This is the tag folder.
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", 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: "url", check: v => v === "" },
           { name: "title", check: v => v === TAG },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
-      { name: "onItemAdded", // This is the tag.
+      { eventType: "bookmark-added", // This is the tag.
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", 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: "url", check: v => v == uri.spec },
           { name: "title", check: v => v === "" },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
       { name: "onItemChanged",
         args: [
@@ -474,66 +515,66 @@ add_task(async function onItemRemoved_fo
 });
 
 add_task(async function onItemRemoved_folder_recursive() {
   const title = "Folder 3";
   const BMTITLE = "Bookmark 1";
   let uri = Services.io.newURI("http://1.mozilla.org/");
   let promise = Promise.all([
     gBookmarkSkipObserver.setup([
-      "onItemAdded", "onItemAdded", "onItemAdded", "onItemAdded",
+      "bookmark-added", "bookmark-added", "bookmark-added", "bookmark-added",
       "onItemRemoved",
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-added",
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => v === gUnfiledFolderId },
           { name: "index", check: v => v === 0 },
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-          { name: "uri", check: v => v === null },
+          { name: "url", check: v => v === "" },
           { name: "title", check: v => v === title },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
-      { name: "onItemAdded",
+      { eventType: "bookmark-added",
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", 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: "url", check: v => v == uri.spec },
           { name: "title", check: v => v === BMTITLE },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
-      { name: "onItemAdded",
+      { eventType: "bookmark-added",
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
           { name: "index", check: v => v === 1 },
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-          { name: "uri", check: v => v === null },
+          { name: "url", check: v => v === "" },
           { name: "title", check: v => v === title },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
-      { name: "onItemAdded",
+      { eventType: "bookmark-added",
         args: [
-          { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "id", 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: "url", check: v => v == uri.spec },
           { name: "title", check: v => v === BMTITLE },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
       { name: "onItemRemoved",
         args: [
@@ -603,9 +644,11 @@ add_task(async function onItemRemoved_fo
 
   await PlacesUtils.bookmarks.remove(folder);
   await promise;
 });
 
 add_task(function cleanup() {
   PlacesUtils.bookmarks.removeObserver(gBookmarksObserver);
   PlacesUtils.bookmarks.removeObserver(gBookmarkSkipObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], gBookmarksObserver.handlePlacesEvents);
+  PlacesUtils.observers.removeListener(["bookmark-added"], gBookmarkSkipObserver.handlePlacesEvents);
 });
--- a/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js
+++ b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js
@@ -23,29 +23,36 @@ add_task(async function test_removeFolde
   });
 
   let notifications = [];
   function checkNotifications(expected, message) {
     deepEqual(notifications, expected, message);
     notifications.length = 0;
   }
 
+  let listener = events => {
+    for (let event of events) {
+      notifications.push(["bookmark-added",
+                         event.id,
+                         event.parentId,
+                         event.guid,
+                         event.parentGuid]);
+    }
+  };
   let observer = {
     __proto__: NavBookmarkObserver.prototype,
-    onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, guid,
-                parentGuid) {
-      notifications.push(["onItemAdded", itemId, parentId, guid, parentGuid]);
-    },
     onItemRemoved(itemId, parentId, index, type, uri, guid, parentGuid) {
       notifications.push(["onItemRemoved", itemId, parentId, guid, parentGuid]);
     },
   };
   PlacesUtils.bookmarks.addObserver(observer);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
   PlacesUtils.registerShutdownFunction(function() {
     PlacesUtils.bookmarks.removeObserver(observer);
+    PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   });
 
   let transaction = PlacesTransactions.Remove({guid: folder.guid});
 
   let folderId = await PlacesUtils.promiseItemId(folder.guid);
   let fxId = await PlacesUtils.promiseItemId(fx.guid);
   let tbId = await PlacesUtils.promiseItemId(tb.guid);
 
@@ -60,20 +67,20 @@ add_task(async function test_removeFolde
 
   await PlacesTransactions.undo();
 
   folderId = await PlacesUtils.promiseItemId(folder.guid);
   fxId = await PlacesUtils.promiseItemId(fx.guid);
   tbId = await PlacesUtils.promiseItemId(tb.guid);
 
   checkNotifications([
-    ["onItemAdded", folderId, PlacesUtils.bookmarksMenuFolderId, folder.guid,
+    ["bookmark-added", folderId, PlacesUtils.bookmarksMenuFolderId, folder.guid,
       PlacesUtils.bookmarks.menuGuid],
-    ["onItemAdded", fxId, folderId, fx.guid, folder.guid],
-    ["onItemAdded", tbId, folderId, tb.guid, folder.guid],
+    ["bookmark-added", fxId, folderId, fx.guid, folder.guid],
+    ["bookmark-added", tbId, folderId, tb.guid, folder.guid],
   ], "Undo should reinsert folder with different id but same GUID");
 
   await PlacesTransactions.redo();
 
   checkNotifications([
     ["onItemRemoved", tbId, folderId, tb.guid, folder.guid],
     ["onItemRemoved", fxId, folderId, fx.guid, folder.guid],
     ["onItemRemoved", folderId, PlacesUtils.bookmarksMenuFolderId, folder.guid,
--- a/toolkit/components/places/tests/chrome/test_371798.xul
+++ b/toolkit/components/places/tests/chrome/test_371798.xul
@@ -22,17 +22,16 @@ ChromeUtils.import("resource://gre/modul
 
 const TEST_URI = NetUtil.newURI("http://foo.com");
 
 function promiseOnItemChanged() {
   return new Promise(resolve => {
     PlacesUtils.bookmarks.addObserver({
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
-      onItemAdded() {},
       onItemRemoved() {},
       onItemVisited() {},
       onItemMoved() {},
 
       onItemChanged() {
         PlacesUtils.bookmarks.removeObserver(this);
         resolve();
       },
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -690,17 +690,16 @@ function do_compare_arrays(a1, a2, sorte
  * Generic nsINavBookmarkObserver that doesn't implement anything, but provides
  * dummy methods to prevent errors about an object not having a certain method.
  */
 function NavBookmarkObserver() {}
 
 NavBookmarkObserver.prototype = {
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
-  onItemAdded() {},
   onItemRemoved() {},
   onItemChanged() {},
   onItemVisited() {},
   onItemMoved() {},
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsINavBookmarkObserver,
   ]),
 };
--- a/toolkit/components/places/tests/legacy/test_bookmarks.js
+++ b/toolkit/components/places/tests/legacy/test_bookmarks.js
@@ -1,47 +1,50 @@
 /* -*- 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/. */
 
 var bs = PlacesUtils.bookmarks;
 var hs = PlacesUtils.history;
+var os = PlacesUtils.observers;
 var anno = PlacesUtils.annotations;
 
 
 var bookmarksObserver = {
-  onBeginUpdateBatch() {
-    this._beginUpdateBatch = true;
-  },
-  onEndUpdateBatch() {
-    this._endUpdateBatch = true;
-  },
-  onItemAdded(id, folder, index, itemType, uri, title, dateAdded,
-                        guid) {
-    this._itemAddedId = id;
-    this._itemAddedParent = folder;
-    this._itemAddedIndex = index;
-    this._itemAddedURI = uri;
-    this._itemAddedTitle = title;
+  handlePlacesEvents(events) {
+    Assert.equal(events.length, 1);
+    let event = events[0];
+    bookmarksObserver._itemAddedId = event.id;
+    bookmarksObserver._itemAddedParent = event.parentId;
+    bookmarksObserver._itemAddedIndex = event.index;
+    bookmarksObserver._itemAddedURI = event.url ? Services.io.newURI(event.url) : null;
+    bookmarksObserver._itemAddedTitle = event.title;
 
     // Ensure that we've created a guid for this item.
     let stmt = DBConn().createStatement(
       `SELECT guid
        FROM moz_bookmarks
        WHERE id = :item_id`
     );
-    stmt.params.item_id = id;
+    stmt.params.item_id = event.id;
     Assert.ok(stmt.executeStep());
     Assert.ok(!stmt.getIsNull(0));
     do_check_valid_places_guid(stmt.row.guid);
-    Assert.equal(stmt.row.guid, guid);
+    Assert.equal(stmt.row.guid, event.guid);
     stmt.finalize();
   },
+
+  onBeginUpdateBatch() {
+    this._beginUpdateBatch = true;
+  },
+  onEndUpdateBatch() {
+    this._endUpdateBatch = true;
+  },
   onItemRemoved(id, folder, index, itemType) {
     this._itemRemovedId = id;
     this._itemRemovedFolder = folder;
     this._itemRemovedIndex = index;
   },
   onItemChanged(id, property, isAnnotationProperty, value,
                           lastModified, itemType, parentId, guid, parentGuid,
                           oldValue) {
@@ -72,16 +75,17 @@ var bookmarksObserver = {
 
 // Get bookmarks menu folder id.
 var root = bs.bookmarksMenuFolder;
 // Index at which items should begin.
 var bmStartIndex = 0;
 
 add_task(async function test_bookmarks() {
   bs.addObserver(bookmarksObserver);
+  os.addListener(["bookmark-added"], bookmarksObserver.handlePlacesEvents);
 
   // test special folders
   Assert.ok(bs.placesRoot > 0);
   Assert.ok(bs.bookmarksMenuFolder > 0);
   Assert.ok(bs.tagsFolder > 0);
   Assert.ok(bs.toolbarFolder > 0);
 
   // test getFolderIdForItem() with bogus item id will throw
--- a/toolkit/components/places/tests/sync/head_sync.js
+++ b/toolkit/components/places/tests/sync/head_sync.js
@@ -212,31 +212,44 @@ async function openMirror(name, options 
   });
   return buf;
 }
 
 function BookmarkObserver({ ignoreDates = true, skipTags = false } = {}) {
   this.notifications = [];
   this.ignoreDates = ignoreDates;
   this.skipTags = skipTags;
+  this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
 }
 
 BookmarkObserver.prototype = {
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      if (this.skipTags && event.isTagging) {
+        continue;
+      }
+      let params = {
+        itemId: event.id,
+        parentId: event.parentId,
+        index: event.index,
+        type: event.itemType,
+        urlHref: event.url,
+        title: event.title,
+        guid: event.guid,
+        parentGuid: event.parentGuid,
+        source: event.source,
+      };
+      if (!this.ignoreDates) {
+        params.dateAdded = event.dateAdded * 1000;
+      }
+      this.notifications.push({ name: "bookmark-added", params });
+    }
+  },
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
-  onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, guid,
-              parentGuid, source) {
-    let urlHref = uri ? uri.spec : null;
-    let params = { itemId, parentId, index, type, urlHref, title, guid,
-                   parentGuid, source };
-    if (!this.ignoreDates) {
-      params.dateAdded = dateAdded;
-    }
-    this.notifications.push({ name: "onItemAdded", params });
-  },
   onItemRemoved(itemId, parentId, index, type, uri, guid, parentGuid, source) {
     let urlHref = uri ? uri.spec : null;
     this.notifications.push({
       name: "onItemRemoved",
       params: { itemId, parentId, index, type, urlHref, guid, parentGuid,
                  source },
     });
   },
@@ -260,30 +273,32 @@ BookmarkObserver.prototype = {
   },
 
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsINavBookmarkObserver,
   ]),
 
   check(expectedNotifications) {
     PlacesUtils.bookmarks.removeObserver(this);
+    PlacesUtils.observers.removeListener(["bookmark-added"], this.handlePlacesEvents);
     if (!ObjectUtils.deepEqual(this.notifications, expectedNotifications)) {
       info(`Expected notifications: ${JSON.stringify(expectedNotifications)}`);
       info(`Actual notifications: ${JSON.stringify(this.notifications)}`);
       throw new Assert.constructor.AssertionError({
         actual: this.notifications,
         expected: expectedNotifications,
       });
     }
   },
 };
 
 function expectBookmarkChangeNotifications(options) {
   let observer = new BookmarkObserver(options);
   PlacesUtils.bookmarks.addObserver(observer);
+  PlacesUtils.observers.addListener(["bookmark-added"], observer.handlePlacesEvents);
   return observer;
 }
 
 // Copies a support file to a temporary fixture file, allowing the support
 // file to be reused for multiple tests.
 async function setupFixtureFile(fixturePath) {
   let fixtureFile = do_get_file(fixturePath);
   let tempFile = FileTestUtils.getTempFile(fixturePath);
--- a/toolkit/components/places/tests/sync/test_bookmark_kinds.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_kinds.js
@@ -253,28 +253,28 @@ add_task(async function test_livemarks()
     observer.check([{
       name: "onItemChanged",
       params: { itemId: livemarkB.id, property: "guid", isAnnoProperty: false,
                 newValue: "livemarkB111", parentId: PlacesUtils.toolbarFolderId,
                 type: PlacesUtils.bookmarks.TYPE_FOLDER, guid: "livemarkB111",
                 parentGuid: "toolbar_____", oldValue: "livemarkBBBB",
                 source: PlacesUtils.bookmarks.SOURCES.SYNC },
     }, {
-      name: "onItemAdded",
+      name: "bookmark-added",
       params: { itemId: livemarkC.id, parentId: PlacesUtils.toolbarFolderId,
                 index: 0, type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                urlHref: null, title: "C (remote)", guid: "livemarkCCCC",
+                urlHref: "", title: "C (remote)", guid: "livemarkCCCC",
                 parentGuid: PlacesUtils.bookmarks.toolbarGuid,
                 source: PlacesUtils.bookmarks.SOURCES.SYNC },
     }, {
-      name: "onItemAdded",
+      name: "bookmark-added",
       params: { itemId: livemarkE.id,
                 parentId: unfiledFolderId,
                 index: 0, type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                urlHref: null, title: "E", guid: "livemarkEEEE",
+                urlHref: "", title: "E", guid: "livemarkEEEE",
                 parentGuid: "unfiled_____",
                 source: PlacesUtils.bookmarks.SOURCES.SYNC },
     }, {
       name: "onItemMoved",
       params: { itemId: livemarkB.id, oldParentId: PlacesUtils.toolbarFolderId,
                 oldIndex: 0, newParentId: PlacesUtils.toolbarFolderId,
                 newIndex: 1, type: PlacesUtils.bookmarks.TYPE_FOLDER,
                 guid: "livemarkB111", uri: null,
--- a/toolkit/components/places/tests/sync/test_bookmark_observer_recorder.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_observer_recorder.js
@@ -315,17 +315,17 @@ add_task(async function test_apply_then_
     params: { itemId: localItemIds.get("bookmarkEEEE"), property: "guid",
               isAnnoProperty: false, newValue: "bookmarkEEEE",
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               parentId: PlacesUtils.bookmarksMenuFolderId, guid: "bookmarkEEEE",
               parentGuid: PlacesUtils.bookmarks.menuGuid,
               oldValue: "bookmarkEEE1",
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("bookmarkFFFF"),
               parentId: PlacesUtils.bookmarksMenuFolderId, index: 1,
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               urlHref: "http://example.com/f", title: "F",
               guid: "bookmarkFFFF",
               parentGuid: PlacesUtils.bookmarks.menuGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
--- a/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
@@ -501,21 +501,21 @@ add_task(async function test_move_into_p
   deepEqual(idsToUpload, {
     updated: [],
     deleted: [],
   }, "Should not upload records for remote-only structure changes");
 
   let localItemIds = await PlacesUtils.promiseManyItemIds(["folderCCCCCC",
     "bookmarkBBBB", "folderAAAAAA"]);
   observer.check([{
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("folderCCCCCC"),
               parentId: PlacesUtils.bookmarksMenuFolderId, index: 1,
               type: PlacesUtils.bookmarks.TYPE_FOLDER,
-              urlHref: null, title: "C",
+              urlHref: "", title: "C",
               guid: "folderCCCCCC",
               parentGuid: PlacesUtils.bookmarks.menuGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
     name: "onItemMoved",
     params: { itemId: localItemIds.get("bookmarkBBBB"),
               oldParentId: localItemIds.get("folderAAAAAA"),
               oldIndex: 0, newParentId: localItemIds.get("folderCCCCCC"),
@@ -656,17 +656,17 @@ add_task(async function test_complex_mov
   deepEqual(idsToUpload, {
     updated: ["bookmarkDDDD", "folderAAAAAA"],
     deleted: [],
   }, "Should upload new records for (A D)");
 
   let localItemIds = await PlacesUtils.promiseManyItemIds(["bookmarkEEEE",
     "folderAAAAAA", "bookmarkCCCC"]);
   observer.check([{
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("bookmarkEEEE"),
               parentId: localItemIds.get("folderAAAAAA"), index: 1,
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               urlHref: "http://example.com/e", title: "E",
               guid: "bookmarkEEEE",
               parentGuid: "folderAAAAAA",
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
--- a/toolkit/components/places/tests/sync/test_bookmark_value_changes.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_value_changes.js
@@ -114,34 +114,34 @@ add_task(async function test_value_combo
         children: ["fxBmk_______", "tFolder_____", "bzBmk_______"],
       },
     },
   }, "Should upload new local bookmarks and parents");
 
   let localItemIds = await PlacesUtils.promiseManyItemIds(["fxBmk_______",
     "tFolder_____", "tbBmk_______", "bzBmk_______", "mozBmk______"]);
   observer.check([{
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("fxBmk_______"),
               parentId: PlacesUtils.toolbarFolderId, index: 0,
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               urlHref: "http://getfirefox.com/", title: "Get Firefox",
               guid: "fxBmk_______",
               parentGuid: PlacesUtils.bookmarks.toolbarGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("tFolder_____"),
               parentId: PlacesUtils.toolbarFolderId,
               index: 1, type: PlacesUtils.bookmarks.TYPE_FOLDER,
-              urlHref: null, title: "Mail", guid: "tFolder_____",
+              urlHref: "", title: "Mail", guid: "tFolder_____",
               parentGuid: PlacesUtils.bookmarks.toolbarGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("tbBmk_______"),
               parentId: localItemIds.get("tFolder_____"), index: 0,
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               urlHref: "http://getthunderbird.com/", title: "Get Thunderbird",
               guid: "tbBmk_______", parentGuid: "tFolder_____",
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
     name: "onItemMoved",
@@ -671,35 +671,35 @@ add_task(async function test_keywords_co
   expectedIdsToUpload.updated.sort();
   deepEqual(idsToUpload, expectedIdsToUpload,
     "Should reupload all local records with corrected keywords");
 
   let localItemIds = await PlacesUtils.promiseManyItemIds(["bookmarkAAAA",
     "bookmarkAAA1", "bookmarkBBB1", "bookmarkBBBB", "bookmarkCCCC",
     "bookmarkDDDD", "bookmarkEEEE"]);
   let expectedNotifications = [{
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("bookmarkAAAA"),
               parentId: PlacesUtils.bookmarksMenuFolderId, index: 0,
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               urlHref: "http://example.com/a", title: "A",
               guid: "bookmarkAAAA",
               parentGuid: PlacesUtils.bookmarks.menuGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("bookmarkAAA1"),
               parentId: PlacesUtils.bookmarksMenuFolderId, index: 1,
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               urlHref: "http://example.com/a", title: "A (copy)",
               guid: "bookmarkAAA1",
               parentGuid: PlacesUtils.bookmarks.menuGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-added",
     params: { itemId: localItemIds.get("bookmarkBBB1"),
               parentId: unfiledFolderId, index: 0,
               type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
               urlHref: "http://example.com/b", title: "B",
               guid: "bookmarkBBB1",
               parentGuid: PlacesUtils.bookmarks.unfiledGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -1,15 +1,16 @@
 /* -*- 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 obsvc    = PlacesUtils.observers;
 const tagssvc  = PlacesUtils.tagging;
 const annosvc  = PlacesUtils.annotations;
 const PT       = PlacesTransactions;
 const menuGuid = PlacesUtils.bookmarks.menuGuid;
 
 Cu.importGlobalProperties(["URL"]);
 ChromeUtils.defineModuleGetter(this, "Preferences",
                                "resource://gre/modules/Preferences.jsm");
@@ -25,42 +26,41 @@ var observer = {
     this.itemsAdded = new Map();
     this.itemsRemoved = new Map();
     this.itemsChanged = new Map();
     this.itemsMoved = new Map();
     this.beginUpdateBatch = false;
     this.endUpdateBatch = false;
   },
 
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      // Ignore tag items.
+      if (event.isTagging) {
+        this.tagRelatedGuids.add(event.guid);
+        return;
+      }
+
+      this.itemsAdded.set(event.guid, { itemId:         event.id,
+                                        parentGuid:     event.parentGuid,
+                                        index:          event.index,
+                                        itemType:       event.itemType,
+                                        title:          event.title,
+                                        url:            event.url });
+    }
+  },
+
   onBeginUpdateBatch() {
     this.beginUpdateBatch = true;
   },
 
   onEndUpdateBatch() {
     this.endUpdateBatch = true;
   },
 
-  onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
-           aGuid, aParentGuid) {
-    // Ignore tag items.
-    if (aParentId == PlacesUtils.tagsFolderId ||
-        (aParentId != PlacesUtils.placesRootId &&
-         bmsvc.getFolderIdForItem(aParentId) == PlacesUtils.tagsFolderId)) {
-      this.tagRelatedGuids.add(aGuid);
-      return;
-    }
-
-    this.itemsAdded.set(aGuid, { itemId:         aItemId,
-                                 parentGuid:     aParentGuid,
-                                 index:          aIndex,
-                                 itemType:       aItemType,
-                                 title:          aTitle,
-                                 url:            aURI });
-  },
-
   onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGuid, aParentGuid) {
     if (this.tagRelatedGuids.has(aGuid))
       return;
 
     this.itemsRemoved.set(aGuid, { parentGuid: aParentGuid,
                                    index:      aIndex,
                                    itemType:   aItemType });
   },
@@ -103,18 +103,21 @@ var observer = {
 };
 observer.reset();
 
 // index at which items should begin
 var bmStartIndex = 0;
 
 function run_test() {
   bmsvc.addObserver(observer);
+  observer.handlePlacesEvents = observer.handlePlacesEvents.bind(observer);
+  obsvc.addListener(["bookmark-added"], observer.handlePlacesEvents);
   registerCleanupFunction(function() {
     bmsvc.removeObserver(observer);
+    obsvc.removeListener(["bookmark-added"], observer.handlePlacesEvents);
   });
 
   run_next_test();
 }
 
 function sanityCheckTransactionHistory() {
   Assert.ok(PT.undoPosition <= PT.length);
 
@@ -175,17 +178,17 @@ function ensureItemsAdded(...items) {
     let info = observer.itemsAdded.get(item.guid);
     Assert.equal(info.parentGuid, item.parentGuid,
       "Should have notified the correct parentGuid");
     for (let propName of ["title", "index", "itemType"]) {
       if (propName in item)
         Assert.equal(info[propName], item[propName]);
     }
     if ("url" in item)
-      Assert.ok(info.url.equals(Services.io.newURI(item.url)),
+      Assert.ok(Services.io.newURI(info.url).equals(Services.io.newURI(item.url)),
         "Should have the correct url");
   }
 
   Assert.equal(observer.itemsAdded.size, expectedResultsCount,
     "Should have added the correct number of items");
 }
 
 function ensureItemsRemoved(...items) {
@@ -1605,17 +1608,17 @@ add_task(async function test_livemark_tx
   let livemark_info =
     { feedUrl: "http://test.feed.uri/",
       parentGuid: PlacesUtils.bookmarks.unfiledGuid,
       title: "Test Livemark" };
   function ensureLivemarkAdded() {
     ensureItemsAdded({ guid:       livemark_info.guid,
                        title:      livemark_info.title,
                        parentGuid: livemark_info.parentGuid,
-                       itemType:   bmsvc.TYPE_FOLDER });
+                       itemType:   PlacesUtils.bookmarks.TYPE_FOLDER });
     let annos = [{ name:  PlacesUtils.LMANNO_FEEDURI,
                    value: livemark_info.feedUrl }];
     if ("siteUrl" in livemark_info) {
       annos.push({ name: PlacesUtils.LMANNO_SITEURI,
                    value: livemark_info.siteUrl });
     }
     ensureAnnotationsSet(livemark_info.guid, annos);
   }
--- a/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
+++ b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
@@ -154,28 +154,27 @@ add_task(async function test_addLivemark
     do_throw("Invoking addLivemark with a bad guid should throw");
   } catch (ex) {
     Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
   }
 });
 
 add_task(async function test_addLivemark_parentId_succeeds() {
   let onItemAddedCalled = false;
-  PlacesUtils.bookmarks.addObserver({
-    __proto__: NavBookmarkObserver.prototype,
-    onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType,
-                                      aURI, aTitle) {
-      onItemAddedCalled = true;
-      PlacesUtils.bookmarks.removeObserver(this);
-      Assert.equal(aParentId, unfiledFolderId);
-      Assert.equal(aIndex, 0);
-      Assert.equal(aItemType, Ci.nsINavBookmarksService.TYPE_FOLDER);
-      Assert.equal(aTitle, "test");
-    },
-  });
+  let listener = events => {
+    Assert.equal(events.length, 1);
+    let event = events[0];
+    onItemAddedCalled = true;
+    PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+    Assert.equal(event.parentId, unfiledFolderId);
+    Assert.equal(event.index, 0);
+    Assert.equal(event.itemType, PlacesUtils.bookmarks.TYPE_FOLDER);
+    Assert.equal(event.title, "test");
+  };
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   await PlacesUtils.livemarks.addLivemark(
     { title: "test",
       parentId: unfiledFolderId,
       feedURI: FEED_URI });
   Assert.ok(onItemAddedCalled);
 });
 
--- a/toolkit/components/places/tests/unit/test_onItemChanged_tags.js
+++ b/toolkit/components/places/tests/unit/test_onItemChanged_tags.js
@@ -34,17 +34,16 @@ add_task(async function run_test() {
     onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGuid) {
       if (aGuid == bookmark.guid) {
         PlacesUtils.bookmarks.removeObserver(this);
         Assert.equal(this._changedCount, 2);
         promise.resolve();
       }
     },
 
-    onItemAdded() {},
     onBeginUpdateBatch() {},
     onEndUpdateBatch() {},
     onItemVisited() {},
     onItemMoved() {},
   };
   PlacesUtils.bookmarks.addObserver(bookmarksObserver);
 
   PlacesUtils.tagging.tagURI(uri, ["d"]);