Bug 1426245 - Test changes r=mak
authorDoug Thayer <dothayer@mozilla.com>
Tue, 09 Oct 2018 14:47:31 +0000
changeset 498726 85a806b69f15dcf8d4ebf1a1a847be5641323013
parent 498725 50ca67245a715b0e37014f2066102ad9feec9f1f
child 498727 1961aeb46e981b607fa2330aa06ab056aeb7e859
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1426245
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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"]);