Bug 1239116 - Places messages enabled for remote newtab r=rrosario
authorOlivier Yiptong <olivier@olivieryiptong.com>
Thu, 10 Mar 2016 16:39:04 -0500
changeset 291885 ce7ed1d0b81b7b49c2ff00b067f07492124aa76f
parent 291884 d34db6d094d874e0f54bd61c39d0433785c71443
child 291886 d924e3a25a7da65bb192924070d2d453bf710a90
push id74710
push userolivier@olivieryiptong.com
push dateWed, 06 Apr 2016 13:47:03 +0000
treeherdermozilla-inbound@ce7ed1d0b81b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrrosario
bugs1239116
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1239116 - Places messages enabled for remote newtab r=rrosario MozReview-Commit-ID: FUXUcVckh7A
browser/components/newtab/NewTabMessages.jsm
browser/components/newtab/PlacesProvider.jsm
browser/components/newtab/tests/browser/browser.ini
browser/components/newtab/tests/browser/browser_newtabmessages.js
browser/components/newtab/tests/browser/newtabmessages_places.html
browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
--- a/browser/components/newtab/NewTabMessages.jsm
+++ b/browser/components/newtab/NewTabMessages.jsm
@@ -1,71 +1,93 @@
 /*global
   NewTabWebChannel,
   NewTabPrefsProvider,
+  PlacesProvider,
   Preferences,
   XPCOMUtils
 */
 
 /* exported NewTabMessages */
 
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesProvider",
+                                  "resource:///modules/PlacesProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PreviewProvider",
                                   "resource:///modules/PreviewProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
                                   "resource:///modules/NewTabPrefsProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
                                   "resource:///modules/NewTabWebChannel.jsm");
 
 this.EXPORTED_SYMBOLS = ["NewTabMessages"];
 
 const PREF_ENABLED = "browser.newtabpage.remote";
 
 // Action names are from the content's perspective. in from chrome == out from content
 // Maybe replace the ACTION objects by a bi-directional Map a bit later?
 const ACTIONS = {
+  inboundActions: [
+    "REQUEST_PREFS",
+    "REQUEST_THUMB",
+    "REQUEST_FRECENT",
+  ],
   prefs: {
     inPrefs: "REQUEST_PREFS",
     outPrefs: "RECEIVE_PREFS",
-    action_types: new Set(["REQUEST_PREFS"]),
   },
   preview: {
     inThumb: "REQUEST_THUMB",
     outThumb: "RECEIVE_THUMB",
-    action_types: new Set(["REQUEST_THUMB"]),
+  },
+  links: {
+    inFrecent: "REQUEST_FRECENT",
+    outFrecent: "RECEIVE_FRECENT",
+    outPlacesChange: "RECEIVE_PLACES_CHANGE",
   },
 };
 
 let NewTabMessages = {
 
   _prefs: {},
 
   /** NEWTAB EVENT HANDLERS **/
 
-  /*
-   * Return to the originator all newtabpage prefs. A point-to-point request.
-   */
-  handlePrefRequest(actionName, {target}) {
-    if (ACTIONS.prefs.inPrefs === actionName) {
-      let results = NewTabPrefsProvider.prefs.newtabPagePrefs;
-      NewTabWebChannel.send(ACTIONS.prefs.outPrefs, results, target);
+  handleContentRequest(actionName, {data, target}) {
+    switch (actionName) {
+      case ACTIONS.prefs.inPrefs:
+        // Return to the originator all newtabpage prefs
+        let results = NewTabPrefsProvider.prefs.newtabPagePrefs;
+        NewTabWebChannel.send(ACTIONS.prefs.outPrefs, results, target);
+        break;
+      case ACTIONS.preview.inThumb:
+        // Return to the originator a preview URL
+        PreviewProvider.getThumbnail(data).then(imgData => {
+          NewTabWebChannel.send(ACTIONS.preview.outThumb, {url: data, imgData}, target);
+        });
+        break;
+      case ACTIONS.links.inFrecent:
+        // Return to the originator the top frecent links
+        PlacesProvider.links.getLinks().then(links => {
+          NewTabWebChannel.send(ACTIONS.links.outFrecent, links, target);
+        });
+        break;
     }
   },
 
-  handlePreviewRequest(actionName, {data, target}) {
-    if (ACTIONS.preview.inThumb === actionName) {
-      PreviewProvider.getThumbnail(data).then(imgData => {
-        NewTabWebChannel.send(ACTIONS.preview.outThumb, {url: data, imgData}, target);
-      });
-    }
+  /*
+   * Broadcast places change to all open newtab pages
+   */
+  handlePlacesChange(type, data) {
+    NewTabWebChannel.broadcast(ACTIONS.links.outPlacesChange, {type, data});
   },
 
   /*
    * Broadcast preference changes to all open newtab pages
    */
   handlePrefChange(actionName, value) {
     let prefChange = {};
     prefChange[actionName] = value;
@@ -78,47 +100,54 @@ let NewTabMessages = {
         this.uninit();
       } else if (!this._prefs.enabled && value) {
         this.init();
       }
     }
   },
 
   init() {
-    this.handlePrefRequest = this.handlePrefRequest.bind(this);
-    this.handlePreviewRequest = this.handlePreviewRequest.bind(this);
-    this.handlePrefChange = this.handlePrefChange.bind(this);
+    this.handleContentRequest = this.handleContentRequest.bind(this);
     this._handleEnabledChange = this._handleEnabledChange.bind(this);
 
+    PlacesProvider.links.init();
     NewTabPrefsProvider.prefs.init();
     NewTabWebChannel.init();
 
     this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
 
     if (this._prefs.enabled) {
-      NewTabWebChannel.on(ACTIONS.prefs.inPrefs, this.handlePrefRequest);
-      NewTabWebChannel.on(ACTIONS.preview.inThumb, this.handlePreviewRequest);
-
+      for (let action of ACTIONS.inboundActions) {
+        NewTabWebChannel.on(action, this.handleContentRequest);
+      }
       NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handleEnabledChange);
 
       for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
         NewTabPrefsProvider.prefs.on(pref, this.handlePrefChange);
       }
+
+      PlacesProvider.links.on("deleteURI", this.handlePlacesChange);
+      PlacesProvider.links.on("clearHistory", this.handlePlacesChange);
+      PlacesProvider.links.on("linkChanged", this.handlePlacesChange);
+      PlacesProvider.links.on("manyLinksChanged", this.handlePlacesChange);
     }
   },
 
   uninit() {
     this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
 
     if (this._prefs.enabled) {
       NewTabPrefsProvider.prefs.off(PREF_ENABLED, this._handleEnabledChange);
 
-      NewTabWebChannel.off(ACTIONS.prefs.inPrefs, this.handlePrefRequest);
-      NewTabWebChannel.off(ACTIONS.prefs.inThumb, this.handlePreviewRequest);
+      for (let action of ACTIONS.inboundActions) {
+        NewTabWebChannel.off(action, this.handleContentRequest);
+      }
+
       for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
         NewTabPrefsProvider.prefs.off(pref, this.handlePrefChange);
       }
     }
 
+    PlacesProvider.links.uninit();
     NewTabPrefsProvider.prefs.uninit();
     NewTabWebChannel.uninit();
   }
 };
