summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab')
-rw-r--r--browser/components/newtab/NewTabComponents.manifest2
-rw-r--r--browser/components/newtab/NewTabMessages.jsm242
-rw-r--r--browser/components/newtab/NewTabPrefsProvider.jsm114
-rw-r--r--browser/components/newtab/NewTabRemoteResources.jsm15
-rw-r--r--browser/components/newtab/NewTabSearchProvider.jsm103
-rw-r--r--browser/components/newtab/NewTabURL.jsm36
-rw-r--r--browser/components/newtab/NewTabWebChannel.jsm299
-rw-r--r--browser/components/newtab/PlacesProvider.jsm211
-rw-r--r--browser/components/newtab/PreviewProvider.jsm49
-rw-r--r--browser/components/newtab/aboutNewTabService.js289
-rw-r--r--browser/components/newtab/moz.build33
-rw-r--r--browser/components/newtab/nsIAboutNewTabService.idl63
-rw-r--r--browser/components/newtab/tests/browser/.eslintrc.js7
-rw-r--r--browser/components/newtab/tests/browser/blue_page.html9
-rw-r--r--browser/components/newtab/tests/browser/browser.ini16
-rw-r--r--browser/components/newtab/tests/browser/browser_PreviewProvider.js90
-rw-r--r--browser/components/newtab/tests/browser/browser_newtab_overrides.js139
-rw-r--r--browser/components/newtab/tests/browser/browser_newtabmessages.js222
-rw-r--r--browser/components/newtab/tests/browser/browser_newtabwebchannel.js251
-rw-r--r--browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js52
-rw-r--r--browser/components/newtab/tests/browser/dummy_page.html10
-rw-r--r--browser/components/newtab/tests/browser/newtabmessages_places.html49
-rw-r--r--browser/components/newtab/tests/browser/newtabmessages_prefs.html32
-rw-r--r--browser/components/newtab/tests/browser/newtabmessages_preview.html37
-rw-r--r--browser/components/newtab/tests/browser/newtabmessages_search.html113
-rw-r--r--browser/components/newtab/tests/browser/newtabwebchannel_basic.html36
-rw-r--r--browser/components/newtab/tests/xpcshell/.eslintrc.js7
-rw-r--r--browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js236
-rw-r--r--browser/components/newtab/tests/xpcshell/test_NewTabPrefsProvider.js50
-rw-r--r--browser/components/newtab/tests/xpcshell/test_NewTabSearchProvider.js82
-rw-r--r--browser/components/newtab/tests/xpcshell/test_NewTabURL.js52
-rw-r--r--browser/components/newtab/tests/xpcshell/test_PlacesProvider.js358
-rw-r--r--browser/components/newtab/tests/xpcshell/xpcshell.ini11
33 files changed, 3315 insertions, 0 deletions
diff --git a/browser/components/newtab/NewTabComponents.manifest b/browser/components/newtab/NewTabComponents.manifest
new file mode 100644
index 000000000..42db65acd
--- /dev/null
+++ b/browser/components/newtab/NewTabComponents.manifest
@@ -0,0 +1,2 @@
+component {dfcd2adc-7867-4d3a-ba70-17501f208142} aboutNewTabService.js
+contract @mozilla.org/browser/aboutnewtab-service;1 {dfcd2adc-7867-4d3a-ba70-17501f208142}
diff --git a/browser/components/newtab/NewTabMessages.jsm b/browser/components/newtab/NewTabMessages.jsm
new file mode 100644
index 000000000..0816ed65a
--- /dev/null
+++ b/browser/components/newtab/NewTabMessages.jsm
@@ -0,0 +1,242 @@
+/* global
+ NewTabWebChannel,
+ NewTabPrefsProvider,
+ PlacesProvider,
+ PreviewProvider,
+ NewTabSearchProvider,
+ Preferences,
+ XPCOMUtils,
+ Task
+*/
+
+/* exported NewTabMessages */
+
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.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, "NewTabSearchProvider",
+ "resource:///modules/NewTabSearchProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
+ "resource:///modules/NewTabWebChannel.jsm");
+
+this.EXPORTED_SYMBOLS = ["NewTabMessages"];
+
+const PREF_ENABLED = "browser.newtabpage.remote";
+const CURRENT_ENGINE = "browser-search-engine-modified";
+
+// 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",
+ "REQUEST_UISTRINGS",
+ "REQUEST_SEARCH_SUGGESTIONS",
+ "REQUEST_MANAGE_ENGINES",
+ "REQUEST_SEARCH_STATE",
+ "REQUEST_REMOVE_FORM_HISTORY",
+ "REQUEST_PERFORM_SEARCH",
+ "REQUEST_CYCLE_ENGINE",
+ ],
+ prefs: {
+ inPrefs: "REQUEST_PREFS",
+ outPrefs: "RECEIVE_PREFS",
+ },
+ preview: {
+ inThumb: "REQUEST_THUMB",
+ outThumb: "RECEIVE_THUMB",
+ },
+ links: {
+ inFrecent: "REQUEST_FRECENT",
+ outFrecent: "RECEIVE_FRECENT",
+ outPlacesChange: "RECEIVE_PLACES_CHANGE",
+ },
+ search: {
+ inSearch: {
+ UIStrings: "REQUEST_UISTRINGS",
+ suggestions: "REQUEST_SEARCH_SUGGESTIONS",
+ manageEngines: "REQUEST_MANAGE_ENGINES",
+ state: "REQUEST_SEARCH_STATE",
+ removeFormHistory: "REQUEST_REMOVE_FORM_HISTORY",
+ performSearch: "REQUEST_PERFORM_SEARCH",
+ cycleEngine: "REQUEST_CYCLE_ENGINE"
+ },
+ outSearch: {
+ UIStrings: "RECEIVE_UISTRINGS",
+ suggestions: "RECEIVE_SEARCH_SUGGESTIONS",
+ state: "RECEIVE_SEARCH_STATE",
+ currentEngine: "RECEIVE_CURRENT_ENGINE"
+ },
+ }
+};
+
+let NewTabMessages = {
+
+ _prefs: {},
+
+ /** NEWTAB EVENT HANDLERS **/
+
+ 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;
+ case ACTIONS.search.inSearch.UIStrings:
+ // Return to the originator all search strings to display
+ let strings = NewTabSearchProvider.search.searchSuggestionUIStrings;
+ NewTabWebChannel.send(ACTIONS.search.outSearch.UIStrings, strings, target);
+ break;
+ case ACTIONS.search.inSearch.suggestions:
+ // Return to the originator all search suggestions
+ Task.spawn(function*() {
+ try {
+ let {engineName, searchString} = data;
+ let suggestions = yield NewTabSearchProvider.search.asyncGetSuggestions(engineName, searchString, target);
+ NewTabWebChannel.send(ACTIONS.search.outSearch.suggestions, suggestions, target);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ });
+ break;
+ case ACTIONS.search.inSearch.manageEngines:
+ // Open about:preferences to manage search state
+ NewTabSearchProvider.search.manageEngines(target.browser);
+ break;
+ case ACTIONS.search.inSearch.state:
+ // Return the state of the search component (i.e current engine and visible engine details)
+ Task.spawn(function*() {
+ try {
+ let state = yield NewTabSearchProvider.search.asyncGetState();
+ NewTabWebChannel.broadcast(ACTIONS.search.outSearch.state, state);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ });
+ break;
+ case ACTIONS.search.inSearch.removeFormHistory:
+ // Remove a form history entry from the search component
+ let suggestion = data;
+ NewTabSearchProvider.search.removeFormHistory(target, suggestion);
+ break;
+ case ACTIONS.search.inSearch.performSearch:
+ // Perform a search
+ NewTabSearchProvider.search.asyncPerformSearch(target, data).catch(Cu.reportError);
+ break;
+ case ACTIONS.search.inSearch.cycleEngine:
+ // Set the new current engine
+ NewTabSearchProvider.search.asyncCycleEngine(data).catch(Cu.reportError);
+ break;
+ }
+ },
+
+ /*
+ * Broadcast places change to all open newtab pages
+ */
+ handlePlacesChange(type, data) {
+ NewTabWebChannel.broadcast(ACTIONS.links.outPlacesChange, {type, data});
+ },
+
+ /*
+ * Broadcast current engine has changed to all open newtab pages
+ */
+ _handleCurrentEngineChange(name, value) { // jshint unused: false
+ let engine = value;
+ NewTabWebChannel.broadcast(ACTIONS.search.outSearch.currentEngine, engine);
+ },
+
+ /*
+ * Broadcast preference changes to all open newtab pages
+ */
+ handlePrefChange(actionName, value) {
+ let prefChange = {};
+ prefChange[actionName] = value;
+ NewTabWebChannel.broadcast(ACTIONS.prefs.outPrefs, prefChange);
+ },
+
+ _handleEnabledChange(prefName, value) {
+ if (prefName === PREF_ENABLED) {
+ if (this._prefs.enabled && !value) {
+ this.uninit();
+ } else if (!this._prefs.enabled && value) {
+ this.init();
+ }
+ }
+ },
+
+ init() {
+ this.handleContentRequest = this.handleContentRequest.bind(this);
+ this._handleEnabledChange = this._handleEnabledChange.bind(this);
+ this._handleCurrentEngineChange = this._handleCurrentEngineChange.bind(this);
+
+ PlacesProvider.links.init();
+ NewTabPrefsProvider.prefs.init();
+ NewTabSearchProvider.search.init();
+ NewTabWebChannel.init();
+
+ this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
+
+ if (this._prefs.enabled) {
+ for (let action of ACTIONS.inboundActions) {
+ NewTabWebChannel.on(action, this.handleContentRequest);
+ }
+
+ NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handleEnabledChange);
+ NewTabSearchProvider.search.on(CURRENT_ENGINE, this._handleCurrentEngineChange);
+
+ 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);
+ NewTabSearchProvider.search.off(CURRENT_ENGINE, this._handleCurrentEngineChange);
+
+ for (let action of ACTIONS.inboundActions) {
+ NewTabWebChannel.off(action, this.handleContentRequest);
+ }
+
+ for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
+ NewTabPrefsProvider.prefs.off(pref, this.handlePrefChange);
+ }
+ }
+
+ NewTabPrefsProvider.prefs.uninit();
+ NewTabSearchProvider.search.uninit();
+ NewTabWebChannel.uninit();
+ }
+};
diff --git a/browser/components/newtab/NewTabPrefsProvider.jsm b/browser/components/newtab/NewTabPrefsProvider.jsm
new file mode 100644
index 000000000..c1d8b4149
--- /dev/null
+++ b/browser/components/newtab/NewTabPrefsProvider.jsm
@@ -0,0 +1,114 @@
+/* global Services, Preferences, EventEmitter, XPCOMUtils */
+/* exported NewTabPrefsProvider */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["NewTabPrefsProvider"];
+
+const {interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
+ const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+ return EventEmitter;
+});
+
+// Supported prefs and data type
+const gPrefsMap = new Map([
+ ["browser.newtabpage.remote", "bool"],
+ ["browser.newtabpage.remote.mode", "str"],
+ ["browser.newtabpage.remote.version", "str"],
+ ["browser.newtabpage.enabled", "bool"],
+ ["browser.newtabpage.enhanced", "bool"],
+ ["browser.newtabpage.introShown", "bool"],
+ ["browser.newtabpage.updateIntroShown", "bool"],
+ ["browser.newtabpage.pinned", "str"],
+ ["browser.newtabpage.blocked", "str"],
+ ["intl.locale.matchOS", "bool"],
+ ["general.useragent.locale", "localized"],
+ ["browser.search.hiddenOneOffs", "str"],
+]);
+
+// prefs that are important for the newtab page
+const gNewtabPagePrefs = new Set([
+ "browser.newtabpage.enabled",
+ "browser.newtabpage.enhanced",
+ "browser.newtabpage.pinned",
+ "browser.newtabpage.blocked",
+ "browser.newtabpage.introShown",
+ "browser.newtabpage.updateIntroShown",
+ "browser.search.hiddenOneOffs",
+]);
+
+let PrefsProvider = function PrefsProvider() {
+ EventEmitter.decorate(this);
+};
+
+PrefsProvider.prototype = {
+
+ observe(subject, topic, data) { // jshint ignore:line
+ if (topic === "nsPref:changed") {
+ if (gPrefsMap.has(data)) {
+ switch (gPrefsMap.get(data)) {
+ case "bool":
+ this.emit(data, Preferences.get(data, false));
+ break;
+ case "str":
+ this.emit(data, Preferences.get(data, ""));
+ break;
+ case "localized":
+ try {
+ this.emit(data, Preferences.get(data, "", Ci.nsIPrefLocalizedString));
+ } catch (e) {
+ this.emit(data, Preferences.get(data, ""));
+ }
+ break;
+ default:
+ this.emit(data);
+ break;
+ }
+ }
+ } else {
+ Cu.reportError(new Error("NewTabPrefsProvider observing unknown topic"));
+ }
+ },
+
+ /*
+ * Return the preferences that are important to the newtab page
+ */
+ get newtabPagePrefs() {
+ let results = {};
+ for (let pref of gNewtabPagePrefs) {
+ results[pref] = Preferences.get(pref, null);
+ }
+ return results;
+ },
+
+ get prefsMap() {
+ return gPrefsMap;
+ },
+
+ init() {
+ for (let pref of gPrefsMap.keys()) {
+ Services.prefs.addObserver(pref, this, false);
+ }
+ },
+
+ uninit() {
+ for (let pref of gPrefsMap.keys()) {
+ Services.prefs.removeObserver(pref, this, false);
+ }
+ }
+};
+
+/**
+ * Singleton that serves as the default new tab pref provider for the grid.
+ */
+const gPrefs = new PrefsProvider();
+
+let NewTabPrefsProvider = {
+ prefs: gPrefs,
+ newtabPagePrefSet: gNewtabPagePrefs,
+};
diff --git a/browser/components/newtab/NewTabRemoteResources.jsm b/browser/components/newtab/NewTabRemoteResources.jsm
new file mode 100644
index 000000000..57351b15c
--- /dev/null
+++ b/browser/components/newtab/NewTabRemoteResources.jsm
@@ -0,0 +1,15 @@
+/* exported NewTabRemoteResources */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["NewTabRemoteResources"];
+
+const NewTabRemoteResources = {
+ MODE_CHANNEL_MAP: {
+ production: {origin: "https://content.cdn.mozilla.net"},
+ staging: {origin: "https://s3_proxy_tiles.stage.mozaws.net"},
+ test: {origin: "https://example.com"},
+ test2: {origin: "http://mochi.test:8888"},
+ dev: {origin: "http://localhost:8888"}
+ }
+};
diff --git a/browser/components/newtab/NewTabSearchProvider.jsm b/browser/components/newtab/NewTabSearchProvider.jsm
new file mode 100644
index 000000000..a50d8c706
--- /dev/null
+++ b/browser/components/newtab/NewTabSearchProvider.jsm
@@ -0,0 +1,103 @@
+/* global XPCOMUtils, ContentSearch, Task, Services, EventEmitter */
+/* exported NewTabSearchProvider */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["NewTabSearchProvider"];
+
+const {utils: Cu, interfaces: Ci} = Components;
+const CURRENT_ENGINE = "browser-search-engine-modified";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
+ "resource:///modules/ContentSearch.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
+ const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+ return EventEmitter;
+});
+
+function SearchProvider() {
+ EventEmitter.decorate(this);
+}
+
+SearchProvider.prototype = {
+
+ observe(subject, topic, data) { // jshint unused:false
+ // all other topics are not relevant to content searches and can be
+ // ignored by NewTabSearchProvider
+ if (data === "engine-current" && topic === CURRENT_ENGINE) {
+ Task.spawn(function* () {
+ try {
+ let state = yield ContentSearch.currentStateObj(true);
+ let engine = state.currentEngine;
+ this.emit(CURRENT_ENGINE, engine);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }.bind(this));
+ }
+ },
+
+ init() {
+ try {
+ Services.obs.addObserver(this, CURRENT_ENGINE, true);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ]),
+
+ uninit() {
+ try {
+ Services.obs.removeObserver(this, CURRENT_ENGINE, true);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ get searchSuggestionUIStrings() {
+ return ContentSearch.searchSuggestionUIStrings;
+ },
+
+ removeFormHistory({browser}, suggestion) {
+ ContentSearch.removeFormHistoryEntry({target: browser}, suggestion);
+ },
+
+ manageEngines(browser) {
+ const browserWin = browser.ownerGlobal;
+ browserWin.openPreferences("paneSearch");
+ },
+
+ asyncGetState: Task.async(function*() {
+ let state = yield ContentSearch.currentStateObj(true);
+ return state;
+ }),
+
+ asyncPerformSearch: Task.async(function*({browser}, searchData) {
+ ContentSearch.performSearch({target: browser}, searchData);
+ yield ContentSearch.addFormHistoryEntry({target: browser}, searchData.searchString);
+ }),
+
+ asyncCycleEngine: Task.async(function*(engineName) {
+ Services.search.currentEngine = Services.search.getEngineByName(engineName);
+ let state = yield ContentSearch.currentStateObj(true);
+ let newEngine = state.currentEngine;
+ this.emit(CURRENT_ENGINE, newEngine);
+ }),
+
+ asyncGetSuggestions: Task.async(function*(engineName, searchString, target) {
+ let suggestions = ContentSearch.getSuggestions(engineName, searchString, target.browser);
+ return suggestions;
+ }),
+};
+
+const NewTabSearchProvider = {
+ search: new SearchProvider(),
+};
diff --git a/browser/components/newtab/NewTabURL.jsm b/browser/components/newtab/NewTabURL.jsm
new file mode 100644
index 000000000..5000eae2e
--- /dev/null
+++ b/browser/components/newtab/NewTabURL.jsm
@@ -0,0 +1,36 @@
+/* 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/. */
+
+/* globals XPCOMUtils, aboutNewTabService*/
+/* exported NewTabURL */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+this.EXPORTED_SYMBOLS = ["NewTabURL"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService");
+
+this.NewTabURL = {
+
+ get: function() {
+ return aboutNewTabService.newTabURL;
+ },
+
+ get overridden() {
+ return aboutNewTabService.overridden;
+ },
+
+ override: function(newURL) {
+ aboutNewTabService.newTabURL = newURL;
+ },
+
+ reset: function() {
+ aboutNewTabService.resetNewTabURL();
+ }
+};
diff --git a/browser/components/newtab/NewTabWebChannel.jsm b/browser/components/newtab/NewTabWebChannel.jsm
new file mode 100644
index 000000000..40ee73684
--- /dev/null
+++ b/browser/components/newtab/NewTabWebChannel.jsm
@@ -0,0 +1,299 @@
+/* global
+ NewTabPrefsProvider,
+ Services,
+ EventEmitter,
+ Preferences,
+ XPCOMUtils,
+ WebChannel,
+ NewTabRemoteResources
+*/
+/* exported NewTabWebChannel */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["NewTabWebChannel"];
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+ "resource:///modules/NewTabPrefsProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabRemoteResources",
+ "resource:///modules/NewTabRemoteResources.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+ "resource://gre/modules/WebChannel.jsm");
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
+ const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+ return EventEmitter;
+});
+
+const CHAN_ID = "newtab";
+const PREF_ENABLED = "browser.newtabpage.remote";
+const PREF_MODE = "browser.newtabpage.remote.mode";
+
+/**
+ * NewTabWebChannel is the conduit for all communication with unprivileged newtab instances.
+ *
+ * It allows for the ability to broadcast to all newtab browsers.
+ * If the browser.newtab.remote pref is false, the object will be in an uninitialized state.
+ *
+ * Mode choices:
+ * 'production': pages from our production CDN
+ * 'staging': pages from our staging CDN
+ * 'test': intended for tests
+ * 'test2': intended for tests
+ * 'dev': intended for development
+ *
+ * An unknown mode will result in 'production' mode, which is the default
+ *
+ * Incoming messages are expected to be JSON-serialized and in the format:
+ *
+ * {
+ * type: "REQUEST_SCREENSHOT",
+ * data: {
+ * url: "https://example.com"
+ * }
+ * }
+ *
+ * Or:
+ *
+ * {
+ * type: "REQUEST_SCREENSHOT",
+ * }
+ *
+ * Outgoing messages are expected to be objects serializable by structured cloning, in a similar format:
+ * {
+ * type: "RECEIVE_SCREENSHOT",
+ * data: {
+ * "url": "https://example.com",
+ * "image": "dataURi:....."
+ * }
+ * }
+ */
+let NewTabWebChannelImpl = function NewTabWebChannelImpl() {
+ EventEmitter.decorate(this);
+ this._handlePrefChange = this._handlePrefChange.bind(this);
+ this._incomingMessage = this._incomingMessage.bind(this);
+};
+
+NewTabWebChannelImpl.prototype = {
+ _prefs: {},
+ _channel: null,
+
+ // a WeakMap containing browsers as keys and a weak ref to their principal
+ // as value
+ _principals: null,
+
+ // a Set containing weak refs to browsers
+ _browsers: null,
+
+ /*
+ * Returns current channel's ID
+ */
+ get chanId() {
+ return CHAN_ID;
+ },
+
+ /*
+ * Returns the number of browsers currently tracking
+ */
+ get numBrowsers() {
+ return this._getBrowserRefs().length;
+ },
+
+ /*
+ * Returns current channel's origin
+ */
+ get origin() {
+ if (!(this._prefs.mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) {
+ this._prefs.mode = "production";
+ }
+ return NewTabRemoteResources.MODE_CHANNEL_MAP[this._prefs.mode].origin;
+ },
+
+ /*
+ * Unloads all browsers and principals
+ */
+ _unloadAll() {
+ if (this._principals != null) {
+ this._principals = new WeakMap();
+ }
+ this._browsers = new Set();
+ this.emit("targetUnloadAll");
+ },
+
+ /*
+ * Checks if a browser is known
+ *
+ * This will cause an iteration through all known browsers.
+ * That's ok, we don't expect a lot of browsers
+ */
+ _isBrowserKnown(browser) {
+ for (let bRef of this._getBrowserRefs()) {
+ let b = bRef.get();
+ if (b && b.permanentKey === browser.permanentKey) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /*
+ * Obtains all known browser refs
+ */
+ _getBrowserRefs() {
+ // Some code may try to emit messages after teardown.
+ if (!this._browsers) {
+ return [];
+ }
+ let refs = [];
+ for (let bRef of this._browsers) {
+ /*
+ * even though we hold a weak ref to browser, it seems that browser
+ * objects aren't gc'd immediately after a tab closes. They stick around
+ * in memory, but thankfully they don't have a documentURI in that case
+ */
+ let browser = bRef.get();
+ if (browser && browser.documentURI) {
+ refs.push(bRef);
+ } else {
+ // need to clean up principals because the browser object is not gc'ed
+ // immediately
+ this._principals.delete(browser);
+ this._browsers.delete(bRef);
+ this.emit("targetUnload");
+ }
+ }
+ return refs;
+ },
+
+ /*
+ * Receives a message from content.
+ *
+ * Keeps track of browsers for broadcast, relays messages to listeners.
+ */
+ _incomingMessage(id, message, target) {
+ if (this.chanId !== id) {
+ Cu.reportError(new Error("NewTabWebChannel unexpected message destination"));
+ }
+
+ /*
+ * need to differentiate by browser, because event targets are created each
+ * time a message is sent.
+ */
+ if (!this._isBrowserKnown(target.browser)) {
+ this._browsers.add(Cu.getWeakReference(target.browser));
+ this._principals.set(target.browser, Cu.getWeakReference(target.principal));
+ this.emit("targetAdd");
+ }
+
+ try {
+ let msg = JSON.parse(message);
+ this.emit(msg.type, {data: msg.data, target: target});
+ } catch (err) {
+ Cu.reportError(err);
+ }
+ },
+
+ /*
+ * Sends a message to all known browsers
+ */
+ broadcast(actionType, message) {
+ for (let bRef of this._getBrowserRefs()) {
+ let browser = bRef.get();
+ try {
+ let principal = this._principals.get(browser).get();
+ if (principal && browser && browser.documentURI) {
+ this._channel.send({type: actionType, data: message}, {browser, principal});
+ }
+ } catch (e) {
+ Cu.reportError(new Error("NewTabWebChannel WeakRef is dead"));
+ this._principals.delete(browser);
+ }
+ }
+ },
+
+ /*
+ * Sends a message to a specific target
+ */
+ send(actionType, message, target) {
+ try {
+ this._channel.send({type: actionType, data: message}, target);
+ } catch (e) {
+ // Web Channel might be dead
+ Cu.reportError(e);
+ }
+ },
+
+ /*
+ * Pref change observer callback
+ */
+ _handlePrefChange(prefName, newState, forceState) { // eslint-disable-line no-unused-vars
+ switch (prefName) {
+ case PREF_ENABLED:
+ if (!this._prefs.enabled && newState) {
+ // changing state from disabled to enabled
+ this.setupState();
+ } else if (this._prefs.enabled && !newState) {
+ // changing state from enabled to disabled
+ this.tearDownState();
+ }
+ break;
+ case PREF_MODE:
+ if (this._prefs.mode !== newState) {
+ // changing modes
+ this.tearDownState();
+ this.setupState();
+ }
+ break;
+ }
+ },
+
+ /*
+ * Sets up the internal state
+ */
+ setupState() {
+ this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
+
+ let mode = Preferences.get(PREF_MODE, "production");
+ if (!(mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) {
+ mode = "production";
+ }
+ this._prefs.mode = mode;
+ this._principals = new WeakMap();
+ this._browsers = new Set();
+
+ if (this._prefs.enabled) {
+ this._channel = new WebChannel(this.chanId, Services.io.newURI(this.origin, null, null));
+ this._channel.listen(this._incomingMessage);
+ }
+ },
+
+ tearDownState() {
+ if (this._channel) {
+ this._channel.stopListening();
+ }
+ this._prefs = {};
+ this._unloadAll();
+ this._channel = null;
+ this._principals = null;
+ this._browsers = null;
+ },
+
+ init() {
+ this.setupState();
+ NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handlePrefChange);
+ NewTabPrefsProvider.prefs.on(PREF_MODE, this._handlePrefChange);
+ },
+
+ uninit() {
+ this.tearDownState();
+ NewTabPrefsProvider.prefs.off(PREF_ENABLED, this._handlePrefChange);
+ NewTabPrefsProvider.prefs.off(PREF_MODE, this._handlePrefChange);
+ }
+};
+
+let NewTabWebChannel = new NewTabWebChannelImpl();
diff --git a/browser/components/newtab/PlacesProvider.jsm b/browser/components/newtab/PlacesProvider.jsm
new file mode 100644
index 000000000..f478b5c5c
--- /dev/null
+++ b/browser/components/newtab/PlacesProvider.jsm
@@ -0,0 +1,211 @@
+/* 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, PlacesUtils, 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, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
+ const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+ return EventEmitter;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
+ "resource://gre/modules/NewTabUtils.jsm");
+
+// The maximum number of results PlacesProvider retrieves from history.
+const HISTORY_RESULTS_LIMIT = 100;
+
+/* Queries history to retrieve the most visited sites. Emits events when the
+ * history changes.
+ * Implements the EventEmitter interface.
+ */
+let Links = function Links() {
+ EventEmitter.decorate(this);
+};
+
+Links.prototype = {
+ /**
+ * Set this to change the maximum number of links the provider will provide.
+ */
+ get maxNumLinks() {
+ // getter, so it can't be replaced dynamically
+ return HISTORY_RESULTS_LIMIT;
+ },
+
+ /**
+ * A set of functions called by @mozilla.org/browser/nav-historyservice
+ * All history events are emitted from this object.
+ */
+ historyObserver: {
+ onDeleteURI: function historyObserver_onDeleteURI(aURI) {
+ // let observers remove sensetive data associated with deleted visit
+ gLinks.emit("deleteURI", {
+ url: aURI.spec,
+ });
+ },
+
+ onClearHistory: function historyObserver_onClearHistory() {
+ gLinks.emit("clearHistory");
+ },
+
+ onFrecencyChanged: function historyObserver_onFrecencyChanged(aURI,
+ aNewFrecency, aGUID, aHidden, aLastVisitDate) { // jshint ignore:line
+ // The implementation of the query in getLinks excludes hidden and
+ // unvisited pages, so it's important to exclude them here, too.
+ if (!aHidden && aLastVisitDate &&
+ NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) {
+ gLinks.emit("linkChanged", {
+ url: aURI.spec,
+ frecency: aNewFrecency,
+ lastVisitDate: aLastVisitDate,
+ type: "history",
+ });
+ }
+ },
+
+ onManyFrecenciesChanged: function historyObserver_onManyFrecenciesChanged() {
+ // Called when frecencies are invalidated and also when clearHistory is called
+ // See toolkit/components/places/tests/unit/test_frecency_observers.js
+ gLinks.emit("manyLinksChanged");
+ },
+
+ onTitleChanged: function historyObserver_onTitleChanged(aURI, aNewTitle) {
+ if (NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) {
+ gLinks.emit("linkChanged", {
+ url: aURI.spec,
+ title: aNewTitle
+ });
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
+ Ci.nsISupportsWeakReference])
+ },
+
+ /**
+ * Must be called before the provider is used.
+ * Makes it easy to disable under pref
+ */
+ init: function PlacesProvider_init() {
+ try {
+ PlacesUtils.history.addObserver(this.historyObserver, true);
+ } 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*() {
+ // Select a single page per host with highest frecency, highest recency.
+ // Choose N top such pages. Note +rev_host, to turn off optimizer per :mak
+ // suggestion.
+ let sqlQuery = `SELECT url, title, frecency,
+ last_visit_date as lastVisitDate,
+ "history" as type
+ FROM moz_places
+ WHERE frecency in (
+ SELECT MAX(frecency) as frecency
+ FROM moz_places
+ WHERE hidden = 0 AND last_visit_date NOTNULL
+ GROUP BY +rev_host
+ ORDER BY frecency DESC
+ LIMIT :limit
+ )
+ GROUP BY rev_host HAVING MAX(lastVisitDate)
+ ORDER BY frecency DESC, lastVisitDate DESC, url`;
+
+ let links = yield this.executePlacesQuery(sqlQuery, {
+ columns: ["url", "title", "lastVisitDate", "frecency", "type"],
+ params: {limit: this.maxNumLinks}
+ });
+
+ return links.filter(link => NewTabUtils.linkChecker.checkLoadURI(link.url));
+ }),
+
+ /**
+ * Executes arbitrary query against places database
+ *
+ * @param {String} aSql
+ * SQL query to execute
+ * @param {Object} [optional] aOptions
+ * aOptions.columns - an array of column names. if supplied the returned
+ * items will consist of objects keyed on column names. Otherwise
+ * an array of raw values is returned in the select order
+ * aOptions.param - an object of SQL binding parameters
+ * aOptions.callback - a callback to handle query rows
+ *
+ * @returns {Promise} Returns a promise with the array of retrieved items
+ */
+ executePlacesQuery: Task.async(function*(aSql, aOptions={}) {
+ let {columns, params, callback} = aOptions;
+ let items = [];
+ let queryError = null;
+ let conn = yield PlacesUtils.promiseDBConnection();
+ yield conn.executeCached(aSql, params, aRow => {
+ try {
+ // check if caller wants to handle query raws
+ if (callback) {
+ callback(aRow);
+ }
+ // otherwise fill in the item and add items array
+ else {
+ let item = null;
+ // if columns array is given construct an object
+ if (columns && Array.isArray(columns)) {
+ item = {};
+ columns.forEach(column => {
+ item[column] = aRow.getResultByName(column);
+ });
+ } else {
+ // if no columns - make an array of raw values
+ item = [];
+ for (let i = 0; i < aRow.numEntries; i++) {
+ item.push(aRow.getResultByIndex(i));
+ }
+ }
+ items.push(item);
+ }
+ } catch (e) {
+ queryError = e;
+ throw StopIteration;
+ }
+ });
+ if (queryError) {
+ throw new Error(queryError);
+ }
+ return items;
+ }),
+};
+
+/**
+ * Singleton that serves as the default link provider for the grid.
+ */
+const gLinks = new Links(); // jshint ignore:line
+
+let PlacesProvider = {
+ links: gLinks,
+};
+
+// Kept only for backwards-compatibility
+XPCOMUtils.defineLazyGetter(PlacesProvider, "LinkChecker",
+ () => NewTabUtils.linkChecker);
+
diff --git a/browser/components/newtab/PreviewProvider.jsm b/browser/components/newtab/PreviewProvider.jsm
new file mode 100644
index 000000000..8624b8544
--- /dev/null
+++ b/browser/components/newtab/PreviewProvider.jsm
@@ -0,0 +1,49 @@
+/* global XPCOMUtils, BackgroundPageThumbs, FileUtils, PageThumbsStorage, Task, MIMEService */
+/* exported PreviewProvider */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PreviewProvider"];
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/PageThumbs.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+const {OS} = Cu.import("resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BackgroundPageThumbs",
+ "resource://gre/modules/BackgroundPageThumbs.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "MIMEService",
+ "@mozilla.org/mime;1", "nsIMIMEService");
+
+let PreviewProvider = {
+ /**
+ * Returns a thumbnail as a data URI for a url, creating it if necessary
+ *
+ * @param {String} url
+ * a url to obtain a thumbnail for
+ * @return {Promise} A Promise that resolves with a base64 encoded thumbnail
+ */
+ getThumbnail: Task.async(function* PreviewProvider_getThumbnail(url) {
+ try {
+ yield BackgroundPageThumbs.captureIfMissing(url);
+ let imgPath = PageThumbsStorage.getFilePathForURL(url);
+
+ // OS.File object used to easily read off-thread
+ let file = yield OS.File.open(imgPath, {read: true, existing: true});
+
+ // nsIFile object needed for MIMEService
+ let nsFile = FileUtils.File(imgPath);
+
+ let contentType = MIMEService.getTypeFromFile(nsFile);
+ let bytes = yield file.read();
+ let encodedData = btoa(String.fromCharCode.apply(null, bytes));
+ file.close();
+ return `data:${contentType};base64,${encodedData}`;
+ } catch (err) {
+ Cu.reportError(`PreviewProvider_getThumbnail error: ${err}`);
+ throw err;
+ }
+ })
+};
diff --git a/browser/components/newtab/aboutNewTabService.js b/browser/components/newtab/aboutNewTabService.js
new file mode 100644
index 000000000..54c3749e8
--- /dev/null
+++ b/browser/components/newtab/aboutNewTabService.js
@@ -0,0 +1,289 @@
+/*
+ * 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/.
+*/
+
+/* globals XPCOMUtils, NewTabPrefsProvider, Services,
+ Locale, UpdateUtils, NewTabRemoteResources
+*/
+"use strict";
+
+const {utils: Cu, interfaces: Ci} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+ "resource:///modules/NewTabPrefsProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+ "resource://gre/modules/Locale.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabRemoteResources",
+ "resource:///modules/NewTabRemoteResources.jsm");
+
+const LOCAL_NEWTAB_URL = "chrome://browser/content/newtab/newTab.xhtml";
+
+const REMOTE_NEWTAB_PATH = "/newtab/v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
+
+const ABOUT_URL = "about:newtab";
+
+// Pref that tells if remote newtab is enabled
+const PREF_REMOTE_ENABLED = "browser.newtabpage.remote";
+
+// Pref branch necesssary for testing
+const PREF_REMOTE_CS_TEST = "browser.newtabpage.remote.content-signing-test";
+
+// The preference that tells whether to match the OS locale
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+
+// The preference that tells what locale the user selected
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+// The preference that tells what remote mode is enabled.
+const PREF_REMOTE_MODE = "browser.newtabpage.remote.mode";
+
+// The preference that tells which remote version is expected.
+const PREF_REMOTE_VERSION = "browser.newtabpage.remote.version";
+
+const VALID_CHANNELS = new Set(["esr", "release", "beta", "aurora", "nightly"]);
+
+function AboutNewTabService() {
+ NewTabPrefsProvider.prefs.on(PREF_REMOTE_ENABLED, this._handleToggleEvent.bind(this));
+
+ this._updateRemoteMaybe = this._updateRemoteMaybe.bind(this);
+
+ // trigger remote change if needed, according to pref
+ this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED));
+}
+
+/*
+ * A service that allows for the overriding, at runtime, of the newtab page's url.
+ * Additionally, the service manages pref state between a remote and local newtab page.
+ *
+ * There is tight coupling with browser/about/AboutRedirector.cpp.
+ *
+ * 1. Browser chrome access:
+ *
+ * When the user issues a command to open a new tab page, usually clicking a button
+ * in the browser chrome or using shortcut keys, the browser chrome code invokes the
+ * service to obtain the newtab URL. It then loads that URL in a new tab.
+ *
+ * When not overridden, the default URL emitted by the service is "about:newtab".
+ * When overridden, it returns the overriden URL.
+ *
+ * 2. Redirector Access:
+ *
+ * When the URL loaded is about:newtab, the default behavior, or when entered in the
+ * URL bar, the redirector is hit. The service is then called to return either of
+ * two URLs, a chrome or remote one, based on the browser.newtabpage.remote pref.
+ *
+ * NOTE: "about:newtab" will always result in a default newtab page, and never an overridden URL.
+ *
+ * Access patterns:
+ *
+ * The behavior is different when accessing the service via browser chrome or via redirector
+ * largely to maintain compatibility with expectations of add-on developers.
+ *
+ * Loading a chrome resource, or an about: URL in the redirector with either the
+ * LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip
+ * to the redirector from browser chrome is avoided.
+ */
+AboutNewTabService.prototype = {
+
+ _newTabURL: ABOUT_URL,
+ _remoteEnabled: false,
+ _remoteURL: null,
+ _overridden: false,
+
+ classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutNewTabService]),
+ _xpcom_categories: [{
+ service: true
+ }],
+
+ _handleToggleEvent(prefName, stateEnabled, forceState) { // jshint unused:false
+ if (this.toggleRemote(stateEnabled, forceState)) {
+ Services.obs.notifyObservers(null, "newtab-url-changed", ABOUT_URL);
+ }
+ },
+
+ /**
+ * React to changes to the remote newtab pref.
+ *
+ * If browser.newtabpage.remote is true, this will change the default URL to the
+ * remote newtab page URL. If browser.newtabpage.remote is false, the default URL
+ * will be a local chrome URL.
+ *
+ * This will only act if there is a change of state and if not overridden.
+ *
+ * @returns {Boolean} Returns if there has been a state change
+ *
+ * @param {Boolean} stateEnabled remote state to set to
+ * @param {Boolean} forceState force state change
+ */
+ toggleRemote(stateEnabled, forceState) {
+
+ if (!forceState && (this._overriden || stateEnabled === this._remoteEnabled)) {
+ // exit there is no change of state
+ return false;
+ }
+
+ let csTest = Services.prefs.getBoolPref(PREF_REMOTE_CS_TEST);
+ if (stateEnabled) {
+ if (!csTest) {
+ this._remoteURL = this.generateRemoteURL();
+ } else {
+ this._remoteURL = this._newTabURL;
+ }
+ NewTabPrefsProvider.prefs.on(
+ PREF_SELECTED_LOCALE,
+ this._updateRemoteMaybe);
+ NewTabPrefsProvider.prefs.on(
+ PREF_MATCH_OS_LOCALE,
+ this._updateRemoteMaybe);
+ NewTabPrefsProvider.prefs.on(
+ PREF_REMOTE_MODE,
+ this._updateRemoteMaybe);
+ NewTabPrefsProvider.prefs.on(
+ PREF_REMOTE_VERSION,
+ this._updateRemoteMaybe);
+ this._remoteEnabled = true;
+ } else {
+ NewTabPrefsProvider.prefs.off(PREF_SELECTED_LOCALE, this._updateRemoteMaybe);
+ NewTabPrefsProvider.prefs.off(PREF_MATCH_OS_LOCALE, this._updateRemoteMaybe);
+ NewTabPrefsProvider.prefs.off(PREF_REMOTE_MODE, this._updateRemoteMaybe);
+ NewTabPrefsProvider.prefs.off(PREF_REMOTE_VERSION, this._updateRemoteMaybe);
+ this._remoteEnabled = false;
+ }
+ if (!csTest) {
+ this._newTabURL = ABOUT_URL;
+ }
+ return true;
+ },
+
+ /*
+ * Generate a default url based on remote mode, version, locale and update channel
+ */
+ generateRemoteURL() {
+ let releaseName = this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
+ let path = REMOTE_NEWTAB_PATH
+ .replace("%VERSION%", this.remoteVersion)
+ .replace("%LOCALE%", Locale.getLocale())
+ .replace("%CHANNEL%", releaseName);
+ let mode = Services.prefs.getCharPref(PREF_REMOTE_MODE, "production");
+ if (!(mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) {
+ mode = "production";
+ }
+ return NewTabRemoteResources.MODE_CHANNEL_MAP[mode].origin + path;
+ },
+
+ /*
+ * Returns the default URL.
+ *
+ * This URL only depends on the browser.newtabpage.remote pref. Overriding
+ * the newtab page has no effect on the result of this function.
+ *
+ * The result is also the remote URL if this is in a test (PREF_REMOTE_CS_TEST)
+ *
+ * @returns {String} the default newtab URL, remote or local depending on browser.newtabpage.remote
+ */
+ get defaultURL() {
+ let csTest = Services.prefs.getBoolPref(PREF_REMOTE_CS_TEST);
+ if (this._remoteEnabled || csTest) {
+ return this._remoteURL;
+ }
+ return LOCAL_NEWTAB_URL;
+ },
+
+ /*
+ * Updates the remote location when the page is not overriden.
+ *
+ * Useful when there is a dependent pref change
+ */
+ _updateRemoteMaybe() {
+ if (!this._remoteEnabled || this._overridden) {
+ return;
+ }
+
+ let url = this.generateRemoteURL();
+ if (url !== this._remoteURL) {
+ this._remoteURL = url;
+ Services.obs.notifyObservers(null, "newtab-url-changed",
+ this._remoteURL);
+ }
+ },
+
+ /**
+ * Returns the release name from an Update Channel name
+ *
+ * @returns {String} a release name based on the update channel. Defaults to nightly
+ */
+ releaseFromUpdateChannel(channelName) {
+ return VALID_CHANNELS.has(channelName) ? channelName : "nightly";
+ },
+
+ get newTabURL() {
+ return this._newTabURL;
+ },
+
+ get remoteVersion() {
+ return Services.prefs.getCharPref(PREF_REMOTE_VERSION, "1");
+ },
+
+ get remoteReleaseName() {
+ return this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
+ },
+
+ set newTabURL(aNewTabURL) {
+ let csTest = Services.prefs.getBoolPref(PREF_REMOTE_CS_TEST);
+ aNewTabURL = aNewTabURL.trim();
+ if (aNewTabURL === ABOUT_URL) {
+ // avoid infinite redirects in case one sets the URL to about:newtab
+ this.resetNewTabURL();
+ return;
+ } else if (aNewTabURL === "") {
+ aNewTabURL = "about:blank";
+ }
+ let remoteURL = this.generateRemoteURL();
+ let prefRemoteEnabled = Services.prefs.getBoolPref(PREF_REMOTE_ENABLED);
+ let isResetLocal = !prefRemoteEnabled && aNewTabURL === LOCAL_NEWTAB_URL;
+ let isResetRemote = prefRemoteEnabled && aNewTabURL === remoteURL;
+
+ if (isResetLocal || isResetRemote) {
+ if (this._overriden && !csTest) {
+ // only trigger a reset if previously overridden and this is no test
+ this.resetNewTabURL();
+ }
+ return;
+ }
+ // turn off remote state if needed
+ if (!csTest) {
+ this.toggleRemote(false);
+ } else {
+ // if this is a test, we want the remoteURL to be set
+ this._remoteURL = aNewTabURL;
+ }
+ this._newTabURL = aNewTabURL;
+ this._overridden = true;
+ Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
+ },
+
+ get overridden() {
+ return this._overridden;
+ },
+
+ get remoteEnabled() {
+ return this._remoteEnabled;
+ },
+
+ resetNewTabURL() {
+ this._overridden = false;
+ this._newTabURL = ABOUT_URL;
+ this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED), true);
+ Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AboutNewTabService]);
diff --git a/browser/components/newtab/moz.build b/browser/components/newtab/moz.build
new file mode 100644
index 000000000..37c9983f7
--- /dev/null
+++ b/browser/components/newtab/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'tests/xpcshell/xpcshell.ini',
+]
+
+EXTRA_JS_MODULES += [
+ 'NewTabMessages.jsm',
+ 'NewTabPrefsProvider.jsm',
+ 'NewTabRemoteResources.jsm',
+ 'NewTabSearchProvider.jsm',
+ 'NewTabURL.jsm',
+ 'NewTabWebChannel.jsm',
+ 'PlacesProvider.jsm',
+ 'PreviewProvider.jsm'
+]
+
+XPIDL_SOURCES += [
+ 'nsIAboutNewTabService.idl',
+]
+
+XPIDL_MODULE = 'browser-newtab'
+
+EXTRA_COMPONENTS += [
+ 'aboutNewTabService.js',
+ 'NewTabComponents.manifest',
+]
diff --git a/browser/components/newtab/nsIAboutNewTabService.idl b/browser/components/newtab/nsIAboutNewTabService.idl
new file mode 100644
index 000000000..bc25c7492
--- /dev/null
+++ b/browser/components/newtab/nsIAboutNewTabService.idl
@@ -0,0 +1,63 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Allows to override about:newtab to point to a different location
+ * than the one specified within AboutRedirector.cpp
+ */
+
+[scriptable, uuid(dfcd2adc-7867-4d3a-ba70-17501f208142)]
+interface nsIAboutNewTabService : nsISupports
+{
+ /**
+ * Returns the url of the resource for the newtab page if not overridden,
+ * otherwise a string represenation of the new URL.
+ */
+ attribute ACString newTabURL;
+
+ /**
+ * Returns the default URL (remote or local depending on pref)
+ */
+ attribute ACString defaultURL;
+
+ /**
+ * Returns true if the default resource got overridden.
+ */
+ readonly attribute bool overridden;
+
+ /**
+ * Returns true if the default resource is remotely hosted and isn't
+ * overridden
+ */
+ readonly attribute bool remoteEnabled;
+
+
+ /**
+ * Returns the version of the remote newtab page expected
+ */
+ readonly attribute ACString remoteVersion;
+
+ /**
+ * Returns the expected channel for the remote the newtab page
+ */
+ readonly attribute ACString remoteReleaseName;
+
+ /**
+ * Generates and returns the remote newtab page url
+ */
+ ACString generateRemoteURL();
+
+ /**
+ * Returns a remote new tab release name given an update channel name
+ */
+ ACString releaseFromUpdateChannel(in ACString channelName);
+
+ /**
+ * Resets to the default resource and also resets the
+ * overridden attribute to false.
+ */
+ void resetNewTabURL();
+};
diff --git a/browser/components/newtab/tests/browser/.eslintrc.js b/browser/components/newtab/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/components/newtab/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/components/newtab/tests/browser/blue_page.html b/browser/components/newtab/tests/browser/blue_page.html
new file mode 100644
index 000000000..a7f000bfd
--- /dev/null
+++ b/browser/components/newtab/tests/browser/blue_page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body style="background-color: blue">
+</body>
+</html>
diff --git a/browser/components/newtab/tests/browser/browser.ini b/browser/components/newtab/tests/browser/browser.ini
new file mode 100644
index 000000000..fa740be9e
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files =
+ blue_page.html
+ dummy_page.html
+ newtabwebchannel_basic.html
+ newtabmessages_places.html
+ newtabmessages_prefs.html
+ newtabmessages_preview.html
+ newtabmessages_search.html
+
+[browser_PreviewProvider.js]
+[browser_remotenewtab_pageloads.js]
+[browser_newtab_overrides.js]
+[browser_newtabmessages.js]
+skip-if = true # Bug 1271177, bug 1262719
+[browser_newtabwebchannel.js]
diff --git a/browser/components/newtab/tests/browser/browser_PreviewProvider.js b/browser/components/newtab/tests/browser/browser_PreviewProvider.js
new file mode 100644
index 000000000..b1e3eda9f
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_PreviewProvider.js
@@ -0,0 +1,90 @@
+/* globals XPCOMUtils, Services, PreviewProvider, registerCleanupFunction */
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PreviewProvider",
+ "resource:///modules/PreviewProvider.jsm");
+
+var oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
+Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false);
+
+registerCleanupFunction(function() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ }
+ Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref);
+});
+
+const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/blue_page.html";
+
+function pixelsForDataURI(dataURI, options) {
+ return new Promise(resolve => {
+ if (!options) {
+ options = {};
+ }
+ let {width, height} = options;
+ if (!width) {
+ width = 100;
+ }
+ if (!height) {
+ height = 100;
+ }
+
+ let htmlns = "http://www.w3.org/1999/xhtml";
+ let img = document.createElementNS(htmlns, "img");
+ img.setAttribute("src", dataURI);
+
+ img.addEventListener("load", function onLoad() {
+ img.removeEventListener("load", onLoad, true);
+ let canvas = document.createElementNS(htmlns, "canvas");
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0, width, height);
+ let result = ctx.getImageData(0, 0, width, height).data;
+ resolve(result);
+ });
+ });
+}
+
+function* chunk_four(listData) {
+ let index = 0;
+ while (index < listData.length) {
+ yield listData.slice(index, index + 5);
+ index += 4;
+ }
+}
+
+add_task(function* open_page() {
+ let dataURI = yield PreviewProvider.getThumbnail(TEST_URL);
+ let pixels = yield pixelsForDataURI(dataURI, {width: 10, height: 10});
+ let rgbCount = {r: 0, g: 0, b: 0, a: 0};
+ for (let [r, g, b, a] of chunk_four(pixels)) {
+ if (r === 255) {
+ rgbCount.r += 1;
+ }
+ if (g === 255) {
+ rgbCount.g += 1;
+ }
+ if (b === 255) {
+ rgbCount.b += 1;
+ }
+ if (a === 255) {
+ rgbCount.a += 1;
+ }
+ }
+ Assert.equal(`${rgbCount.r},${rgbCount.g},${rgbCount.b},${rgbCount.a}`,
+ "0,0,100,100", "there should be 100 blue-only pixels at full opacity");
+});
+
+add_task(function* invalid_url() {
+ try {
+ yield PreviewProvider.getThumbnail("invalid:URL");
+ } catch (err) {
+ Assert.ok(true, "URL Failed");
+ }
+});
diff --git a/browser/components/newtab/tests/browser/browser_newtab_overrides.js b/browser/components/newtab/tests/browser/browser_newtab_overrides.js
new file mode 100644
index 000000000..eab9092a0
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_newtab_overrides.js
@@ -0,0 +1,139 @@
+/* globals
+ XPCOMUtils,
+ aboutNewTabService,
+ Services,
+ ContentTask,
+ TestUtils,
+ BrowserOpenTab,
+ registerCleanupFunction,
+ is,
+ content
+*/
+
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService");
+
+registerCleanupFunction(function() {
+ Services.prefs.setBoolPref("browser.newtabpage.remote", false);
+ aboutNewTabService.resetNewTabURL();
+});
+
+/*
+ * Tests that the default newtab page is always returned when one types "about:newtab" in the URL bar,
+ * even when overridden.
+ */
+add_task(function* redirector_ignores_override() {
+ let overrides = [
+ "chrome://browser/content/downloads/contentAreaDownloadsView.xul",
+ "about:home",
+ ];
+
+ for (let overrideURL of overrides) {
+ let notificationPromise = nextChangeNotificationPromise(overrideURL, `newtab page now points to ${overrideURL}`);
+ aboutNewTabService.newTabURL = overrideURL;
+
+ yield notificationPromise;
+ Assert.ok(aboutNewTabService.overridden, "url has been overridden");
+
+ let tabOptions = {
+ gBrowser,
+ url: "about:newtab",
+ };
+
+ /*
+ * Simulate typing "about:newtab" in the url bar.
+ *
+ * Bug 1240169 - We expect the redirector to lead the user to "about:newtab", the default URL,
+ * due to invoking AboutRedirector. A user interacting with the chrome otherwise would lead
+ * to the overriding URLs.
+ */
+ yield BrowserTestUtils.withNewTab(tabOptions, function*(browser) {
+ yield ContentTask.spawn(browser, {}, function*() {
+ Assert.equal(content.location.href, "about:newtab", "Got right URL");
+ Assert.equal(content.document.location.href, "about:newtab", "Got right URL");
+ Assert.equal(content.document.nodePrincipal,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ "nodePrincipal should match systemPrincipal");
+ });
+ }); // jshint ignore:line
+ }
+});
+
+/*
+ * Tests loading an overridden newtab page by simulating opening a newtab page from chrome
+ */
+add_task(function* override_loads_in_browser() {
+ let overrides = [
+ "chrome://browser/content/downloads/contentAreaDownloadsView.xul",
+ "about:home",
+ " about:home",
+ ];
+
+ for (let overrideURL of overrides) {
+ let notificationPromise = nextChangeNotificationPromise(overrideURL.trim(), `newtab page now points to ${overrideURL}`);
+ aboutNewTabService.newTabURL = overrideURL;
+
+ yield notificationPromise;
+ Assert.ok(aboutNewTabService.overridden, "url has been overridden");
+
+ // simulate a newtab open as a user would
+ BrowserOpenTab(); // jshint ignore:line
+
+ let browser = gBrowser.selectedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ yield ContentTask.spawn(browser, {url: overrideURL}, function*(args) {
+ Assert.equal(content.location.href, args.url.trim(), "Got right URL");
+ Assert.equal(content.document.location.href, args.url.trim(), "Got right URL");
+ }); // jshint ignore:line
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+/*
+ * Tests edge cases when someone overrides the newtabpage with whitespace
+ */
+add_task(function* override_blank_loads_in_browser() {
+ let overrides = [
+ "",
+ " ",
+ "\n\t",
+ " about:blank",
+ ];
+
+ for (let overrideURL of overrides) {
+ let notificationPromise = nextChangeNotificationPromise("about:blank", "newtab page now points to about:blank");
+ aboutNewTabService.newTabURL = overrideURL;
+
+ yield notificationPromise;
+ Assert.ok(aboutNewTabService.overridden, "url has been overridden");
+
+ // simulate a newtab open as a user would
+ BrowserOpenTab(); // jshint ignore:line
+
+ let browser = gBrowser.selectedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ yield ContentTask.spawn(browser, {}, function*() {
+ Assert.equal(content.location.href, "about:blank", "Got right URL");
+ Assert.equal(content.document.location.href, "about:blank", "Got right URL");
+ }); // jshint ignore:line
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+ return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
+ Assert.equal(aData, aNewURL, testMessage);
+ return true;
+ }.bind(this));
+}
diff --git a/browser/components/newtab/tests/browser/browser_newtabmessages.js b/browser/components/newtab/tests/browser/browser_newtabmessages.js
new file mode 100644
index 000000000..319ca1c34
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_newtabmessages.js
@@ -0,0 +1,222 @@
+/* globals Cu, XPCOMUtils, Preferences, is, registerCleanupFunction, NewTabWebChannel,
+PlacesTestUtils, NewTabMessages, ok, Services, PlacesUtils, NetUtil, Task */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.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");
+
+let setup = Task.async(function*() {
+ Preferences.set("browser.newtabpage.enhanced", true);
+ Preferences.set("browser.newtabpage.remote.mode", "test");
+ Preferences.set("browser.newtabpage.remote", true);
+ NewTabMessages.init();
+ yield PlacesTestUtils.clearHistory();
+});
+
+let cleanup = Task.async(function*() {
+ NewTabMessages.uninit();
+ Preferences.set("browser.newtabpage.remote", false);
+ Preferences.set("browser.newtabpage.remote.mode", "production");
+});
+registerCleanupFunction(cleanup);
+
+/*
+ * Sanity tests for pref messages
+ */
+add_task(function* prefMessages_request() {
+ yield setup();
+
+ let testURL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabmessages_prefs.html";
+
+ let tabOptions = {
+ gBrowser,
+ url: testURL
+ };
+
+ let prefResponseAck = new Promise(resolve => {
+ NewTabWebChannel.once("responseAck", () => {
+ ok(true, "a request response has been received");
+ resolve();
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab(tabOptions, function*() {
+ yield prefResponseAck;
+ let prefChangeAck = new Promise(resolve => {
+ NewTabWebChannel.once("responseAck", () => {
+ ok(true, "a change response has been received");
+ resolve();
+ });
+ });
+ Preferences.set("browser.newtabpage.enhanced", false);
+ yield prefChangeAck;
+ });
+ yield cleanup();
+});
+
+/*
+ * Sanity tests for preview messages
+ */
+add_task(function* previewMessages_request() {
+ yield setup();
+ var oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
+ Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false);
+
+ let testURL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabmessages_preview.html";
+
+ let tabOptions = {
+ gBrowser,
+ url: testURL
+ };
+
+ let previewResponseAck = new Promise(resolve => {
+ NewTabWebChannel.once("responseAck", () => {
+ ok(true, "a request response has been received");
+ resolve();
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab(tabOptions, function*() {
+ yield previewResponseAck;
+ });
+ yield cleanup();
+ Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref);
+});
+
+/*
+ * Sanity tests for places messages
+ */
+add_task(function* placesMessages_request() {
+ yield 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 expected number of history items");
+ resolve();
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab(tabOptions, function*() {
+ yield placesResponseAck;
+ ok(true, "a change response has been received");
+ let placesChangeAck = new Promise(resolve => {
+ NewTabWebChannel.once("clearHistoryAck", (_, msg) => {
+ is(msg.data, "clearHistory", "a clear history message has been received");
+ resolve();
+ });
+ });
+ yield PlacesTestUtils.clearHistory();
+ yield placesChangeAck;
+ });
+ yield cleanup();
+});
+
+/*
+ * Sanity tests for search messages
+ */
+add_task(function* searchMessages_request() {
+ yield setup();
+ let testURL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabmessages_search.html";
+
+ // create dummy test engines
+ Services.search.addEngineWithDetails("Engine1", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ Services.search.addEngineWithDetails("Engine2", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+
+ let tabOptions = {
+ gBrowser,
+ url: testURL
+ };
+
+ let UIStringsResponseAck = new Promise(resolve => {
+ NewTabWebChannel.once("UIStringsAck", (_, msg) => {
+ ok(true, "a search request response for UI string has been received");
+ ok(msg.data, "received the UI Strings");
+ resolve();
+ });
+ });
+ let suggestionsResponseAck = new Promise(resolve => {
+ NewTabWebChannel.once("suggestionsAck", (_, msg) => {
+ ok(true, "a search request response for suggestions has been received");
+ ok(msg.data, "received the suggestions");
+ resolve();
+ });
+ });
+ let stateResponseAck = new Promise(resolve => {
+ NewTabWebChannel.once("stateAck", (_, msg) => {
+ ok(true, "a search request response for state has been received");
+ ok(msg.data, "received a state object");
+ resolve();
+ });
+ });
+ let currentEngineResponseAck = new Promise(resolve => {
+ NewTabWebChannel.once("currentEngineAck", (_, msg) => {
+ ok(true, "a search request response for current engine has been received");
+ ok(msg.data, "received a current engine");
+ resolve();
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab(tabOptions, function*() {
+ yield UIStringsResponseAck;
+ yield suggestionsResponseAck;
+ yield stateResponseAck;
+ yield currentEngineResponseAck;
+ });
+
+ cleanup();
+});
diff --git a/browser/components/newtab/tests/browser/browser_newtabwebchannel.js b/browser/components/newtab/tests/browser/browser_newtabwebchannel.js
new file mode 100644
index 000000000..f003b105b
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_newtabwebchannel.js
@@ -0,0 +1,251 @@
+/* globals XPCOMUtils, Cu, Preferences, NewTabWebChannel, is, registerCleanupFunction */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
+ "resource:///modules/NewTabWebChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabMessages",
+ "resource:///modules/NewTabMessages.jsm");
+
+const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabwebchannel_basic.html";
+const TEST_URL_2 = "http://mochi.test:8888/browser/browser/components/newtab/tests/browser/newtabwebchannel_basic.html";
+
+function setup(mode = "test") {
+ Preferences.set("browser.newtabpage.remote.mode", mode);
+ Preferences.set("browser.newtabpage.remote", true);
+ NewTabWebChannel.init();
+ NewTabMessages.init();
+}
+
+function cleanup() {
+ NewTabMessages.uninit();
+ NewTabWebChannel.uninit();
+ Preferences.set("browser.newtabpage.remote", false);
+ Preferences.set("browser.newtabpage.remote.mode", "production");
+}
+registerCleanupFunction(cleanup);
+
+/*
+ * Tests flow of messages from newtab to chrome and chrome to newtab
+ */
+add_task(function* open_webchannel_basic() {
+ setup();
+
+ let tabOptions = {
+ gBrowser,
+ url: TEST_URL
+ };
+
+ let messagePromise = new Promise(resolve => {
+ NewTabWebChannel.once("foo", function(name, msg) {
+ is(name, "foo", "Correct message type sent: foo");
+ is(msg.data, "bar", "Correct data sent: bar");
+ resolve(msg.target);
+ });
+ });
+
+ let replyPromise = new Promise(resolve => {
+ NewTabWebChannel.once("reply", function(name, msg) {
+ is(name, "reply", "Correct message type sent: reply");
+ is(msg.data, "quuz", "Correct data sent: quuz");
+ resolve(msg.target);
+ });
+ });
+
+ let unloadPromise = new Promise(resolve => {
+ NewTabWebChannel.once("targetUnload", function(name) {
+ is(name, "targetUnload", "Correct message type sent: targetUnload");
+ resolve();
+ });
+ });
+
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+ yield BrowserTestUtils.withNewTab(tabOptions, function*(browser) {
+ let target = yield messagePromise;
+ is(NewTabWebChannel.numBrowsers, 1, "One target expected");
+ is(target.browser, browser, "Same browser");
+ NewTabWebChannel.send("respond", null, target);
+ yield replyPromise;
+ });
+
+ Cu.forceGC();
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+ yield unloadPromise;
+ cleanup();
+});
+
+/*
+ * Tests message broadcast reaches all open newtab pages
+ */
+add_task(function* webchannel_broadcast() {
+ setup();
+
+ let countingMessagePromise = new Promise(resolve => {
+ let count = 0;
+ NewTabWebChannel.on("foo", function test_message(name, msg) {
+ count += 1;
+ if (count === 2) {
+ NewTabWebChannel.off("foo", test_message);
+ resolve(msg.target);
+ }
+ }.bind(this));
+ });
+
+ let countingReplyPromise = new Promise(resolve => {
+ let count = 0;
+ NewTabWebChannel.on("reply", function test_message(name, msg) {
+ count += 1;
+ if (count === 2) {
+ NewTabWebChannel.off("reply", test_message);
+ resolve(msg.target);
+ }
+ }.bind(this));
+ });
+
+ let countingUnloadPromise = new Promise(resolve => {
+ let count = 0;
+ NewTabWebChannel.on("targetUnload", function test_message() {
+ count += 1;
+ if (count === 2) {
+ NewTabWebChannel.off("targetUnload", test_message);
+ resolve();
+ }
+ });
+ });
+
+ let tabs = [];
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+ tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL));
+ tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL));
+
+ yield countingMessagePromise;
+ is(NewTabWebChannel.numBrowsers, 2, "Two targets expected");
+
+ NewTabWebChannel.broadcast("respond", null);
+ yield countingReplyPromise;
+
+ for (let tab of tabs) {
+ yield BrowserTestUtils.removeTab(tab);
+ }
+ Cu.forceGC();
+
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+ yield countingUnloadPromise;
+ cleanup();
+});
+
+/*
+ * Tests switching modes
+ */
+add_task(function* webchannel_switch() {
+ setup();
+
+ function newMessagePromise() {
+ return new Promise(resolve => {
+ NewTabWebChannel.once("foo", function(name, msg) {
+ resolve(msg.target);
+ }.bind(this));
+ });
+ }
+
+ let replyCount = 0;
+ function newReplyPromise() {
+ return new Promise(resolve => {
+ NewTabWebChannel.on("reply", function() {
+ replyCount += 1;
+ resolve();
+ });
+ });
+ }
+
+ let unloadPromise = new Promise(resolve => {
+ NewTabWebChannel.once("targetUnload", function() {
+ resolve();
+ });
+ });
+
+ let unloadAllPromise = new Promise(resolve => {
+ NewTabWebChannel.once("targetUnloadAll", function() {
+ resolve();
+ });
+ });
+
+ let tabs = [];
+ let messagePromise;
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+
+ messagePromise = newMessagePromise();
+ tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL));
+ yield messagePromise;
+ is(NewTabWebChannel.numBrowsers, 1, "Correct number of targets");
+
+ messagePromise = newMessagePromise();
+ Preferences.set("browser.newtabpage.remote.mode", "test2");
+ tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL_2));
+ yield unloadAllPromise;
+ yield messagePromise;
+ is(NewTabWebChannel.numBrowsers, 1, "Correct number of targets");
+
+ NewTabWebChannel.broadcast("respond", null);
+ yield newReplyPromise();
+ is(replyCount, 1, "only current channel is listened to for replies");
+
+ const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+ let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+ let newWhitelist = origWhitelist + " http://mochi.test:8888";
+ Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+ try {
+ NewTabWebChannel.broadcast("respond_object", null);
+ yield newReplyPromise();
+ } finally {
+ Services.prefs.clearUserPref(webchannelWhitelistPref);
+ }
+
+ for (let tab of tabs) {
+ yield BrowserTestUtils.removeTab(tab);
+ }
+
+ Cu.forceGC();
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+ yield unloadPromise;
+ cleanup();
+});
+
+add_task(function* open_webchannel_reload() {
+ setup();
+
+ let tabOptions = {
+ gBrowser,
+ url: TEST_URL
+ };
+
+ let messagePromise = new Promise(resolve => {
+ NewTabWebChannel.once("foo", function(name, msg) {
+ is(name, "foo", "Correct message type sent: foo");
+ is(msg.data, "bar", "Correct data sent: bar");
+ resolve(msg.target);
+ });
+ });
+ let unloadPromise = new Promise(resolve => {
+ NewTabWebChannel.once("targetUnload", function() {
+ resolve();
+ });
+ });
+
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+ yield BrowserTestUtils.withNewTab(tabOptions, function*(browser) {
+ let target = yield messagePromise;
+ is(NewTabWebChannel.numBrowsers, 1, "One target expected");
+ is(target.browser, browser, "Same browser");
+
+ browser.reload();
+ });
+
+ Cu.forceGC();
+ is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+ yield unloadPromise;
+ cleanup();
+});
diff --git a/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js b/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
new file mode 100644
index 000000000..e99aeffc2
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
@@ -0,0 +1,52 @@
+/* globals Cu, XPCOMUtils, TestUtils, aboutNewTabService, ContentTask, content, is */
+"use strict";
+
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService");
+
+const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/dummy_page.html";
+
+/*
+ * Tests opening a newtab page with a remote URL. Simulates a newtab open from chrome
+ */
+add_task(function* open_newtab() {
+ let notificationPromise = nextChangeNotificationPromise(TEST_URL, "newtab page now points to test url");
+ aboutNewTabService.newTabURL = TEST_URL;
+
+ yield notificationPromise;
+ Assert.ok(aboutNewTabService.overridden, "url has been overridden");
+
+ /*
+ * Simulate a newtab open as a user would.
+ *
+ * Bug 1240169 - We cannot set the URL to about:newtab because that would invoke the redirector.
+ * The redirector always yields the loading of a default newtab URL. We expect the user to use
+ * the browser UI to access overriding URLs, for istance by click on the "+" button in the tab
+ * bar, or by using the new tab shortcut key.
+ */
+ BrowserOpenTab(); // jshint ignore:line
+
+ let browser = gBrowser.selectedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ yield ContentTask.spawn(browser, {url: TEST_URL}, function*(args) {
+ Assert.equal(content.document.location.href, args.url,
+ "document.location should match the external resource");
+ Assert.equal(content.document.documentURI, args.url,
+ "document.documentURI should match the external resource");
+ Assert.equal(content.document.nodePrincipal.URI.spec, args.url,
+ "nodePrincipal should match the external resource");
+ });
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+ return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
+ Assert.equal(aData, aNewURL, testMessage);
+ return true;
+ }.bind(this));
+}
diff --git a/browser/components/newtab/tests/browser/dummy_page.html b/browser/components/newtab/tests/browser/dummy_page.html
new file mode 100644
index 000000000..4b0689bde
--- /dev/null
+++ b/browser/components/newtab/tests/browser/dummy_page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<p>Dummy Page</p>
+</body>
+</html>
diff --git a/browser/components/newtab/tests/browser/newtabmessages_places.html b/browser/components/newtab/tests/browser/newtabmessages_places.html
new file mode 100644
index 000000000..e89bc4a22
--- /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: JSON.stringify({
+ 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: JSON.stringify({
+ 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: JSON.stringify({
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_FRECENT"}),
+ })
+ });
+ window.dispatchEvent(msg);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/components/newtab/tests/browser/newtabmessages_prefs.html b/browser/components/newtab/tests/browser/newtabmessages_prefs.html
new file mode 100644
index 000000000..9b38af4b9
--- /dev/null
+++ b/browser/components/newtab/tests/browser/newtabmessages_prefs.html
@@ -0,0 +1,32 @@
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Newtab WebChannel test</title>
+ </head>
+ <body>
+ <script>
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ if (e.detail.message && e.detail.message.type === "RECEIVE_PREFS") {
+ let reply = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "newtab",
+ message: JSON.stringify({type: "responseAck"}),
+ })
+ });
+ window.dispatchEvent(reply);
+ }
+ }, true);
+
+ document.onreadystatechange = function () {
+ let msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_PREFS"}),
+ })
+ });
+ window.dispatchEvent(msg);
+ };
+
+ </script>
+ </body>
+</html>
diff --git a/browser/components/newtab/tests/browser/newtabmessages_preview.html b/browser/components/newtab/tests/browser/newtabmessages_preview.html
new file mode 100644
index 000000000..4fe55132d
--- /dev/null
+++ b/browser/components/newtab/tests/browser/newtabmessages_preview.html
@@ -0,0 +1,37 @@
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Newtab WebChannel test</title>
+ </head>
+ <body>
+ <script>
+ let thumbURL = "https://example.com/browser/browser/components/newtab/tests/browser/blue_page.html";
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ if (e.detail.message && e.detail.message.type === "RECEIVE_THUMB") {
+ if (e.detail.message.data.imgData && e.detail.message.data.url === thumbURL) {
+ let reply = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "newtab",
+ message: JSON.stringify({type: "responseAck"}),
+ })
+ });
+ window.dispatchEvent(reply);
+ }
+ }
+ }, true);
+
+ document.onreadystatechange = function () {
+ if (document.readyState === "complete") {
+ let msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_THUMB", data: thumbURL}),
+ })
+ });
+ window.dispatchEvent(msg);
+ }
+ };
+ </script>
+ </body>
+</html>
diff --git a/browser/components/newtab/tests/browser/newtabmessages_search.html b/browser/components/newtab/tests/browser/newtabmessages_search.html
new file mode 100644
index 000000000..b8b21c42a
--- /dev/null
+++ b/browser/components/newtab/tests/browser/newtabmessages_search.html
@@ -0,0 +1,113 @@
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Newtab WebChannel test</title>
+ </head>
+ <body>
+ <script>
+ let suggestionsData = {
+ engineName: "Engine1",
+ searchString: "test",
+ };
+ let removeFormHistoryData = "test";
+ let performSearchData = {
+ engineName: "Engine1",
+ healthReportKey: "1",
+ searchPurpose: "d",
+ searchString: "test",
+ };
+ let cycleEngineData = "Engine2";
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ if (e.detail.message) {
+ let reply;
+ switch (e.detail.message.type) {
+ case "RECEIVE_UISTRINGS":
+ reply = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "UIStringsAck", data: e.detail.message.data}),
+ }
+ });
+ window.dispatchEvent(reply);
+ break;
+ case "RECEIVE_SEARCH_SUGGESTIONS":
+ reply = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "suggestionsAck", data: e.detail.message.data}),
+ }
+ });
+ window.dispatchEvent(reply);
+ break;
+ case "RECEIVE_SEARCH_STATE":
+ reply = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "stateAck", data: e.detail.message.data}),
+ }
+ });
+ window.dispatchEvent(reply);
+ break;
+ case "RECEIVE_CURRENT_ENGINE":
+ reply = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "currentEngineAck", data: e.detail.message.data}),
+ }
+ });
+ 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_UISTRINGS"}),
+ }
+ });
+ window.dispatchEvent(msg);
+ msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_SEARCH_SUGGESTIONS", data: suggestionsData}),
+ }
+ });
+ window.dispatchEvent(msg);
+ msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_SEARCH_STATE"}),
+ }
+ });
+ window.dispatchEvent(msg);
+ msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_REMOVE_FORM_HISTORY", data: removeFormHistoryData}),
+ }
+ });
+ window.dispatchEvent(msg);
+ msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_PERFORM_SEARCH", data: performSearchData}),
+ }
+ });
+ window.dispatchEvent(msg);
+ msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "newtab",
+ message: JSON.stringify({type: "REQUEST_CYCLE_ENGINE", data: cycleEngineData}),
+ }
+ });
+ window.dispatchEvent(msg);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/components/newtab/tests/browser/newtabwebchannel_basic.html b/browser/components/newtab/tests/browser/newtabwebchannel_basic.html
new file mode 100644
index 000000000..7f3c79920
--- /dev/null
+++ b/browser/components/newtab/tests/browser/newtabwebchannel_basic.html
@@ -0,0 +1,36 @@
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Newtab WebChannel test</title>
+ </head>
+ <body>
+ <script>
+ document.onreadystatechange = function () {
+ let msg = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "newtab",
+ message: JSON.stringify({type: "foo", data: "bar"}),
+ })
+ });
+ window.dispatchEvent(msg);
+ };
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ if (e.detail.message && e.detail.message.type.startsWith("respond")) {
+ var detail = {
+ id: "newtab",
+ message: JSON.stringify({type: "reply", data: "quuz"}),
+ };
+ if (e.detail.message.type !== "respond_object") {
+ detail = JSON.stringify(detail);
+ }
+ let reply = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: detail
+ });
+ window.dispatchEvent(reply);
+ }
+ }, true);
+
+ </script>
+ </body>
+</html>
diff --git a/browser/components/newtab/tests/xpcshell/.eslintrc.js b/browser/components/newtab/tests/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js b/browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js
new file mode 100644
index 000000000..21f68ab70
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* globals Services, XPCOMUtils, NewTabPrefsProvider, Preferences, aboutNewTabService, do_register_cleanup */
+
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+ "resource:///modules/NewTabPrefsProvider.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+ "resource://gre/modules/Locale.jsm");
+
+const DEFAULT_HREF = aboutNewTabService.generateRemoteURL();
+const DEFAULT_CHROME_URL = "chrome://browser/content/newtab/newTab.xhtml";
+const DOWNLOADS_URL = "chrome://browser/content/downloads/contentAreaDownloadsView.xul";
+const DEFAULT_VERSION = aboutNewTabService.remoteVersion;
+
+function cleanup() {
+ Services.prefs.setBoolPref("browser.newtabpage.remote", false);
+ Services.prefs.setCharPref("browser.newtabpage.remote.version", DEFAULT_VERSION);
+ aboutNewTabService.resetNewTabURL();
+ NewTabPrefsProvider.prefs.uninit();
+}
+
+do_register_cleanup(cleanup);
+
+/**
+ * Test the overriding of the default URL
+ */
+add_task(function* test_override_remote_disabled() {
+ NewTabPrefsProvider.prefs.init();
+ let notificationPromise;
+ Services.prefs.setBoolPref("browser.newtabpage.remote", false);
+
+ // tests default is the local newtab resource
+ Assert.equal(aboutNewTabService.defaultURL, DEFAULT_CHROME_URL,
+ `Default newtab URL should be ${DEFAULT_CHROME_URL}`);
+
+ // override with some remote URL
+ let url = "http://example.com/";
+ notificationPromise = nextChangeNotificationPromise(url);
+ aboutNewTabService.newTabURL = url;
+ yield notificationPromise;
+ Assert.ok(aboutNewTabService.overridden, "Newtab URL should be overridden");
+ Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
+ Assert.equal(aboutNewTabService.newTabURL, url, "Newtab URL should be the custom URL");
+
+ // test reset with remote disabled
+ notificationPromise = nextChangeNotificationPromise("about:newtab");
+ aboutNewTabService.resetNewTabURL();
+ yield notificationPromise;
+ Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
+ Assert.equal(aboutNewTabService.newTabURL, "about:newtab", "Newtab URL should be the default");
+
+ // test override to a chrome URL
+ notificationPromise = nextChangeNotificationPromise(DOWNLOADS_URL);
+ aboutNewTabService.newTabURL = DOWNLOADS_URL;
+ yield notificationPromise;
+ Assert.ok(aboutNewTabService.overridden, "Newtab URL should be overridden");
+ Assert.equal(aboutNewTabService.newTabURL, DOWNLOADS_URL, "Newtab URL should be the custom URL");
+
+ cleanup();
+});
+
+add_task(function* test_override_remote_enabled() {
+ NewTabPrefsProvider.prefs.init();
+ let notificationPromise;
+ // change newtab page to remote
+ notificationPromise = nextChangeNotificationPromise("about:newtab");
+ Services.prefs.setBoolPref("browser.newtabpage.remote", true);
+ yield notificationPromise;
+ let remoteHref = aboutNewTabService.generateRemoteURL();
+ Assert.equal(aboutNewTabService.defaultURL, remoteHref, "Newtab URL should be the default remote URL");
+ Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
+ Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
+
+ // change to local newtab page while remote is enabled
+ notificationPromise = nextChangeNotificationPromise(DEFAULT_CHROME_URL);
+ aboutNewTabService.newTabURL = DEFAULT_CHROME_URL;
+ yield notificationPromise;
+ Assert.equal(aboutNewTabService.newTabURL, DEFAULT_CHROME_URL,
+ "Newtab URL set to chrome url");
+ Assert.equal(aboutNewTabService.defaultURL, DEFAULT_CHROME_URL,
+ "Newtab URL defaultURL set to the default chrome URL");
+ Assert.ok(aboutNewTabService.overridden, "Newtab URL should be overridden");
+ Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
+
+ cleanup();
+});
+
+/**
+ * Tests reponse to updates to prefs
+ */
+add_task(function* test_updates() {
+ /*
+ * Simulates a "cold-boot" situation, with some pref already set before testing a series
+ * of changes.
+ */
+ Preferences.set("browser.newtabpage.remote", true);
+ aboutNewTabService.resetNewTabURL(); // need to set manually because pref notifs are off
+ let notificationPromise;
+ let productionModeBaseUrl = "https://content.cdn.mozilla.net";
+ let testModeBaseUrl = "https://example.com";
+ let expectedPath = `/newtab` +
+ `/v${aboutNewTabService.remoteVersion}` +
+ `/${aboutNewTabService.remoteReleaseName}` +
+ "/en-GB" +
+ "/index.html";
+ let expectedHref = productionModeBaseUrl + expectedPath;
+ Preferences.set("intl.locale.matchOS", true);
+ Preferences.set("general.useragent.locale", "en-GB");
+ Preferences.set("browser.newtabpage.remote.mode", "production");
+ NewTabPrefsProvider.prefs.init();
+
+ // test update checks for prefs
+ notificationPromise = nextChangeNotificationPromise(
+ expectedHref, "Remote href should be updated");
+ Preferences.set("intl.locale.matchOS", false);
+ yield notificationPromise;
+
+ notificationPromise = nextChangeNotificationPromise(
+ DEFAULT_HREF, "Remote href changes back to default");
+ Preferences.set("general.useragent.locale", "en-US");
+ yield notificationPromise;
+
+ // test update fires when mode is changed
+ expectedPath = expectedPath.replace("/en-GB/", "/en-US/");
+ notificationPromise = nextChangeNotificationPromise(
+ testModeBaseUrl + expectedPath, "Remote href changes back to origin of test mode");
+ Preferences.set("browser.newtabpage.remote.mode", "test");
+ yield notificationPromise;
+
+ // test invalid mode ends up pointing to production url
+ notificationPromise = nextChangeNotificationPromise(
+ DEFAULT_HREF, "Remote href changes back to production default");
+ Preferences.set("browser.newtabpage.remote.mode", "invalid");
+ yield notificationPromise;
+
+ // test update fires on override and reset
+ let testURL = "https://example.com/";
+ notificationPromise = nextChangeNotificationPromise(
+ testURL, "a notification occurs on override");
+ aboutNewTabService.newTabURL = testURL;
+ yield notificationPromise;
+
+ // from overridden to default
+ notificationPromise = nextChangeNotificationPromise(
+ "about:newtab", "a notification occurs on reset");
+ aboutNewTabService.resetNewTabURL();
+ Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
+ Assert.equal(aboutNewTabService.defaultURL, DEFAULT_HREF, "Default URL should be the remote page");
+ yield notificationPromise;
+
+ // override to default URL from default URL
+ notificationPromise = nextChangeNotificationPromise(
+ testURL, "a notification only occurs for a change in overridden urls");
+ aboutNewTabService.newTabURL = aboutNewTabService.generateRemoteURL();
+ Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
+ aboutNewTabService.newTabURL = testURL;
+ yield notificationPromise;
+ Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
+
+ // reset twice, only one notification for default URL
+ notificationPromise = nextChangeNotificationPromise(
+ "about:newtab", "reset occurs");
+ aboutNewTabService.resetNewTabURL();
+ yield notificationPromise;
+
+ cleanup();
+});
+
+/**
+ * Verifies that releaseFromUpdateChannel
+ * Returns the correct release names
+ */
+add_task(function* test_release_names() {
+ let valid_channels = ["esr", "release", "beta", "aurora", "nightly"];
+ let invalid_channels = new Set(["default", "invalid"]);
+
+ for (let channel of valid_channels) {
+ Assert.equal(channel, aboutNewTabService.releaseFromUpdateChannel(channel),
+ "release == channel name when valid");
+ }
+
+ for (let channel of invalid_channels) {
+ Assert.equal("nightly", aboutNewTabService.releaseFromUpdateChannel(channel),
+ "release == nightly when invalid");
+ }
+});
+
+/**
+ * Verifies that remote version updates changes the remote newtab url
+ */
+add_task(function* test_version_update() {
+ NewTabPrefsProvider.prefs.init();
+
+ Services.prefs.setBoolPref("browser.newtabpage.remote", true);
+ Assert.ok(aboutNewTabService.remoteEnabled, "remote mode enabled");
+
+ let productionModeBaseUrl = "https://content.cdn.mozilla.net";
+ let version_incr = String(parseInt(DEFAULT_VERSION) + 1);
+ let expectedPath = `/newtab` +
+ `/v${version_incr}` +
+ `/${aboutNewTabService.remoteReleaseName}` +
+ `/${Locale.getLocale()}` +
+ `/index.html`;
+ let expectedHref = productionModeBaseUrl + expectedPath;
+
+ let notificationPromise;
+ notificationPromise = nextChangeNotificationPromise(expectedHref);
+ Preferences.set("browser.newtabpage.remote.version", version_incr);
+ yield notificationPromise;
+
+ cleanup();
+});
+
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
+ Services.obs.removeObserver(observer, aTopic);
+ Assert.equal(aData, aNewURL, testMessage);
+ resolve();
+ }, "newtab-url-changed", false);
+ });
+}
diff --git a/browser/components/newtab/tests/xpcshell/test_NewTabPrefsProvider.js b/browser/components/newtab/tests/xpcshell/test_NewTabPrefsProvider.js
new file mode 100644
index 000000000..f364d0300
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_NewTabPrefsProvider.js
@@ -0,0 +1,50 @@
+"use strict";
+
+/* global XPCOMUtils, equal, Preferences, NewTabPrefsProvider, run_next_test */
+/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+ "resource:///modules/NewTabPrefsProvider.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_observe() {
+ let prefsMap = NewTabPrefsProvider.prefs.prefsMap;
+ for (let prefName of prefsMap.keys()) {
+ let prefValueType = prefsMap.get(prefName);
+
+ let beforeVal;
+ let afterVal;
+
+ switch (prefValueType) {
+ case "bool":
+ beforeVal = false;
+ afterVal = true;
+ Preferences.set(prefName, beforeVal);
+ break;
+ case "localized":
+ case "str":
+ beforeVal = "";
+ afterVal = "someStr";
+ Preferences.set(prefName, beforeVal);
+ break;
+ }
+ NewTabPrefsProvider.prefs.init();
+ let promise = new Promise(resolve => {
+ NewTabPrefsProvider.prefs.once(prefName, (name, data) => { // jshint ignore:line
+ resolve([name, data]);
+ });
+ });
+ Preferences.set(prefName, afterVal);
+ let [actualName, actualData] = yield promise;
+ equal(prefName, actualName, `emitter sent the correct pref: ${prefName}`);
+ equal(afterVal, actualData, `emitter collected correct pref data for ${prefName}`);
+ NewTabPrefsProvider.prefs.uninit();
+ }
+});
diff --git a/browser/components/newtab/tests/xpcshell/test_NewTabSearchProvider.js b/browser/components/newtab/tests/xpcshell/test_NewTabSearchProvider.js
new file mode 100644
index 000000000..3e60b282a
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_NewTabSearchProvider.js
@@ -0,0 +1,82 @@
+"use strict";
+
+/* global XPCOMUtils, NewTabSearchProvider, run_next_test, ok, equal, do_check_true, do_get_profile, Services */
+/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabSearchProvider",
+ "resource:///modules/NewTabSearchProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
+ "resource:///modules/ContentSearch.jsm");
+
+// ensure a profile exists
+do_get_profile();
+
+function run_test() {
+ run_next_test();
+}
+
+function hasProp(obj) {
+ return function(aProp) {
+ ok(obj.hasOwnProperty(aProp), `expect to have property ${aProp}`);
+ };
+}
+
+add_task(function* test_search() {
+ ContentSearch.init();
+ let observerPromise = new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ if (aData === "init-complete" && aTopic === "browser-search-service") {
+ Services.obs.removeObserver(observer, "browser-search-service");
+ resolve();
+ }
+ }, "browser-search-service", false);
+ });
+ Services.search.init();
+ yield observerPromise;
+ do_check_true(Services.search.isInitialized);
+
+ // get initial state of search and check it has correct properties
+ let state = yield NewTabSearchProvider.search.asyncGetState();
+ let stateProps = hasProp(state);
+ ["engines", "currentEngine"].forEach(stateProps);
+
+ // check that the current engine is correct and has correct properties
+ let {currentEngine} = state;
+ equal(currentEngine.name, Services.search.currentEngine.name, "Current engine has been correctly set");
+ var engineProps = hasProp(currentEngine);
+ ["name", "placeholder", "iconBuffer"].forEach(engineProps);
+
+ // create dummy test engines to test observer
+ Services.search.addEngineWithDetails("TestSearch1", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ Services.search.addEngineWithDetails("TestSearch2", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+
+ // set one of the dummy test engines to the default engine
+ Services.search.defaultEngine = Services.search.getEngineByName("TestSearch1");
+
+ // test that the event emitter is working by setting a new current engine "TestSearch2"
+ let engineName = "TestSearch2";
+ NewTabSearchProvider.search.init();
+
+ // event emitter will fire when current engine is changed
+ let promise = new Promise(resolve => {
+ NewTabSearchProvider.search.once("browser-search-engine-modified", (name, data) => { // jshint ignore:line
+ resolve([name, data.name]);
+ });
+ });
+
+ // set a new current engine
+ Services.search.currentEngine = Services.search.getEngineByName(engineName);
+ let expectedEngineName = Services.search.currentEngine.name;
+
+ // emitter should fire and return the new engine
+ let [eventName, actualEngineName] = yield promise;
+ equal(eventName, "browser-search-engine-modified", `emitter sent the correct event ${eventName}`);
+ equal(expectedEngineName, actualEngineName, `emitter set the correct engine ${expectedEngineName}`);
+ NewTabSearchProvider.search.uninit();
+});
diff --git a/browser/components/newtab/tests/xpcshell/test_NewTabURL.js b/browser/components/newtab/tests/xpcshell/test_NewTabURL.js
new file mode 100644
index 000000000..1505e638c
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_NewTabURL.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* globals Services, NewTabURL, XPCOMUtils, aboutNewTabService, NewTabPrefsProvider */
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/NewTabURL.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+ "resource:///modules/NewTabPrefsProvider.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService");
+
+add_task(function*() {
+ let defaultURL = aboutNewTabService.newTabURL;
+ Services.prefs.setBoolPref("browser.newtabpage.remote", false);
+
+ Assert.equal(NewTabURL.get(), defaultURL, `Default newtab URL should be ${defaultURL}`);
+ let url = "http://example.com/";
+ let notificationPromise = promiseNewtabURLNotification(url);
+ NewTabURL.override(url);
+ yield notificationPromise;
+ Assert.ok(NewTabURL.overridden, "Newtab URL should be overridden");
+ Assert.equal(NewTabURL.get(), url, "Newtab URL should be the custom URL");
+
+ notificationPromise = promiseNewtabURLNotification(defaultURL);
+ NewTabURL.reset();
+ yield notificationPromise;
+ Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden");
+ Assert.equal(NewTabURL.get(), defaultURL, "Newtab URL should be the default");
+
+ // change newtab page to remote
+ NewTabPrefsProvider.prefs.init();
+ Services.prefs.setBoolPref("browser.newtabpage.remote", true);
+ Assert.equal(NewTabURL.get(), "about:newtab", `Newtab URL should be about:newtab`);
+ Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden");
+ NewTabPrefsProvider.prefs.uninit();
+});
+
+function promiseNewtabURLNotification(aNewURL) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint ignore:line
+ Services.obs.removeObserver(observer, aTopic);
+ Assert.equal(aData, aNewURL, "Data for newtab-url-changed notification should be new URL.");
+ resolve();
+ }, "newtab-url-changed", false);
+ });
+}
diff --git a/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js b/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
new file mode 100644
index 000000000..22815741b
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
@@ -0,0 +1,358 @@
+"use strict";
+
+/* global XPCOMUtils, PlacesUtils, PlacesTestUtils, PlacesProvider, NetUtil */
+/* global do_get_profile, run_next_test, add_task */
+/* global equal, ok */
+
+const {
+ utils: Cu,
+ interfaces: Ci,
+} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesProvider",
+ "resource:///modules/PlacesProvider.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+// ensure a profile exists
+do_get_profile();
+
+function run_test() {
+ PlacesProvider.links.init();
+ run_next_test();
+}
+
+// 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 timestap
+function timeDaysAgo(numDays) {
+ return TIME_NOW - (numDays * 24 * 60 * 60 * 1000);
+}
+
+// utility function to make a visit for insetion 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,
+ };
+}
+
+/** Test LinkChecker **/
+
+add_task(function test_LinkChecker_securityCheck() {
+
+ let urls = [
+ {url: "javascript:alert('hello')", expected: false}, // jshint ignore:line
+ {url: "data:image/png;base64,XXX", expected: false},
+ {url: "about:newtab", expected: true},
+ {url: "https://example.com", expected: true},
+ {url: "ftp://example.com", expected: true},
+ {url: "file://home/file/image.png", expected: true},
+ {url: "resource:///modules/PlacesProvider.jsm", expected: true},
+ ];
+ for (let {url, expected} of urls) {
+ let observed = PlacesProvider.LinkChecker.checkLoadURI(url);
+ equal(observed, expected, `can load "${url}"?`);
+ }
+});
+
+/** Test Provider **/
+
+add_task(function* test_Links_getLinks() {
+ yield PlacesTestUtils.clearHistory();
+ let provider = PlacesProvider.links;
+
+ let links = yield provider.getLinks();
+ equal(links.length, 0, "empty history yields empty links");
+
+ // add a visit
+ let testURI = NetUtil.newURI("http://mozilla.com");
+ yield PlacesTestUtils.addVisits(testURI);
+
+ links = yield provider.getLinks();
+ equal(links.length, 1, "adding a visit yields a link");
+ equal(links[0].url, testURI.spec, "added visit corresponds to added url");
+});
+
+add_task(function* test_Links_getLinks_Order() {
+ yield PlacesTestUtils.clearHistory();
+ let provider = PlacesProvider.links;
+
+ // 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
+ ];
+
+ let links = yield provider.getLinks();
+ equal(links.length, 0, "empty history yields empty links");
+ yield PlacesTestUtils.addVisits(visits);
+
+ links = yield provider.getLinks();
+ equal(links.length, visits.length, "number of links added is the same as obtain by getLinks");
+ for (let i = 0; i < links.length; i++) {
+ equal(links[i].url, visits[i].uri.spec, "links are obtained in the expected order");
+ }
+});
+
+add_task(function* test_Links_getLinks_Deduplication() {
+ yield PlacesTestUtils.clearHistory();
+ let provider = PlacesProvider.links;
+
+ // all for visits must come from different domains to avoid deduplication
+ let visits = [
+ makeVisit(0, 2, true, "http://bar.com/"), // frecency 200, 2 days ago
+ makeVisit(1, 0, true, "http://bar.com/"), // frecency 200, today
+ makeVisit(2, 0, false, "http://foo.com/"), // frecency 10, today
+ makeVisit(3, 0, true, "http://foo.com/"), // frecency 200, today
+ ];
+
+ let links = yield provider.getLinks();
+ equal(links.length, 0, "empty history yields empty links");
+ yield PlacesTestUtils.addVisits(visits);
+
+ links = yield provider.getLinks();
+ equal(links.length, 2, "only two links must be left after deduplication");
+ equal(links[0].url, visits[1].uri.spec, "earliest link is present");
+ equal(links[1].url, visits[3].uri.spec, "most fresent link is present");
+});
+
+add_task(function* test_Links_onLinkChanged() {
+ let provider = PlacesProvider.links;
+
+ let url = "https://example.com/onFrecencyChanged1";
+ let linkChangedMsgCount = 0;
+
+ let linkChangedPromise = new Promise(resolve => {
+ let handler = (_, link) => { // jshint ignore:line
+ /* There are 3 linkChanged events:
+ * 1. visit insertion (-1 frecency by default)
+ * 2. frecency score update (after transition type calculation etc)
+ * 3. title change
+ */
+ if (link.url === url) {
+ equal(link.url, url, `expected url on linkChanged event`);
+ linkChangedMsgCount += 1;
+ if (linkChangedMsgCount === 3) {
+ ok(true, `all linkChanged events captured`);
+ provider.off("linkChanged", this);
+ resolve();
+ }
+ }
+ };
+ provider.on("linkChanged", handler);
+ });
+
+ // add a visit
+ let testURI = NetUtil.newURI(url);
+ yield PlacesTestUtils.addVisits(testURI);
+ yield linkChangedPromise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_Links_onClearHistory() {
+ let provider = PlacesProvider.links;
+
+ let clearHistoryPromise = new Promise(resolve => {
+ let handler = () => {
+ ok(true, `clearHistory event captured`);
+ provider.off("clearHistory", handler);
+ resolve();
+ };
+ provider.on("clearHistory", handler);
+ });
+
+ // 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;
+});
+
+add_task(function* test_Links_onDeleteURI() {
+ let provider = PlacesProvider.links;
+
+ let testURL = "https://example.com/toDelete";
+
+ let deleteURIPromise = new Promise(resolve => {
+ let handler = (_, {url}) => { // jshint ignore:line
+ equal(testURL, url, "deleted url and expected url are the same");
+ provider.off("deleteURI", handler);
+ resolve();
+ };
+
+ provider.on("deleteURI", handler);
+ });
+
+ let testURI = NetUtil.newURI(testURL);
+ yield PlacesTestUtils.addVisits(testURI);
+ yield PlacesUtils.history.remove(testURL);
+ yield deleteURIPromise;
+});
+
+add_task(function* test_Links_onManyLinksChanged() {
+ let provider = PlacesProvider.links;
+
+ let promise = new Promise(resolve => {
+ let handler = () => {
+ ok(true);
+ provider.off("manyLinksChanged", handler);
+ resolve();
+ };
+
+ provider.on("manyLinksChanged", handler);
+ });
+
+ let testURL = "https://example.com/toDelete";
+ let testURI = NetUtil.newURI(testURL);
+ yield PlacesTestUtils.addVisits(testURI);
+
+ // trigger DecayFrecency
+ PlacesUtils.history.QueryInterface(Ci.nsIObserver).
+ observe(null, "idle-daily", "");
+
+ yield promise;
+});
+
+add_task(function* test_Links_execute_query() {
+ yield PlacesTestUtils.clearHistory();
+ let provider = PlacesProvider.links;
+
+ let visits = [
+ makeVisit(0, 0, true), // frecency 200, today
+ makeVisit(1, 0, true), // frecency 200, today
+ makeVisit(2, 2, true), // frecency 200, 2 days ago
+ makeVisit(3, 2, false), // frecency 10, 2 days ago, transition
+ ];
+
+ yield PlacesTestUtils.addVisits(visits);
+
+ function testItemValue(results, index, value) {
+ equal(results[index][0], `${TEST_URL}${value}`, "raw url");
+ equal(results[index][1], `test visit for ${TEST_URL}${value}`, "raw title");
+ }
+
+ function testItemObject(results, index, columnValues) {
+ Object.keys(columnValues).forEach(name => {
+ equal(results[index][name], columnValues[name], "object name " + name);
+ });
+ }
+
+ // select all 4 records
+ let results = yield provider.executePlacesQuery("select url, title from moz_places");
+ equal(results.length, 4, "expect 4 items");
+ // check for insert order sequence
+ for (let i = 0; i < results.length; i++) {
+ testItemValue(results, i, i);
+ }
+
+ // test parameter passing
+ results = yield provider.executePlacesQuery(
+ "select url, title from moz_places limit :limit",
+ {params: {limit: 2}}
+ );
+ equal(results.length, 2, "expect 2 items");
+ for (let i = 0; i < results.length; i++) {
+ testItemValue(results, i, i);
+ }
+
+ // test extracting items by name
+ results = yield provider.executePlacesQuery(
+ "select url, title from moz_places limit :limit",
+ {columns: ["url", "title"], params: {limit: 4}}
+ );
+ equal(results.length, 4, "expect 4 items");
+ for (let i = 0; i < results.length; i++) {
+ testItemObject(results, i, {
+ "url": `${TEST_URL}${i}`,
+ "title": `test visit for ${TEST_URL}${i}`,
+ });
+ }
+
+ // test ordering
+ results = yield provider.executePlacesQuery(
+ "select url, title, last_visit_date, frecency from moz_places " +
+ "order by frecency DESC, last_visit_date DESC, url DESC limit :limit",
+ {columns: ["url", "title", "last_visit_date", "frecency"], params: {limit: 4}}
+ );
+ equal(results.length, 4, "expect 4 items");
+ testItemObject(results, 0, {url: `${TEST_URL}1`});
+ testItemObject(results, 1, {url: `${TEST_URL}0`});
+ testItemObject(results, 2, {url: `${TEST_URL}2`});
+ testItemObject(results, 3, {url: `${TEST_URL}3`});
+
+ // test callback passing
+ results = [];
+ function handleRow(aRow) {
+ results.push({
+ url: aRow.getResultByName("url"),
+ title: aRow.getResultByName("title"),
+ last_visit_date: aRow.getResultByName("last_visit_date"),
+ frecency: aRow.getResultByName("frecency")
+ });
+ }
+ yield provider.executePlacesQuery(
+ "select url, title, last_visit_date, frecency from moz_places " +
+ "order by frecency DESC, last_visit_date DESC, url DESC",
+ {callback: handleRow}
+ );
+ equal(results.length, 4, "expect 4 items");
+ testItemObject(results, 0, {url: `${TEST_URL}1`});
+ testItemObject(results, 1, {url: `${TEST_URL}0`});
+ testItemObject(results, 2, {url: `${TEST_URL}2`});
+ testItemObject(results, 3, {url: `${TEST_URL}3`});
+
+ // negative test cases
+ // bad sql
+ try {
+ yield provider.executePlacesQuery("select from moz");
+ do_throw("bad sql should've thrown");
+ }
+ catch (e) {
+ do_check_true("expected failure - bad sql");
+ }
+ // missing bindings
+ try {
+ yield provider.executePlacesQuery("select * from moz_places limit :limit");
+ do_throw("bad sql should've thrown");
+ }
+ catch (e) {
+ do_check_true("expected failure - missing bidning");
+ }
+ // non-existent column name
+ try {
+ yield provider.executePlacesQuery("select * from moz_places limit :limit",
+ {columns: ["no-such-column"], params: {limit: 4}});
+ do_throw("bad sql should've thrown");
+ }
+ catch (e) {
+ do_check_true("expected failure - wrong column name");
+ }
+
+ // cleanup
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/browser/components/newtab/tests/xpcshell/xpcshell.ini b/browser/components/newtab/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..c249ee3e2
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_AboutNewTabService.js]
+[test_NewTabPrefsProvider.js]
+[test_NewTabSearchProvider.js]
+[test_NewTabURL.js]
+[test_PlacesProvider.js]