Bug 1340498 - Update onVisits tests to use 'page-visited' r?mak draft
authorDoug Thayer <dothayer@mozilla.com>
Wed, 14 Feb 2018 09:17:41 -0800
changeset 811529 1274b21b579cdd08ce37b92ea296eec336022544
parent 811528 6554d385aaa2aef87f6c7475a07daa79703f0d93
child 811530 2c502173138b1b248d1644d2bcf6edad5d1503ad
push id114328
push userbmo:dothayer@mozilla.com
push dateWed, 27 Jun 2018 18:14:18 +0000
reviewersmak
bugs1340498
milestone63.0a1
Bug 1340498 - Update onVisits tests to use 'page-visited' r?mak MozReview-Commit-ID: FxC3gcUF9hl
browser/components/migration/tests/unit/test_automigration.js
docshell/test/browser/browser_bug420605.js
docshell/test/browser/browser_bug503832.js
docshell/test/browser/browser_bug655270.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/test_history_store.js
toolkit/components/downloads/test/unit/common_test_Download.js
toolkit/components/downloads/test/unit/head.js
toolkit/components/places/tests/PlacesTestUtils.jsm
toolkit/components/places/tests/browser/browser_bug399606.js
toolkit/components/places/tests/browser/browser_bug646422.js
toolkit/components/places/tests/browser/browser_double_redirect.js
toolkit/components/places/tests/browser/browser_multi_redirect_frecency.js
toolkit/components/places/tests/browser/browser_notfound.js
toolkit/components/places/tests/browser/browser_onvisit_title_null_for_navigation.js
toolkit/components/places/tests/browser/browser_redirect.js
toolkit/components/places/tests/browser/browser_settitle.js
toolkit/components/places/tests/browser/browser_visited_notfound.js
toolkit/components/places/tests/browser/head.js
toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js
toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/history/test_async_history_api.js
toolkit/components/places/tests/history/test_remove.js
toolkit/components/places/tests/history/test_removeByFilter.js
toolkit/components/places/tests/history/test_removeVisitsByFilter.js
toolkit/components/places/tests/unit/test_454977.js
toolkit/components/places/tests/unit/test_download_history.js
toolkit/components/places/tests/unit/test_history_observer.js
toolkit/components/places/tests/unit/test_markpageas.js
--- a/browser/components/migration/tests/unit/test_automigration.js
+++ b/browser/components/migration/tests/unit/test_automigration.js
@@ -626,19 +626,16 @@ add_task(async function checkUndoVisitsS
   ]);
   let uriDeletedExpected = new Map([
     ["http://www.mozilla.org/", PromiseUtils.defer()],
   ]);
   let wrongMethodDeferred = PromiseUtils.defer();
   let observer = {
     onBeginUpdateBatch() {},
     onEndUpdateBatch() {},
-    onVisits(visits) {
-      wrongMethodDeferred.reject(new Error("Unexpected call to onVisits " + visits.length));
-    },
     onTitleChanged(uri) {
       wrongMethodDeferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
     },
     onClearHistory() {
       wrongMethodDeferred.reject("Unexpected call to onClearHistory");
     },
     onPageChanged(uri) {
       wrongMethodDeferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
--- a/docshell/test/browser/browser_bug420605.js
+++ b/docshell/test/browser/browser_bug420605.js
@@ -50,17 +50,16 @@ function test() {
                                                  gBrowser.selectedBrowser);
       }
     }
 
     /* Global history observer that triggers for the two test URLs above. */
     var historyObserver = {
         onBeginUpdateBatch: function() {},
         onEndUpdateBatch: function() {},
-        onVisits: function() {},
         onTitleChanged: function(aURI, aPageTitle) {},
         onDeleteURI: function(aURI) {},
         onClearHistory: function() {},
         onPageChanged: function(aURI, aWhat, aValue) {
             if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) {
                 return;
             }
             aURI = aURI.spec;
--- a/docshell/test/browser/browser_bug503832.js
+++ b/docshell/test/browser/browser_bug503832.js
@@ -10,17 +10,16 @@ add_task(async function() {
     var historyService = Cc["@mozilla.org/browser/nav-history-service;1"]
                          .getService(Ci.nsINavHistoryService);
 
     let fragmentPromise = new Promise(resolve => {
         /* Global history observer that triggers for the two test URLs above. */
         var historyObserver = {
             onBeginUpdateBatch: function() {},
             onEndUpdateBatch: function() {},
-            onVisits: function() {},
             onTitleChanged: function(aURI, aPageTitle) {
                 aURI = aURI.spec;
                 switch (aURI) {
                 case pageurl:
                     is(aPageTitle, pagetitle, "Correct page title for " + aURI);
                     return;
                 case fragmenturl:
                     is(aPageTitle, pagetitle, "Correct page title for " + aURI);
--- a/docshell/test/browser/browser_bug655270.js
+++ b/docshell/test/browser/browser_bug655270.js
@@ -43,17 +43,16 @@ function test() {
         gBrowser.removeTab(tab);
         PlacesUtils.history.removeObserver(this);
         finish();
       }
     },
 
     onBeginUpdateBatch: function() { },
     onEndUpdateBatch: function() { },
-    onVisits: function() { },
     onTitleChanged: function() { },
     onDeleteURI: function() { },
     onClearHistory: function() { },
     onDeleteVisits: function() { },
     QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryObserver])
   };
 
   PlacesUtils.history.addObserver(observer);
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -544,39 +544,44 @@ async function serverForFoo(engine, call
 }
 
 // Places notifies history observers asynchronously, so `addVisits` might return
 // before the tracker receives the notification. This helper registers an
 // observer that resolves once the expected notification fires.
 async function promiseVisit(expectedType, expectedURI) {
   return new Promise(resolve => {
     function done(type, uri) {
-      if (uri.equals(expectedURI) && type == expectedType) {
+      if (uri == expectedURI.spec && type == expectedType) {
         PlacesUtils.history.removeObserver(observer);
+        PlacesObservers.removeListener(["page-visited"],
+                                       observer.handlePlacesEvents);
         resolve();
       }
     }
     let observer = {
-      onVisits(visits) {
-        Assert.equal(visits.length, 1);
-        done("added", visits[0].uri);
+      handlePlacesEvents(events) {
+        Assert.equal(events.length, 1);
+        Assert.equal(events[0].type, "page-visited");
+        done("added", events[0].url);
       },
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
       onTitleChanged() {},
       onFrecencyChanged() {},
       onManyFrecenciesChanged() {},
       onDeleteURI(uri) {
-        done("removed", uri);
+        done("removed", uri.spec);
       },
       onClearHistory() {},
       onPageChanged() {},
       onDeleteVisits() {},
     };
     PlacesUtils.history.addObserver(observer, false);
+    PlacesObservers.addListener(["page-visited"],
+                                observer.handlePlacesEvents);
   });
 }
 
 async function addVisit(suffix, referrer = null, transition = PlacesUtils.history.TRANSITION_LINK) {
   let uriString = "http://getfirefox.com/" + suffix;
   let uri = CommonUtils.makeURI(uriString);
   _("Adding visit for URI " + uriString);
 
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -8,36 +8,21 @@ ChromeUtils.import("resource://services-
 ChromeUtils.import("resource://services-sync/util.js");
 
 const TIMESTAMP1 = (Date.now() - 103406528) * 1000;
 const TIMESTAMP2 = (Date.now() - 6592903) * 1000;
 const TIMESTAMP3 = (Date.now() - 123894) * 1000;
 
 function promiseOnVisitObserved() {
   return new Promise(res => {
-    PlacesUtils.history.addObserver({
-      onBeginUpdateBatch: function onBeginUpdateBatch() {},
-      onEndUpdateBatch: function onEndUpdateBatch() {},
-      onPageChanged: function onPageChanged() {},
-      onTitleChanged: function onTitleChanged() {
-      },
-      onVisits: function onVisits() {
-        PlacesUtils.history.removeObserver(this);
-        res();
-      },
-      onDeleteVisits: function onDeleteVisits() {},
-      onPageExpired: function onPageExpired() {},
-      onDeleteURI: function onDeleteURI() {},
-      onClearHistory: function onClearHistory() {},
-      QueryInterface: ChromeUtils.generateQI([
-        Ci.nsINavHistoryObserver,
-        Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS,
-        Ci.nsISupportsWeakReference
-      ])
-    }, true);
+    let listener = new PlacesWeakCallbackWrapper((events) => {
+      PlacesObservers.removeListener(["page-visited"], listener);
+      res();
+    });
+    PlacesObservers.addListener(["page-visited"], listener);
   });
 }
 
 function isDateApproximately(actual, expected, skewMillis = 1000) {
   let lowerBound = expected - skewMillis;
   let upperBound = expected + skewMillis;
   return actual >= lowerBound && actual <= upperBound;
 }
--- a/toolkit/components/downloads/test/unit/common_test_Download.js
+++ b/toolkit/components/downloads/test/unit/common_test_Download.js
@@ -2395,17 +2395,17 @@ add_task(async function test_history() {
   await PlacesUtils.history.clear();
   let promiseVisit = promiseWaitForVisit(httpUrl("interruptible.txt"));
 
   // Start a download that is not allowed to finish yet.
   let download = await promiseStartDownload(httpUrl("interruptible.txt"));
 
   // The history notifications should be received before the download completes.
   let [time, transitionType] = await promiseVisit;
-  Assert.equal(time, download.startTime.getTime() * 1000);
+  Assert.equal(time, download.startTime.getTime());
   Assert.equal(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
 
   // Restart and complete the download after clearing history.
   await PlacesUtils.history.clear();
   download.cancel();
   continueResponses();
   await download.start();
 
@@ -2429,17 +2429,17 @@ add_task(async function test_history_try
 
   // The history notifications should be received before the download completes.
   let [time, transitionType] = await promiseVisit;
   Assert.equal(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
 
   // The time set by nsIHelperAppService may be different than the start time in
   // the download object, thus we only check that it is a meaningful time.  Note
   // that we subtract one second from the earliest time to account for rounding.
-  Assert.ok(time >= beforeStartTimeMs * 1000 - 1000000);
+  Assert.ok(time >= beforeStartTimeMs - 1000);
 
   // Complete the download before finishing the test.
   continueResponses();
   await promiseDownloadStopped(download);
 });
 
 /**
  * Tests that the temp download files are removed on exit and exiting private
--- a/toolkit/components/downloads/test/unit/head.js
+++ b/toolkit/components/downloads/test/unit/head.js
@@ -141,42 +141,26 @@ function promiseTimeout(aTime) {
  *        String containing the URI that will be visited.
  *
  * @return {Promise}
  * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
  * @rejects Never.
  */
 function promiseWaitForVisit(aUrl) {
   return new Promise(resolve => {
-
-    let uri = NetUtil.newURI(aUrl);
-
-    PlacesUtils.history.addObserver({
-      QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryObserver]),
-      onBeginUpdateBatch() {},
-      onEndUpdateBatch() {},
-      onVisits(aVisits) {
-        Assert.equal(aVisits.length, 1);
-        let {
-          uri: visitUri,
-          time,
-          transitionType,
-        } = aVisits[0];
-        if (visitUri.equals(uri)) {
-          PlacesUtils.history.removeObserver(this);
-          resolve([time, transitionType]);
-        }
-      },
-      onTitleChanged() {},
-      onDeleteURI() {},
-      onClearHistory() {},
-      onPageChanged() {},
-      onDeleteVisits() {},
-    });
-
+    function listener(aEvents) {
+      Assert.equal(aEvents.length, 1);
+      let event = aEvents[0];
+      Assert.equal(event.type, "page-visited");
+      if (event.url == aUrl) {
+        PlacesObservers.removeListener(["page-visited"], listener);
+        resolve([event.visitTime, event.transitionType]);
+      }
+    }
+    PlacesObservers.addListener(["page-visited"], listener);
   });
 }
 
 /**
  * Creates a new Download object, setting a temporary file as the target.
  *
  * @param aSourceUrl
  *        String containing the URI for the download source, or null to use
--- a/toolkit/components/places/tests/PlacesTestUtils.jsm
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -313,16 +313,28 @@ var PlacesTestUtils = Object.freeze({
       ORDER BY guid`);
     return rows.map(row => ({
       guid: row.getResultByName("guid"),
       dateRemoved: PlacesUtils.toDate(row.getResultByName("dateRemoved")),
     }));
   },
 
   waitForNotification(notification, conditionFn = () => true, type = "bookmarks") {
+    if (type == "places") {
+      return new Promise(resolve => {
+        function listener(events) {
+          if (conditionFn(events)) {
+            PlacesObservers.removeListener([notification], listener);
+            resolve();
+          }
+        }
+        PlacesObservers.addListener([notification], listener);
+      });
+    }
+
     let iface = type == "bookmarks" ? Ci.nsINavBookmarkObserver
                                     : Ci.nsINavHistoryObserver;
     return new Promise(resolve => {
       let proxifiedObserver = new Proxy({}, {
         get: (target, name) => {
           if (name == "QueryInterface")
             return ChromeUtils.generateQI([iface]);
           if (name == notification)
--- a/toolkit/components/places/tests/browser/browser_bug399606.js
+++ b/toolkit/components/places/tests/browser/browser_bug399606.js
@@ -10,48 +10,38 @@ add_task(async function() {
     "http://example.com/tests/toolkit/components/places/tests/browser/399606-history.go-0.html",
     "http://example.com/tests/toolkit/components/places/tests/browser/399606-location.replace.html",
     "http://example.com/tests/toolkit/components/places/tests/browser/399606-location.reload.html",
     "http://example.com/tests/toolkit/components/places/tests/browser/399606-httprefresh.html",
     "http://example.com/tests/toolkit/components/places/tests/browser/399606-window.location.html",
   ];
 
   // Create and add history observer.
-  let historyObserver = {
-    count: 0,
-    expectedURI: null,
-    onVisits(aVisits) {
-      for (let {uri} of aVisits) {
-        info("Received onVisits: " + uri.spec);
-        if (uri.equals(this.expectedURI)) {
-          this.count++;
-        }
+  let count = 0;
+  let expectedURI = null;
+  function onVisitsListener(aEvents) {
+    for (let event of aEvents) {
+      info("Received onVisits: " + event.url);
+      if (event.url == expectedURI) {
+        count++;
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onTitleChanged() {},
-    onDeleteURI() {},
-    onClearHistory() {},
-    onPageChanged() {},
-    onDeleteVisits() {},
-    QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryObserver])
-  };
+    }
+  }
 
   async function promiseLoadedThreeTimes(uri) {
-    historyObserver.count = 0;
-    historyObserver.expectedURI = Services.io.newURI(uri);
+    count = 0;
+    expectedURI = uri;
     let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
-    PlacesUtils.history.addObserver(historyObserver);
+    PlacesObservers.addListener(["page-visited"], onVisitsListener);
     gBrowser.loadURI(uri);
     await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
     await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
     await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
-    PlacesUtils.history.removeObserver(historyObserver);
+    PlacesObservers.removeListener(["page-visited"], onVisitsListener);
     BrowserTestUtils.removeTab(tab);
   }
 
   for (let uri of URIS) {
     await promiseLoadedThreeTimes(uri);
-    is(historyObserver.count, 1,
-      "onVisit has been received right number of times for " + uri);
+    is(count, 1,
+      "'page-visited' has been received right number of times for " + uri);
   }
 });
--- a/toolkit/components/places/tests/browser/browser_bug646422.js
+++ b/toolkit/components/places/tests/browser/browser_bug646422.js
@@ -18,17 +18,16 @@ add_task(async function() {
         if (/new_page$/.test(uri.spec)) {
           resolve(title);
           PlacesUtils.history.removeObserver(observer);
         }
       },
 
       onBeginUpdateBatch() { },
       onEndUpdateBatch() { },
-      onVisits() { },
       onDeleteURI() { },
       onClearHistory() { },
       onPageChanged() { },
       onDeleteVisits() { },
       QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryObserver])
     };
 
     PlacesUtils.history.addObserver(observer);
--- a/toolkit/components/places/tests/browser/browser_double_redirect.js
+++ b/toolkit/components/places/tests/browser/browser_double_redirect.js
@@ -6,29 +6,28 @@ add_task(async function() {
   await PlacesUtils.history.clear();
 
   const BASE_URL = "http://example.com/tests/toolkit/components/places/tests/browser/";
   const TEST_URI = NetUtil.newURI(BASE_URL + "begin.html");
   const FIRST_REDIRECTING_URI = NetUtil.newURI(BASE_URL + "redirect_twice.sjs");
   const FINAL_URI = NetUtil.newURI(BASE_URL + "final.html");
 
   let promiseVisits = new Promise(resolve => {
-    PlacesUtils.history.addObserver({
-      __proto__: NavHistoryObserver.prototype,
+    let observer = {
       _notified: [],
       onVisit(uri, id, time, referrerId, transition) {
-        info("Received onVisit: " + uri.spec);
+        info("Received onVisit: " + uri);
         this._notified.push(uri);
 
-        if (!uri.equals(FINAL_URI)) {
+        if (uri != FINAL_URI.spec) {
           return;
         }
 
         is(this._notified.length, 4);
-        PlacesUtils.history.removeObserver(this);
+        PlacesObservers.removeListener(["page-visited"], this.handleEvents);
 
         (async function() {
           // Get all pages visited from the original typed one
           let db = await PlacesUtils.promiseDBConnection();
           let rows = await db.execute(
             `SELECT url FROM moz_historyvisits
              JOIN moz_places h ON h.id = place_id
              WHERE from_visit IN
@@ -40,28 +39,31 @@ add_task(async function() {
           is(rows.length, 1, "Found right number of visits");
           let visitedUrl = rows[0].getResultByName("url");
           // Check that redirect from_visit is not from the original typed one
           is(visitedUrl, FIRST_REDIRECTING_URI.spec, "Check referrer for " + visitedUrl);
 
           resolve();
         })();
       },
-      onVisits(visits) {
-        is(visits.length, 1, "Right number of visits notified");
+      handleEvents(events) {
+        is(events.length, 1, "Right number of visits notified");
+        is(events[0].type, "page-visited");
         let {
-          uri,
+          url,
           visitId,
-          time,
-          referrerId,
+          visitTime,
+          referringVisitId,
           transitionType,
-        } = visits[0];
-        this.onVisit(uri, visitId, time, referrerId, transitionType);
+        } = events[0];
+        this.onVisit(url, visitId, visitTime, referringVisitId, transitionType);
       }
-    });
+    };
+    observer.handleEvents = observer.handleEvents.bind(observer);
+    PlacesObservers.addListener(["page-visited"], observer.handleEvents);
   });
 
   PlacesUtils.history.markPageAsTyped(TEST_URI);
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: TEST_URI.spec,
   }, async function(browser) {
     // Load begin page, click link on page to record visits.
--- a/toolkit/components/places/tests/browser/browser_multi_redirect_frecency.js
+++ b/toolkit/components/places/tests/browser/browser_multi_redirect_frecency.js
@@ -27,25 +27,25 @@ async function check_uri(uri, frecency, 
   is(await PlacesTestUtils.fieldInDB(uri, "frecency"), frecency,
     "Frecency of the page is the expected one");
   is(await PlacesTestUtils.fieldInDB(uri, "hidden"), hidden,
     "Hidden value of the page is the expected one");
 }
 
 async function waitVisitedNotifications() {
   let redirectNotified = false;
-  await PlacesTestUtils.waitForNotification("onVisits", visits => {
+  await PlacesTestUtils.waitForNotification("page-visited", visits => {
     is(visits.length, 1, "Was notified for the right number of visits.");
-    let {uri} = visits[0];
-    info("Received onVisits: " + uri.spec);
-    if (uri.equals(REDIRECT_URI)) {
+    let {url} = visits[0];
+    info("Received 'page-visited': " + url);
+    if (url == REDIRECT_URI.spec) {
       redirectNotified = true;
     }
-    return uri.equals(TARGET_URI);
-  }, "history");
+    return url == TARGET_URI.spec;
+  }, "places");
   return redirectNotified;
 }
 
 let firstRedirectBonus = 0;
 let nextRedirectBonus = 0;
 let targetBonus = 0;
 
 add_task(async function test_multiple_redirect() {
--- a/toolkit/components/places/tests/browser/browser_notfound.js
+++ b/toolkit/components/places/tests/browser/browser_notfound.js
@@ -5,52 +5,34 @@
 add_task(async function() {
   const TEST_URL = "http://mochi.test:8888/notFoundPage.html";
 
   // Used to verify errors are not marked as typed.
   PlacesUtils.history.markPageAsTyped(NetUtil.newURI(TEST_URL));
 
   // Create and add history observer.
   let visitedPromise = new Promise(resolve => {
-    let historyObserver = {
-      onVisit(aURI, aVisitID, aTime, aReferringID, aTransitionType) {
-        PlacesUtils.history.removeObserver(historyObserver);
-        info("Received onVisit: " + aURI.spec);
-        fieldForUrl(aURI, "frecency", function(aFrecency) {
-          is(aFrecency, 0, "Frecency should be 0");
-          fieldForUrl(aURI, "hidden", function(aHidden) {
-            is(aHidden, 0, "Page should not be hidden");
-            fieldForUrl(aURI, "typed", function(aTyped) {
-              is(aTyped, 0, "page should not be marked as typed");
-              resolve();
-            });
+    function listener(aEvents) {
+      is(aEvents.length, 1, "Right number of visits notified");
+      is(aEvents[0].type, "page-visited");
+      let uri = NetUtil.newURI(aEvents[0].url);
+      PlacesObservers.removeListener(["page-visited"], listener);
+      info("Received 'page-visited': " + uri.spec);
+      fieldForUrl(uri, "frecency", function(aFrecency) {
+        is(aFrecency, 0, "Frecency should be 0");
+        fieldForUrl(uri, "hidden", function(aHidden) {
+          is(aHidden, 0, "Page should not be hidden");
+          fieldForUrl(uri, "typed", function(aTyped) {
+            is(aTyped, 0, "page should not be marked as typed");
+            resolve();
           });
         });
-      },
-      onVisits(aVisits) {
-        is(aVisits.length, 1, "Right number of visits notified");
-        let {
-          uri,
-          visitId,
-          time,
-          referrerId,
-          transitionType,
-        } = aVisits[0];
-        this.onVisit(uri, visitId, time, referrerId, transitionType);
-      },
-      onBeginUpdateBatch() {},
-      onEndUpdateBatch() {},
-      onTitleChanged() {},
-      onDeleteURI() {},
-      onClearHistory() {},
-      onPageChanged() {},
-      onDeleteVisits() {},
-      QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryObserver])
-    };
-    PlacesUtils.history.addObserver(historyObserver);
+      });
+    }
+    PlacesObservers.addListener(["page-visited"], listener);
   });
 
   let newTabPromise = BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
   await Promise.all([visitedPromise, newTabPromise]);
 
   await PlacesUtils.history.clear();
   gBrowser.removeCurrentTab();
 });
--- a/toolkit/components/places/tests/browser/browser_onvisit_title_null_for_navigation.js
+++ b/toolkit/components/places/tests/browser/browser_onvisit_title_null_for_navigation.js
@@ -1,32 +1,34 @@
 const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
 
 add_task(async function checkTitleNotificationForNavigation() {
   const EXPECTED_URL = Services.io.newURI(TEST_PATH + "empty_page.html");
   let promiseTitleChanged = new Promise(resolve => {
+    function onVisits(aEvents) {
+      Assert.equal(aEvents.length, 1, "Right number of visits notified");
+      Assert.equal(aEvents[0].type, "page-visited");
+      let {
+        url,
+        lastKnownTitle,
+      } = aEvents[0];
+      info("'page-visited': " + url);
+      if (url == EXPECTED_URL.spec) {
+        Assert.equal(lastKnownTitle, null, "Should not have a title");
+      }
+      PlacesObservers.removeListener(["page-visited"], onVisits);
+    }
     let obs = {
-      onVisits(aVisits) {
-        Assert.equal(aVisits.length, 1, "Right number of visits notified");
-        let {
-          uri,
-          lastKnownTitle,
-        } = aVisits[0];
-        info("onVisits: " + uri.spec);
-        if (uri.equals(EXPECTED_URL)) {
-          Assert.equal(lastKnownTitle, null, "Should not have a title");
-        }
-      },
-
       onTitleChanged(aURI, aTitle, aGuid) {
         if (aURI.equals(EXPECTED_URL)) {
           is(aTitle, "I am an empty page", "Should have correct title in titlechanged notification");
           PlacesUtils.history.removeObserver(obs);
           resolve();
         }
       },
     };
     PlacesUtils.history.addObserver(obs);
+    PlacesObservers.addListener(["page-visited"], onVisits);
   });
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, EXPECTED_URL.spec);
   await promiseTitleChanged;
   BrowserTestUtils.removeTab(tab);
 });
--- a/toolkit/components/places/tests/browser/browser_redirect.js
+++ b/toolkit/components/places/tests/browser/browser_redirect.js
@@ -34,25 +34,25 @@ async function check_uri(uri, frecency, 
 add_task(async function redirect_check_new_typed_visit() {
   // Used to verify the redirect bonus overrides the typed bonus.
   PlacesUtils.history.markPageAsTyped(REDIRECT_URI);
 
   redirectSourceFrecency += REDIRECT_SOURCE_VISIT_BONUS;
   redirectTargetFrecency += TYPED_VISIT_BONUS;
   let redirectNotified = false;
 
-  let visitedPromise = PlacesTestUtils.waitForNotification("onVisits", visits => {
+  let visitedPromise = PlacesTestUtils.waitForNotification("page-visited", visits => {
     is(visits.length, 1, "Was notified for the right number of visits.");
-    let {uri} = visits[0];
-    info("Received onVisits for: " + uri.spec);
-    if (uri.equals(REDIRECT_URI)) {
+    let {url} = visits[0];
+    info("Received 'page-visited': " + url);
+    if (url == REDIRECT_URI.spec) {
       redirectNotified = true;
     }
-    return uri.equals(TARGET_URI);
-  }, "history");
+    return url == TARGET_URI.spec;
+  }, "places");
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, REDIRECT_URI.spec);
   info("Waiting for onVisits");
   await visitedPromise;
   ok(redirectNotified, "The redirect should have been notified");
 
   await check_uri(REDIRECT_URI, redirectSourceFrecency, 1);
   await check_uri(TARGET_URI, redirectTargetFrecency, 0);
@@ -63,25 +63,25 @@ add_task(async function redirect_check_n
 add_task(async function redirect_check_second_typed_visit() {
   // A second visit with a typed url.
   PlacesUtils.history.markPageAsTyped(REDIRECT_URI);
 
   redirectSourceFrecency += REDIRECT_SOURCE_VISIT_BONUS;
   redirectTargetFrecency += TYPED_VISIT_BONUS;
   let redirectNotified = false;
 
-  let visitedPromise = PlacesTestUtils.waitForNotification("onVisits", visits => {
+  let visitedPromise = PlacesTestUtils.waitForNotification("page-visited", visits => {
     is(visits.length, 1, "Was notified for the right number of visits.");
-    let {uri} = visits[0];
-    info("Received onVisits: " + uri.spec);
-    if (uri.equals(REDIRECT_URI)) {
+    let {url} = visits[0];
+    info("Received 'page-visited': " + url);
+    if (url == REDIRECT_URI.spec) {
       redirectNotified = true;
     }
-    return uri.equals(TARGET_URI);
-  }, "history");
+    return url == TARGET_URI.spec;
+  }, "places");
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, REDIRECT_URI.spec);
   info("Waiting for onVisits");
   await visitedPromise;
   ok(redirectNotified, "The redirect should have been notified");
 
   await check_uri(REDIRECT_URI, redirectSourceFrecency, 1);
   await check_uri(TARGET_URI, redirectTargetFrecency, 0);
@@ -90,25 +90,25 @@ add_task(async function redirect_check_s
 });
 
 add_task(async function redirect_check_subsequent_link_visit() {
   // Another visit, but this time as a visited url.
   redirectSourceFrecency += REDIRECT_SOURCE_VISIT_BONUS;
   redirectTargetFrecency += LINK_VISIT_BONUS;
   let redirectNotified = false;
 
-  let visitedPromise = PlacesTestUtils.waitForNotification("onVisits", visits => {
+  let visitedPromise = PlacesTestUtils.waitForNotification("page-visited", visits => {
     is(visits.length, 1, "Was notified for the right number of visits.");
-    let {uri} = visits[0];
-    info("Received onVisits: " + uri.spec);
-    if (uri.equals(REDIRECT_URI)) {
+    let {url} = visits[0];
+    info("Received 'page-visited': " + url);
+    if (url == REDIRECT_URI.spec) {
       redirectNotified = true;
     }
-    return uri.equals(TARGET_URI);
-  }, "history");
+    return url == TARGET_URI.spec;
+  }, "places");
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, REDIRECT_URI.spec);
   info("Waiting for onVisits");
   await visitedPromise;
   ok(redirectNotified, "The redirect should have been notified");
 
   await check_uri(REDIRECT_URI, redirectSourceFrecency, 1);
   await check_uri(TARGET_URI, redirectTargetFrecency, 0);
--- a/toolkit/components/places/tests/browser/browser_settitle.js
+++ b/toolkit/components/places/tests/browser/browser_settitle.js
@@ -20,17 +20,16 @@ add_task(async function() {
   // notifications.
 
   // Create and add history observer.
   let titleChangedPromise = new Promise(resolve => {
     var historyObserver = {
       data: [],
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
-      onVisits() {},
       onTitleChanged(aURI, aPageTitle, aGUID) {
         this.data.push({ uri: aURI, title: aPageTitle, guid: aGUID });
 
         // We only expect one title change.
         //
         // Although we are loading two different pages, the first page does not
         // have a title.  Since the title starts out as empty and then is set
         // to empty, there is no title change notification.
--- a/toolkit/components/places/tests/browser/browser_visited_notfound.js
+++ b/toolkit/components/places/tests/browser/browser_visited_notfound.js
@@ -15,26 +15,24 @@ add_task(async function test() {
   await PlacesTestUtils.addVisits({ uri: TEST_URL });
   let frecency = await PlacesTestUtils.fieldInDB(TEST_URL, "frecency");
   is(frecency, 100, "Check initial frecency");
 
   // Used to verify errors are not marked as typed.
   PlacesUtils.history.markPageAsTyped(NetUtil.newURI(TEST_URL));
 
   let promiseVisit = new Promise(resolve => {
-    let historyObserver = {
-      __proto__: NavHistoryObserver.prototype,
-      onVisits(visits) {
-        PlacesUtils.history.removeObserver(historyObserver);
-        is(visits.length, 1, "Right number of visits");
-        is(visits[0].uri.spec, TEST_URL, "Check visited url");
-        resolve();
-      }
-    };
-    PlacesUtils.history.addObserver(historyObserver);
+    function onVisits(events) {
+      PlacesObservers.removeListener(["page-visited"], onVisits);
+      is(events.length, 1, "Right number of visits");
+      is(events[0].type, "page-visited");
+      is(events[0].url, TEST_URL, "Check visited url");
+      resolve();
+    }
+    PlacesObservers.addListener(["page-visited"], onVisits);
   });
   gBrowser.selectedBrowser.loadURI(TEST_URL);
   await promiseVisit;
 
   is(await PlacesTestUtils.fieldInDB(TEST_URL, "frecency"), frecency, "Frecency should be unchanged");
   is(await PlacesTestUtils.fieldInDB(TEST_URL, "hidden"), 0, "Page should not be hidden");
   is(await PlacesTestUtils.fieldInDB(TEST_URL, "typed"), 0, "page should not be marked as typed");
 });
--- a/toolkit/components/places/tests/browser/head.js
+++ b/toolkit/components/places/tests/browser/head.js
@@ -72,17 +72,16 @@ function promiseFieldForUrl(aURI, aField
  * Generic nsINavHistoryObserver that doesn't implement anything, but provides
  * dummy methods to prevent errors about an object not having a certain method.
  */
 function NavHistoryObserver() {}
 
 NavHistoryObserver.prototype = {
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
-  onVisits() {},
   onTitleChanged() {},
   onDeleteURI() {},
   onClearHistory() {},
   onPageChanged() {},
   onDeleteVisits() {},
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsINavHistoryObserver,
   ])
--- a/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js
+++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js
@@ -72,17 +72,16 @@ add_task(async function test_notificatio
       currentTest.bookmarks.push(page);
     }
 
     // Observe history.
     let historyObserver = {
       onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {},
       onEndUpdateBatch: function PEX_onEndUpdateBatch() {},
       onClearHistory() {},
-      onVisits() {},
       onTitleChanged() {},
       onDeleteURI(aURI, aGUID, aReason) {
         currentTest.receivedNotifications++;
         // Check this uri was not bookmarked.
         Assert.equal(currentTest.bookmarks.indexOf(aURI.spec), -1);
         do_check_valid_places_guid(aGUID);
         Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED);
       },
--- a/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js
+++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js
@@ -97,17 +97,16 @@ add_task(async function test_notificatio
       currentTest.bookmarks.push(page);
     }
 
     // Observe history.
     let historyObserver = {
       onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {},
       onEndUpdateBatch: function PEX_onEndUpdateBatch() {},
       onClearHistory() {},
-      onVisits() {},
       onTitleChanged() {},
       onDeleteURI(aURI, aGUID, aReason) {
         // Check this uri was not bookmarked.
         Assert.equal(currentTest.bookmarks.indexOf(aURI.spec), -1);
         do_check_valid_places_guid(aGUID);
         Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED);
       },
       onPageChanged() {},
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -729,17 +729,16 @@ NavBookmarkObserver.prototype = {
  * Generic nsINavHistoryObserver that doesn't implement anything, but provides
  * dummy methods to prevent errors about an object not having a certain method.
  */
 function NavHistoryObserver() {}
 
 NavHistoryObserver.prototype = {
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
-  onVisits() {},
   onTitleChanged() {},
   onDeleteURI() {},
   onClearHistory() {},
   onPageChanged() {},
   onDeleteVisits() {},
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsINavHistoryObserver,
   ])
--- a/toolkit/components/places/tests/history/test_async_history_api.js
+++ b/toolkit/components/places/tests/history/test_async_history_api.js
@@ -75,58 +75,57 @@ TitleChangedObserver.prototype = {
     Assert.equal(aTitle, this.expectedTitle);
     do_check_guid_for_uri(aURI, aGUID);
     this.callback();
   },
 };
 
 /**
  * Listens for a visit notification, and calls aCallback when it gets it.
- *
- * @param aURI
- *        The URI of the page we expect a notification for.
- * @param aCallback
- *        The method to call when we have gotten the proper notification about
- *        being visited.
  */
-function VisitObserver(aURI,
-                       aGUID,
-                       aCallback) {
-  this.uri = aURI;
-  this.guid = aGUID;
-  this.callback = aCallback;
-}
-VisitObserver.prototype = {
-  __proto__: NavHistoryObserver.prototype,
-  onVisits(aVisits) {
-    info("onVisits()!!!");
-    Assert.equal(aVisits.length, 1, "Right number of visits notified");
+class VisitObserver {
+  constructor(aURI,
+              aGUID,
+              aCallback) {
+    this.uri = aURI;
+    this.guid = aGUID;
+    this.callback = aCallback;
+    this.handlePlacesEvent = this.handlePlacesEvent.bind(this);
+    PlacesObservers.addListener(["page-visited"], this.handlePlacesEvent);
+  }
+
+  handlePlacesEvent(aEvents) {
+    info("'page-visited'!!!");
+    Assert.equal(aEvents.length, 1, "Right number of visits notified");
+    Assert.equal(aEvents[0].type, "page-visited");
     let {
-      uri,
+      url,
       visitId,
-      time,
-      referrerId,
+      visitTime,
+      referringVisitId,
       transitionType,
-      guid,
+      pageGuid,
       hidden,
       visitCount,
-      typed,
+      typedCount,
       lastKnownTitle,
-    } = aVisits[0];
+    } = aEvents[0];
     let args = [
-      visitId, time, referrerId, transitionType, guid,
-      hidden, visitCount, typed, lastKnownTitle,
+      visitId, visitTime, referringVisitId, transitionType, pageGuid,
+      hidden, visitCount, typedCount, lastKnownTitle,
     ];
-    info("onVisit(" + uri.spec + args.join(", ") + ")");
-    if (!this.uri.equals(uri) || this.guid != guid) {
+    info("'page-visited' (" + url + args.join(", ") + ")");
+    if (this.uri.spec != url || this.guid != pageGuid) {
       return;
     }
-    this.callback(time, transitionType, lastKnownTitle);
-  },
-};
+    this.callback(visitTime * 1000, transitionType, lastKnownTitle);
+
+    PlacesObservers.removeListener(["page-visited"], this.handlePlacesEvent);
+  }
+}
 
 /**
  * Tests that a title was set properly in the database.
  *
  * @param aURI
  *        The uri to check.
  * @param aTitle
  *        The expected title in the database.
@@ -979,25 +978,25 @@ add_task(async function test_title_chang
         PlacesUtils.history.removeObserver(titleChangeObserver);
         resolve();
       }
     });
     PlacesUtils.history.addObserver(titleChangeObserver);
   });
 
   let visitPromise = new Promise(resolve => {
-    PlacesUtils.history.addObserver({
-      onVisits(visits) {
-        Assert.equal(visits.length, 1, "Should only get notified for one visit.");
-        let {uri} = visits[0];
-        Assert.equal(uri.spec, place.uri.spec, "Should get notified for visiting the new URI.");
-        PlacesUtils.history.removeObserver(this);
-        resolve();
-      }
-    });
+    function onVisits(events) {
+      Assert.equal(events.length, 1, "Should only get notified for one visit.");
+      Assert.equal(events[0].type, "page-visited");
+      let {url} = events[0];
+      Assert.equal(url, place.uri.spec, "Should get notified for visiting the new URI.");
+      PlacesObservers.removeListener(["page-visited"], onVisits);
+      resolve();
+    }
+    PlacesObservers.addListener(["page-visited"], onVisits);
   });
   asyncHistory.updatePlaces(place);
   await visitPromise;
 
   // The third case to test is to make sure we get a notification when
   // we change an existing place.
   expectedNotification = true;
   titleChangeObserver.expectedTitle = place.title = "title 2";
@@ -1023,27 +1022,25 @@ add_task(async function test_visit_notif
   function promiseVisitObserver(aPlace) {
     return new Promise((resolve, reject) => {
       let callbackCount = 0;
       let finisher = function() {
         if (++callbackCount == 2) {
           resolve();
         }
       };
-      let visitObserver = new VisitObserver(place.uri, place.guid,
+      new VisitObserver(place.uri, place.guid,
                                             function(aVisitDate,
                                                      aTransitionType) {
         let visit = place.visits[0];
         Assert.equal(visit.visitDate, aVisitDate);
         Assert.equal(visit.transitionType, aTransitionType);
 
-        PlacesUtils.history.removeObserver(visitObserver);
         finisher();
       });
-      PlacesUtils.history.addObserver(visitObserver);
       let observer = function(aSubject, aTopic, aData) {
         info("observe(" + aSubject + ", " + aTopic + ", " + aData + ")");
         Assert.ok(aSubject instanceof Ci.nsIURI);
         Assert.ok(aSubject.equals(place.uri));
 
         Services.obs.removeObserver(observer, URI_VISIT_SAVED);
         finisher();
       };
@@ -1264,69 +1261,63 @@ add_task(async function test_title_on_in
     uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_title"),
     title: "My title",
     visits: [
       new VisitInfo(),
     ],
     guid: "mnopqrstuvwx",
   };
   let visitPromise = new Promise(resolve => {
-    let visitObserver = new VisitObserver(place.uri, place.guid,
-                                          function(aVisitDate,
-                                                   aTransitionType,
-                                                   aLastKnownTitle) {
+    new VisitObserver(place.uri, place.guid,
+                      function(aVisitDate,
+                               aTransitionType,
+                               aLastKnownTitle) {
       Assert.equal(place.title, aLastKnownTitle);
 
-      PlacesUtils.history.removeObserver(visitObserver);
       resolve();
     });
-    PlacesUtils.history.addObserver(visitObserver);
   });
   await promiseUpdatePlaces(place);
   await visitPromise;
 
   // Now check an empty title doesn't get reported as null
   place = {
     uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_title"),
     title: "",
     visits: [
       new VisitInfo(),
     ],
     guid: "fghijklmnopq",
   };
   visitPromise = new Promise(resolve => {
-    let visitObserver = new VisitObserver(place.uri, place.guid,
-                                          function(aVisitDate,
-                                                   aTransitionType,
-                                                   aLastKnownTitle) {
+    new VisitObserver(place.uri, place.guid,
+                      function(aVisitDate,
+                               aTransitionType,
+                               aLastKnownTitle) {
       Assert.equal(place.title, aLastKnownTitle);
 
-      PlacesUtils.history.removeObserver(visitObserver);
       resolve();
     });
-    PlacesUtils.history.addObserver(visitObserver);
   });
   await promiseUpdatePlaces(place);
   await visitPromise;
 
   // and that a missing title correctly gets reported as null.
   place = {
     uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_title"),
     visits: [
       new VisitInfo(),
     ],
     guid: "fghijklmnopq",
   };
   visitPromise = new Promise(resolve => {
-    let visitObserver = new VisitObserver(place.uri, place.guid,
-                                          function(aVisitDate,
-                                                   aTransitionType,
-                                                   aLastKnownTitle) {
+    new VisitObserver(place.uri, place.guid,
+                      function(aVisitDate,
+                               aTransitionType,
+                               aLastKnownTitle) {
       Assert.equal(null, aLastKnownTitle);
 
-      PlacesUtils.history.removeObserver(visitObserver);
       resolve();
     });
-    PlacesUtils.history.addObserver(visitObserver);
   });
   await promiseUpdatePlaces(place);
   await visitPromise;
 });
--- a/toolkit/components/places/tests/history/test_remove.js
+++ b/toolkit/components/places/tests/history/test_remove.js
@@ -39,19 +39,16 @@ add_task(async function test_remove_sing
     }
 
     let shouldRemove = !options.addBookmark;
     let observer;
     let promiseObserved = new Promise((resolve, reject) => {
       observer = {
         onBeginUpdateBatch() {},
         onEndUpdateBatch() {},
-        onVisits(aVisits) {
-          reject(new Error("Unexpected call to onVisits " + aVisits.length));
-        },
         onTitleChanged(aUri) {
           reject(new Error("Unexpected call to onTitleChanged " + aUri.spec));
         },
         onClearHistory() {
           reject("Unexpected call to onClearHistory");
         },
         onPageChanged(aUri) {
           reject(new Error("Unexpected call to onPageChanged " + aUri.spec));
--- a/toolkit/components/places/tests/history/test_removeByFilter.js
+++ b/toolkit/components/places/tests/history/test_removeByFilter.js
@@ -287,19 +287,16 @@ function getObserverPromise(bookmarkedUr
   if (!bookmarkedUri) {
     return { observer: null, promiseObserved: Promise.resolve() };
   }
   let observer;
   let promiseObserved = new Promise((resolve, reject) => {
     observer = {
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
-      onVisits() {
-        reject(new Error("Unexpected call to onVisits"));
-      },
       onTitleChanged(aUri) {
         reject(new Error("Unexpected call to onTitleChanged"));
       },
       onClearHistory() {
         reject(new Error("Unexpected call to onClearHistory"));
       },
       onPageChanged(aUri) {
         reject(new Error("Unexpected call to onPageChanged"));
--- a/toolkit/components/places/tests/history/test_removeVisitsByFilter.js
+++ b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js
@@ -129,19 +129,16 @@ add_task(async function test_removeVisit
         uriDeletePromises.set(removedItems[i].uri.spec, PromiseUtils.defer());
       }
     }
 
     let observer = {
       deferred: PromiseUtils.defer(),
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
-      onVisits(aVisits) {
-        this.deferred.reject(new Error("Unexpected call to onVisits " + aVisits.length));
-      },
       onTitleChanged(uri) {
         this.deferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
       },
       onClearHistory() {
         this.deferred.reject("Unexpected call to onClearHistory");
       },
       onPageChanged(uri) {
         this.deferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
--- a/toolkit/components/places/tests/unit/test_454977.js
+++ b/toolkit/components/places/tests/unit/test_454977.js
@@ -6,21 +6,21 @@
 
 // Cache actual visit_count value, filled by add_visit, used by check_results
 var visit_count = 0;
 
 // Returns the Place ID corresponding to an added visit.
 async function task_add_visit(aURI, aVisitType) {
   // Wait for a visits notification and get the visitId.
   let visitId;
-  let visitsPromise = PlacesTestUtils.waitForNotification("onVisits", visits => {
+  let visitsPromise = PlacesTestUtils.waitForNotification("page-visited", visits => {
     visitId = visits[0].visitId;
-    let {uri} = visits[0];
-    return uri.equals(aURI);
-  }, "history");
+    let {url} = visits[0];
+    return url == aURI.spec;
+  }, "places");
 
   // Add visits.
   await PlacesTestUtils.addVisits([{
     uri: aURI,
     transition: aVisitType
   }]);
 
   if (aVisitType != TRANSITION_EMBED) {
--- a/toolkit/components/places/tests/unit/test_download_history.js
+++ b/toolkit/components/places/tests/unit/test_download_history.js
@@ -15,39 +15,38 @@ const PRIVATE_URI = NetUtil.newURI("http
 
 /**
  * Waits for the first visit notification to be received.
  *
  * @param aCallback
  *        Callback function to be called with the same arguments of onVisit.
  */
 function waitForOnVisit(aCallback) {
-  let historyObserver = {
-    __proto__: NavHistoryObserver.prototype,
-    onVisits: function HO_onVisit(aVisits) {
-      Assert.equal(aVisits.length, 1, "Right number of visits notified");
-      let {
-        uri,
-        visitId,
-        time,
-        referrerId,
-        transitionType,
-        guid,
-        hidden,
-        visitCount,
-        typed,
-        lastKnownTitle,
-      } = aVisits[0];
-      PlacesUtils.history.removeObserver(this);
-      aCallback(uri, visitId, time, 0, referrerId,
-                transitionType, guid, hidden, visitCount,
-                typed, lastKnownTitle);
-    }
-  };
-  PlacesUtils.history.addObserver(historyObserver);
+  function listener(aEvents) {
+    Assert.equal(aEvents.length, 1, "Right number of visits notified");
+    Assert.equal(aEvents[0].type, "page-visited");
+    let {
+      url,
+      visitId,
+      visitTime,
+      referringVisitId,
+      transitionType,
+      pageGuid,
+      hidden,
+      visitCount,
+      typedCount,
+      lastKnownTitle,
+    } = aEvents[0];
+    PlacesObservers.removeListener(["page-visited"], listener);
+    let uriArg = NetUtil.newURI(url);
+    aCallback(uriArg, visitId, visitTime, 0, referringVisitId,
+              transitionType, pageGuid, hidden, visitCount,
+              typedCount, lastKnownTitle);
+  }
+  PlacesObservers.addListener(["page-visited"], listener);
 }
 
 /**
  * Waits for the first onDeleteURI notification to be received.
  *
  * @param aCallback
  *        Callback function to be called with the same arguments of onDeleteURI.
  */
@@ -152,44 +151,45 @@ add_task(async function test_dh_addBookm
 
     gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
   });
 });
 
 add_task(async function test_dh_addDownload_referrer() {
   // Wait for visits notification and get the visit id.
   let visitId;
-  let referrerPromise = PlacesTestUtils.waitForNotification("onVisits", visits => {
+  let referrerPromise = PlacesTestUtils.waitForNotification("page-visited", visits => {
     visitId = visits[0].visitId;
-    let {uri} = visits[0];
-    return uri.equals(REFERRER_URI);
-  }, "history");
+    let {url} = visits[0];
+    return url == REFERRER_URI.spec;
+  }, "places");
 
   await PlacesTestUtils.addVisits([{
     uri: REFERRER_URI,
     transition: Ci.nsINavHistoryService.TRANSITION_TYPED
   }]);
   await referrerPromise;
 
   // Verify results for referrer uri.
   Assert.ok(!!PlacesTestUtils.isPageInDB(REFERRER_URI));
   Assert.equal(visitId, 1);
 
   // Wait for visits notification and get the referrer Id.
   let referrerId;
-  let downloadPromise = PlacesTestUtils.waitForNotification("onVisits", visits => {
-    referrerId = visits[0].referrerId;
-    let {uri} = visits[0];
-    return uri.equals(DOWNLOAD_URI);
-  }, "history");
+  let downloadPromise = PlacesTestUtils.waitForNotification("page-visited", visits => {
+    referrerId = visits[0].referringVisitId;
+    let {url} = visits[0];
+    return url == DOWNLOAD_URI.spec;
+  }, "places");
 
   gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
   await downloadPromise;
 
   // Verify results for download uri.
+  // ensure that we receive the 'page-visited' notification before we call addDownload.
   Assert.ok(!!PlacesTestUtils.isPageInDB(DOWNLOAD_URI));
   Assert.equal(visitId, referrerId);
 
   await PlacesUtils.history.clear();
 });
 
 add_test(function test_dh_addDownload_disabledHistory() {
   waitForOnVisit(function DHAD_onVisit(aURI) {
@@ -255,17 +255,16 @@ add_test(function test_dh_details() {
     onItemAnnotationSet() {},
     onPageAnnotationRemoved() {},
     onItemAnnotationRemoved() {}
   };
 
   let historyObserver = {
     onBeginUpdateBatch() {},
     onEndUpdateBatch() {},
-    onVisits() {},
     onTitleChanged: function HO_onTitleChanged(aURI, aPageTitle) {
       if (aURI.equals(SOURCE_URI)) {
         titleSet = true;
         Assert.equal(aPageTitle, DEST_FILE_NAME);
         checkFinished();
       }
     },
     onDeleteURI() {},
--- a/toolkit/components/places/tests/unit/test_history_observer.js
+++ b/toolkit/components/places/tests/unit/test_history_observer.js
@@ -5,17 +5,16 @@
  * Generic nsINavHistoryObserver that doesn't implement anything, but provides
  * dummy methods to prevent errors about an object not having a certain method.
  */
 function NavHistoryObserver() {
 }
 NavHistoryObserver.prototype = {
   onBeginUpdateBatch() { },
   onEndUpdateBatch() { },
-  onVisits() { },
   onTitleChanged() { },
   onDeleteURI() { },
   onClearHistory() { },
   onPageChanged() { },
   onDeleteVisits() { },
   QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryObserver])
 };
 
@@ -32,110 +31,124 @@ function onNotify(callback) {
       callback.apply(this, arguments);
       resolve();
     };
     PlacesUtils.history.addObserver(obs);
   });
 }
 
 /**
+ * Registers a one-time places observer for 'page-visited',
+ * which resolves a promise on being called.
+ */
+function promiseVisitAdded(callback) {
+  return new Promise(resolve => {
+    function listener(events) {
+      PlacesObservers.removeListener(["page-visited"], listener);
+      Assert.equal(events.length, 1, "Right number of visits notified");
+      Assert.equal(events[0].type, "page-visited");
+      callback(events[0]);
+      resolve();
+    }
+    PlacesObservers.addListener(["page-visited"], listener);
+  });
+}
+
+/**
  * Asynchronous task that adds a visit to the history database.
  */
 async function task_add_visit(uri, timestamp, transition) {
   uri = uri || NetUtil.newURI("http://firefox.com/");
   timestamp = timestamp || Date.now() * 1000;
   await PlacesTestUtils.addVisits({
     uri,
     transition: transition || TRANSITION_TYPED,
     visitDate: timestamp
   });
   return [uri, timestamp];
 }
 
-add_task(async function test_onVisits() {
-  let promiseNotify = onNotify(function onVisits(aVisits) {
-    Assert.equal(aVisits.length, 1, "Right number of visits notified");
-    let visit = aVisits[0];
-    Assert.ok(visit.uri.equals(testuri));
+add_task(async function test_visitAdded() {
+  let promiseNotify = promiseVisitAdded(function(visit) {
     Assert.ok(visit.visitId > 0);
-    Assert.equal(visit.time, testtime);
-    Assert.equal(visit.referrerId, 0);
+    Assert.equal(visit.url, testuri.spec);
+    Assert.equal(visit.visitTime, testtime / 1000);
+    Assert.equal(visit.referringVisitId, 0);
     Assert.equal(visit.transitionType, TRANSITION_TYPED);
-    do_check_guid_for_uri(visit.uri, visit.guid);
+    let uri = NetUtil.newURI(visit.url);
+    do_check_guid_for_uri(uri, visit.pageGuid);
     Assert.ok(!visit.hidden);
     Assert.equal(visit.visitCount, 1);
-    Assert.equal(visit.typed, 1);
+    Assert.equal(visit.typedCount, 1);
   });
   let testuri = NetUtil.newURI("http://firefox.com/");
   let testtime = Date.now() * 1000;
   await task_add_visit(testuri, testtime);
   await promiseNotify;
 });
 
-add_task(async function test_onVisits() {
-  let promiseNotify = onNotify(function onVisits(aVisits) {
-    Assert.equal(aVisits.length, 1, "Right number of visits notified");
-    let visit = aVisits[0];
-    Assert.ok(visit.uri.equals(testuri));
+add_task(async function test_visitAdded() {
+  let promiseNotify = promiseVisitAdded(function(visit) {
     Assert.ok(visit.visitId > 0);
-    Assert.equal(visit.time, testtime);
-    Assert.equal(visit.referrerId, 0);
+    Assert.equal(visit.url, testuri.spec);
+    Assert.equal(visit.visitTime, testtime / 1000);
+    Assert.equal(visit.referringVisitId, 0);
     Assert.equal(visit.transitionType, TRANSITION_FRAMED_LINK);
-    do_check_guid_for_uri(visit.uri, visit.guid);
+    let uri = NetUtil.newURI(visit.url);
+    do_check_guid_for_uri(uri, visit.pageGuid);
     Assert.ok(visit.hidden);
     Assert.equal(visit.visitCount, 1);
-    Assert.equal(visit.typed, 0);
+    Assert.equal(visit.typedCount, 0);
   });
   let testuri = NetUtil.newURI("http://hidden.firefox.com/");
   let testtime = Date.now() * 1000;
   await task_add_visit(testuri, testtime, TRANSITION_FRAMED_LINK);
   await promiseNotify;
 });
 
 add_task(async function test_multiple_onVisit() {
   let testuri = NetUtil.newURI("http://self.firefox.com/");
   let promiseNotifications = new Promise(resolve => {
-    let observer = {
-      __proto__: NavHistoryObserver.prototype,
-      onVisits(aVisits) {
-        Assert.equal(aVisits.length, 3, "Right number of visits notified");
-        for (let i = 0; i < aVisits.length; i++) {
-          let visit = aVisits[i];
-          Assert.ok(testuri.equals(visit.uri));
-          Assert.ok(visit.visitId > 0);
-          Assert.ok(visit.time > 0);
-          Assert.ok(!visit.hidden);
-          do_check_guid_for_uri(visit.uri, visit.guid);
-          switch (i) {
-            case 0:
-              Assert.equal(visit.referrerId, 0);
-              Assert.equal(visit.transitionType, TRANSITION_LINK);
-              Assert.equal(visit.visitCount, 1);
-              Assert.equal(visit.typed, 0);
-              break;
-            case 1:
-              Assert.ok(visit.referrerId > 0);
-              Assert.equal(visit.transitionType, TRANSITION_LINK);
-              Assert.equal(visit.visitCount, 2);
-              Assert.equal(visit.typed, 0);
-              break;
-            case 2:
-              Assert.equal(visit.referrerId, 0);
-              Assert.equal(visit.transitionType, TRANSITION_TYPED);
-              Assert.equal(visit.visitCount, 3);
-              Assert.equal(visit.typed, 1);
+    function listener(aEvents) {
+      Assert.equal(aEvents.length, 3, "Right number of visits notified");
+      for (let i = 0; i < aEvents.length; i++) {
+        Assert.equal(aEvents[i].type, "page-visited");
+        let visit = aEvents[i];
+        Assert.equal(testuri.spec, visit.url);
+        Assert.ok(visit.visitId > 0);
+        Assert.ok(visit.visitTime > 0);
+        Assert.ok(!visit.hidden);
+        let uri = NetUtil.newURI(visit.url);
+        do_check_guid_for_uri(uri, visit.pageGuid);
+        switch (i) {
+          case 0:
+            Assert.equal(visit.referringVisitId, 0);
+            Assert.equal(visit.transitionType, TRANSITION_LINK);
+            Assert.equal(visit.visitCount, 1);
+            Assert.equal(visit.typedCount, 0);
+            break;
+          case 1:
+            Assert.ok(visit.referringVisitId > 0);
+            Assert.equal(visit.transitionType, TRANSITION_LINK);
+            Assert.equal(visit.visitCount, 2);
+            Assert.equal(visit.typedCount, 0);
+            break;
+          case 2:
+            Assert.equal(visit.referringVisitId, 0);
+            Assert.equal(visit.transitionType, TRANSITION_TYPED);
+            Assert.equal(visit.visitCount, 3);
+            Assert.equal(visit.typedCount, 1);
 
-              PlacesUtils.history.removeObserver(observer, false);
-              resolve();
-              break;
-          }
+            PlacesObservers.removeListener(["page-visited"], listener);
+            resolve();
+            break;
         }
-      },
-    };
-    PlacesUtils.history.addObserver(observer);
+      }
+    }
+    PlacesObservers.addListener(["page-visited"], listener);
   });
   await PlacesTestUtils.addVisits([
     { uri: testuri, transition: TRANSITION_LINK },
     { uri: testuri, referrer: testuri, transition: TRANSITION_LINK },
     { uri: testuri, transition: TRANSITION_TYPED },
   ]);
   await promiseNotifications;
 });
--- a/toolkit/components/places/tests/unit/test_markpageas.js
+++ b/toolkit/components/places/tests/unit/test_markpageas.js
@@ -7,53 +7,34 @@
 var gVisits = [{url: "http://www.mozilla.com/",
                 transition: TRANSITION_TYPED},
                {url: "http://www.google.com/",
                 transition: TRANSITION_BOOKMARK},
                {url: "http://www.espn.com/",
                 transition: TRANSITION_LINK}];
 
 add_task(async function test_execute() {
-  let observer;
   let completionPromise = new Promise(resolveCompletionPromise => {
-    observer = {
-      __proto__: NavHistoryObserver.prototype,
-      _visitCount: 0,
-      onVisit(aURI, aVisitID, aTime, aSessionID, aReferringID,
-                        aTransitionType, aAdded) {
-        Assert.equal(aURI.spec, gVisits[this._visitCount].url);
-        Assert.equal(aTransitionType, gVisits[this._visitCount].transition);
-        this._visitCount++;
+    let visitCount = 0;
+    function listener(aEvents) {
+      Assert.equal(aEvents.length, 1, "Right number of visits notified");
+      Assert.equal(aEvents[0].type, "page-visited");
+      let event = aEvents[0];
+      Assert.equal(event.url, gVisits[visitCount].url);
+      Assert.equal(event.transitionType, gVisits[visitCount].transition);
+      visitCount++;
 
-        if (this._visitCount == gVisits.length) {
-          resolveCompletionPromise();
-        }
-      },
-      onVisits(aVisits) {
-        Assert.equal(aVisits.length, 1, "Right number of visits notified");
-        let {
-          uri,
-          visitId,
-          time,
-          referrerId,
-          transitionType,
-          guid,
-          hidden,
-          visitCount,
-          typed,
-          lastKnownTitle,
-        } = aVisits[0];
-        this.onVisit(uri, visitId, time, 0, referrerId,
-                     transitionType, guid, hidden, visitCount,
-                     typed, lastKnownTitle);
-      },
-    };
+      if (visitCount == gVisits.length) {
+        resolveCompletionPromise();
+        PlacesObservers.removeListener(["page-visited"], listener);
+      }
+    }
+    PlacesObservers.addListener(["page-visited"], listener);
   });
 
-  PlacesUtils.history.addObserver(observer);
 
   for (var visit of gVisits) {
     if (visit.transition == TRANSITION_TYPED)
       PlacesUtils.history.markPageAsTyped(uri(visit.url));
     else if (visit.transition == TRANSITION_BOOKMARK)
       PlacesUtils.history.markPageAsFollowedBookmark(uri(visit.url));
     else {
      // because it is a top level visit with no referrer,
@@ -62,10 +43,9 @@ add_task(async function test_execute() {
     await PlacesTestUtils.addVisits({
       uri: uri(visit.url),
       transition: visit.transition
     });
   }
 
   await completionPromise;
 
-  PlacesUtils.history.removeObserver(observer);
 });