--- a/browser/components/newtab/PlacesProvider.jsm
+++ b/browser/components/newtab/PlacesProvider.jsm
@@ -1,27 +1,24 @@
 /* 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/. */
 
-/* global XPCOMUtils, Services, BinarySearch, PlacesUtils, gPrincipal, EventEmitter */
+/* global XPCOMUtils, Services, PlacesUtils, gPrincipal, EventEmitter */
 /* global gLinks */
 /* exported PlacesProvider */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["PlacesProvider"];
 
 const {interfaces: Ci, utils: Cu} = Components;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
-  "resource://gre/modules/BinarySearch.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
   const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
   return EventEmitter;
 });
 
@@ -135,24 +132,32 @@ Links.prototype = {
                         Ci.nsISupportsWeakReference])
   },
 
   /**
    * Must be called before the provider is used.
    * Makes it easy to disable under pref
    */
   init: function PlacesProvider_init() {
-    PlacesUtils.history.addObserver(this.historyObserver, true);
+    try {
+      PlacesUtils.history.addObserver(this.historyObserver, true);
+    } catch (e) {
+      Cu.reportError(e);
+    }
   },
 
   /**
    * Must be called before the provider is unloaded.
    */
-  destroy: function PlacesProvider_destroy() {
-    PlacesUtils.history.removeObserver(this.historyObserver);
+  uninit: function PlacesProvider_uninit() {
+    try {
+      PlacesUtils.history.removeObserver(this.historyObserver);
+    } catch (e) {
+      Cu.reportError(e);
+    }
   },
 
   /**
    * Gets the current set of links delivered by this provider.
    *
    * @returns {Promise} Returns a promise with the array of links as payload.
    */
   getLinks: Task.async(function*() {
--- a/browser/components/newtab/tests/browser/browser.ini
+++ b/browser/components/newtab/tests/browser/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 support-files =
   blue_page.html
   dummy_page.html
   newtabwebchannel_basic.html
+  newtabmessages_places.html
   newtabmessages_prefs.html
   newtabmessages_preview.html
 
 [browser_PreviewProvider.js]
 [browser_remotenewtab_pageloads.js]
 [browser_newtab_overrides.js]
 [browser_newtabmessages.js]
 [browser_newtabwebchannel.js]
--- a/browser/components/newtab/tests/browser/browser_newtabmessages.js
+++ b/browser/components/newtab/tests/browser/browser_newtabmessages.js
@@ -1,17 +1,19 @@
-/* globals Cu, XPCOMUtils, Preferences, is, registerCleanupFunction, NewTabWebChannel */
+/* globals Cu, XPCOMUtils, Preferences, is, registerCleanupFunction, NewTabWebChannel, PlacesTestUtils */
 
 "use strict";
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
                                   "resource:///modules/NewTabWebChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabMessages",
                                   "resource:///modules/NewTabMessages.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+                                  "resource://testing-common/PlacesTestUtils.jsm");
 
 function setup() {
   Preferences.set("browser.newtabpage.enhanced", true);
   Preferences.set("browser.newtabpage.remote.mode", "test");
   Preferences.set("browser.newtabpage.remote", true);
   NewTabMessages.init();
 }
 
@@ -78,8 +80,81 @@ add_task(function* previewMessages_reque
   });
 
   yield BrowserTestUtils.withNewTab(tabOptions, function*() {
     yield previewResponseAck;
   });
   cleanup();
   Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref);
 });
