--- a/browser/components/newtab/NewTabMessages.jsm
+++ b/browser/components/newtab/NewTabMessages.jsm
@@ -1,58 +1,89 @@
/*global
NewTabWebChannel,
NewTabPrefsProvider,
PlacesProvider,
+ PreviewProvider,
+ NewTabSearchProvider,
Preferences,
- XPCOMUtils
+ 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 **/
@@ -70,27 +101,80 @@ let NewTabMessages = {
});
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);
},
@@ -102,28 +186,32 @@ let NewTabMessages = {
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);
@@ -131,23 +219,25 @@ let NewTabMessages = {
}
},
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);
}
}
PlacesProvider.links.uninit();
NewTabPrefsProvider.prefs.uninit();
+ NewTabSearchProvider.search.uninit();
NewTabWebChannel.uninit();
}
};
--- a/browser/components/newtab/NewTabPrefsProvider.jsm
+++ b/browser/components/newtab/NewTabPrefsProvider.jsm
@@ -23,26 +23,28 @@ const gPrefsMap = new Map([
["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.newtabpage.updateIntroShown",
+ "browser.search.hiddenOneOffs",
]);
let PrefsProvider = function PrefsProvider() {
EventEmitter.decorate(this);
};
PrefsProvider.prototype = {
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/NewTabSearchProvider.jsm
@@ -0,0 +1,112 @@
+/* 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
+ switch (data) {
+ case "engine-current":
+ if (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));
+ }
+ break;
+ case "engine-default":
+ // engine-default is always sent with engine-current and isn't
+ // relevant to content searches.
+ break;
+ default:
+ Cu.reportError(new Error("NewTabSearchProvider observing unknown topic"));
+ break;
+ }
+ },
+
+ 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.ownerDocument.defaultView;
+ 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(),
+};
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -9,16 +9,17 @@ BROWSER_CHROME_MANIFESTS += ['tests/brow
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',
--- a/browser/components/newtab/tests/browser/browser.ini
+++ b/browser/components/newtab/tests/browser/browser.ini
@@ -1,14 +1,15 @@
[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]
[browser_newtabwebchannel.js]
--- a/browser/components/newtab/tests/browser/browser_newtabmessages.js
+++ b/browser/components/newtab/tests/browser/browser_newtabmessages.js
@@ -1,13 +1,15 @@
-/* globals Cu, XPCOMUtils, Preferences, is, registerCleanupFunction, NewTabWebChannel, PlacesTestUtils, Task */
+/* 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");
@@ -156,8 +158,65 @@ add_task(function* placesMessages_reques
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();
+});
new file mode 100644
--- /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>
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_NewTabSearchProvider.js
@@ -0,0 +1,84 @@
+"use strict";
+
+/* global XPCOMUtils, NewTabSearchProvider, run_next_test, ok, equal, do_check_true, do_get_profile, Services */
+/* exported run_test */
+/* 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();
+});
+
--- a/browser/components/newtab/tests/xpcshell/xpcshell.ini
+++ b/browser/components/newtab/tests/xpcshell/xpcshell.ini
@@ -1,10 +1,11 @@
[DEFAULT]
head =
tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_AboutNewTabService.js]
[test_NewTabPrefsProvider.js]
+[test_NewTabSearchProvider.js]
[test_NewTabURL.js]
[test_PlacesProvider.js]
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -1,12 +1,12 @@
/* 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, Services, Task, Promise, SearchSuggestionController, FormHistory, PrivateBrowsingUtils */
"use strict";
this.EXPORTED_SYMBOLS = [
"ContentSearch",
];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
@@ -68,20 +68,20 @@ const MAX_SUGGESTIONS = 6;
*
* Outbound messages have the following types:
*
* CurrentEngine
* Broadcast when the current engine changes.
* data: see _currentEngineObj
* CurrentState
* Broadcast when the current search state changes.
- * data: see _currentStateObj
+ * data: see currentStateObj
* State
* Sent in reply to GetState.
- * data: see _currentStateObj
+ * data: see currentStateObj
* Strings
* Sent in reply to GetStrings
* data: Object containing string names and values for the current locale.
* Suggestions
* Sent in reply to GetSuggestions.
* data: see _onMessageGetSuggestions
* SuggestionsCancelled
* Sent in reply to GetSuggestions when pending GetSuggestions events are
@@ -121,16 +121,17 @@ this.ContentSearch = {
get searchSuggestionUIStrings() {
if (this._searchSuggestionUIStrings) {
return this._searchSuggestionUIStrings;
}
this._searchSuggestionUIStrings = {};
let searchBundle = Services.strings.createBundle("chrome://browser/locale/search.properties");
let stringNames = ["searchHeader", "searchPlaceholder", "searchForSomethingWith",
"searchWithHeader", "searchSettings"];
+
for (let name of stringNames) {
this._searchSuggestionUIStrings[name] = searchBundle.GetStringFromName(name);
}
return this._searchSuggestionUIStrings;
},
destroy: function () {
if (this._destroyedPromise) {
@@ -139,17 +140,18 @@ this.ContentSearch = {
Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager).
removeMessageListener(INBOUND_MESSAGE, this);
Services.obs.removeObserver(this, "browser-search-engine-modified");
Services.obs.removeObserver(this, "shutdown-leaks-before-check");
this._eventQueue.length = 0;
- return this._destroyedPromise = Promise.resolve(this._currentEventPromise);
+ this._destroyedPromise = Promise.resolve(this._currentEventPromise);
+ return this._destroyedPromise;
},
/**
* Focuses the search input in the page with the given message manager.
* @param messageManager
* The MessageManager object of the selected browser.
*/
focusInput: function (messageManager) {
@@ -172,17 +174,17 @@ this.ContentSearch = {
msg.target.removeEventListener("SwapDocShells", msg, true);
msg.target = event.detail;
msg.target.addEventListener("SwapDocShells", msg, true);
};
msg.target.addEventListener("SwapDocShells", msg, true);
// Search requests cause cancellation of all Suggestion requests from the
// same browser.
- if (msg.data.type == "Search") {
+ if (msg.data.type === "Search") {
this._cancelSuggestions(msg);
}
this._eventQueue.push({
type: "Message",
data: msg,
});
this._processEventQueue();
@@ -200,16 +202,165 @@ this.ContentSearch = {
break;
case "shutdown-leaks-before-check":
subj.wrappedJSObject.client.addBlocker(
"ContentSearch: Wait until the service is destroyed", () => this.destroy());
break;
}
},
+ removeFormHistoryEntry: function (msg, entry) {
+ let browserData = this._suggestionDataForBrowser(msg.target);
+ if (browserData && browserData.previousFormHistoryResult) {
+ let { previousFormHistoryResult } = browserData;
+ for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
+ if (previousFormHistoryResult.getValueAt(i) === entry) {
+ previousFormHistoryResult.removeValueAt(i, true);
+ break;
+ }
+ }
+ }
+ },
+
+ performSearch: function (msg, data) {
+ this._ensureDataHasProperties(data, [
+ "engineName",
+ "searchString",
+ "healthReportKey",
+ "searchPurpose",
+ ]);
+ let engine = Services.search.getEngineByName(data.engineName);
+ let submission = engine.getSubmission(data.searchString, "", data.searchPurpose);
+ let browser = msg.target;
+ let win;
+ try {
+ win = browser.ownerDocument.defaultView;
+ }
+ catch (err) {
+ // The browser may have been closed between the time its content sent the
+ // message and the time we handle it. In that case, trying to call any
+ // method on it will throw.
+ return;
+ }
+
+ let where = win.whereToOpenLink(data.originalEvent);
+
+ // There is a chance that by the time we receive the search message, the user
+ // has switched away from the tab that triggered the search. If, based on the
+ // event, we need to load the search in the same tab that triggered it (i.e.
+ // where === "current"), openUILinkIn will not work because that tab is no
+ // longer the current one. For this case we manually load the URI.
+ if (where === "current") {
+ browser.loadURIWithFlags(submission.uri.spec,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null,
+ submission.postData);
+ } else {
+ let params = {
+ postData: submission.postData,
+ inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
+ };
+ win.openUILinkIn(submission.uri.spec, where, params);
+ }
+ win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
+ data.selection || null);
+ return;
+ },
+
+ getSuggestions: Task.async(function* (engineName, searchString, browser, remoteTimeout=null) {
+ let engine = Services.search.getEngineByName(engineName);
+ if (!engine) {
+ throw new Error("Unknown engine name: " + engineName);
+ }
+
+ let browserData = this._suggestionDataForBrowser(browser, true);
+ let { controller } = browserData;
+ let ok = SearchSuggestionController.engineOffersSuggestions(engine);
+ controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
+ controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
+ controller.remoteTimeout = remoteTimeout || undefined;
+ let priv = PrivateBrowsingUtils.isBrowserPrivate(browser);
+ // fetch() rejects its promise if there's a pending request, but since we
+ // process our event queue serially, there's never a pending request.
+ this._currentSuggestion = { controller: controller, target: browser };
+ let suggestions = yield controller.fetch(searchString, priv, engine);
+ this._currentSuggestion = null;
+
+ // suggestions will be null if the request was cancelled
+ let result = {};
+ if (!suggestions) {
+ return result;
+ }
+
+ // Keep the form history result so RemoveFormHistoryEntry can remove entries
+ // from it. Keeping only one result isn't foolproof because the client may
+ // try to remove an entry from one set of suggestions after it has requested
+ // more but before it's received them. In that case, the entry may not
+ // appear in the new suggestions. But that should happen rarely.
+ browserData.previousFormHistoryResult = suggestions.formHistoryResult;
+ result = {
+ engineName,
+ term: suggestions.term,
+ local: suggestions.local,
+ remote: suggestions.remote,
+ };
+ return result;
+ }),
+
+ addFormHistoryEntry: Task.async(function* (browser, entry="") {
+ let isPrivate = false;
+ try {
+ // isBrowserPrivate assumes that the passed-in browser has all the normal
+ // properties, which won't be true if the browser has been destroyed.
+ // That may be the case here due to the asynchronous nature of messaging.
+ isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser.target);
+ } catch (err) {
+ return false;
+ }
+ if (isPrivate || entry === "") {
+ return false;
+ }
+ let browserData = this._suggestionDataForBrowser(browser.target, true);
+ FormHistory.update({
+ op: "bump",
+ fieldname: browserData.controller.formHistoryParam,
+ value: entry,
+ }, {
+ handleCompletion: () => {},
+ handleError: err => {
+ Cu.reportError("Error adding form history entry: " + err);
+ },
+ });
+ return true;
+ }),
+
+ currentStateObj: Task.async(function* (uriFlag=false) {
+ let state = {
+ engines: [],
+ currentEngine: yield this._currentEngineObj(),
+ };
+ if (uriFlag) {
+ state.currentEngine.iconBuffer = Services.search.currentEngine.getIconURLBySize(16, 16);
+ }
+ let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
+ let hiddenList = pref ? pref.split(",") : [];
+ for (let engine of Services.search.getVisibleEngines()) {
+ let uri = engine.getIconURLBySize(16, 16);
+ let iconBuffer = uri;
+ if (!uriFlag) {
+ iconBuffer = yield this._arrayBufferFromDataURI(uri);
+ }
+ state.engines.push({
+ name: engine.name,
+ iconBuffer,
+ hidden: hiddenList.indexOf(engine.name) !== -1,
+ });
+ }
+ return state;
+ }),
+
_processEventQueue: function () {
if (this._currentEventPromise || !this._eventQueue.length) {
return;
}
let event = this._eventQueue.shift();
this._currentEventPromise = Task.spawn(function* () {
@@ -222,24 +373,24 @@ this.ContentSearch = {
this._processEventQueue();
}
}.bind(this));
},
_cancelSuggestions: function (msg) {
let cancelled = false;
// cancel active suggestion request
- if (this._currentSuggestion && this._currentSuggestion.target == msg.target) {
+ if (this._currentSuggestion && this._currentSuggestion.target === msg.target) {
this._currentSuggestion.controller.stop();
cancelled = true;
}
// cancel queued suggestion requests
for (let i = 0; i < this._eventQueue.length; i++) {
let m = this._eventQueue[i].data;
- if (msg.target == m.target && m.data.type == "GetSuggestions") {
+ if (msg.target === m.target && m.data.type === "GetSuggestions") {
this._eventQueue.splice(i, 1);
cancelled = true;
i--;
}
}
if (cancelled) {
this._reply(msg, "SuggestionsCancelled");
}
@@ -250,186 +401,83 @@ this.ContentSearch = {
if (methodName in this) {
yield this._initService();
yield this[methodName](msg, msg.data.data);
msg.target.removeEventListener("SwapDocShells", msg, true);
}
}),
_onMessageGetState: function (msg, data) {
- return this._currentStateObj().then(state => {
+ return this.currentStateObj().then(state => {
this._reply(msg, "State", state);
});
},
_onMessageGetStrings: function (msg, data) {
this._reply(msg, "Strings", this.searchSuggestionUIStrings);
},
_onMessageSearch: function (msg, data) {
- this._ensureDataHasProperties(data, [
- "engineName",
- "searchString",
- "healthReportKey",
- "searchPurpose",
- ]);
- let engine = Services.search.getEngineByName(data.engineName);
- let submission = engine.getSubmission(data.searchString, "", data.searchPurpose);
- let browser = msg.target;
- let win;
- try {
- win = browser.ownerDocument.defaultView;
- }
- catch (err) {
- // The browser may have been closed between the time its content sent the
- // message and the time we handle it. In that case, trying to call any
- // method on it will throw.
- return Promise.resolve();
- }
-
- let where = win.whereToOpenLink(data.originalEvent);
-
- // There is a chance that by the time we receive the search message, the user
- // has switched away from the tab that triggered the search. If, based on the
- // event, we need to load the search in the same tab that triggered it (i.e.
- // where == "current"), openUILinkIn will not work because that tab is no
- // longer the current one. For this case we manually load the URI.
- if (where == "current") {
- browser.loadURIWithFlags(submission.uri.spec,
- Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null,
- submission.postData);
- } else {
- let params = {
- postData: submission.postData,
- inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
- };
- win.openUILinkIn(submission.uri.spec, where, params);
- }
- win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
- data.selection || null);
- return Promise.resolve();
+ this.performSearch(msg, data);
},
_onMessageSetCurrentEngine: function (msg, data) {
Services.search.currentEngine = Services.search.getEngineByName(data);
- return Promise.resolve();
},
_onMessageManageEngines: function (msg, data) {
let browserWin = msg.target.ownerDocument.defaultView;
browserWin.openPreferences("paneSearch");
- return Promise.resolve();
},
_onMessageGetSuggestions: Task.async(function* (msg, data) {
this._ensureDataHasProperties(data, [
"engineName",
"searchString",
]);
-
- let engine = Services.search.getEngineByName(data.engineName);
- if (!engine) {
- throw new Error("Unknown engine name: " + data.engineName);
- }
-
- let browserData = this._suggestionDataForBrowser(msg.target, true);
- let { controller } = browserData;
- let ok = SearchSuggestionController.engineOffersSuggestions(engine);
- controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
- controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
- controller.remoteTimeout = data.remoteTimeout || undefined;
- let priv = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
- // fetch() rejects its promise if there's a pending request, but since we
- // process our event queue serially, there's never a pending request.
- this._currentSuggestion = { controller: controller, target: msg.target };
- let suggestions = yield controller.fetch(data.searchString, priv, engine);
- this._currentSuggestion = null;
-
- // suggestions will be null if the request was cancelled
- if (!suggestions) {
- return;
- }
-
- // Keep the form history result so RemoveFormHistoryEntry can remove entries
- // from it. Keeping only one result isn't foolproof because the client may
- // try to remove an entry from one set of suggestions after it has requested
- // more but before it's received them. In that case, the entry may not
- // appear in the new suggestions. But that should happen rarely.
- browserData.previousFormHistoryResult = suggestions.formHistoryResult;
+ let {engineName, searchString} = data;
+ let suggestions = yield this.getSuggestions(engineName, searchString, msg.target);
this._reply(msg, "Suggestions", {
engineName: data.engineName,
searchString: suggestions.term,
formHistory: suggestions.local,
remote: suggestions.remote,
});
}),
- _onMessageAddFormHistoryEntry: function (msg, entry) {
- let isPrivate = true;
- try {
- // isBrowserPrivate assumes that the passed-in browser has all the normal
- // properties, which won't be true if the browser has been destroyed.
- // That may be the case here due to the asynchronous nature of messaging.
- isPrivate = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
- } catch (err) {}
- if (isPrivate || entry === "") {
- return Promise.resolve();
- }
- let browserData = this._suggestionDataForBrowser(msg.target, true);
- if (FormHistory.enabled) {
- FormHistory.update({
- op: "bump",
- fieldname: browserData.controller.formHistoryParam,
- value: entry,
- }, {
- handleCompletion: () => {},
- handleError: err => {
- Cu.reportError("Error adding form history entry: " + err);
- },
- });
- }
- return Promise.resolve();
- },
+ _onMessageAddFormHistoryEntry: Task.async(function* (msg, entry) {
+ yield this.addFormHistoryEntry(msg, entry);
+ }),
_onMessageRemoveFormHistoryEntry: function (msg, entry) {
- let browserData = this._suggestionDataForBrowser(msg.target);
- if (browserData && browserData.previousFormHistoryResult) {
- let { previousFormHistoryResult } = browserData;
- for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
- if (previousFormHistoryResult.getValueAt(i) == entry) {
- previousFormHistoryResult.removeValueAt(i, true);
- break;
- }
- }
- }
- return Promise.resolve();
+ this.removeFormHistoryEntry(msg, entry);
},
_onMessageSpeculativeConnect: function (msg, engineName) {
let engine = Services.search.getEngineByName(engineName);
if (!engine) {
throw new Error("Unknown engine name: " + engineName);
}
if (msg.target.contentWindow) {
engine.speculativeConnect({
window: msg.target.contentWindow,
});
}
},
_onObserve: Task.async(function* (data) {
- if (data == "engine-current") {
+ if (data === "engine-current") {
let engine = yield this._currentEngineObj();
this._broadcast("CurrentEngine", engine);
}
- else if (data != "engine-default") {
+ else if (data !== "engine-default") {
// engine-default is always sent with engine-current and isn't otherwise
// relevant to content searches.
- let state = yield this._currentStateObj();
+ let state = yield this.currentStateObj();
this._broadcast("CurrentState", state);
}
}),
_suggestionDataForBrowser: function (browser, create=false) {
let data = this._suggestionMap.get(browser);
if (!data && create) {
// Since one SearchSuggestionController instance is meant to be used per
@@ -459,34 +507,16 @@ this.ContentSearch = {
_msgArgs: function (type, data) {
return [OUTBOUND_MESSAGE, {
type: type,
data: data,
}];
},
- _currentStateObj: Task.async(function* () {
- let state = {
- engines: [],
- currentEngine: yield this._currentEngineObj(),
- };
- let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
- let hiddenList = pref ? pref.split(",") : [];
- for (let engine of Services.search.getVisibleEngines()) {
- let uri = engine.getIconURLBySize(16, 16);
- state.engines.push({
- name: engine.name,
- iconBuffer: yield this._arrayBufferFromDataURI(uri),
- hidden: hiddenList.indexOf(engine.name) != -1,
- });
- }
- return state;
- }),
-
_currentEngineObj: Task.async(function* () {
let engine = Services.search.currentEngine;
let favicon = engine.getIconURLBySize(16, 16);
let placeholder = this._stringBundle.formatStringFromName(
"searchWithEngine", [engine.name], 1);
let obj = {
name: engine.name,
placeholder: placeholder,