diff --git a/browser/components/newtab/RemoteAboutNewTab.jsm b/browser/components/newtab/RemoteAboutNewTab.jsm index a8d13d842269..e089ddf7a407 100644 --- a/browser/components/newtab/RemoteAboutNewTab.jsm +++ b/browser/components/newtab/RemoteAboutNewTab.jsm @@ -1,9 +1,9 @@ /* 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 Services, XPCOMUtils, Task, SearchProvider, RemoteNewTabUtils, BackgroundPageThumbs, + RemotePages, PageThumbs, RemoteDirectoryLinksProvider, RemoteNewTabLocation*/ -/* globals Services, XPCOMUtils, RemotePages, RemoteNewTabLocation, RemoteNewTabUtils, Task */ -/* globals BackgroundPageThumbs, PageThumbs, RemoteDirectoryLinksProvider */ /* exported RemoteAboutNewTab */ "use strict"; @@ -31,6 +31,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "RemoteDirectoryLinksProvider", "resource:///modules/RemoteDirectoryLinksProvider.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabLocation", "resource:///modules/RemoteNewTabLocation.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SearchProvider", + "resource:///modules/SearchProvider.jsm"); let RemoteAboutNewTab = { @@ -44,13 +46,69 @@ let RemoteAboutNewTab = { this.pageListener.addMessageListener("NewTab:InitializeGrid", this.initializeGrid.bind(this)); this.pageListener.addMessageListener("NewTab:UpdateGrid", this.updateGrid.bind(this)); this.pageListener.addMessageListener("NewTab:CaptureBackgroundPageThumbs", - this.captureBackgroundPageThumb.bind(this)); + this.captureBackgroundPageThumb.bind(this)); this.pageListener.addMessageListener("NewTab:PageThumbs", this.createPageThumb.bind(this)); + this.pageListener.addMessageListener("NewTab:Search", this.search.bind(this)); + this.pageListener.addMessageListener("NewTab:GetState", this.getState.bind(this)); + this.pageListener.addMessageListener("NewTab:GetStrings", this.getStrings.bind(this)); + this.pageListener.addMessageListener("NewTab:GetSuggestions", this.getSuggestions.bind(this)); + this.pageListener.addMessageListener("NewTab:RemoveFormHistoryEntry", this.removeFormHistoryEntry.bind(this)); + this.pageListener.addMessageListener("NewTab:ManageEngines", this.manageEngines.bind(this)); + this.pageListener.addMessageListener("NewTab:SetCurrentEngine", this.setCurrentEngine.bind(this)); this.pageListener.addMessageListener("NewTabFrame:GetInit", this.initContentFrame.bind(this)); this._addObservers(); }, + search: function(message) { + SearchProvider.performSearch(message.target.browser, message.data); + }, + + getState: Task.async(function* (message) { + let state = yield SearchProvider.state; + message.target.sendAsyncMessage("NewTab:ContentSearchService", { + state, + name: "State", + }); + }), + + getStrings: function(message) { + let strings = SearchProvider.searchSuggestionUIStrings; + message.target.sendAsyncMessage("NewTab:ContentSearchService", { + strings, + name: "Strings", + }); + }, + + getSuggestions: Task.async(function* (message) { + try { + let suggestion = yield SearchProvider.getSuggestions(message.target.browser, message.data); + + // In the case where there is no suggestion available, do not send a message. + if (suggestion !== null) { + message.target.sendAsyncMessage("NewTab:ContentSearchService", { + suggestion, + name: "Suggestions", + }); + } + } catch(e) { + Cu.reportError(e); + } + }), + + removeFormHistoryEntry: function(message) { + SearchProvider.removeFormHistoryEntry(message.target.browser, message.data.suggestionStr); + }, + + manageEngines: function(message) { + let browserWin = message.target.browser.ownerDocument.defaultView; + browserWin.openPreferences("paneSearch"); + }, + + setCurrentEngine: function(message) { + Services.search.currentEngine = Services.search.getEngineByName(message.data.engineName); + }, + /** * Initializes the grid for the first time when the page loads. * Fetch all the links and send them down to the child to populate @@ -147,7 +205,7 @@ let RemoteAboutNewTab = { let canvas = doc.createElementNS(XHTML_NAMESPACE, "canvas"); let enhanced = Services.prefs.getBoolPref("browser.newtabpage.enhanced"); - img.onload = function(e) { // jshint ignore:line + img.onload = function() { canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; var ctx = canvas.getContext("2d"); @@ -179,14 +237,14 @@ let RemoteAboutNewTab = { }, /** - * Listens for a preference change or session purge for all pages and sends - * a message to update the pages that are open. If a session purge occured, - * also clear the links cache and update the set of links to display, as they - * may have changed, then proceed with the page update. + * Listens for a preference change, a session purge for all pages, or if the + * current search engine is modified, and sends a message to update the pages + * that are open. If a session purge occured, also clear the links cache and + * update the set of links to display, as they may have changed, then proceed + * with the page update. */ observe: function(aSubject, aTopic, aData) { // jshint ignore:line let extraData; - let refreshPage = false; if (aTopic === "browser:purge-session-history") { RemoteNewTabUtils.links.resetCache(); RemoteNewTabUtils.links.populateCache(() => { @@ -195,6 +253,28 @@ let RemoteAboutNewTab = { enhancedLinks: this.getEnhancedLinks(), }); }); + } else if (aTopic === "browser-search-engine-modified" && aData === "engine-current") { + Task.spawn(function* () { + try { + let engine = yield SearchProvider.currentEngine; + this.pageListener.sendAsyncMessage("NewTab:ContentSearchService", { + engine, name: "CurrentEngine" + }); + } catch (e) { + Cu.reportError(e); + } + }.bind(this)); + } else if (aTopic === "nsPref:changed" && aData === "browser.search.hiddenOneOffs") { + Task.spawn(function* () { + try { + let state = yield SearchProvider.state; + this.pageListener.sendAsyncMessage("NewTab:ContentSearchService", { + state, name: "CurrentState" + }); + } catch (e) { + Cu.reportError(e); + } + }.bind(this)); } if (extraData !== undefined || aTopic === "page-thumbnail:create") { @@ -202,7 +282,10 @@ let RemoteAboutNewTab = { // Change the topic for enhanced and enabled observers. aTopic = aData; } - this.pageListener.sendAsyncMessage("NewTab:Observe", {topic: aTopic, data: extraData}); + this.pageListener.sendAsyncMessage("NewTab:Observe", { + topic: aTopic, + data: extraData + }); } }, @@ -212,6 +295,8 @@ let RemoteAboutNewTab = { _addObservers: function() { Services.obs.addObserver(this, "page-thumbnail:create", true); Services.obs.addObserver(this, "browser:purge-session-history", true); + Services.prefs.addObserver("browser.search.hiddenOneOffs", this, false); + Services.obs.addObserver(this, "browser-search-engine-modified", true); }, /** @@ -220,10 +305,13 @@ let RemoteAboutNewTab = { _removeObservers: function() { Services.obs.removeObserver(this, "page-thumbnail:create"); Services.obs.removeObserver(this, "browser:purge-session-history"); + Services.prefs.removeObserver("browser.search.hiddenOneOffs", this); + Services.obs.removeObserver(this, "browser-search-engine-modified"); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), + Ci.nsISupportsWeakReference + ]), uninit: function() { this._removeObservers(); diff --git a/browser/components/newtab/RemoteNewTabLocation.jsm b/browser/components/newtab/RemoteNewTabLocation.jsm index 9ec171987721..15f1330360ae 100644 --- a/browser/components/newtab/RemoteNewTabLocation.jsm +++ b/browser/components/newtab/RemoteNewTabLocation.jsm @@ -8,7 +8,7 @@ Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.importGlobalProperties(["URL"]); // TODO: will get dynamically set in bug 1210478 -const DEFAULT_PAGE_LOCATION = "https://newtab.cdn.mozilla.net/v0/nightly/en-US/index.html"; +const DEFAULT_PAGE_LOCATION = "https://newtab.cdn.mozilla.net/v2/nightly/en-US/index.html"; this.RemoteNewTabLocation = { _url: new URL(DEFAULT_PAGE_LOCATION), diff --git a/browser/components/newtab/SearchProvider.jsm b/browser/components/newtab/SearchProvider.jsm new file mode 100644 index 000000000000..4f3cc92fc071 --- /dev/null +++ b/browser/components/newtab/SearchProvider.jsm @@ -0,0 +1,300 @@ + /* 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 Components, Services, XPCOMUtils, Task, SearchSuggestionController, PrivateBrowsingUtils, FormHistory*/ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "SearchProvider", +]; + +const { + classes: Cc, + interfaces: Ci, + utils: Cu, +} = Components; + +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.importGlobalProperties(["URL", "Blob"]); + +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController", + "resource://gre/modules/SearchSuggestionController.jsm"); + +const stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties"); +const searchBundle = Services.strings.createBundle("chrome://browser/locale/search.properties"); +const MAX_LOCAL_SUGGESTIONS = 3; +const MAX_SUGGESTIONS = 6; +// splits data urls into component parts [url, type, data] +const dataURLParts = /(?:^data:)(.+)(?:;base64,)(.*)$/; +const l10nKeysNames = [ + "searchHeader", + "searchPlaceholder", + "searchWithHeader", + "searchSettings", + "searchForSomethingWith", +]; + +this.SearchProvider = { + // This is used to handle search suggestions. It maps xul:browsers to objects + // { controller, previousFormHistoryResult }. See getSuggestions(). + _suggestionMap: new WeakMap(), + + _searchSuggestionUIStrings: new Map(), + + /** + * Makes a copy of the current search suggestions. + * @return {Object} Key/value pairs representing the search suggestions. + */ + get searchSuggestionUIStrings() { + let result = Object.create(null); + Array.from(this._searchSuggestionUIStrings.entries()) + .forEach(([key, value]) => result[key] = value); + return result; + }, + + /** + * Gets the state + * @return {Promise} Resolves to an object: + * engines {Object[]}: list of engines. + * currentEngine {Object}: the current engine. + */ + get state() { + return Task.spawn(function* () { + let state = { + engines: [], + currentEngine: yield this.currentEngine, + }; + let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs"); + let hiddenList = pref ? pref.split(",") : []; + for (let engine of Services.search.getVisibleEngines()) { + if (hiddenList.indexOf(engine.name) !== -1) { + continue; + } + let uri = engine.getIconURLBySize(16, 16); + state.engines.push({ + name: engine.name, + iconBuffer: yield this._arrayBufferFromDataURL(uri), + }); + } + return state; + }.bind(this)); + }, + + /** + * Get a browser to peform a search by opening a new window. + * @param {XULBrowser} browser The browser that performs the search. + * @param {Object} data The data used to perform the search. + * @return {Window} win The window that is performing the search. + */ + performSearch(browser, data) { + return Task.spawn(function* () { + let engine = Services.search.getEngineByName(data.engineName); + let submission = engine.getSubmission(data.searchString, "", data.searchPurpose); + // 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. + let win = browser.ownerDocument.defaultView; + + 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.recordSearchInHealthReport(engine, data.healthReportKey, + data.selection || null); + + yield this.addFormHistoryEntry(browser, data.searchString); + return win; + }.bind(this)); + }, + + /** + * Returns the current search engine. + * @return {Object} An object the describes the current engine. + */ + get currentEngine() { + return Task.spawn(function* () { + let engine = Services.search.currentEngine; + let favicon = engine.getIconURLBySize(16, 16); + let uri1x = engine.getIconURLBySize(65, 26); + let uri2x = engine.getIconURLBySize(130, 52); + let placeholder = stringBundle.formatStringFromName( + "searchWithEngine", [engine.name], 1); + let obj = { + name: engine.name, + placeholder: placeholder, + iconBuffer: yield this._arrayBufferFromDataURL(favicon), + logoBuffer: yield this._arrayBufferFromDataURL(uri1x), + logo2xBuffer: yield this._arrayBufferFromDataURL(uri2x), + preconnectOrigin: new URL(engine.searchForm).origin, + }; + return obj; + }.bind(this)); + }, + + getSuggestions: function(browser, data) { + return Task.spawn(function* () { + let engine = Services.search.getEngineByName(data.engineName); + if (!engine) { + throw new Error(`Unknown engine name: ${data.engineName}`); + } + let { + controller + } = this._suggestionDataForBrowser(browser); + 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 isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser); + + let suggestions; + try { + // If fetch() rejects due to it's asynchronous behaviour, the suggestions + // is null and is then handled. + suggestions = yield controller.fetch(data.searchString, isPrivate, engine); + } catch (e) { + Cu.reportError(e); + } + + let result = null; + if (suggestions) { + this._suggestionMap.get(browser) + .previousFormHistoryResult = suggestions.formHistoryResult; + + result = { + engineName: data.engineName, + searchString: suggestions.term, + formHistory: suggestions.local, + remote: suggestions.remote, + }; + } + + return result; + }.bind(this)); + }, + + addFormHistoryEntry: function(browser, entry = "") { + return Task.spawn(function* () { + let isPrivate = false; + try { + isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser); + } catch (err) { + // The browser might have already been destroyed. + return false; + } + if (isPrivate || entry === "") { + return false; + } + let { + controller + } = this._suggestionDataForBrowser(browser); + let result = yield new Promise((resolve, reject) => { + let ops = { + op: "bump", + fieldname: controller.formHistoryParam, + value: entry, + }; + let callbacks = { + handleCompletion: () => resolve(true), + handleError: reject, + }; + FormHistory.update(ops, callbacks); + }); + return result; + }.bind(this)); + }, + + /** + * Removes an entry from the form history for a given browser. + * + * @param {XULBrowser} browser the browser to delete from. + * @param {String} suggestion The suggestion to delete. + * @return {Boolean} True if removed, false otherwise. + */ + removeFormHistoryEntry(browser, suggestion) { + let { + previousFormHistoryResult + } = this._suggestionMap.get(browser); + if (!previousFormHistoryResult) { + return false; + } + for (let i = 0; i < previousFormHistoryResult.matchCount; i++) { + if (previousFormHistoryResult.getValueAt(i) === suggestion) { + previousFormHistoryResult.removeValueAt(i, true); + return true; + } + } + return false; + }, + + _suggestionDataForBrowser(browser) { + let data = this._suggestionMap.get(browser); + if (!data) { + // Since one SearchSuggestionController instance is meant to be used per + // autocomplete widget, this means that we assume each xul:browser has at + // most one such widget. + data = { + controller: new SearchSuggestionController(), + previousFormHistoryResult: undefined, + }; + this._suggestionMap.set(browser, data); + } + return data; + }, + + _arrayBufferFromDataURL(dataURL = "") { + if (!dataURL) { + return Promise.resolve(null); + } + return new Promise((resolve, reject) => { + try { + let fileReader = Cc["@mozilla.org/files/filereader;1"] + .createInstance(Ci.nsIDOMFileReader); + let [type, data] = dataURLParts.exec(dataURL).slice(1); + let bytes = atob(data); + let uInt8Array = new Uint8Array(bytes.length); + for (let i = 0; i < bytes.length; ++i) { + uInt8Array[i] = bytes.charCodeAt(i); + } + let blob = new Blob([uInt8Array], { + type + }); + fileReader.onload = () => resolve(fileReader.result); + fileReader.onerror = () => resolve(null); + fileReader.readAsArrayBuffer(blob); + } catch (e) { + reject(e); + } + }); + }, + + init() { + // Perform localization + l10nKeysNames.map( + name => [name, searchBundle.GetStringFromName(name)] + ).forEach( + ([key, value]) => this._searchSuggestionUIStrings.set(key, value) + ); + } +}; +this.SearchProvider.init(); diff --git a/browser/components/newtab/moz.build b/browser/components/newtab/moz.build index dde71d1ab9df..4121c163bab6 100644 --- a/browser/components/newtab/moz.build +++ b/browser/components/newtab/moz.build @@ -17,4 +17,5 @@ EXTRA_JS_MODULES += [ 'RemoteDirectoryLinksProvider.jsm', 'RemoteNewTabLocation.jsm', 'RemoteNewTabUtils.jsm', + 'SearchProvider.jsm', ] diff --git a/browser/components/newtab/tests/browser/browser.ini b/browser/components/newtab/tests/browser/browser.ini index 8ec39550ccff..bbf1bc783a74 100644 --- a/browser/components/newtab/tests/browser/browser.ini +++ b/browser/components/newtab/tests/browser/browser.ini @@ -3,3 +3,4 @@ support-files = dummy_page.html [browser_remotenewtab_pageloads.js] +[browser_SearchProvider.js] diff --git a/browser/components/newtab/tests/browser/browser_SearchProvider.js b/browser/components/newtab/tests/browser/browser_SearchProvider.js new file mode 100644 index 000000000000..59d4d69863bf --- /dev/null +++ b/browser/components/newtab/tests/browser/browser_SearchProvider.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* globals ok, is, Services */ +"use strict"; +let imports = {}; +Components.utils.import("resource:///modules/SearchProvider.jsm", imports); + +// create test engine called MozSearch +Services.search.addEngineWithDetails("TestSearch", "", "", "", "GET", + "http://example.com/?q={searchTerms}"); +Services.search.defaultEngine = Services.search.getEngineByName("TestSearch"); + +function hasProp(obj) { + return function(aProp) { + ok(obj.hasOwnProperty(aProp), `expect to have property ${aProp}`); + }; +} + +add_task(function* testState() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:newTab", + }, function* () { + + ok(imports.SearchProvider, "Search provider was created"); + + // state returns promise and eventually returns a state object + var state = yield imports.SearchProvider.state; + var stateProps = hasProp(state); + ["engines", "currentEngine"].forEach(stateProps); + + var { + engines + } = state; + + // engines should be an iterable + var proto = Object.getPrototypeOf(engines); + var isIterable = Object.getOwnPropertySymbols(proto)[0] === Symbol.iterator; + ok(isIterable, "Engines should be iterable."); + + // current engine should be the current engine from Services.search + var { + currentEngine + } = state; + is(currentEngine.name, Services.search.currentEngine.name, "Current engine has been correctly set to default engine"); + + // current engine should properties + var engineProps = hasProp(currentEngine); + ["name", "placeholder", "iconBuffer", "logoBuffer", "logo2xBuffer", "preconnectOrigin"].forEach(engineProps); + + }); +}); + +add_task(function* testSearch() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:newTab", + }, function* () { + + // perform a search + var searchData = { + engineName: Services.search.currentEngine.name, + searchString: "test", + healthReportKey: "newtab", + searchPurpose: "newtab", + originalEvent: { + shiftKey: false, + ctrlKey: false, + metaKey: false, + altKey: false, + button: false, + }, + }; + + // adding an entry to the form history will trigger a 'formhistory-add' notification, so we need to wait for + // this to resolve before checking that the search string has been added to the suggestions list + let addHistoryPromise = new Promise((resolve, reject) => { + Services.obs.addObserver(function onAdd(subject, topic, data) { // jshint ignore:line + if (data === "formhistory-add") { + Services.obs.removeObserver(onAdd, "satchel-storage-changed"); + resolve(data); + } else { + reject(); + } + }, "satchel-storage-changed", false); + }); + + var win = yield imports.SearchProvider.performSearch(gBrowser, searchData); + var result = yield new Promise(resolve => { + const pageShow = function() { + win.gBrowser.tabs[1].linkedBrowser.removeEventListener("pageshow", pageShow); + var tab = win.gBrowser.tabContainer.tabbrowser; + var url = tab.selectedTab.linkedBrowser.contentWindow.location.href; + BrowserTestUtils.removeTab(tab.selectedTab); + resolve(url); + }; + + ok(win.gBrowser.tabs[1], "search opened a new tab"); + win.gBrowser.tabs[1].linkedBrowser.addEventListener("pageshow", pageShow); + }); + is(result, "http://example.com/?q=test", "should match search URL of default engine."); + + // suggestions has correct properties + var suggestionData = { + engineName: Services.search.currentEngine.name, + searchString: "test", + }; + var suggestions = yield imports.SearchProvider.getSuggestions(gBrowser, suggestionData); + var suggestionProps = hasProp(suggestions); + ["engineName", "searchString", "formHistory", "remote"].forEach(suggestionProps); + + // ensure that the search string has been added to the form history suggestions + yield addHistoryPromise; + suggestions = yield imports.SearchProvider.getSuggestions(gBrowser, suggestionData); + var { + formHistory + } = suggestions; + ok(formHistory.length !== 0, "a form history was created"); + is(formHistory[0], searchData.searchString, "the search string has been added to form history"); + + // remove the entry we just added from the form history and ensure it no longer appears as a suggestion + let removeHistoryPromise = new Promise((resolve, reject) => { + Services.obs.addObserver(function onAdd(subject, topic, data) { // jshint ignore:line + if (data === "formhistory-remove") { + Services.obs.removeObserver(onAdd, "satchel-storage-changed"); + resolve(data); + } else { + reject(); + } + }, "satchel-storage-changed", false); + }); + + yield imports.SearchProvider.removeFormHistoryEntry(gBrowser, searchData.searchString); + yield removeHistoryPromise; + suggestions = yield imports.SearchProvider.getSuggestions(gBrowser, suggestionData); + ok(suggestions.formHistory.length === 0, "entry has been removed from form history"); + }); +}); + +add_task(function* testFetchFailure() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:newTab", + }, function* () { + + // ensure that the fetch failure is handled when the suggestions return a null value due to their + // asynchronous nature + let { controller } = imports.SearchProvider._suggestionDataForBrowser(gBrowser); + let oldFetch = controller.fetch; + controller.fetch = function(searchTerm, privateMode, engine) { //jshint ignore:line + let promise = new Promise((resolve, reject) => { //jshint ignore:line + reject(); + }); + return promise; + }; + + // this should throw, since the promise rejected + let suggestionData = { + engineName: Services.search.currentEngine.name, + searchString: "test", + }; + + let suggestions = yield imports.SearchProvider.getSuggestions(gBrowser, suggestionData); + + ok(suggestions === null, "suggestions returned null and the function handled the rejection"); + controller.fetch = oldFetch; + }); +});