+
+/*
+ * Sanity tests for places messages
+ */
+add_task(function* placesMessages_request() {
+  setup();
+  let testURL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabmessages_places.html";
+
+  // url prefix for test history population
+  const TEST_URL = "https://mozilla.com/";
+  // time when the test starts execution
+  const TIME_NOW = (new Date()).getTime();
+
+  // utility function to compute past timestamp
+  function timeDaysAgo(numDays) {
+    return TIME_NOW - (numDays * 24 * 60 * 60 * 1000);
+  }
+
+  // utility function to make a visit for insertion into places db
+  function makeVisit(index, daysAgo, isTyped, domain=TEST_URL) {
+    let {
+      TRANSITION_TYPED,
+      TRANSITION_LINK
+    } = PlacesUtils.history;
+
+    return {
+      uri: NetUtil.newURI(`${domain}${index}`),
+      visitDate: timeDaysAgo(daysAgo),
+      transition: (isTyped) ? TRANSITION_TYPED : TRANSITION_LINK,
+    };
+  }
+
+  yield PlacesTestUtils.clearHistory();
+
+  // all four visits must come from different domains to avoid deduplication
+  let visits = [
+    makeVisit(0, 0, true, "http://bar.com/"), // frecency 200, today
+    makeVisit(1, 0, true, "http://foo.com/"), // frecency 200, today
+    makeVisit(2, 2, true, "http://buz.com/"), // frecency 200, 2 days ago
+    makeVisit(3, 2, false, "http://aaa.com/"), // frecency 10, 2 days ago, transition
+  ];
+
+  yield PlacesTestUtils.addVisits(visits);
+
+  /** Test Begins **/
+
+  let tabOptions = {
+    gBrowser,
+    url: testURL
+  };
+
+  let placesResponseAck = new Promise(resolve => {
+    NewTabWebChannel.once("numItemsAck", (_, msg) => {
+      ok(true, "a request response has been received");
+      is(msg.data, visits.length + 1, "received an expect number of history items");
+      resolve();
+    });
+  });
+
+  yield BrowserTestUtils.withNewTab(tabOptions, function*() {
+    yield placesResponseAck;
+    let placesChangeAck = new Promise(resolve => {
+      NewTabWebChannel.once("clearHistoryAck", (_, msg) => {
+        ok(true, "a change response has been received");
+        is(msg.data, "clearHistory", "a clear history message has been received");
+        resolve();
+      });
+    });
+    yield PlacesTestUtils.clearHistory();
+    yield placesChangeAck;
+  });
+  cleanup();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/browser/newtabmessages_places.html
@@ -0,0 +1,49 @@
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>Newtab WebChannel test</title>
+  </head>
+  <body>
+    <script>
+      window.addEventListener("WebChannelMessageToContent", function(e) {
+        if (e.detail.message) {
+          let reply;
+          switch (e.detail.message.type) {
+            case "RECEIVE_FRECENT":
+              reply = new window.CustomEvent("WebChannelMessageToChrome", {
+                detail: {
+                  id: "newtab",
+                  message: JSON.stringify({type: "numItemsAck", data: e.detail.message.data.length}),
+                }
+              });
+              window.dispatchEvent(reply);
+              break;
+            case "RECEIVE_PLACES_CHANGE":
+              if (e.detail.message.data.type === "clearHistory") {
+                reply = new window.CustomEvent("WebChannelMessageToChrome", {
+                  detail: {
+                    id: "newtab",
+                    message: JSON.stringify({type: "clearHistoryAck", data: e.detail.message.data.type}),
+                  }
+                });
+                window.dispatchEvent(reply);
+              }
+              break;
+          }
+        }
+      }, true);
+
+      document.onreadystatechange = function () {
+        if (document.readyState === "complete") {
+          let msg = new window.CustomEvent("WebChannelMessageToChrome", {
+            detail: {
+              id: "newtab",
+              message: JSON.stringify({type: "REQUEST_FRECENT"}),
+            }
+          });
+          window.dispatchEvent(msg);
+        }
+      }
+    </script>
+  </body>
+</html>
--- a/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
+++ b/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
@@ -165,17 +165,17 @@ add_task(function* test_Links_onLinkChan
   });
 
   // add a visit
   let testURI = NetUtil.newURI(url);
   yield PlacesTestUtils.addVisits(testURI);
   yield linkChangedPromise;
 
   yield PlacesTestUtils.clearHistory();
-  provider.destroy();
+  provider.uninit();
 });
 
 add_task(function* test_Links_onClearHistory() {
   let provider = PlacesProvider.links;
   provider.init();
 
   let clearHistoryPromise = new Promise(resolve => {
     let handler = () => {
@@ -189,17 +189,17 @@ add_task(function* test_Links_onClearHis
   // add visits
   for (let i = 0; i <= 10; i++) {
     let url = `https://example.com/onClearHistory${i}`;
     let testURI = NetUtil.newURI(url);
     yield PlacesTestUtils.addVisits(testURI);
   }
   yield PlacesTestUtils.clearHistory();
   yield clearHistoryPromise;
-  provider.destroy();
+  provider.uninit();
 });
 
 add_task(function* test_Links_onDeleteURI() {
   let provider = PlacesProvider.links;
   provider.init();
 
   let testURL = "https://example.com/toDelete";
 
@@ -212,17 +212,17 @@ add_task(function* test_Links_onDeleteUR
 
     provider.on("deleteURI", handler);
   });
 
   let testURI = NetUtil.newURI(testURL);
   yield PlacesTestUtils.addVisits(testURI);
   yield PlacesUtils.history.remove(testURL);
   yield deleteURIPromise;
-  provider.destroy();
+  provider.uninit();
 });
 
 add_task(function* test_Links_onManyLinksChanged() {
   let provider = PlacesProvider.links;
   provider.init();
 
   let promise = new Promise(resolve => {
     let handler = () => {
@@ -238,17 +238,17 @@ add_task(function* test_Links_onManyLink
   let testURI = NetUtil.newURI(testURL);
   yield PlacesTestUtils.addVisits(testURI);
 
   // trigger DecayFrecency
   PlacesUtils.history.QueryInterface(Ci.nsIObserver).
     observe(null, "idle-daily", "");
 
   yield promise;
-  provider.destroy();
+  provider.uninit();
 });
 
 add_task(function* test_Links_execute_query() {
   yield PlacesTestUtils.clearHistory();
   let provider = PlacesProvider.links;
 
   let visits = [
     makeVisit(0, 0, true), // frecency 200, today