Bug 1426245 - Test changes draft
authorDoug Thayer <dothayer@mozilla.com>
Fri, 06 Jul 2018 11:12:29 -0700
changeset 819554 4f39261a050dcdc9c2541722e6526007d9594cc2
parent 819553 77a6b95bdb8d7f2976966e79c01986b62b16de10
push id116579
push userbmo:dothayer@mozilla.com
push dateWed, 18 Jul 2018 04:18:30 +0000
bugs1426245
milestone63.0a1
Bug 1426245 - Test changes MozReview-Commit-ID: 4fhhzspxLJZ
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/places/tests/browser/browser_editBookmark_keywords.js
browser/components/places/tests/browser/browser_views_liveupdate.js
toolkit/components/places/tests/bookmarks/test_393498.js
toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.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_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/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -473,38 +473,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-item-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-item-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-item-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-item-added"], listener);
           resolve();
         }
       },
 
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
       onItemChanged() {},
       onItemVisited() {},
       onItemMoved() {},
     };
     PlacesUtils.bookmarks.addObserver(bmObserver);
+    PlacesUtils.observers.addListener(["bookmark-item-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 == "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.parentItemId);
         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-item-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, {
     id: "default",
   });
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-item-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 == "folder" ? "folders" : "bookmarks"]++;
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
 
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-item-added"], listener);
   const PROFILE = {
     id: "Default",
     name: "Default",
   };
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, PROFILE);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-item-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,38 +418,43 @@ 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 {
+        itemId,
+        itemType,
+        url,
+        title,
+        dateAdded,
+        itemGuid,
+        index,
+        parentItemGuid: parentGuid,
+        parentItemId: 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() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bookmarkObserver);
+  PlacesUtils.observers.addListener(["bookmark-item-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-item-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");
@@ -465,17 +470,17 @@ add_task(async function() {
 
   let edgeNameStr = MigrationUtils.getLocalizedString("sourceNameEdge");
   let importParentFolderName = MigrationUtils.getLocalizedString("importedBookmarksFolder", [edgeNameStr]);
 
   for (let bookmark of seenBookmarks) {
     let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title);
     let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title);
     if (bookmark.title == "Folder" || bookmark.title == importParentFolderName) {
-      Assert.equal(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER,
+      Assert.equal(bookmark.itemType, "folder",
           "Bookmark " + bookmark.title + " should be a folder");
     } else {
       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");
@@ -490,54 +495,59 @@ add_task(async function() {
       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");
     } 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) {
+  listener = events => {
+    for (let event of events) {
+      let {
+        itemId,
+        itemType,
+        url,
+        title,
+        dateAdded,
+        itemGuid,
+        index,
+        parentItemGuid: parentGuid,
+        parentItemId: parentId,
+      } = event;
       seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid});
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bookmarkObserver);
+  PlacesUtils.observers.addListener(["bookmark-item-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-item-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.parentItemId);
         Assert.notEqual(index, -1);
         expectedParents.splice(index, 1);
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-item-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-item-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 == "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.parentItemId);
         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-item-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-item-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/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_views_liveupdate.js
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -83,16 +83,17 @@ add_task(async function test() {
   ok(popup, "Menu popup element exists");
   fakeOpenPopup(popup);
 
   // Open bookmarks sidebar.
   await withSidebarTree("bookmarks", async () => {
     // Add observers.
     PlacesUtils.bookmarks.addObserver(bookmarksObserver);
     PlacesUtils.annotations.addObserver(bookmarksObserver);
+    PlacesUtils.observers.addListener(["bookmark-item-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");
@@ -109,16 +110,17 @@ add_task(async function test() {
       try {
         await PlacesUtils.bookmarks.remove(bm);
       } catch (ex) {}
     }
 
     // Remove observers.
     PlacesUtils.bookmarks.removeObserver(bookmarksObserver);
     PlacesUtils.annotations.removeObserver(bookmarksObserver);
+    PlacesUtils.observers.removeListener(["bookmark-item-added"], bookmarksObserver.handlePlacesEvents);
   });
 
   // Collapse the personal toolbar if needed.
   if (wasCollapsed) {
     await promiseSetToolbarVisibility(toolbar, false);
   }
 });
 
@@ -133,30 +135,31 @@ var bookmarksObserver = {
   ]),
 
   // nsIAnnotationObserver
   onItemAnnotationSet() {},
   onItemAnnotationRemoved() {},
   onPageAnnotationSet() {},
   onPageAnnotationRemoved() {},
 
-  // nsINavBookmarkObserver
-  onItemAdded: function PSB_onItemAdded(aItemId, aFolderId, aIndex,
-                                        aItemType, aURI) {
-    var views = getViewsForFolder(aFolderId);
-    ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      var views = getViewsForFolder(event.parentItemId);
+      ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
 
-    // Check that item has been added in the correct position.
-    for (var i = 0; i < views.length; i++) {
-      var [node, index] = searchItemInView(aItemId, views[i]);
-      isnot(node, null, "Found new Places node in " + views[i]);
-      is(index, aIndex, "Node is at index " + index);
+      // Check that item has been added in the correct position.
+      for (var i = 0; i < views.length; i++) {
+        var [node, index] = searchItemInView(event.itemId, views[i]);
+        isnot(node, null, "Found new Places node in " + views[i]);
+        is(index, event.index, "Node is at index " + index);
+      }
     }
   },
 
+  // nsINavBookmarkObserver
   onItemRemoved: function PSB_onItemRemoved(aItemId, aFolderId, aIndex,
                                             aItemType, url, aGuid) {
     var views = getViewsForFolder(aFolderId);
     ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
     // Check that item has been removed.
     for (var i = 0; i < views.length; i++) {
       var node = null;
       [node, ] = searchItemInView(aItemId, views[i]);
--- 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].itemId;
+    this._itemAddedParent = events[0].parentItemId;
+    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-item-added"], observer.handlePlacesEvents);
 
 registerCleanupFunction(function() {
   PlacesUtils.bookmarks.removeObserver(observer);
+PlacesUtils.observers.removeListener(["bookmark-item-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.itemId, 0, "Should have a valid itemId");
+    }
   };
-  PlacesUtils.bookmarks.addObserver(obs);
+  PlacesUtils.observers.addListener(["bookmark-item-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-item-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.itemId, 0, "Should have a valid itemId");
+    }
   };
-  PlacesUtils.bookmarks.addObserver(obs);
+  PlacesUtils.observers.addListener(["bookmark-item-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-item-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.itemId,
+        parentId: event.parentItemId,
+        index: event.index,
+        title: event.title,
+        guid: event.itemGuid,
+        parentGuid: event.parentItemGuid,
+      });
+    }
   };
-  PlacesUtils.bookmarks.addObserver(obs);
+  PlacesUtils.observers.addListener(["bookmark-item-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-item-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_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 == "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.itemGuid), "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-item-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-item-added"], obs.handlePlacesEvent);
 });
 
--- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -5,40 +5,61 @@
 
 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);
@@ -56,33 +77,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.itemType != "tag" && e.itemType != "bookmark-tag-copy");
+    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);
@@ -94,93 +130,99 @@ var gBookmarkSkipObserver = {
   // nsISupports
   QueryInterface: ChromeUtils.generateQI([Ci.nsINavBookmarkObserver]),
 };
 
 
 add_task(function setup() {
   PlacesUtils.bookmarks.addObserver(gBookmarksObserver);
   PlacesUtils.bookmarks.addObserver(gBookmarkSkipObserver);
+  gBookmarksObserver.handlePlacesEvents =
+    gBookmarksObserver.handlePlacesEvents.bind(gBookmarksObserver);
+  gBookmarkSkipObserver.handlePlacesEvents =
+    gBookmarkSkipObserver.handlePlacesEvents.bind(gBookmarkSkipObserver);
+  PlacesUtils.observers.addListener(["bookmark-item-added"], gBookmarksObserver.handlePlacesEvents);
+  PlacesUtils.observers.addListener(["bookmark-item-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-item-added"
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-item-added",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "parentItemId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
           { name: "index", check: v => v === 0 },
-          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
-          { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+          { name: "itemType", check: v => v === "bookmark" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", 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-item-added"
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-item-added",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "parentItemId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
           { name: "index", check: v => v === 1 },
-          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
-          { name: "uri", check: v => v === null },
+          { name: "itemType", check: v => v === "separator" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", 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-item-added"
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-item-added",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "parentItemId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
           { name: "index", check: v => v === 2 },
-          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-          { name: "uri", check: v => v === null },
+          { name: "itemType", check: v => v === "folder" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", 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,
     title,
     type: PlacesUtils.bookmarks.TYPE_FOLDER
   });
@@ -224,40 +266,40 @@ 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-item-added", // This is the tag folder.
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
+          { name: "parentItemId", 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: "itemType", check: v => v === "tag" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", 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-item-added", // This is the tag.
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentItemId", 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: "itemType", check: v => v === "bookmark-tag-copy" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
       { name: "onItemChanged",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
           { name: "property", check: v => v === "tags" },
           { name: "isAnno", check: v => v === false },
           { name: "newValue", check: v => v === "" },
@@ -301,17 +343,19 @@ add_task(async function onItemChanged_ta
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
           { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
           { name: "guid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "oldValue", check: v => typeof(v) == "string" },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
   ])]);
+  dump("tagging\n");
   PlacesUtils.tagging.tagURI(uri, [TAG]);
+  dump("untagging\n");
   PlacesUtils.tagging.untagURI(uri, [TAG]);
   await promise;
 });
 
 add_task(async function onItemMoved_bookmark() {
   let bm = await PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     index: 0
@@ -470,70 +514,70 @@ 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-item-added", "bookmark-item-added", "bookmark-item-added", "bookmark-item-added",
       "onItemRemoved"
     ]),
     gBookmarksObserver.setup([
-      { name: "onItemAdded",
+      { eventType: "bookmark-item-added",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+          { name: "parentItemId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
           { name: "index", check: v => v === 0 },
-          { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
-          { name: "uri", check: v => v === null },
+          { name: "itemType", check: v => v === "folder" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
-      { name: "onItemAdded",
+      { eventType: "bookmark-item-added",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentItemId", 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: "itemType", check: v => v === "bookmark" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
-      { name: "onItemAdded",
+      { eventType: "bookmark-item-added",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentItemId", 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: "itemType", check: v => v === "folder" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
-      { name: "onItemAdded",
+      { eventType: "bookmark-item-added",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
-          { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+          { name: "parentItemId", 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: "itemType", check: v => v === "bookmark" },
+          { 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: "itemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
+          { name: "parentItemGuid", check: v => typeof(v) == "string" && PlacesUtils.isValidGuid(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
       { name: "onItemRemoved",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
           { name: "index", check: v => v === 0 },
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
@@ -599,9 +643,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-item-added"], gBookmarksObserver.handlePlacesEvents);
+  PlacesUtils.observers.removeListener(["bookmark-item-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-item-added",
+                         event.itemId,
+                         event.parentItemId,
+                         event.itemGuid,
+                         event.parentItemGuid]);
+    }
+  };
   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-item-added"], listener);
   PlacesUtils.registerShutdownFunction(function() {
     PlacesUtils.bookmarks.removeObserver(observer);
+    PlacesUtils.observers.removeListener(["bookmark-item-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-item-added", folderId, PlacesUtils.bookmarksMenuFolderId, folder.guid,
       PlacesUtils.bookmarks.menuGuid],
-    ["onItemAdded", fxId, folderId, fx.guid, folder.guid],
-    ["onItemAdded", tbId, folderId, tb.guid, folder.guid],
+    ["bookmark-item-added", fxId, folderId, fx.guid, folder.guid],
+    ["bookmark-item-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
@@ -710,17 +710,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.itemId;
+    bookmarksObserver._itemAddedParent = event.parentItemId;
+    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.itemId;
     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.itemGuid);
     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-item-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);
   Assert.ok(bs.unfiledBookmarksFolder > 0);
 
--- 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 && ["tag", "bookmark-tag-copy"].includes(event.itemType)) {
+        continue;
+      }
+      let params = {
+        itemId: event.itemId,
+        parentId: event.parentItemId,
+        index: event.index,
+        type: event.itemType,
+        urlHref: event.url,
+        title: event.title,
+        guid: event.itemGuid,
+        parentGuid: event.parentItemGuid,
+        source: event.source,
+      };
+      if (!this.ignoreDates) {
+        params.dateAdded = event.dateAdded * 1000;
+      }
+      this.notifications.push({ name: "bookmark-item-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 },
     });
   },
@@ -278,31 +291,33 @@ BookmarkObserver.prototype = {
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsINavBookmarkObserver,
     Ci.nsIAnnotationObserver,
   ]),
 
   check(expectedNotifications) {
     PlacesUtils.bookmarks.removeObserver(this);
     PlacesUtils.annotations.removeObserver(this);
+    PlacesUtils.observers.removeListener(["bookmark-item-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.annotations.addObserver(observer);
+  PlacesUtils.observers.addListener(["bookmark-item-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_structure_changes.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
@@ -500,21 +500,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-item-added",
     params: { itemId: localItemIds.get("folderCCCCCC"),
               parentId: PlacesUtils.bookmarksMenuFolderId, index: 1,
-              type: PlacesUtils.bookmarks.TYPE_FOLDER,
-              urlHref: null, title: "C",
+              type: "folder",
+              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"),
@@ -655,20 +655,20 @@ 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-item-added",
     params: { itemId: localItemIds.get("bookmarkEEEE"),
               parentId: localItemIds.get("folderAAAAAA"), index: 1,
-              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+              type: "bookmark",
               urlHref: "http://example.com/e", title: "E",
               guid: "bookmarkEEEE",
               parentGuid: "folderAAAAAA",
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
     name: "onItemMoved",
     params: { itemId: localItemIds.get("bookmarkCCCC"),
               oldParentId: localItemIds.get("folderAAAAAA"),
--- a/toolkit/components/places/tests/sync/test_bookmark_value_changes.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_value_changes.js
@@ -107,37 +107,37 @@ 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-item-added",
     params: { itemId: localItemIds.get("fxBmk_______"),
               parentId: PlacesUtils.toolbarFolderId, index: 0,
-              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+              type: "bookmark",
               urlHref: "http://getfirefox.com/", title: "Get Firefox",
               guid: "fxBmk_______",
               parentGuid: PlacesUtils.bookmarks.toolbarGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-item-added",
     params: { itemId: localItemIds.get("tFolder_____"),
               parentId: PlacesUtils.toolbarFolderId,
-              index: 1, type: PlacesUtils.bookmarks.TYPE_FOLDER,
-              urlHref: null, title: "Mail", guid: "tFolder_____",
+              index: 1, type: "folder",
+              urlHref: "", title: "Mail", guid: "tFolder_____",
               parentGuid: PlacesUtils.bookmarks.toolbarGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-item-added",
     params: { itemId: localItemIds.get("tbBmk_______"),
               parentId: localItemIds.get("tFolder_____"), index: 0,
-              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+              type: "bookmark",
               urlHref: "http://getthunderbird.com/", title: "Get Thunderbird",
               guid: "tbBmk_______", parentGuid: "tFolder_____",
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
     name: "onItemMoved",
     params: { itemId: localItemIds.get("bzBmk_______"),
               oldParentId: PlacesUtils.toolbarFolderId,
               oldIndex: 0, newParentId: PlacesUtils.toolbarFolderId,
@@ -664,38 +664,38 @@ 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-item-added",
     params: { itemId: localItemIds.get("bookmarkAAAA"),
               parentId: PlacesUtils.bookmarksMenuFolderId, index: 0,
-              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+              type: "bookmark",
               urlHref: "http://example.com/a", title: "A",
               guid: "bookmarkAAAA",
               parentGuid: PlacesUtils.bookmarks.menuGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
-    name: "onItemAdded",
+    name: "bookmark-item-added",
     params: { itemId: localItemIds.get("bookmarkAAA1"),
               parentId: PlacesUtils.bookmarksMenuFolderId, index: 1,
-              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+              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-item-added",
     params: { itemId: localItemIds.get("bookmarkBBB1"),
               parentId: PlacesUtils.unfiledBookmarksFolderId, index: 0,
-              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+              type: "bookmark",
               urlHref: "http://example.com/b", title: "B",
               guid: "bookmarkBBB1",
               parentGuid: PlacesUtils.bookmarks.unfiledGuid,
               source: PlacesUtils.bookmarks.SOURCES.SYNC },
   }, {
     // These `onItemMoved` notifications aren't necessary: we only moved
     // (B C D E) to accomodate (A A1 B1), and Places doesn't usually fire move
     // notifications for repositioned siblings. However, detecting and filtering
--- 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 (["tag", "bookmark-tag-copy"].includes(event.itemType)) {
+        this.tagRelatedGuids.add(event.itemGuid);
+        return;
+      }
+
+      this.itemsAdded.set(event.itemGuid, { itemId:         event.itemId,
+                                            parentGuid:     event.parentItemGuid,
+                                            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-item-added"], observer.handlePlacesEvents);
   registerCleanupFunction(function() {
     bmsvc.removeObserver(observer);
+    obsvc.removeListener(["bookmark-item-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:   "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
@@ -149,28 +149,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, PlacesUtils.unfiledBookmarksFolderId);
-      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-item-added"], listener);
+    Assert.equal(event.parentItemId, PlacesUtils.unfiledBookmarksFolderId);
+    Assert.equal(event.index, 0);
+    Assert.equal(event.itemType, "folder");
+    Assert.equal(event.title, "test");
+  };
+  PlacesUtils.observers.addListener(["bookmark-item-added"], listener);
 
   await PlacesUtils.livemarks.addLivemark(
     { title: "test",
       parentId: PlacesUtils.unfiledBookmarksFolderId,
       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"]);