summaryrefslogtreecommitdiffstats
path: root/browser/components/search
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search')
-rw-r--r--browser/components/search/content/search.xml2090
-rw-r--r--browser/components/search/content/searchReset.js90
-rw-r--r--browser/components/search/content/searchReset.xhtml61
-rw-r--r--browser/components/search/content/searchbarBindings.css18
-rw-r--r--browser/components/search/jar.mn9
-rw-r--r--browser/components/search/moz.build14
-rw-r--r--browser/components/search/test/.eslintrc.js7
-rw-r--r--browser/components/search/test/426329.xml11
-rw-r--r--browser/components/search/test/483086-1.xml10
-rw-r--r--browser/components/search/test/483086-2.xml10
-rw-r--r--browser/components/search/test/browser.ini44
-rw-r--r--browser/components/search/test/browser_426329.js250
-rw-r--r--browser/components/search/test/browser_483086.js49
-rw-r--r--browser/components/search/test/browser_aboutSearchReset.js159
-rw-r--r--browser/components/search/test/browser_abouthome_behavior.js144
-rw-r--r--browser/components/search/test/browser_addEngine.js105
-rw-r--r--browser/components/search/test/browser_amazon.js82
-rw-r--r--browser/components/search/test/browser_amazon_behavior.js166
-rw-r--r--browser/components/search/test/browser_bing.js118
-rw-r--r--browser/components/search/test/browser_bing_behavior.js166
-rw-r--r--browser/components/search/test/browser_contextSearchTabPosition.js62
-rw-r--r--browser/components/search/test/browser_contextmenu.js101
-rw-r--r--browser/components/search/test/browser_google.js100
-rw-r--r--browser/components/search/test/browser_google_behavior.js165
-rw-r--r--browser/components/search/test/browser_google_codes.js161
-rw-r--r--browser/components/search/test/browser_healthreport.js82
-rw-r--r--browser/components/search/test/browser_hiddenOneOffs_cleanup.js99
-rw-r--r--browser/components/search/test/browser_hiddenOneOffs_diacritics.js59
-rw-r--r--browser/components/search/test/browser_oneOffContextMenu.js105
-rw-r--r--browser/components/search/test/browser_oneOffContextMenu_setDefault.js195
-rw-r--r--browser/components/search/test/browser_oneOffHeader.js142
-rw-r--r--browser/components/search/test/browser_private_search_perwindowpb.js76
-rw-r--r--browser/components/search/test/browser_searchbar_keyboard_navigation.js425
-rw-r--r--browser/components/search/test/browser_searchbar_openpopup.js521
-rw-r--r--browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js354
-rw-r--r--browser/components/search/test/browser_webapi.js92
-rw-r--r--browser/components/search/test/browser_yahoo.js132
-rw-r--r--browser/components/search/test/browser_yahoo_behavior.js166
-rw-r--r--browser/components/search/test/head.js156
-rw-r--r--browser/components/search/test/opensearch.html9
-rw-r--r--browser/components/search/test/test.html8
-rw-r--r--browser/components/search/test/testEngine.xml12
-rw-r--r--browser/components/search/test/testEngine_diacritics.xml12
-rw-r--r--browser/components/search/test/testEngine_dupe.xml12
-rw-r--r--browser/components/search/test/testEngine_mozsearch.xml14
-rw-r--r--browser/components/search/test/webapi.html16
46 files changed, 6879 insertions, 0 deletions
diff --git a/browser/components/search/content/search.xml b/browser/components/search/content/search.xml
new file mode 100644
index 000000000..5c67bc649
--- /dev/null
+++ b/browser/components/search/content/search.xml
@@ -0,0 +1,2090 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE bindings [
+<!ENTITY % searchBarDTD SYSTEM "chrome://browser/locale/searchbar.dtd" >
+%searchBarDTD;
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+%browserDTD;
+]>
+
+<bindings id="SearchBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="searchbar">
+ <resources>
+ <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
+ <stylesheet src="chrome://browser/skin/searchbar.css"/>
+ </resources>
+ <content>
+ <xul:stringbundle src="chrome://browser/locale/search.properties"
+ anonid="searchbar-stringbundle"/>
+ <!--
+ There is a dependency between "maxrows" attribute and
+ "SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
+ one of them requires changing the other one.
+ -->
+ <xul:textbox class="searchbar-textbox"
+ anonid="searchbar-textbox"
+ type="autocomplete"
+ inputtype="search"
+ flex="1"
+ autocompletepopup="PopupSearchAutoComplete"
+ autocompletesearch="search-autocomplete"
+ autocompletesearchparam="searchbar-history"
+ maxrows="10"
+ completeselectedindex="true"
+ minresultsforpopup="0"
+ xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines">
+ <!--
+ Empty <box> to properly position the icon within the autocomplete
+ binding's anonymous children (the autocomplete binding positions <box>
+ children differently)
+ -->
+ <xul:box>
+ <xul:hbox class="searchbar-search-button-container">
+ <xul:image class="searchbar-search-button"
+ anonid="searchbar-search-button"
+ xbl:inherits="addengines"
+ tooltiptext="&searchEndCap.label;"/>
+ </xul:hbox>
+ </xul:box>
+ <xul:hbox class="search-go-container">
+ <xul:image class="search-go-button" hidden="true"
+ anonid="search-go-button"
+ onclick="handleSearchCommand(event);"
+ tooltiptext="&searchEndCap.label;"/>
+ </xul:hbox>
+ </xul:textbox>
+ </content>
+
+ <implementation implements="nsIObserver">
+ <constructor><![CDATA[
+ if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
+ return;
+ // Make sure we rebuild the popup in onpopupshowing
+ this._needToBuildPopup = true;
+
+ Services.obs.addObserver(this, "browser-search-engine-modified", false);
+
+ this._initialized = true;
+
+ Services.search.init((function search_init_cb(aStatus) {
+ // Bail out if the binding's been destroyed
+ if (!this._initialized)
+ return;
+
+ if (Components.isSuccessCode(aStatus)) {
+ // Refresh the display (updating icon, etc)
+ this.updateDisplay();
+ BrowserSearch.updateOpenSearchBadge();
+ } else {
+ Components.utils.reportError("Cannot initialize search service, bailing out: " + aStatus);
+ }
+ }).bind(this));
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this.destroy();
+ ]]></destructor>
+
+ <method name="destroy">
+ <body><![CDATA[
+ if (this._initialized) {
+ this._initialized = false;
+
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ }
+
+ // Make sure to break the cycle from _textbox to us. Otherwise we leak
+ // the world. But make sure it's actually pointing to us.
+ // Also make sure the textbox has ever been constructed, otherwise the
+ // _textbox getter will cause the textbox constructor to run, add an
+ // observer, and leak the world too.
+ if (this._textboxInitialized && this._textbox.mController.input == this)
+ this._textbox.mController.input = null;
+ ]]></body>
+ </method>
+
+ <field name="_ignoreFocus">false</field>
+ <field name="_clickClosedPopup">false</field>
+ <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
+ "anonid", "searchbar-stringbundle");</field>
+ <field name="_textboxInitialized">false</field>
+ <field name="_textbox">document.getAnonymousElementByAttribute(this,
+ "anonid", "searchbar-textbox");</field>
+ <field name="_engines">null</field>
+ <field name="FormHistory" readonly="true">
+ (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
+ </field>
+
+ <property name="engines" readonly="true">
+ <getter><![CDATA[
+ if (!this._engines)
+ this._engines = Services.search.getVisibleEngines();
+ return this._engines;
+ ]]></getter>
+ </property>
+
+ <property name="currentEngine">
+ <setter><![CDATA[
+ Services.search.currentEngine = val;
+ return val;
+ ]]></setter>
+ <getter><![CDATA[
+ var currentEngine = Services.search.currentEngine;
+ // Return a dummy engine if there is no currentEngine
+ return currentEngine || {name: "", uri: null};
+ ]]></getter>
+ </property>
+
+ <!-- textbox is used by sanitize.js to clear the undo history when
+ clearing form information. -->
+ <property name="textbox" readonly="true"
+ onget="return this._textbox;"/>
+
+ <property name="value" onget="return this._textbox.value;"
+ onset="return this._textbox.value = val;"/>
+
+ <method name="focus">
+ <body><![CDATA[
+ this._textbox.focus();
+ ]]></body>
+ </method>
+
+ <method name="select">
+ <body><![CDATA[
+ this._textbox.select();
+ ]]></body>
+ </method>
+
+ <method name="observe">
+ <parameter name="aEngine"/>
+ <parameter name="aTopic"/>
+ <parameter name="aVerb"/>
+ <body><![CDATA[
+ if (aTopic == "browser-search-engine-modified") {
+ switch (aVerb) {
+ case "engine-removed":
+ this.offerNewEngine(aEngine);
+ break;
+ case "engine-added":
+ this.hideNewEngine(aEngine);
+ break;
+ case "engine-changed":
+ // An engine was removed (or hidden) or added, or an icon was
+ // changed. Do nothing special.
+ }
+
+ // Make sure the engine list is refetched next time it's needed
+ this._engines = null;
+
+ // Update the popup header and update the display after any modification.
+ this._textbox.popup.updateHeader();
+ this.updateDisplay();
+ }
+ ]]></body>
+ </method>
+
+ <!-- There are two seaprate lists of search engines, whose uses intersect
+ in this file. The search service (nsIBrowserSearchService and
+ nsSearchService.js) maintains a list of Engine objects which is used to
+ populate the searchbox list of available engines and to perform queries.
+ That list is accessed here via this.SearchService, and it's that sort of
+ Engine that is passed to this binding's observer as aEngine.
+
+ In addition, browser.js fills two lists of autodetected search engines
+ (browser.engines and browser.hiddenEngines) as properties of
+ mCurrentBrowser. Those lists contain unnamed JS objects of the form
+ { uri:, title:, icon: }, and that's what the searchbar uses to determine
+ whether to show any "Add <EngineName>" menu items in the drop-down.
+
+ The two types of engines are currently related by their identifying
+ titles (the Engine object's 'name'), although that may change; see bug
+ 335102. -->
+
+ <!-- If the engine that was just removed from the searchbox list was
+ autodetected on this page, move it to each browser's active list so it
+ will be offered to be added again. -->
+ <method name="offerNewEngine">
+ <parameter name="aEngine"/>
+ <body><![CDATA[
+ for (let browser of gBrowser.browsers) {
+ if (browser.hiddenEngines) {
+ // XXX This will need to be changed when engines are identified by
+ // URL rather than title; see bug 335102.
+ var removeTitle = aEngine.wrappedJSObject.name;
+ for (var i = 0; i < browser.hiddenEngines.length; i++) {
+ if (browser.hiddenEngines[i].title == removeTitle) {
+ if (!browser.engines)
+ browser.engines = [];
+ browser.engines.push(browser.hiddenEngines[i]);
+ browser.hiddenEngines.splice(i, 1);
+ break;
+ }
+ }
+ }
+ }
+ BrowserSearch.updateOpenSearchBadge();
+ ]]></body>
+ </method>
+
+ <!-- If the engine that was just added to the searchbox list was
+ autodetected on this page, move it to each browser's hidden list so it is
+ no longer offered to be added. -->
+ <method name="hideNewEngine">
+ <parameter name="aEngine"/>
+ <body><![CDATA[
+ for (let browser of gBrowser.browsers) {
+ if (browser.engines) {
+ // XXX This will need to be changed when engines are identified by
+ // URL rather than title; see bug 335102.
+ var removeTitle = aEngine.wrappedJSObject.name;
+ for (var i = 0; i < browser.engines.length; i++) {
+ if (browser.engines[i].title == removeTitle) {
+ if (!browser.hiddenEngines)
+ browser.hiddenEngines = [];
+ browser.hiddenEngines.push(browser.engines[i]);
+ browser.engines.splice(i, 1);
+ break;
+ }
+ }
+ }
+ }
+ BrowserSearch.updateOpenSearchBadge();
+ ]]></body>
+ </method>
+
+ <method name="setIcon">
+ <parameter name="element"/>
+ <parameter name="uri"/>
+ <body><![CDATA[
+ element.setAttribute("src", uri);
+ ]]></body>
+ </method>
+
+ <method name="updateDisplay">
+ <body><![CDATA[
+ var uri = this.currentEngine.iconURI;
+ this.setIcon(this, uri ? uri.spec : "");
+
+ var name = this.currentEngine.name;
+ var text = this._stringBundle.getFormattedString("searchtip", [name]);
+
+ this._textbox.placeholder = this._stringBundle.getString("searchPlaceholder");
+ this._textbox.label = text;
+ this._textbox.tooltipText = text;
+ ]]></body>
+ </method>
+
+ <method name="updateGoButtonVisibility">
+ <body><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-go-button")
+ .hidden = !this._textbox.value;
+ ]]></body>
+ </method>
+
+ <method name="openSuggestionsPanel">
+ <parameter name="aShowOnlySettingsIfEmpty"/>
+ <body><![CDATA[
+ if (this._textbox.open)
+ return;
+
+ this._textbox.showHistoryPopup();
+
+ if (this._textbox.value) {
+ // showHistoryPopup does a startSearch("") call, ensure the
+ // controller handles the text from the input box instead:
+ this._textbox.mController.handleText();
+ }
+ else if (aShowOnlySettingsIfEmpty) {
+ this.setAttribute("showonlysettings", "true");
+ }
+ ]]></body>
+ </method>
+
+ <method name="selectEngine">
+ <parameter name="aEvent"/>
+ <parameter name="isNextEngine"/>
+ <body><![CDATA[
+ // Find the new index
+ var newIndex = this.engines.indexOf(this.currentEngine);
+ newIndex += isNextEngine ? 1 : -1;
+
+ if (newIndex >= 0 && newIndex < this.engines.length) {
+ this.currentEngine = this.engines[newIndex];
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+
+ this.openSuggestionsPanel();
+ ]]></body>
+ </method>
+
+ <method name="handleSearchCommand">
+ <parameter name="aEvent"/>
+ <parameter name="aEngine"/>
+ <parameter name="aForceNewTab"/>
+ <body><![CDATA[
+ var where = "current";
+ let params;
+
+ // Open ctrl/cmd clicks on one-off buttons in a new background tab.
+ if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
+ if (aEvent.button == 2)
+ return;
+ where = whereToOpenLink(aEvent, false, true);
+ }
+ else if (aForceNewTab) {
+ where = "tab";
+ if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
+ where += "-background";
+ }
+ else {
+ var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
+ if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
+ where = "tab";
+ if ((aEvent instanceof MouseEvent) &&
+ (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
+ where = "tab";
+ params = {
+ inBackground: true,
+ };
+ }
+ }
+
+ this.handleSearchCommandWhere(aEvent, aEngine, where, params);
+ ]]></body>
+ </method>
+
+ <method name="handleSearchCommandWhere">
+ <parameter name="aEvent"/>
+ <parameter name="aEngine"/>
+ <parameter name="aWhere"/>
+ <parameter name="aParams"/>
+ <body><![CDATA[
+ var textBox = this._textbox;
+ var textValue = textBox.value;
+
+ let selection = this.telemetrySearchDetails;
+ let oneOffRecorded = false;
+
+ if (!selection || (selection.index == -1)) {
+ oneOffRecorded = this.textbox.popup.oneOffButtons
+ .maybeRecordTelemetry(aEvent, aWhere, aParams);
+ if (!oneOffRecorded) {
+ let source = "unknown";
+ let type = "unknown";
+ let target = aEvent.originalTarget;
+ if (aEvent instanceof KeyboardEvent) {
+ type = "key";
+ } else if (aEvent instanceof MouseEvent) {
+ type = "mouse";
+ if (target.classList.contains("search-panel-header") ||
+ target.parentNode.classList.contains("search-panel-header")) {
+ source = "header";
+ }
+ } else if (aEvent instanceof XULCommandEvent) {
+ if (target.getAttribute("anonid") == "paste-and-search") {
+ source = "paste";
+ }
+ }
+ if (!aEngine) {
+ aEngine = this.currentEngine;
+ }
+ BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
+ aWhere);
+ }
+ }
+
+ // This is a one-off search only if oneOffRecorded is true.
+ this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
+
+ if (aWhere == "tab" && aParams && aParams.inBackground)
+ this.focus();
+ ]]></body>
+ </method>
+
+ <method name="doSearch">
+ <parameter name="aData"/>
+ <parameter name="aWhere"/>
+ <parameter name="aEngine"/>
+ <parameter name="aParams"/>
+ <parameter name="aOneOff"/>
+ <body><![CDATA[
+ var textBox = this._textbox;
+
+ // Save the current value in the form history
+ if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
+ this.FormHistory.update(
+ { op : "bump",
+ fieldname : textBox.getAttribute("autocompletesearchparam"),
+ value : aData },
+ { handleError : function(aError) {
+ Components.utils.reportError("Saving search to form history failed: " + aError.message);
+ }});
+ }
+
+ let engine = aEngine || this.currentEngine;
+ var submission = engine.getSubmission(aData, null, "searchbar");
+ let telemetrySearchDetails = this.telemetrySearchDetails;
+ this.telemetrySearchDetails = null;
+ if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
+ telemetrySearchDetails = null;
+ }
+ // If we hit here, we come either from a one-off, a plain search or a suggestion.
+ const details = {
+ isOneOff: aOneOff,
+ isSuggestion: (!aOneOff && telemetrySearchDetails),
+ selection: telemetrySearchDetails
+ };
+ BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
+ // null parameter below specifies HTML response for search
+ let params = {
+ postData: submission.postData,
+ };
+ if (aParams) {
+ for (let key in aParams) {
+ params[key] = aParams[key];
+ }
+ }
+ openUILinkIn(submission.uri.spec, aWhere, params);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="command"><![CDATA[
+ const target = event.originalTarget;
+ if (target.engine) {
+ this.currentEngine = target.engine;
+ } else if (target.classList.contains("addengine-item")) {
+ // Select the installed engine if the installation succeeds
+ var installCallback = {
+ onSuccess: engine => this.currentEngine = engine
+ }
+ Services.search.addEngine(target.getAttribute("uri"), null,
+ target.getAttribute("src"), false,
+ installCallback);
+ }
+ else
+ return;
+
+ this.focus();
+ this.select();
+ ]]></handler>
+
+ <handler event="DOMMouseScroll"
+ phase="capturing"
+ modifiers="accel"
+ action="this.selectEngine(event, (event.detail > 0));"/>
+
+ <handler event="input" action="this.updateGoButtonVisibility();"/>
+ <handler event="drop" action="this.updateGoButtonVisibility();"/>
+
+ <handler event="blur">
+ <![CDATA[
+ // If the input field is still focused then a different window has
+ // received focus, ignore the next focus event.
+ this._ignoreFocus = (document.activeElement == this._textbox.inputField);
+ ]]></handler>
+
+ <handler event="focus">
+ <![CDATA[
+ // Speculatively connect to the current engine's search URI (and
+ // suggest URI, if different) to reduce request latency
+ this.currentEngine.speculativeConnect({window: window});
+
+ if (this._ignoreFocus) {
+ // This window has been re-focused, don't show the suggestions
+ this._ignoreFocus = false;
+ return;
+ }
+
+ // Don't open the suggestions if there is no text in the textbox.
+ if (!this._textbox.value)
+ return;
+
+ // Don't open the suggestions if the mouse was used to focus the
+ // textbox, that will be taken care of in the click handler.
+ if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
+ return;
+
+ this.openSuggestionsPanel();
+ ]]></handler>
+
+ <handler event="mousedown" phase="capturing">
+ <![CDATA[
+ if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") {
+ this._clickClosedPopup = this._textbox.popup._isHiding;
+ }
+ ]]></handler>
+
+ <handler event="click" button="0">
+ <![CDATA[
+ // Ignore clicks on the search go button.
+ if (event.originalTarget.getAttribute("anonid") == "search-go-button") {
+ return;
+ }
+
+ let isIconClick = event.originalTarget.getAttribute("anonid") == "searchbar-search-button";
+
+ // Ignore clicks on the icon if they were made to close the popup
+ if (isIconClick && this._clickClosedPopup) {
+ return;
+ }
+
+ // Open the suggestions whenever clicking on the search icon or if there
+ // is text in the textbox.
+ if (isIconClick || this._textbox.value) {
+ this.openSuggestionsPanel(true);
+ }
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+ <binding id="searchbar-textbox"
+ extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
+ <implementation implements="nsIObserver">
+ <constructor><![CDATA[
+ const kXULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ if (document.getBindingParent(this).parentNode.parentNode.localName ==
+ "toolbarpaletteitem")
+ return;
+
+ // Initialize fields
+ this._stringBundle = document.getBindingParent(this)._stringBundle;
+ this._suggestEnabled =
+ Services.prefs.getBoolPref("browser.search.suggest.enabled");
+
+ if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
+ this.setAttribute("clickSelectsAll", true);
+
+ // Add items to context menu and attach controller to handle them
+ var textBox = document.getAnonymousElementByAttribute(this,
+ "anonid", "textbox-input-box");
+ var cxmenu = document.getAnonymousElementByAttribute(textBox,
+ "anonid", "input-box-contextmenu");
+ var pasteAndSearch;
+ cxmenu.addEventListener("popupshowing", function() {
+ BrowserSearch.searchBar._textbox.closePopup();
+ if (!pasteAndSearch)
+ return;
+ var controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
+ var enabled = controller.isCommandEnabled("cmd_paste");
+ if (enabled)
+ pasteAndSearch.removeAttribute("disabled");
+ else
+ pasteAndSearch.setAttribute("disabled", "true");
+ }, false);
+
+ var element, label, akey;
+
+ element = document.createElementNS(kXULNS, "menuseparator");
+ cxmenu.appendChild(element);
+
+ this.setAttribute("aria-owns", this.popup.id);
+
+ var insertLocation = cxmenu.firstChild;
+ while (insertLocation.nextSibling &&
+ insertLocation.getAttribute("cmd") != "cmd_paste")
+ insertLocation = insertLocation.nextSibling;
+ if (insertLocation) {
+ element = document.createElementNS(kXULNS, "menuitem");
+ label = this._stringBundle.getString("cmd_pasteAndSearch");
+ element.setAttribute("label", label);
+ element.setAttribute("anonid", "paste-and-search");
+ element.setAttribute("oncommand", "BrowserSearch.pasteAndSearch(event)");
+ cxmenu.insertBefore(element, insertLocation.nextSibling);
+ pasteAndSearch = element;
+ }
+
+ element = document.createElementNS(kXULNS, "menuitem");
+ label = this._stringBundle.getString("cmd_clearHistory");
+ akey = this._stringBundle.getString("cmd_clearHistory_accesskey");
+ element.setAttribute("label", label);
+ element.setAttribute("accesskey", akey);
+ element.setAttribute("cmd", "cmd_clearhistory");
+ cxmenu.appendChild(element);
+
+ element = document.createElementNS(kXULNS, "menuitem");
+ label = this._stringBundle.getString("cmd_showSuggestions");
+ akey = this._stringBundle.getString("cmd_showSuggestions_accesskey");
+ element.setAttribute("anonid", "toggle-suggest-item");
+ element.setAttribute("label", label);
+ element.setAttribute("accesskey", akey);
+ element.setAttribute("cmd", "cmd_togglesuggest");
+ element.setAttribute("type", "checkbox");
+ element.setAttribute("checked", this._suggestEnabled);
+ element.setAttribute("autocheck", "false");
+ this._suggestMenuItem = element;
+ cxmenu.appendChild(element);
+
+ this.addEventListener("keypress", aEvent => {
+ if (navigator.platform.startsWith("Mac") && aEvent.keyCode == KeyEvent.VK_F4)
+ this.openSearch()
+ }, true);
+
+ this.controllers.appendController(this.searchbarController);
+ document.getBindingParent(this)._textboxInitialized = true;
+
+ // Add observer for suggest preference
+ Services.prefs.addObserver("browser.search.suggest.enabled", this, false);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ Services.prefs.removeObserver("browser.search.suggest.enabled", this);
+
+ // Because XBL and the customize toolbar code interacts poorly,
+ // there may not be anything to remove here
+ try {
+ this.controllers.removeController(this.searchbarController);
+ } catch (ex) { }
+ ]]></destructor>
+
+ <field name="_stringBundle"/>
+ <field name="_suggestMenuItem"/>
+ <field name="_suggestEnabled"/>
+
+ <!--
+ This overrides the searchParam property in autocomplete.xml. We're
+ hijacking this property as a vehicle for delivering the privacy
+ information about the window into the guts of nsSearchSuggestions.
+
+ Note that the setter is the same as the parent. We were not sure whether
+ we can override just the getter. If that proves to be the case, the setter
+ can be removed.
+ -->
+ <property name="searchParam"
+ onget="return this.getAttribute('autocompletesearchparam') +
+ (PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');"
+ onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
+
+ <!-- This is implemented so that when textbox.value is set directly (e.g.,
+ by tests), the one-off query is updated. -->
+ <method name="onBeforeValueSet">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this.popup.oneOffButtons.query = aValue;
+ return aValue;
+ ]]></body>
+ </method>
+
+ <!--
+ This method overrides the autocomplete binding's openPopup (essentially
+ duplicating the logic from the autocomplete popup binding's
+ openAutocompletePopup method), modifying it so that the popup is aligned with
+ the inner textbox, but sized to not extend beyond the search bar border.
+ -->
+ <method name="openPopup">
+ <body><![CDATA[
+ var popup = this.popup;
+ if (!popup.mPopupOpen) {
+ // Initially the panel used for the searchbar (PopupSearchAutoComplete
+ // in browser.xul) is hidden to avoid impacting startup / new
+ // window performance. The base binding's openPopup would normally
+ // call the overriden openAutocompletePopup in urlbarBindings.xml's
+ // browser-autocomplete-result-popup binding to unhide the popup,
+ // but since we're overriding openPopup we need to unhide the panel
+ // ourselves.
+ popup.hidden = false;
+
+ // Don't roll up on mouse click in the anchor for the search UI.
+ if (popup.id == "PopupSearchAutoComplete") {
+ popup.setAttribute("norolluponanchor", "true");
+ }
+
+ popup.mInput = this;
+ popup.view = this.controller.QueryInterface(Ci.nsITreeView);
+ popup.invalidate();
+
+ popup.showCommentColumn = this.showCommentColumn;
+ popup.showImageColumn = this.showImageColumn;
+
+ document.popupNode = null;
+
+ const isRTL = getComputedStyle(this, "").direction == "rtl";
+
+ var outerRect = this.getBoundingClientRect();
+ var innerRect = this.inputField.getBoundingClientRect();
+ let width = isRTL ?
+ innerRect.right - outerRect.left :
+ outerRect.right - innerRect.left;
+ popup.setAttribute("width", width > 100 ? width : 100);
+
+ var yOffset = outerRect.bottom - innerRect.bottom;
+ popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false);
+ }
+ ]]></body>
+ </method>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ if (aTopic == "nsPref:changed") {
+ this._suggestEnabled =
+ Services.prefs.getBoolPref("browser.search.suggest.enabled");
+ this._suggestMenuItem.setAttribute("checked", this._suggestEnabled);
+ }
+ ]]></body>
+ </method>
+
+ <method name="openSearch">
+ <body>
+ <![CDATA[
+ if (!this.popupOpen) {
+ document.getBindingParent(this).openSuggestionsPanel();
+ return false;
+ }
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <!-- override |onTextEntered| in autocomplete.xml -->
+ <method name="onTextEntered">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let engine;
+ let oneOff = this.selectedButton;
+ if (oneOff) {
+ if (!oneOff.engine) {
+ oneOff.doCommand();
+ return;
+ }
+ engine = oneOff.engine;
+ }
+ if (this._selectionDetails &&
+ this._selectionDetails.currentIndex != -1) {
+ BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
+ this._selectionDetails = null;
+ }
+ document.getBindingParent(this).handleSearchCommand(aEvent, engine);
+ ]]></body>
+ </method>
+
+ <property name="selectedButton">
+ <getter><![CDATA[
+ return this.popup.oneOffButtons.selectedButton;
+ ]]></getter>
+ <setter><![CDATA[
+ return this.popup.oneOffButtons.selectedButton = val;
+ ]]></setter>
+ </property>
+
+ <method name="handleKeyboardNavigation">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let popup = this.popup;
+ if (!popup.popupOpen)
+ return;
+
+ // accel + up/down changes the default engine and shouldn't affect
+ // the selection on the one-off buttons.
+ if (aEvent.getModifierState("Accel"))
+ return;
+
+ let suggestions =
+ document.getAnonymousElementByAttribute(popup, "anonid", "tree");
+ let suggestionsHidden =
+ suggestions.getAttribute("collapsed") == "true";
+ let numItems = suggestionsHidden ? 0 : this.popup.view.rowCount;
+ this.popup.oneOffButtons.handleKeyPress(aEvent, numItems, true);
+ ]]></body>
+ </method>
+
+ <!-- nsIController -->
+ <field name="searchbarController" readonly="true"><![CDATA[({
+ _self: this,
+ supportsCommand: function(aCommand) {
+ return aCommand == "cmd_clearhistory" ||
+ aCommand == "cmd_togglesuggest";
+ },
+
+ isCommandEnabled: function(aCommand) {
+ return true;
+ },
+
+ doCommand: function (aCommand) {
+ switch (aCommand) {
+ case "cmd_clearhistory":
+ var param = this._self.getAttribute("autocompletesearchparam");
+
+ BrowserSearch.searchBar.FormHistory.update({ op : "remove", fieldname : param }, null);
+ this._self.value = "";
+ break;
+ case "cmd_togglesuggest":
+ // The pref observer will update _suggestEnabled and the menu
+ // checkmark.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled",
+ !this._self._suggestEnabled);
+ break;
+ default:
+ // do nothing with unrecognized command
+ }
+ }
+ })]]></field>
+ </implementation>
+
+ <handlers>
+ <handler event="input"><![CDATA[
+ this.popup.removeAttribute("showonlysettings");
+ ]]></handler>
+
+ <handler event="keypress" phase="capturing"
+ action="return this.handleKeyboardNavigation(event);"/>
+
+ <handler event="keypress" keycode="VK_UP" modifiers="accel"
+ phase="capturing"
+ action="document.getBindingParent(this).selectEngine(event, false);"/>
+
+ <handler event="keypress" keycode="VK_DOWN" modifiers="accel"
+ phase="capturing"
+ action="document.getBindingParent(this).selectEngine(event, true);"/>
+
+ <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
+ phase="capturing"
+ action="return this.openSearch();"/>
+
+ <handler event="keypress" keycode="VK_UP" modifiers="alt"
+ phase="capturing"
+ action="return this.openSearch();"/>
+
+ <handler event="dragover">
+ <![CDATA[
+ var types = event.dataTransfer.types;
+ if (types.includes("text/plain") || types.includes("text/x-moz-text-internal"))
+ event.preventDefault();
+ ]]>
+ </handler>
+
+ <handler event="drop">
+ <![CDATA[
+ var dataTransfer = event.dataTransfer;
+ var data = dataTransfer.getData("text/plain");
+ if (!data)
+ data = dataTransfer.getData("text/x-moz-text-internal");
+ if (data) {
+ event.preventDefault();
+ this.value = data;
+ document.getBindingParent(this).openSuggestionsPanel();
+ }
+ ]]>
+ </handler>
+
+ </handlers>
+ </binding>
+
+ <binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup">
+ <resources>
+ <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
+ <stylesheet src="chrome://browser/skin/searchbar.css"/>
+ </resources>
+ <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+ <xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
+ class="search-panel-header search-panel-current-engine">
+ <xul:image class="searchbar-engine-image" xbl:inherits="src"/>
+ <xul:label anonid="searchbar-engine-name" flex="1" crop="end"
+ role="presentation"/>
+ </xul:hbox>
+ <xul:tree anonid="tree" flex="1"
+ class="autocomplete-tree plain search-panel-tree"
+ hidecolumnpicker="true" seltype="single">
+ <xul:treecols anonid="treecols">
+ <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
+ </xul:treecols>
+ <xul:treechildren class="autocomplete-treebody"/>
+ </xul:tree>
+ <xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/>
+ </content>
+ <implementation>
+ <!-- Popup rollup is triggered by native events before the mousedown event
+ reaches the DOM. The will be set to true by the popuphiding event and
+ false after the mousedown event has been triggered to detect what
+ caused rollup. -->
+ <field name="_isHiding">false</field>
+ <field name="_bundle">null</field>
+ <property name="bundle" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._bundle) {
+ const kBundleURI = "chrome://browser/locale/search.properties";
+ this._bundle = Services.strings.createBundle(kBundleURI);
+ }
+ return this._bundle;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="oneOffButtons" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-one-off-buttons");
+ </field>
+
+ <method name="updateHeader">
+ <body><![CDATA[
+ let currentEngine = Services.search.currentEngine;
+ let uri = currentEngine.iconURI;
+ if (uri) {
+ this.setAttribute("src", uri.spec);
+ }
+ else {
+ // If the default has just been changed to a provider without icon,
+ // avoid showing the icon of the previous default provider.
+ this.removeAttribute("src");
+ }
+
+ let headerText = this.bundle.formatStringFromName("searchHeader",
+ [currentEngine.name], 1);
+ document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
+ .setAttribute("value", headerText);
+ document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
+ .engine = currentEngine;
+ ]]></body>
+ </method>
+
+ <!-- This is called when a one-off is clicked and when "search in new tab"
+ is selected from a one-off context menu. -->
+ <method name="handleOneOffSearch">
+ <parameter name="event"/>
+ <parameter name="engine"/>
+ <parameter name="where"/>
+ <parameter name="params"/>
+ <body><![CDATA[
+ let searchbar = document.getElementById("searchbar");
+ searchbar.handleSearchCommandWhere(event, engine, where, params);
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing"><![CDATA[
+ if (!this.oneOffButtons.popup) {
+ // The panel width only spans to the textbox size, but we also want it
+ // to include the magnifier icon's width.
+ let ltr = getComputedStyle(this).direction == "ltr";
+ let magnifierWidth = parseInt(getComputedStyle(this)[
+ ltr ? "marginLeft" : "marginRight"
+ ]) * -1;
+ // Ensure the panel is wide enough to fit at least 3 engines.
+ let minWidth = Math.max(
+ parseInt(this.width) + magnifierWidth,
+ this.oneOffButtons.buttonWidth * 3
+ );
+ this.style.minWidth = minWidth + "px";
+
+ // Set the origin before assigning the popup, as the assignment does
+ // a rebuild and would miss the origin.
+ this.oneOffButtons.telemetryOrigin = "searchbar";
+ // Set popup after setting the minWidth since it builds the buttons.
+ this.oneOffButtons.popup = this;
+ this.oneOffButtons.textbox = this.input;
+ }
+
+ // First handle deciding if we are showing the reduced version of the
+ // popup containing only the preferences button. We do this if the
+ // glass icon has been clicked if the text field is empty.
+ let searchbar = document.getElementById("searchbar");
+ let tree = document.getAnonymousElementByAttribute(this, "anonid",
+ "tree")
+ if (searchbar.hasAttribute("showonlysettings")) {
+ searchbar.removeAttribute("showonlysettings");
+ this.setAttribute("showonlysettings", "true");
+
+ // Setting this with an xbl-inherited attribute gets overridden the
+ // second time the user clicks the glass icon for some reason...
+ tree.collapsed = true;
+ }
+ else {
+ this.removeAttribute("showonlysettings");
+ // Uncollapse as long as we have a tree with a view which has >= 1 row.
+ // The autocomplete binding itself will take care of uncollapsing later,
+ // if we currently have no rows but end up having some in the future
+ // when the search string changes
+ tree.collapsed = !tree.view || !tree.view.rowCount;
+ }
+
+ // Show the current default engine in the top header of the panel.
+ this.updateHeader();
+ ]]></handler>
+
+ <handler event="popuphiding"><![CDATA[
+ this._isHiding = true;
+ setTimeout(() => {
+ this._isHiding = false;
+ }, 0);
+ ]]></handler>
+
+ <!-- This handles clicks on the topmost "Foo Search" header in the
+ popup (hbox[anonid="searchbar-engine"]). -->
+ <handler event="click"><![CDATA[
+ if (event.button == 2) {
+ // Ignore right clicks.
+ return;
+ }
+ let button = event.originalTarget;
+ let engine = button.parentNode.engine;
+ if (!engine) {
+ return;
+ }
+ this.oneOffButtons.handleSearchCommand(event, engine);
+ ]]></handler>
+ </handlers>
+
+ </binding>
+
+ <!-- Used for additional open search providers in the search panel. -->
+ <binding id="addengine-icon" extends="xul:box">
+ <content>
+ <xul:image class="addengine-icon" xbl:inherits="src"/>
+ <xul:image class="addengine-badge"/>
+ </content>
+ </binding>
+
+ <binding id="search-one-offs">
+ <content context="_child">
+ <xul:deck anonid="search-panel-one-offs-header"
+ selectedIndex="0"
+ class="search-panel-header search-panel-current-input">
+ <xul:label anonid="searchbar-oneoffheader-search"
+ value="&searchWithHeader.label;"/>
+ <xul:hbox anonid="search-panel-searchforwith"
+ class="search-panel-current-input">
+ <xul:label anonid="searchbar-oneoffheader-before"
+ value="&searchFor.label;"/>
+ <xul:label anonid="searchbar-oneoffheader-searchtext"
+ class="search-panel-input-value"
+ flex="1"
+ crop="end"/>
+ <xul:label anonid="searchbar-oneoffheader-after"
+ flex="10000"
+ value="&searchWith.label;"/>
+ </xul:hbox>
+ <xul:hbox anonid="search-panel-searchonengine"
+ class="search-panel-current-input">
+ <xul:label anonid="searchbar-oneoffheader-beforeengine"
+ value="&search.label;"/>
+ <xul:label anonid="searchbar-oneoffheader-engine"
+ class="search-panel-input-value"
+ flex="1"
+ crop="end"/>
+ <xul:label anonid="searchbar-oneoffheader-afterengine"
+ flex="10000"
+ value="&searchAfter.label;"/>
+ </xul:hbox>
+ </xul:deck>
+ <xul:description anonid="search-panel-one-offs"
+ role="group"
+ class="search-panel-one-offs"
+ xbl:inherits="compact">
+ <xul:button anonid="search-settings-compact"
+ oncommand="showSettings();"
+ class="searchbar-engine-one-off-item search-setting-button-compact"
+ tooltiptext="&changeSearchSettings.tooltip;"
+ xbl:inherits="compact"/>
+ </xul:description>
+ <xul:vbox anonid="add-engines"/>
+ <xul:button anonid="search-settings"
+ oncommand="showSettings();"
+ class="search-setting-button search-panel-header"
+ label="&changeSearchSettings.button;"
+ xbl:inherits="compact"/>
+ <xul:menupopup anonid="search-one-offs-context-menu">
+ <xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
+ label="&searchInNewTab.label;"
+ accesskey="&searchInNewTab.accesskey;"/>
+ <xul:menuitem anonid="search-one-offs-context-set-default"
+ label="&searchSetAsDefault.label;"
+ accesskey="&searchSetAsDefault.accesskey;"/>
+ </xul:menupopup>
+ </content>
+
+ <implementation implements="nsIDOMEventListener">
+
+ <!-- Width in pixels of the one-off buttons. 49px is the min-width of
+ each search engine button, adapt this const when changing the css.
+ It's actually 48px + 1px of right border. -->
+ <property name="buttonWidth" readonly="true" onget="return 49;"/>
+
+ <field name="_popup">null</field>
+
+ <!-- The popup that contains the one-offs. This is required, so it should
+ never be null or undefined, except possibly before the one-offs are
+ used. -->
+ <property name="popup">
+ <getter><![CDATA[
+ return this._popup;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this._popup == val) {
+ return val;
+ }
+
+ let events = [
+ "popupshowing",
+ "popuphidden",
+ ];
+ if (this._popup) {
+ for (let event of events) {
+ this._popup.removeEventListener(event, this);
+ }
+ }
+ if (val) {
+ for (let event of events) {
+ val.addEventListener(event, this);
+ }
+ }
+ this._popup = val;
+
+ // If the popup is already open, rebuild the one-offs now. The
+ // popup may be opening, so check that the state is not closed
+ // instead of checking popupOpen.
+ if (val && val.state != "closed") {
+ this._rebuild();
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <field name="_textbox">null</field>
+
+ <!-- The textbox associated with the one-offs. Set this to a textbox to
+ automatically keep the related one-offs UI up to date. Otherwise you
+ can leave it null/undefined, and in that case you should update the
+ query property manually. -->
+ <property name="textbox">
+ <getter><![CDATA[
+ return this._textbox;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this._textbox == val) {
+ return val;
+ }
+ if (this._textbox) {
+ this._textbox.removeEventListener("input", this);
+ }
+ if (val) {
+ val.addEventListener("input", this);
+ }
+ return this._textbox = val;
+ ]]></setter>
+ </property>
+
+ <!-- Set this to a string that identifies your one-offs consumer. It'll
+ be appended to telemetry recorded with maybeRecordTelemetry(). -->
+ <field name="telemetryOrigin">""</field>
+
+ <field name="_query">""</field>
+
+ <!-- The query string currently shown in the one-offs. If the textbox
+ property is non-null, then this is automatically updated on
+ input. -->
+ <property name="query">
+ <getter><![CDATA[
+ return this._query;
+ ]]></getter>
+ <setter><![CDATA[
+ this._query = val;
+ if (this.popup && this.popup.popupOpen) {
+ this._updateAfterQueryChanged();
+ }
+ return val;
+ ]]></setter>
+ </property>
+
+ <field name="_selectedButton">null</field>
+
+ <!-- The selected one-off, a xul:button, including the add-engine button
+ and the search-settings button. Null if no one-off is selected. -->
+ <property name="selectedButton">
+ <getter><![CDATA[
+ return this._selectedButton;
+ ]]></getter>
+ <setter><![CDATA[
+ this._changeVisuallySelectedButton(val, true);
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- The index of the selected one-off, including the add-engine button
+ and the search-settings button. -1 if no one-off is selected. -->
+ <property name="selectedButtonIndex">
+ <getter><![CDATA[
+ let buttons = this.getSelectableButtons(true);
+ for (let i = 0; i < buttons.length; i++) {
+ if (buttons[i] == this._selectedButton) {
+ return i;
+ }
+ }
+ return -1;
+ ]]></getter>
+ <setter><![CDATA[
+ let buttons = this.getSelectableButtons(true);
+ this.selectedButton = buttons[val];
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- The visually selected one-off is the same as the selected one-off
+ unless a one-off is moused over. In that case, the visually selected
+ one-off is the moused-over one-off, which may be different from the
+ selected one-off. The visually selected one-off is always the one
+ that is visually highlighted. Includes the add-engine button and the
+ search-settings button. A xul:button. -->
+ <property name="visuallySelectedButton" readonly="true">
+ <getter><![CDATA[
+ return this.getSelectableButtons(true).find(button => {
+ return button.getAttribute("selected") == "true";
+ });
+ ]]></getter>
+ </property>
+
+ <property name="compact" readonly="true">
+ <getter><![CDATA[
+ return this.getAttribute("compact") == "true";
+ ]]></getter>
+ </property>
+
+ <property name="settingsButton" readonly="true">
+ <getter><![CDATA[
+ let id = this.compact ? "search-settings-compact" : "search-settings";
+ return document.getAnonymousElementByAttribute(this, "anonid", id);
+ ]]></getter>
+ </property>
+
+ <field name="_bundle">null</field>
+
+ <property name="bundle" readonly="true">
+ <getter><![CDATA[
+ if (!this._bundle) {
+ const kBundleURI = "chrome://browser/locale/search.properties";
+ this._bundle = Services.strings.createBundle(kBundleURI);
+ }
+ return this._bundle;
+ ]]></getter>
+ </property>
+
+ <!-- When a context menu is opened on a one-off button, this is set to the
+ engine of that button for use with the context menu actions. -->
+ <field name="_contextEngine">null</field>
+
+ <constructor><![CDATA[
+ // Prevent popup events from the context menu from reaching the autocomplete
+ // binding (or other listeners).
+ let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
+ let listener = aEvent => aEvent.stopPropagation();
+ menu.addEventListener("popupshowing", listener);
+ menu.addEventListener("popuphiding", listener);
+ menu.addEventListener("popupshown", aEvent => {
+ this._ignoreMouseEvents = true;
+ aEvent.stopPropagation();
+ });
+ menu.addEventListener("popuphidden", aEvent => {
+ this._ignoreMouseEvents = false;
+ aEvent.stopPropagation();
+ });
+ ]]></constructor>
+
+ <!-- This handles events outside the one-off buttons, like on the popup
+ and textbox. -->
+ <method name="handleEvent">
+ <parameter name="event"/>
+ <body><![CDATA[
+ switch (event.type) {
+ case "input":
+ // Allow the consumer's input to override its value property with
+ // a oneOffSearchQuery property. That way if the value is not
+ // actually what the user typed (e.g., it's autofilled, or it's a
+ // mozaction URI), the consumer has some way of providing it.
+ this.query = event.target.oneOffSearchQuery || event.target.value;
+ break;
+ case "popupshowing":
+ this._rebuild();
+ break;
+ case "popuphidden":
+ Services.tm.mainThread.dispatch(() => {
+ this.selectedButton = null;
+ this._contextEngine = null;
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="showSettings">
+ <body><![CDATA[
+ BrowserUITelemetry.countSearchSettingsEvent(this.telemetryOrigin);
+ openPreferences("paneSearch");
+ // If the preference tab was already selected, the panel doesn't
+ // close itself automatically.
+ this.popup.hidePopup();
+ ]]></body>
+ </method>
+
+ <!-- Updates the parts of the UI that show the query string. -->
+ <method name="_updateAfterQueryChanged">
+ <body><![CDATA[
+ let headerSearchText =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "searchbar-oneoffheader-searchtext");
+ let headerPanel =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs-header");
+ let list = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs");
+ headerSearchText.setAttribute("value", this.query);
+ let groupText;
+ let isOneOffSelected =
+ this.selectedButton &&
+ this.selectedButton.classList.contains("searchbar-engine-one-off-item");
+ // Typing de-selects the settings or opensearch buttons at the bottom
+ // of the search panel, as typing shows the user intends to search.
+ if (this.selectedButton && !isOneOffSelected)
+ this.selectedButton = null;
+ if (this.query) {
+ groupText = headerSearchText.previousSibling.value +
+ '"' + headerSearchText.value + '"' +
+ headerSearchText.nextSibling.value;
+ if (!isOneOffSelected)
+ headerPanel.selectedIndex = 1;
+ }
+ else {
+ let noSearchHeader =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "searchbar-oneoffheader-search");
+ groupText = noSearchHeader.value;
+ if (!isOneOffSelected)
+ headerPanel.selectedIndex = 0;
+ }
+ list.setAttribute("aria-label", groupText);
+ ]]></body>
+ </method>
+
+ <!-- Builds all the UI. -->
+ <method name="_rebuild">
+ <body><![CDATA[
+ // Update the 'Search for <keywords> with:" header.
+ this._updateAfterQueryChanged();
+
+ let list = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs");
+
+ // Handle opensearch items. This needs to be done before building the
+ // list of one off providers, as that code will return early if all the
+ // alternative engines are hidden.
+ let addEngineList =
+ document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
+ while (addEngineList.firstChild)
+ addEngineList.firstChild.remove();
+
+ const kXULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ // Add a button for each engine that the page in the selected browser
+ // offers. But not when the one-offs are compact. Compact one-offs
+ // are shown in the urlbar, and the add-engine buttons span the width
+ // of the popup, so if we added all the engines that a site offers, it
+ // could effectively break the urlbar popup by offering a ton of
+ // engines. We should probably make a smaller version of the buttons
+ // for compact one-offs.
+ if (!this.compact) {
+ for (let engine of gBrowser.selectedBrowser.engines || []) {
+ let button = document.createElementNS(kXULNS, "button");
+ let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
+ [engine.title], 1);
+ button.id = this.telemetryOrigin + "-add-engine-" +
+ engine.title.replace(/ /g, '-');
+ button.setAttribute("class", "addengine-item");
+ button.setAttribute("label", label);
+ button.setAttribute("pack", "start");
+
+ button.setAttribute("crop", "end");
+ button.setAttribute("tooltiptext", engine.uri);
+ button.setAttribute("uri", engine.uri);
+ if (engine.icon) {
+ button.setAttribute("image", engine.icon);
+ }
+ button.setAttribute("title", engine.title);
+ addEngineList.appendChild(button);
+ }
+ }
+
+ let settingsButton =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-settings-compact");
+ // Finally, build the list of one-off buttons.
+ while (list.firstChild != settingsButton)
+ list.firstChild.remove();
+ // Remove the trailing empty text node introduced by the binding's
+ // content markup above.
+ if (settingsButton.nextSibling)
+ settingsButton.nextSibling.remove();
+
+ let Preferences =
+ Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+ let pref = Preferences.get("browser.search.hiddenOneOffs");
+ let hiddenList = pref ? pref.split(",") : [];
+
+ let currentEngineName = Services.search.currentEngine.name;
+ let includeCurrentEngine = this.getAttribute("includecurrentengine");
+ let engines = Services.search.getVisibleEngines().filter(e => {
+ return (includeCurrentEngine || e.name != currentEngineName) &&
+ !hiddenList.includes(e.name);
+ });
+
+ let header = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs-header")
+ // header is a xul:deck so collapsed doesn't work on it, see bug 589569.
+ header.hidden = list.collapsed = !engines.length;
+
+ if (!engines.length)
+ return;
+
+ let panelWidth = parseInt(this.popup.clientWidth);
+ // The + 1 is because the last button doesn't have a right border.
+ let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth);
+ let buttonWidth = Math.floor(panelWidth / enginesPerRow);
+ // There will be an emtpy area of:
+ // panelWidth - enginesPerRow * buttonWidth px
+ // at the end of each row.
+
+ // If the <description> tag with the list of search engines doesn't have
+ // a fixed height, the panel will be sized incorrectly, causing the bottom
+ // of the suggestion <tree> to be hidden.
+ let oneOffCount = engines.length;
+ if (this.compact)
+ ++oneOffCount;
+ let rowCount = Math.ceil(oneOffCount / enginesPerRow);
+ let height = rowCount * 33; // 32px per row, 1px border.
+ list.setAttribute("height", height + "px");
+
+ // Ensure we can refer to the settings buttons by ID:
+ let settingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
+ settingsEl.id = this.telemetryOrigin + "-anon-search-settings";
+ let compactSettingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
+ compactSettingsEl.id = this.telemetryOrigin +
+ "-anon-search-settings-compact";
+
+ let dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow);
+ for (let i = 0; i < engines.length; ++i) {
+ let engine = engines[i];
+ let button = document.createElementNS(kXULNS, "button");
+ button.id = this._buttonIDForEngine(engine);
+ let uri = "chrome://browser/skin/search-engine-placeholder.png";
+ if (engine.iconURI) {
+ uri = engine.iconURI.spec;
+ }
+ button.setAttribute("image", uri);
+ button.setAttribute("class", "searchbar-engine-one-off-item");
+ button.setAttribute("tooltiptext", engine.name);
+ button.setAttribute("width", buttonWidth);
+ button.engine = engine;
+
+ if ((i + 1) % enginesPerRow == 0)
+ button.classList.add("last-of-row");
+
+ if (i + 1 == engines.length)
+ button.classList.add("last-engine");
+
+ if (i >= oneOffCount + dummyItems - enginesPerRow)
+ button.classList.add("last-row");
+
+ list.insertBefore(button, settingsButton);
+ }
+
+ let hasDummyItems = !!dummyItems;
+ while (dummyItems) {
+ let button = document.createElementNS(kXULNS, "button");
+ button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
+ button.setAttribute("width", buttonWidth);
+
+ if (!--dummyItems)
+ button.classList.add("last-of-row");
+
+ list.insertBefore(button, settingsButton);
+ }
+
+ if (this.compact) {
+ this.settingsButton.setAttribute("width", buttonWidth);
+ if (rowCount == 1 && hasDummyItems) {
+ // When there's only one row, make the compact settings button
+ // hug the right edge of the panel. It may not due to the panel's
+ // width not being an integral multiple of the button width. (See
+ // the "There will be an emtpy area" comment above.) Increase the
+ // width of the last dummy item by the remainder.
+ //
+ // There's one weird thing to guard against. When layout pixels
+ // aren't an integral multiple of device pixels, the calculated
+ // remainder can end up being ~1px too big, at least on Windows,
+ // which pushes the settings button to a new row. The remainder
+ // is integral, not a fraction, so that's not the problem. To
+ // work around that, unscale the remainder, floor it, scale it
+ // back, and then floor that.
+ let scale = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .screenPixelsPerCSSPixel;
+ let remainder = panelWidth - (enginesPerRow * buttonWidth);
+ remainder = Math.floor(Math.floor(remainder * scale) / scale);
+ let width = remainder + buttonWidth;
+ let lastDummyItem = this.settingsButton.previousSibling;
+ lastDummyItem.setAttribute("width", width);
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_buttonIDForEngine">
+ <parameter name="engine"/>
+ <body><![CDATA[
+ return this.telemetryOrigin + "-engine-one-off-item-" +
+ engine.name.replace(/ /g, '-');
+ ]]></body>
+ </method>
+
+ <method name="_buttonForEngine">
+ <parameter name="engine"/>
+ <body><![CDATA[
+ return document.getElementById(this._buttonIDForEngine(engine));
+ ]]></body>
+ </method>
+
+ <method name="_changeVisuallySelectedButton">
+ <parameter name="val"/>
+ <parameter name="aUpdateLogicallySelectedButton"/>
+ <body><![CDATA[
+ let visuallySelectedButton = this.visuallySelectedButton;
+ if (visuallySelectedButton)
+ visuallySelectedButton.removeAttribute("selected");
+
+ let header =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs-header");
+ // Avoid selecting dummy buttons.
+ if (val && !val.classList.contains("dummy")) {
+ val.setAttribute("selected", "true");
+ if (val.classList.contains("searchbar-engine-one-off-item") &&
+ val.engine) {
+ let headerEngineText =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "searchbar-oneoffheader-engine");
+ header.selectedIndex = 2;
+ headerEngineText.value = val.engine.name;
+ }
+ else {
+ header.selectedIndex = this.query ? 1 : 0;
+ }
+ if (this.textbox) {
+ this.textbox.setAttribute("aria-activedescendant", val.id);
+ }
+ } else {
+ val = null;
+ header.selectedIndex = this.query ? 1 : 0;
+ if (this.textbox) {
+ this.textbox.removeAttribute("aria-activedescendant");
+ }
+ }
+
+ if (aUpdateLogicallySelectedButton) {
+ this._selectedButton = val;
+ if (val && !val.engine) {
+ // If the button doesn't have an engine, then clear the popup's
+ // selection to indicate that pressing Return while the button is
+ // selected will do the button's command, not search.
+ this.popup.selectedIndex = -1;
+ }
+ let event = document.createEvent("Events");
+ event.initEvent("SelectedOneOffButtonChanged", true, false);
+ this.dispatchEvent(event);
+ }
+ ]]></body>
+ </method>
+
+ <method name="getSelectableButtons">
+ <parameter name="aIncludeNonEngineButtons"/>
+ <body><![CDATA[
+ let buttons = [];
+ let oneOff = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs");
+ for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+ // oneOff may be a text node since the list xul:description contains
+ // whitespace and the compact settings button. See the markup
+ // above. _rebuild removes text nodes, but it may not have been
+ // called yet (because e.g. the popup hasn't been opened yet).
+ if (oneOff.nodeType == Node.ELEMENT_NODE) {
+ if (oneOff.classList.contains("dummy") ||
+ oneOff.classList.contains("search-setting-button-compact"))
+ break;
+ buttons.push(oneOff);
+ }
+ }
+
+ if (!aIncludeNonEngineButtons)
+ return buttons;
+
+ let addEngine =
+ document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
+ for (addEngine = addEngine.firstChild; addEngine; addEngine = addEngine.nextSibling)
+ buttons.push(addEngine);
+
+ buttons.push(this.settingsButton);
+ return buttons;
+ ]]></body>
+ </method>
+
+ <method name="handleSearchCommand">
+ <parameter name="aEvent"/>
+ <parameter name="aEngine"/>
+ <parameter name="aForceNewTab"/>
+ <body><![CDATA[
+ let where = "current";
+ let params;
+
+ // Open ctrl/cmd clicks on one-off buttons in a new background tab.
+ if (aForceNewTab) {
+ where = "tab";
+ if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
+ params = {
+ inBackground: true,
+ };
+ }
+ }
+ else {
+ var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
+ if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
+ where = "tab";
+ if ((aEvent instanceof MouseEvent) &&
+ (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
+ where = "tab";
+ params = {
+ inBackground: true,
+ };
+ }
+ }
+
+ this.popup.handleOneOffSearch(aEvent, aEngine, where, params);
+ ]]></body>
+ </method>
+
+ <!--
+ Increments or decrements the index of the currently selected one-off.
+
+ @param aForward
+ If true, the index is incremented, and if false, the index is
+ decremented.
+ @param aWrapAround
+ This has a couple of effects, depending on whether there is
+ currently a selection.
+ (1) If true and the last one-off is currently selected,
+ incrementing the index will cause the selection to be cleared and
+ this method to return true. Calling advanceSelection again after
+ that (again with aForward=true) will select the first one-off.
+ Likewise if decrementing the index when the first one-off is
+ selected, except in the opposite direction of course.
+ (2) If true and there currently is no selection, decrementing the
+ index will cause the last one-off to become selected and this
+ method to return true. Only the aForward=false case is affected
+ because it is always the case that if aForward=true and there
+ currently is no selection, the first one-off becomes selected and
+ this method returns true.
+ @param aCycleEngines
+ If true, only engine buttons are included.
+ @return True if the selection can continue to advance after this method
+ returns and false if not.
+ -->
+ <method name="advanceSelection">
+ <parameter name="aForward"/>
+ <parameter name="aWrapAround"/>
+ <parameter name="aCycleEngines"/>
+ <body><![CDATA[
+ let selectedButton = this.selectedButton;
+ let buttons = this.getSelectableButtons(aCycleEngines);
+
+ if (selectedButton) {
+ // cycle through one-off buttons.
+ let index = buttons.indexOf(selectedButton);
+ if (aForward)
+ ++index;
+ else
+ --index;
+
+ if (index >= 0 && index < buttons.length)
+ this.selectedButton = buttons[index];
+ else
+ this.selectedButton = null;
+
+ if (this.selectedButton || aWrapAround)
+ return true;
+
+ return false;
+ }
+
+ // If no selection, select the first button or ...
+ if (aForward) {
+ this.selectedButton = buttons[0];
+ return true;
+ }
+
+ if (!aForward && aWrapAround) {
+ // the last button.
+ this.selectedButton = buttons[buttons.length - 1];
+ return true;
+ }
+
+ return false;
+ ]]></body>
+ </method>
+
+ <!--
+ This handles key presses specific to the one-off buttons like Tab and
+ Alt-Up/Down, and Up/Down keys within the buttons. Since one-off buttons
+ are always used in conjunction with a list of some sort (in this.popup),
+ it also handles Up/Down keys that cross the boundaries between list
+ items and the one-off buttons.
+
+ @param event
+ The key event.
+ @param numListItems
+ The number of items in the list. The reason that this is a
+ parameter at all is that the list may contain items at the end
+ that should be ignored, depending on the consumer. That's true
+ for the urlbar for example.
+ @param allowEmptySelection
+ Pass true if it's OK that neither the list nor the one-off
+ buttons contains a selection. Pass false if either the list or
+ the one-off buttons (or both) should always contain a selection.
+ @param textboxUserValue
+ When the last list item is selected and the user presses Down,
+ the first one-off becomes selected and the textbox value is
+ restored to the value that the user typed. Pass that value here.
+ However, if you pass true for allowEmptySelection, you don't need
+ to pass anything for this parameter. (Pass undefined or null.)
+ @return True if this method handled the keypress and false if not. If
+ false, then you should let the autocomplete controller handle
+ the keypress. The value of event.defaultPrevented will be the
+ same as this return value.
+ -->
+ <method name="handleKeyPress">
+ <parameter name="event"/>
+ <parameter name="numListItems"/>
+ <parameter name="allowEmptySelection"/>
+ <parameter name="textboxUserValue"/>
+ <body><![CDATA[
+ if (!this.popup) {
+ return false;
+ }
+
+ let stopEvent = false;
+
+ // Tab cycles through the one-offs and moves the focus out at the end.
+ // But only if non-Shift modifiers aren't also pressed, to avoid
+ // clobbering other shortcuts.
+ if (event.keyCode == KeyEvent.DOM_VK_TAB &&
+ !event.altKey &&
+ !event.ctrlKey &&
+ !event.metaKey &&
+ this.getAttribute("disabletab") != "true") {
+ stopEvent = this.advanceSelection(!event.shiftKey, false, true);
+ }
+
+ // Alt + up/down is very similar to (shift +) tab but differs in that
+ // it loops through the list, whereas tab will move the focus out.
+ else if (event.altKey &&
+ (event.keyCode == KeyEvent.DOM_VK_DOWN ||
+ event.keyCode == KeyEvent.DOM_VK_UP)) {
+ stopEvent =
+ this.advanceSelection(event.keyCode == KeyEvent.DOM_VK_DOWN,
+ true, false);
+ }
+
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP) {
+ if (numListItems > 0) {
+ if (this.popup.selectedIndex > 0) {
+ // The autocomplete controller should handle this case.
+ } else if (this.popup.selectedIndex == 0) {
+ if (!allowEmptySelection) {
+ // Wrap around the selection to the last one-off.
+ this.selectedButton = null;
+ this.popup.selectedIndex = -1;
+ // Call advanceSelection after setting selectedIndex so that
+ // screen readers see the newly selected one-off. Both trigger
+ // accessibility events.
+ this.advanceSelection(false, true, true);
+ stopEvent = true;
+ }
+ } else {
+ let firstButtonSelected =
+ this.selectedButton &&
+ this.selectedButton == this.getSelectableButtons(true)[0];
+ if (firstButtonSelected) {
+ this.selectedButton = null;
+ } else {
+ stopEvent = this.advanceSelection(false, true, true);
+ }
+ }
+ } else {
+ stopEvent = this.advanceSelection(false, true, true);
+ }
+ }
+
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
+ if (numListItems > 0) {
+ if (this.popup.selectedIndex >= 0 &&
+ this.popup.selectedIndex < numListItems - 1) {
+ // The autocomplete controller should handle this case.
+ } else if (this.popup.selectedIndex == numListItems - 1) {
+ this.selectedButton = null;
+ if (!allowEmptySelection) {
+ this.popup.selectedIndex = -1;
+ stopEvent = true;
+ }
+ if (this.textbox && typeof(textboxUserValue) == "string") {
+ this.textbox.value = textboxUserValue;
+ }
+ // Call advanceSelection after setting selectedIndex so that
+ // screen readers see the newly selected one-off. Both trigger
+ // accessibility events.
+ this.advanceSelection(true, true, true);
+ } else {
+ let buttons = this.getSelectableButtons(true);
+ let lastButtonSelected =
+ this.selectedButton &&
+ this.selectedButton == buttons[buttons.length - 1];
+ if (lastButtonSelected) {
+ this.selectedButton = null;
+ stopEvent = allowEmptySelection;
+ } else if (this.selectedButton) {
+ stopEvent = this.advanceSelection(true, true, true);
+ } else {
+ // The autocomplete controller should handle this case.
+ }
+ }
+ } else {
+ stopEvent = this.advanceSelection(true, true, true);
+ }
+ }
+
+ if (stopEvent) {
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+ return false;
+ ]]></body>
+ </method>
+
+ <!--
+ If the given event is related to the one-offs, this method records
+ one-off telemetry for it. this.telemetryOrigin will be appended to the
+ computed source, so make sure you set that first.
+
+ @param aEvent
+ An event, like a click on a one-off button.
+ @param aOpenUILinkWhere
+ The "where" passed to openUILink.
+ @param aOpenUILinkParams
+ The "params" passed to openUILink.
+ @return True if telemetry was recorded and false if not.
+ -->
+ <method name="maybeRecordTelemetry">
+ <parameter name="aEvent"/>
+ <parameter name="aOpenUILinkWhere"/>
+ <parameter name="aOpenUILinkParams"/>
+ <body><![CDATA[
+ if (!aEvent) {
+ return false;
+ }
+
+ let source = null;
+ let type = "unknown";
+ let engine = null;
+ let target = aEvent.originalTarget;
+
+ if (aEvent instanceof KeyboardEvent) {
+ type = "key";
+ if (this.selectedButton) {
+ source = "oneoff";
+ engine = this.selectedButton.engine;
+ }
+ } else if (aEvent instanceof MouseEvent) {
+ type = "mouse";
+ if (target.classList.contains("searchbar-engine-one-off-item")) {
+ source = "oneoff";
+ engine = target.engine;
+ }
+ } else if ((aEvent instanceof XULCommandEvent) &&
+ target.getAttribute("anonid") ==
+ "search-one-offs-context-open-in-new-tab") {
+ source = "oneoff-context";
+ engine = this._contextEngine;
+ }
+
+ if (!source) {
+ return false;
+ }
+
+ if (this.telemetryOrigin) {
+ source += "-" + this.telemetryOrigin;
+ }
+
+ let tabBackground = aOpenUILinkWhere == "tab" &&
+ aOpenUILinkParams &&
+ aOpenUILinkParams.inBackground;
+ let where = tabBackground ? "tab-background" : aOpenUILinkWhere;
+ BrowserSearch.recordOneoffSearchInTelemetry(engine, source, type,
+ where);
+ return true;
+ ]]></body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+
+ <handler event="mousedown"><![CDATA[
+ // Required to receive click events from the buttons on Linux.
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="mousemove"><![CDATA[
+ let target = event.originalTarget;
+ if (target.localName != "button")
+ return;
+
+ // Ignore mouse events when the context menu is open.
+ if (this._ignoreMouseEvents)
+ return;
+
+ if ((target.classList.contains("searchbar-engine-one-off-item") &&
+ !target.classList.contains("dummy")) ||
+ target.classList.contains("addengine-item") ||
+ target.classList.contains("search-setting-button")) {
+ this._changeVisuallySelectedButton(target);
+ }
+ ]]></handler>
+
+ <handler event="mouseout"><![CDATA[
+ let target = event.originalTarget;
+ if (target.localName != "button") {
+ return;
+ }
+
+ // Don't deselect the current button if the context menu is open.
+ if (this._ignoreMouseEvents)
+ return;
+
+ // Unfortunately this will fire before mouseover hits another item.
+ // If this button is selected, we replace that selection only if
+ // we're not moving to a different one-off item:
+ if (target.getAttribute("selected") == "true" &&
+ (!event.relatedTarget ||
+ !event.relatedTarget.classList.contains("searchbar-engine-one-off-item") ||
+ event.relatedTarget.classList.contains("dummy"))) {
+ this._changeVisuallySelectedButton(this.selectedButton);
+ }
+ ]]></handler>
+
+ <handler event="click"><![CDATA[
+ if (event.button == 2)
+ return; // ignore right clicks.
+
+ let button = event.originalTarget;
+ let engine = button.engine;
+
+ if (!engine)
+ return;
+
+ // Select the clicked button so that consumers can easily tell which
+ // button was acted on.
+ this.selectedButton = button;
+ this.handleSearchCommand(event, engine);
+ ]]></handler>
+
+ <handler event="command"><![CDATA[
+ let target = event.originalTarget;
+ if (target.classList.contains("addengine-item")) {
+ // On success, hide the panel and tell event listeners to reshow it to
+ // show the new engine.
+ let installCallback = {
+ onSuccess: engine => {
+ this._rebuild();
+ },
+ onError: function(errorCode) {
+ if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) {
+ // Download error is shown by the search service
+ return;
+ }
+ const kSearchBundleURI = "chrome://global/locale/search/search.properties";
+ let searchBundle = Services.strings.createBundle(kSearchBundleURI);
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandName = brandBundle.getString("brandShortName");
+ let title = searchBundle.GetStringFromName("error_invalid_engine_title");
+ let text = searchBundle.formatStringFromName("error_duplicate_engine_msg",
+ [brandName, target.getAttribute("uri")], 2);
+ Services.prompt.QueryInterface(Ci.nsIPromptFactory);
+ let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt);
+ prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
+ prompt.setPropertyAsBool("allowTabModal", true);
+ prompt.alert(title, text);
+ }
+ }
+ Services.search.addEngine(target.getAttribute("uri"), null,
+ target.getAttribute("image"), false,
+ installCallback);
+ }
+ let anonid = target.getAttribute("anonid");
+ if (anonid == "search-one-offs-context-open-in-new-tab") {
+ // Select the context-clicked button so that consumers can easily
+ // tell which button was acted on.
+ this.selectedButton = this._buttonForEngine(this._contextEngine);
+ this.handleSearchCommand(event, this._contextEngine, true);
+ }
+ if (anonid == "search-one-offs-context-set-default") {
+ let currentEngine = Services.search.currentEngine;
+
+ if (!this.getAttribute("includecurrentengine")) {
+ // Make the target button of the context menu reflect the current
+ // search engine first. Doing this as opposed to rebuilding all the
+ // one-off buttons avoids flicker.
+ let button = this._buttonForEngine(this._contextEngine);
+ button.id = this._buttonIDForEngine(currentEngine);
+ let uri = "chrome://browser/skin/search-engine-placeholder.png";
+ if (currentEngine.iconURI)
+ uri = currentEngine.iconURI.spec;
+ button.setAttribute("image", uri);
+ button.setAttribute("tooltiptext", currentEngine.name);
+ button.engine = currentEngine;
+ }
+
+ Services.search.currentEngine = this._contextEngine;
+ }
+ ]]></handler>
+
+ <handler event="contextmenu"><![CDATA[
+ let target = event.originalTarget;
+ // Prevent the context menu from appearing except on the one off buttons.
+ if (!target.classList.contains("searchbar-engine-one-off-item") ||
+ target.classList.contains("dummy")) {
+ event.preventDefault();
+ return;
+ }
+ document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
+ .setAttribute("disabled", target.engine == Services.search.currentEngine);
+
+ this._contextEngine = target.engine;
+ ]]></handler>
+ </handlers>
+
+ </binding>
+
+</bindings>
diff --git a/browser/components/search/content/searchReset.js b/browser/components/search/content/searchReset.js
new file mode 100644
index 000000000..b541d41da
--- /dev/null
+++ b/browser/components/search/content/searchReset.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const TELEMETRY_RESULT_ENUM = {
+ RESTORED_DEFAULT: 0,
+ KEPT_CURRENT: 1,
+ CHANGED_ENGINE: 2,
+ CLOSED_PAGE: 3,
+ OPENED_SETTINGS: 4
+};
+
+window.onload = function() {
+ let defaultEngine = document.getElementById("defaultEngine");
+ let originalDefault = Services.search.originalDefaultEngine;
+ defaultEngine.textContent = originalDefault.name;
+ defaultEngine.style.backgroundImage =
+ 'url("' + originalDefault.iconURI.spec + '")';
+
+ document.getElementById("searchResetChangeEngine").focus();
+ window.addEventListener("unload", recordPageClosed);
+ document.getElementById("linkSettingsPage")
+ .addEventListener("click", openingSettings);
+};
+
+function doSearch() {
+ let queryString = "";
+ let purpose = "";
+ let params = window.location.href.match(/^about:searchreset\?([^#]*)/);
+ if (params) {
+ params = params[1].split("&");
+ for (let param of params) {
+ if (param.startsWith("data="))
+ queryString = decodeURIComponent(param.slice(5));
+ else if (param.startsWith("purpose="))
+ purpose = param.slice(8);
+ }
+ }
+
+ let engine = Services.search.currentEngine;
+ let submission = engine.getSubmission(queryString, null, purpose);
+
+ window.removeEventListener("unload", recordPageClosed);
+
+ let win = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ win.openUILinkIn(submission.uri.spec, "current", false, submission.postData);
+}
+
+function openingSettings() {
+ record(TELEMETRY_RESULT_ENUM.OPENED_SETTINGS);
+ window.removeEventListener("unload", recordPageClosed);
+}
+
+function record(result) {
+ Services.telemetry.getHistogramById("SEARCH_RESET_RESULT").add(result);
+}
+
+function keepCurrentEngine() {
+ // Calling the currentEngine setter will force a correct loadPathHash to be
+ // written for this engine, so that we don't prompt the user again.
+ Services.search.currentEngine = Services.search.currentEngine;
+ record(TELEMETRY_RESULT_ENUM.KEPT_CURRENT);
+ doSearch();
+}
+
+function changeSearchEngine() {
+ let engine = Services.search.originalDefaultEngine;
+ if (engine.hidden)
+ engine.hidden = false;
+ Services.search.currentEngine = engine;
+
+ record(TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT);
+
+ doSearch();
+}
+
+function recordPageClosed() {
+ record(TELEMETRY_RESULT_ENUM.CLOSED_PAGE);
+}
diff --git a/browser/components/search/content/searchReset.xhtml b/browser/components/search/content/searchReset.xhtml
new file mode 100644
index 000000000..b851dd383
--- /dev/null
+++ b/browser/components/search/content/searchReset.xhtml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % searchresetDTD SYSTEM "chrome://browser/locale/aboutSearchReset.dtd">
+ %searchresetDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <title>&searchreset.tabtitle;</title>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://global/skin/in-content/info-pages.css"/>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/skin/searchReset.css"/>
+ <link rel="icon" type="image/png"
+ href="chrome://browser/skin/favicon-search-16.svg"/>
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://browser/content/search/searchReset.js"/>
+ </head>
+
+ <body dir="&locale.dir;">
+
+ <div class="container">
+ <div class="title">
+ <h1 class="title-text">&searchreset.pageTitle;</h1>
+ </div>
+
+ <div class="description">
+ <p>&searchreset.pageInfo1;</p>
+ <p>&searchreset.selector.label;<span id="defaultEngine"/></p>
+
+ <p>&searchreset.beforelink.pageInfo2;<a id="linkSettingsPage" href="about:preferences#search">&searchreset.link.pageInfo2;</a>&searchreset.afterlink.pageInfo2;</p>
+ </div>
+
+ <div class="button-container">
+ <xul:button id="searchResetKeepCurrent"
+ label="&searchreset.noChangeButton;"
+ accesskey="&searchreset.noChangeButton.access;"
+ oncommand="keepCurrentEngine();"/>
+ <xul:button class="primary"
+ id="searchResetChangeEngine"
+ label="&searchreset.changeEngineButton;"
+ accesskey="&searchreset.changeEngineButton.access;"
+ oncommand="changeSearchEngine();"/>
+ </div>
+ </div>
+
+ </body>
+</html>
diff --git a/browser/components/search/content/searchbarBindings.css b/browser/components/search/content/searchbarBindings.css
new file mode 100644
index 000000000..0429e8811
--- /dev/null
+++ b/browser/components/search/content/searchbarBindings.css
@@ -0,0 +1,18 @@
+/* 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/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+.searchbar-textbox {
+ -moz-binding: url("chrome://browser/content/search/search.xml#searchbar-textbox");
+}
+
+.search-one-offs {
+ -moz-binding: url("chrome://browser/content/search/search.xml#search-one-offs");
+}
+
+.search-setting-button[compact=true],
+.search-setting-button-compact:not([compact=true]) {
+ display: none;
+}
diff --git a/browser/components/search/jar.mn b/browser/components/search/jar.mn
new file mode 100644
index 000000000..089ec4bb9
--- /dev/null
+++ b/browser/components/search/jar.mn
@@ -0,0 +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/.
+
+browser.jar:
+ content/browser/search/search.xml (content/search.xml)
+ content/browser/search/searchbarBindings.css (content/searchbarBindings.css)
+ content/browser/search/searchReset.xhtml (content/searchReset.xhtml)
+ content/browser/search/searchReset.js (content/searchReset.js)
diff --git a/browser/components/search/moz.build b/browser/components/search/moz.build
new file mode 100644
index 000000000..618cd7657
--- /dev/null
+++ b/browser/components/search/moz.build
@@ -0,0 +1,14 @@
+# -*- 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 += [
+ 'test/browser.ini',
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Search')
diff --git a/browser/components/search/test/.eslintrc.js b/browser/components/search/test/.eslintrc.js
new file mode 100644
index 000000000..c764b133d
--- /dev/null
+++ b/browser/components/search/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/components/search/test/426329.xml b/browser/components/search/test/426329.xml
new file mode 100644
index 000000000..e4545cc77
--- /dev/null
+++ b/browser/components/search/test/426329.xml
@@ -0,0 +1,11 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Bug 426329</ShortName>
+ <Description>426329 Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/test.html">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/test.html</moz:SearchForm>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/483086-1.xml b/browser/components/search/test/483086-1.xml
new file mode 100644
index 000000000..9dbba4886
--- /dev/null
+++ b/browser/components/search/test/483086-1.xml
@@ -0,0 +1,10 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>483086a</ShortName>
+ <Description>Bug 483086 Test 1</Description>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>foo://example.com</moz:SearchForm>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/483086-2.xml b/browser/components/search/test/483086-2.xml
new file mode 100644
index 000000000..f130b9068
--- /dev/null
+++ b/browser/components/search/test/483086-2.xml
@@ -0,0 +1,10 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>483086b</ShortName>
+ <Description>Bug 483086 Test 2</Description>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://example.com</moz:SearchForm>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/browser.ini b/browser/components/search/test/browser.ini
new file mode 100644
index 000000000..f1070264d
--- /dev/null
+++ b/browser/components/search/test/browser.ini
@@ -0,0 +1,44 @@
+[DEFAULT]
+support-files =
+ 426329.xml
+ 483086-1.xml
+ 483086-2.xml
+ head.js
+ opensearch.html
+ test.html
+ testEngine.xml
+ testEngine_diacritics.xml
+ testEngine_dupe.xml
+ testEngine_mozsearch.xml
+ webapi.html
+
+[browser_426329.js]
+[browser_483086.js]
+[browser_addEngine.js]
+[browser_amazon.js]
+[browser_amazon_behavior.js]
+[browser_bing.js]
+[browser_bing_behavior.js]
+[browser_contextmenu.js]
+[browser_contextSearchTabPosition.js]
+skip-if = os == "mac" # bug 967013
+[browser_google.js]
+[browser_google_codes.js]
+[browser_google_behavior.js]
+[browser_healthreport.js]
+[browser_hiddenOneOffs_cleanup.js]
+[browser_hiddenOneOffs_diacritics.js]
+[browser_oneOffContextMenu.js]
+[browser_oneOffContextMenu_setDefault.js]
+[browser_oneOffHeader.js]
+[browser_private_search_perwindowpb.js]
+[browser_yahoo.js]
+[browser_yahoo_behavior.js]
+[browser_abouthome_behavior.js]
+skip-if = true # Bug ??????, Bug 1100301 - leaks windows until shutdown when --run-by-dir
+[browser_aboutSearchReset.js]
+[browser_searchbar_openpopup.js]
+skip-if = os == "linux" # Linux has different focus behaviours.
+[browser_searchbar_keyboard_navigation.js]
+[browser_searchbar_smallpanel_keyboard_navigation.js]
+[browser_webapi.js]
diff --git a/browser/components/search/test/browser_426329.js b/browser/components/search/test/browser_426329.js
new file mode 100644
index 000000000..d9cbd3f7a
--- /dev/null
+++ b/browser/components/search/test/browser_426329.js
@@ -0,0 +1,250 @@
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+function expectedURL(aSearchTerms) {
+ const ENGINE_HTML_BASE = "http://mochi.test:8888/browser/browser/components/search/test/test.html";
+ var textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
+ getService(Ci.nsITextToSubURI);
+ var searchArg = textToSubURI.ConvertAndEscape("utf-8", aSearchTerms);
+ return ENGINE_HTML_BASE + "?test=" + searchArg;
+}
+
+function simulateClick(aEvent, aTarget) {
+ var event = document.createEvent("MouseEvent");
+ var ctrlKeyArg = aEvent.ctrlKey || false;
+ var altKeyArg = aEvent.altKey || false;
+ var shiftKeyArg = aEvent.shiftKey || false;
+ var metaKeyArg = aEvent.metaKey || false;
+ var buttonArg = aEvent.button || 0;
+ event.initMouseEvent("click", true, true, window,
+ 0, 0, 0, 0, 0,
+ ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
+ buttonArg, null);
+ aTarget.dispatchEvent(event);
+}
+
+// modified from toolkit/components/satchel/test/test_form_autocomplete.html
+function checkMenuEntries(expectedValues) {
+ var actualValues = getMenuEntries();
+ is(actualValues.length, expectedValues.length, "Checking length of expected menu");
+ for (var i = 0; i < expectedValues.length; i++)
+ is(actualValues[i], expectedValues[i], "Checking menu entry #" + i);
+}
+
+function getMenuEntries() {
+ var entries = [];
+ var autocompleteMenu = searchBar.textbox.popup;
+ // Could perhaps pull values directly from the controller, but it seems
+ // more reliable to test the values that are actually in the tree?
+ var column = autocompleteMenu.tree.columns[0];
+ var numRows = autocompleteMenu.tree.view.rowCount;
+ for (var i = 0; i < numRows; i++) {
+ entries.push(autocompleteMenu.tree.view.getValueAt(i, column));
+ }
+ return entries;
+}
+
+function countEntries(name, value) {
+ return new Promise(resolve => {
+ let count = 0;
+ let obj = name && value ? {fieldname: name, value: value} : {};
+ FormHistory.count(obj,
+ { handleResult: function(result) { count = result; },
+ handleError: function(error) { throw error; },
+ handleCompletion: function(reason) {
+ if (!reason) {
+ resolve(count);
+ }
+ }
+ });
+ });
+}
+
+var searchBar;
+var searchButton;
+var searchEntries = ["test"];
+function promiseSetEngine() {
+ return new Promise(resolve => {
+ var ss = Services.search;
+
+ function observer(aSub, aTopic, aData) {
+ switch (aData) {
+ case "engine-added":
+ var engine = ss.getEngineByName("Bug 426329");
+ ok(engine, "Engine was added.");
+ ss.currentEngine = engine;
+ break;
+ case "engine-current":
+ ok(ss.currentEngine.name == "Bug 426329", "currentEngine set");
+ searchBar = BrowserSearch.searchBar;
+ searchButton = document.getAnonymousElementByAttribute(searchBar,
+ "anonid", "search-go-button");
+ ok(searchButton, "got search-go-button");
+ searchBar.value = "test";
+
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ resolve();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+ ss.addEngine("http://mochi.test:8888/browser/browser/components/search/test/426329.xml",
+ null, "data:image/x-icon,%00", false);
+ });
+}
+
+function promiseRemoveEngine() {
+ return new Promise(resolve => {
+ var ss = Services.search;
+
+ function observer(aSub, aTopic, aData) {
+ if (aData == "engine-removed") {
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ resolve();
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+ var engine = ss.getEngineByName("Bug 426329");
+ ss.removeEngine(engine);
+ });
+}
+
+
+var preSelectedBrowser;
+var preTabNo;
+function* prepareTest() {
+ preSelectedBrowser = gBrowser.selectedBrowser;
+ preTabNo = gBrowser.tabs.length;
+ searchBar = BrowserSearch.searchBar;
+
+ yield SimpleTest.promiseFocus();
+
+ if (document.activeElement == searchBar)
+ return;
+
+ let focusPromise = BrowserTestUtils.waitForEvent(searchBar, "focus");
+ gURLBar.focus();
+ searchBar.focus();
+ yield focusPromise;
+}
+
+add_task(function* testSetupEngine() {
+ yield promiseSetEngine();
+});
+
+add_task(function* testReturn() {
+ yield* prepareTest();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ is(gBrowser.tabs.length, preTabNo, "Return key did not open new tab");
+ is(gBrowser.currentURI.spec, expectedURL(searchBar.value), "testReturn opened correct search page");
+});
+
+add_task(function* testAltReturn() {
+ yield* prepareTest();
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ EventUtils.synthesizeKey("VK_RETURN", { altKey: true });
+ });
+
+ is(gBrowser.tabs.length, preTabNo + 1, "Alt+Return key added new tab");
+ is(gBrowser.currentURI.spec, expectedURL(searchBar.value), "testAltReturn opened correct search page");
+});
+
+// Shift key has no effect for now, so skip it
+add_task(function* testShiftAltReturn() {
+ return;
+ /*
+ yield* prepareTest();
+
+ let url = expectedURL(searchBar.value);
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true, altKey: true });
+ yield newTabPromise;
+
+ is(gBrowser.tabs.length, preTabNo + 1, "Shift+Alt+Return key added new tab");
+ is(gBrowser.currentURI.spec, url, "testShiftAltReturn opened correct search page");
+ */
+});
+
+add_task(function* testLeftClick() {
+ yield* prepareTest();
+ simulateClick({ button: 0 }, searchButton);
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.tabs.length, preTabNo, "LeftClick did not open new tab");
+ is(gBrowser.currentURI.spec, expectedURL(searchBar.value), "testLeftClick opened correct search page");
+});
+
+add_task(function* testMiddleClick() {
+ yield* prepareTest();
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ simulateClick({ button: 1 }, searchButton);
+ });
+ is(gBrowser.tabs.length, preTabNo + 1, "MiddleClick added new tab");
+ is(gBrowser.currentURI.spec, expectedURL(searchBar.value), "testMiddleClick opened correct search page");
+});
+
+add_task(function* testShiftMiddleClick() {
+ yield* prepareTest();
+
+ let url = expectedURL(searchBar.value);
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url);
+ simulateClick({ button: 1, shiftKey: true }, searchButton);
+ let newTab = yield newTabPromise;
+
+ is(gBrowser.tabs.length, preTabNo + 1, "Shift+MiddleClick added new tab");
+ is(newTab.linkedBrowser.currentURI.spec, url, "testShiftMiddleClick opened correct search page");
+});
+
+add_task(function* testRightClick() {
+ preTabNo = gBrowser.tabs.length;
+ gBrowser.selectedBrowser.loadURI("about:blank");
+ yield new Promise(resolve => {
+ setTimeout(function() {
+ is(gBrowser.tabs.length, preTabNo, "RightClick did not open new tab");
+ is(gBrowser.currentURI.spec, "about:blank", "RightClick did nothing");
+ resolve();
+ }, 5000);
+ simulateClick({ button: 2 }, searchButton);
+ });
+ // The click in the searchbox focuses it, which opens the suggestion
+ // panel. Clean up after ourselves.
+ searchBar.textbox.popup.hidePopup();
+});
+
+add_task(function* testSearchHistory() {
+ var textbox = searchBar._textbox;
+ for (var i = 0; i < searchEntries.length; i++) {
+ let count = yield countEntries(textbox.getAttribute("autocompletesearchparam"), searchEntries[i]);
+ ok(count > 0, "form history entry '" + searchEntries[i] + "' should exist");
+ }
+});
+
+add_task(function* testAutocomplete() {
+ var popup = searchBar.textbox.popup;
+ let popupShownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ searchBar.textbox.showHistoryPopup();
+ yield popupShownPromise;
+ checkMenuEntries(searchEntries);
+});
+
+add_task(function* testClearHistory() {
+ let controller = searchBar.textbox.controllers.getControllerForCommand("cmd_clearhistory")
+ ok(controller.isCommandEnabled("cmd_clearhistory"), "Clear history command enabled");
+ controller.doCommand("cmd_clearhistory");
+ let count = yield countEntries();
+ ok(count == 0, "History cleared");
+});
+
+add_task(function* asyncCleanup() {
+ searchBar.value = "";
+ while (gBrowser.tabs.length != 1) {
+ gBrowser.removeTab(gBrowser.tabs[0], {animate: false});
+ }
+ gBrowser.selectedBrowser.loadURI("about:blank");
+ yield promiseRemoveEngine();
+});
diff --git a/browser/components/search/test/browser_483086.js b/browser/components/search/test/browser_483086.js
new file mode 100644
index 000000000..208add867
--- /dev/null
+++ b/browser/components/search/test/browser_483086.js
@@ -0,0 +1,49 @@
+/* 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/. */
+var gSS = Services.search;
+
+function test() {
+ waitForExplicitFinish();
+
+ function observer(aSubject, aTopic, aData) {
+ switch (aData) {
+ case "engine-added":
+ let engine = gSS.getEngineByName("483086a");
+ ok(engine, "Test engine 1 installed");
+ isnot(engine.searchForm, "foo://example.com",
+ "Invalid SearchForm URL dropped");
+ gSS.removeEngine(engine);
+ break;
+ case "engine-removed":
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ test2();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+ gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/483086-1.xml",
+ null, "data:image/x-icon;%00", false);
+}
+
+function test2() {
+ function observer(aSubject, aTopic, aData) {
+ switch (aData) {
+ case "engine-added":
+ let engine = gSS.getEngineByName("483086b");
+ ok(engine, "Test engine 2 installed");
+ is(engine.searchForm, "http://example.com", "SearchForm is correct");
+ gSS.removeEngine(engine);
+ break;
+ case "engine-removed":
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ finish();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+ gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/483086-2.xml",
+ null, "data:image/x-icon;%00", false);
+}
diff --git a/browser/components/search/test/browser_aboutSearchReset.js b/browser/components/search/test/browser_aboutSearchReset.js
new file mode 100644
index 000000000..64376d6da
--- /dev/null
+++ b/browser/components/search/test/browser_aboutSearchReset.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TELEMETRY_RESULT_ENUM = {
+ RESTORED_DEFAULT: 0,
+ KEPT_CURRENT: 1,
+ CHANGED_ENGINE: 2,
+ CLOSED_PAGE: 3,
+ OPENED_SETTINGS: 4
+};
+
+const kSearchStr = "a search";
+const kSearchPurpose = "searchbar";
+
+const kTestEngine = "testEngine.xml";
+
+function checkTelemetryRecords(expectedValue) {
+ let histogram = Services.telemetry.getHistogramById("SEARCH_RESET_RESULT");
+ let snapshot = histogram.snapshot();
+ // The probe is declared with 5 values, but we get 6 back from .counts
+ let expectedCounts = [0, 0, 0, 0, 0, 0];
+ if (expectedValue != null) {
+ expectedCounts[expectedValue] = 1;
+ }
+ Assert.deepEqual(snapshot.counts, expectedCounts,
+ "histogram has expected content");
+ histogram.clear();
+}
+
+function promiseStoppedLoad(expectedURL) {
+ return new Promise(resolve => {
+ let browser = gBrowser.selectedBrowser;
+ let original = browser.loadURIWithFlags;
+ browser.loadURIWithFlags = function(URI) {
+ if (URI == expectedURL) {
+ browser.loadURIWithFlags = original;
+ ok(true, "loaded expected url: " + URI);
+ resolve();
+ return;
+ }
+
+ original.apply(browser, arguments);
+ };
+ });
+}
+
+var gTests = [
+
+{
+ desc: "Test the 'Keep Current Settings' button.",
+ run: function* () {
+ let engine = yield promiseNewEngine(kTestEngine, {setAsCurrent: true});
+
+ let expectedURL = engine.
+ getSubmission(kSearchStr, null, kSearchPurpose).
+ uri.spec;
+
+ let rawEngine = engine.wrappedJSObject;
+ let initialHash = rawEngine.getAttr("loadPathHash");
+ rawEngine.setAttr("loadPathHash", "broken");
+
+ let loadPromise = promiseStoppedLoad(expectedURL);
+ gBrowser.contentDocument.getElementById("searchResetKeepCurrent").click();
+ yield loadPromise;
+
+ is(engine, Services.search.currentEngine,
+ "the custom engine is still default");
+ is(rawEngine.getAttr("loadPathHash"), initialHash,
+ "the loadPathHash has been fixed");
+
+ checkTelemetryRecords(TELEMETRY_RESULT_ENUM.KEPT_CURRENT);
+ }
+},
+
+{
+ desc: "Test the 'Restore Search Defaults' button.",
+ run: function* () {
+ let currentEngine = Services.search.currentEngine;
+ let originalEngine = Services.search.originalDefaultEngine;
+ let doc = gBrowser.contentDocument;
+ let defaultEngineSpan = doc.getElementById("defaultEngine");
+ is(defaultEngineSpan.textContent, originalEngine.name,
+ "the name of the original default engine is displayed");
+
+ let expectedURL = originalEngine.
+ getSubmission(kSearchStr, null, kSearchPurpose).
+ uri.spec;
+ let loadPromise = promiseStoppedLoad(expectedURL);
+ let button = doc.getElementById("searchResetChangeEngine");
+ is(doc.activeElement, button,
+ "the 'Change Search Engine' button is focused");
+ button.click();
+ yield loadPromise;
+
+ is(originalEngine, Services.search.currentEngine,
+ "the default engine is back to the original one");
+
+ checkTelemetryRecords(TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT);
+ Services.search.currentEngine = currentEngine;
+ }
+},
+
+{
+ desc: "Click the settings link.",
+ run: function* () {
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser,
+ false,
+ "about:preferences#search")
+ gBrowser.contentDocument.getElementById("linkSettingsPage").click();
+ yield loadPromise;
+
+ checkTelemetryRecords(TELEMETRY_RESULT_ENUM.OPENED_SETTINGS);
+ }
+},
+
+{
+ desc: "Load another page without clicking any of the buttons.",
+ run: function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "about:mozilla");
+
+ checkTelemetryRecords(TELEMETRY_RESULT_ENUM.CLOSED_PAGE);
+ }
+},
+
+];
+
+function test()
+{
+ waitForExplicitFinish();
+ Task.spawn(function* () {
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ checkTelemetryRecords();
+
+ for (let test of gTests) {
+ info(test.desc);
+
+ // Create a tab to run the test.
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+
+ // Start loading about:searchreset and wait for it to complete.
+ let url = "about:searchreset?data=" + encodeURIComponent(kSearchStr) +
+ "&purpose=" + kSearchPurpose;
+ yield promiseTabLoadEvent(tab, url);
+
+ info("Running test");
+ yield test.run();
+
+ info("Cleanup");
+ gBrowser.removeCurrentTab();
+ }
+
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ }).then(finish, ex => {
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
diff --git a/browser/components/search/test/browser_abouthome_behavior.js b/browser/components/search/test/browser_abouthome_behavior.js
new file mode 100644
index 000000000..3291b41f4
--- /dev/null
+++ b/browser/components/search/test/browser_abouthome_behavior.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test home page search for all plugin URLs
+ */
+
+"use strict";
+
+function test() {
+ // Bug 992270: Ignore uncaught about:home exceptions (related to snippets from IndexedDB)
+ ignoreAllUncaughtExceptions(true);
+
+ let previouslySelectedEngine = Services.search.currentEngine;
+
+ function replaceUrl(base) {
+ return base;
+ }
+
+ let gMutationObserver = null;
+
+ function verify_about_home_search(engine_name) {
+ let engine = Services.search.getEngineByName(engine_name);
+ ok(engine, engine_name + " is installed");
+
+ Services.search.currentEngine = engine;
+
+ // load about:home, but remove the listener first so it doesn't
+ // get in the way
+ gBrowser.removeProgressListener(listener);
+ gBrowser.loadURI("about:home");
+ info("Waiting for about:home load");
+ tab.linkedBrowser.addEventListener("load", function load(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ info("skipping spurious load event");
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", load, true);
+
+ // Observe page setup
+ let doc = gBrowser.contentDocument;
+ gMutationObserver = new MutationObserver(function (mutations) {
+ for (let mutation of mutations) {
+ if (mutation.attributeName == "searchEngineName") {
+ // Re-add the listener, and perform a search
+ gBrowser.addProgressListener(listener);
+ gMutationObserver.disconnect()
+ gMutationObserver = null;
+ executeSoon(function() {
+ doc.getElementById("searchText").value = "foo";
+ doc.getElementById("searchSubmit").click();
+ });
+ }
+ }
+ });
+ gMutationObserver.observe(doc.documentElement, { attributes: true });
+ }, true);
+ }
+ waitForExplicitFinish();
+
+ let gCurrTest;
+ let gTests = [
+ {
+ name: "Search with Bing from about:home",
+ searchURL: replaceUrl("http://www.bing.com/search?q=foo&pc=MOZI&form=MOZSPG"),
+ run: function () {
+ verify_about_home_search("Bing");
+ }
+ },
+ {
+ name: "Search with Yahoo from about:home",
+ searchURL: replaceUrl("https://search.yahoo.com/search?p=foo&ei=UTF-8&fr=moz35"),
+ run: function () {
+ verify_about_home_search("Yahoo");
+ }
+ },
+ {
+ name: "Search with Google from about:home",
+ searchURL: replaceUrl("https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8"),
+ run: function () {
+ verify_about_home_search("Google");
+ }
+ },
+ {
+ name: "Search with Amazon.com from about:home",
+ searchURL: replaceUrl("https://www.amazon.com/exec/obidos/external-search/?field-keywords=foo&mode=blended&tag=mozilla-20&sourceid=Mozilla-search"),
+ run: function () {
+ verify_about_home_search("Amazon.com");
+ }
+ }
+ ];
+
+ function nextTest() {
+ if (gTests.length) {
+ gCurrTest = gTests.shift();
+ info("Running : " + gCurrTest.name);
+ executeSoon(gCurrTest.run);
+ } else {
+ // Make sure we listen again for uncaught exceptions in the next test or cleanup.
+ executeSoon(finish);
+ }
+ }
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let listener = {
+ onStateChange: function onStateChange(webProgress, req, flags, status) {
+ info("onStateChange");
+ // Only care about top-level document starts
+ let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_START;
+ if (!(flags & docStart) || !webProgress.isTopLevel)
+ return;
+
+ if (req.originalURI.spec == "about:blank")
+ return;
+
+ info("received document start");
+
+ ok(req instanceof Ci.nsIChannel, "req is a channel");
+ is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
+ info("Actual URI: " + req.URI.spec);
+
+ req.cancel(Components.results.NS_ERROR_FAILURE);
+
+ executeSoon(nextTest);
+ }
+ }
+
+ registerCleanupFunction(function () {
+ Services.search.currentEngine = previouslySelectedEngine;
+ gBrowser.removeProgressListener(listener);
+ gBrowser.removeTab(tab);
+ if (gMutationObserver)
+ gMutationObserver.disconnect();
+ });
+
+ tab.linkedBrowser.addEventListener("load", function load() {
+ tab.linkedBrowser.removeEventListener("load", load, true);
+ gBrowser.addProgressListener(listener);
+ nextTest();
+ }, true);
+}
diff --git a/browser/components/search/test/browser_addEngine.js b/browser/components/search/test/browser_addEngine.js
new file mode 100644
index 000000000..b971ea5f7
--- /dev/null
+++ b/browser/components/search/test/browser_addEngine.js
@@ -0,0 +1,105 @@
+/* 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/. */
+var gSS = Services.search;
+
+function observer(aSubject, aTopic, aData) {
+ if (!gCurrentTest) {
+ info("Observer called with no test active");
+ return;
+ }
+
+ let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+ info("Observer: " + aData + " for " + engine.name);
+ let method;
+ switch (aData) {
+ case "engine-added":
+ if (gCurrentTest.added)
+ method = "added"
+ break;
+ case "engine-current":
+ if (gCurrentTest.current)
+ method = "current";
+ break;
+ case "engine-removed":
+ if (gCurrentTest.removed)
+ method = "removed";
+ break;
+ }
+
+ if (method)
+ gCurrentTest[method](engine);
+}
+
+function checkEngine(checkObj, engineObj) {
+ info("Checking engine");
+ for (var prop in checkObj)
+ is(checkObj[prop], engineObj[prop], prop + " is correct");
+}
+
+var gTests = [
+ {
+ name: "opensearch install",
+ engine: {
+ name: "Foo",
+ alias: null,
+ description: "Foo Search",
+ searchForm: "http://mochi.test:8888/browser/browser/components/search/test/"
+ },
+ run: function () {
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+
+ gSS.addEngine("http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml",
+ null, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC",
+ false);
+ },
+ added: function (engine) {
+ ok(engine, "engine was added.");
+
+ checkEngine(this.engine, engine);
+
+ let engineFromSS = gSS.getEngineByName(this.engine.name);
+ is(engine, engineFromSS, "engine is obtainable via getEngineByName");
+
+ let aEngine = gSS.getEngineByAlias("fooalias");
+ ok(!aEngine, "Alias was not parsed from engine description");
+
+ gSS.currentEngine = engine;
+ },
+ current: function (engine) {
+ let currentEngine = gSS.currentEngine;
+ is(engine, currentEngine, "engine is current");
+ is(engine.name, this.engine.name, "current engine was changed successfully");
+
+ gSS.removeEngine(engine);
+ },
+ removed: function (engine) {
+ // Remove the observer before calling the currentEngine getter,
+ // as that getter will set the currentEngine to the original default
+ // which will trigger a notification causing the test to loop over all
+ // engines.
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+
+ let currentEngine = gSS.currentEngine;
+ ok(currentEngine, "An engine is present.");
+ isnot(currentEngine.name, this.engine.name, "Current engine reset after removal");
+
+ nextTest();
+ }
+ }
+];
+
+var gCurrentTest = null;
+function nextTest() {
+ if (gTests.length) {
+ gCurrentTest = gTests.shift();
+ info("Running " + gCurrentTest.name);
+ gCurrentTest.run();
+ } else
+ executeSoon(finish);
+}
+
+function test() {
+ waitForExplicitFinish();
+ nextTest();
+}
diff --git a/browser/components/search/test/browser_amazon.js b/browser/components/search/test/browser_amazon.js
new file mode 100644
index 000000000..965a3dcf8
--- /dev/null
+++ b/browser/components/search/test/browser_amazon.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Amazon search plugin URLs
+ */
+
+"use strict";
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+function test() {
+ let engine = Services.search.getEngineByName("Amazon.com");
+ ok(engine, "Amazon.com");
+
+ let base = "https://www.amazon.com/exec/obidos/external-search/?field-keywords=foo&ie=UTF-8&mode=blended&tag=mozilla-20&sourceid=Mozilla-search";
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base, "Check search URL for 'foo'");
+
+ // Check search suggestion URL.
+ url = engine.getSubmission("foo", "application/x-suggestions+json").uri.spec;
+ is(url, "https://completion.amazon.com/search/complete?q=foo&search-alias=aps&mkt=1", "Check search suggestion URL for 'foo'");
+
+ // Check all other engine properties.
+ const EXPECTED_ENGINE = {
+ name: "Amazon.com",
+ alias: null,
+ description: "Amazon.com Search",
+ searchForm: "https://www.amazon.com/exec/obidos/external-search/?field-keywords=&ie=UTF-8&mode=blended&tag=mozilla-20&sourceid=Mozilla-search",
+ hidden: false,
+ wrappedJSObject: {
+ queryCharset: "UTF-8",
+ "_iconURL": "data:image/x-icon;base64,AAABAAIAEBAAAAAAAAC0AQAAJgAAACAgAAAAAAAA6QIAANoBAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAAF7SURBVDjLlZPLasJAFIaFRF+iVV+h6hO0GF+gVB9AaHwDt64qCG03tQgtdCFIuyhUelmGli66MXThSt24kNiFBUlAYi6ezjnNxSuawB/ITP7v/HNmJgQAEaZzpgHs/gwcTyTEXuXl2U6nA8ViEbK5HKler28CVRAwnB9ptVrAh8MrQuCaZ4iA8fzIqSgCxwzpTIaSuN/RWGwdYLwCUBQFZFkGSZLgqdmEE7YEN8VOAKyaSKUW4nNBAFmnYiKZpDRX1WqwBBzP089n5f/NEQsFL4WqqtsBWJlzDAJr5PwSMM1awEzzdxIbGI3Hvc6jCZeVFgRQRwpY7Qcw3ktgfpR8wLRxCPaot/X4GS95MppfF6DX9n2A3f+kAZycaT8bAZjU6r6B/duD6d3BYg9wQq/tkYzHY1blEiz5lmQyGc95mrO6r2CxgpjCBXgNsJVviolpXJiraeOIjJRE10juUa4sR8V+mO17VvmGqtuOcdNlwut8zTQJcJ0njifyB2bgTdKh6w4BAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACsElEQVRYw71XQWsTURBe2LQgeNKLB+tVemt6txcteNSD/QGC6VEIGDx5s+eKPQqFgJhLNdFLBWMP7cU0oSAWjB70koC9WHbVQ5SO8+XtS14mr7svyaYDH9m87Jv55puZt1nPi4yIzjMeMj7T9OwjI88455nGC1cZX+nsDESumJmPFDwIAqrX6z00Gg1qt9vjkJgFgUeuO16Vy3RjeZkyMzM9+MY1fsM9I9h9zyV7ZAznZrA4FAoFVwJ1z+WuOysrg1lnMolkHJX4k0igzI5sARYWF7vEZEk0rvO6iyUSuJfLJUqM7zYSqRDIra4OOUZPmNZsNrsl8UVTpkJAjh1GzmaSpJ8mAWmYeZB5urHRhW5SNOfUCCDo47W1bvPZsp2qAhipy3Nz1kaLG8dUCEBqM5AvpgElqFar01NgIZsdco7Zb7VasU2YigIYL5tjqCL7Q5YkFQXKlcqQ7DbHthIALk/IWAKor82xPIhshxWABCYioDMz51sexcVi0XoG4DPLIyvJjkTArK3scDQnRvO0MdTrUHGiKZCP4tNgO6BAEI08EQH9Z2Qow0hyPypJGIa9p6JWKCn4SA8jSKmJIDgyRvPJkcRxjfUwNGr/i8+Mo32iHzWiThBD4NM60bet9P77/ubA728RlTjMiwiH6zEEfvIrwdZFtQmMJ7W/ofIDBZD5m3mVZGwJcOP2kmILIlCkE45HoPWurwCSg0+UQRD4ZyXxId+T7gQb9+4q9sioY5ltrOG3L5vqXiiJffDx/aUi83ZJ7jr2ohcEu8Hh6/m+I7OWGiVxbWKHsz+O3vSOakqFQdsFgQeJUiKD7Wv9YKXBgCeSUC3v2kM5EJhlHDh3NcgcPlG1BXZu98sDmTuBa4fsMnz9fniJUaGzs+eMC540XuR0aDO2L8Y3qPyMcdOM+R/8XcqRA3qp9gAAAABJRU5ErkJggg==",
+ _urls : [
+ {
+ type: "application/x-suggestions+json",
+ method: "GET",
+ template: "https://completion.amazon.com/search/complete?q={searchTerms}&search-alias=aps&mkt=1",
+ params: "",
+ },
+ {
+ type: "text/html",
+ method: "GET",
+ template: "https://www.amazon.com/exec/obidos/external-search/",
+ params: [
+ {
+ name: "field-keywords",
+ value: "{searchTerms}",
+ purpose: undefined,
+ },
+ {
+ name: "ie",
+ value: "{inputEncoding}",
+ purpose: undefined,
+ },
+ {
+ name: "mode",
+ value: "blended",
+ purpose: undefined,
+ },
+ {
+ name: "tag",
+ value: "mozilla-20",
+ purpose: undefined,
+ },
+ {
+ name: "sourceid",
+ value: "Mozilla-search",
+ purpose: undefined,
+ },
+ ],
+ mozparams: {},
+ },
+ ],
+ },
+ };
+
+ isSubObjectOf(EXPECTED_ENGINE, engine, "Amazon");
+}
diff --git a/browser/components/search/test/browser_amazon_behavior.js b/browser/components/search/test/browser_amazon_behavior.js
new file mode 100644
index 000000000..22d16581a
--- /dev/null
+++ b/browser/components/search/test/browser_amazon_behavior.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Amazon search plugin URLs
+ */
+
+"use strict";
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+
+function test() {
+ let engine = Services.search.getEngineByName("Amazon.com");
+ ok(engine, "Amazon is installed");
+
+ let previouslySelectedEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+ engine.alias = "a";
+
+ let base = "https://www.amazon.com/exec/obidos/external-search/?field-keywords=foo&ie=UTF-8&mode=blended&tag=mozilla-20&sourceid=Mozilla-search";
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base, "Check search URL for 'foo'");
+
+ waitForExplicitFinish();
+
+ var gCurrTest;
+ var gTests = [
+ {
+ name: "context menu search",
+ searchURL: base,
+ run: function () {
+ // Simulate a contextmenu search
+ // FIXME: This is a bit "low-level"...
+ BrowserSearch.loadSearch("foo", false, "contextmenu");
+ }
+ },
+ {
+ name: "keyword search",
+ searchURL: base,
+ run: function () {
+ gURLBar.value = "? foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "keyword search",
+ searchURL: base,
+ run: function () {
+ gURLBar.value = "a foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "search bar search",
+ searchURL: base,
+ run: function () {
+ let sb = BrowserSearch.searchBar;
+ sb.focus();
+ sb.value = "foo";
+ registerCleanupFunction(function () {
+ sb.value = "";
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "new tab search",
+ searchURL: base,
+ run: function () {
+ function doSearch(doc) {
+ // Re-add the listener, and perform a search
+ gBrowser.addProgressListener(listener);
+ doc.getElementById("newtab-search-text").value = "foo";
+ doc.getElementById("newtab-search-submit").click();
+ }
+
+ // load about:newtab, but remove the listener first so it doesn't
+ // get in the way
+ gBrowser.removeProgressListener(listener);
+ gBrowser.loadURI("about:newtab");
+ info("Waiting for about:newtab load");
+ tab.linkedBrowser.addEventListener("load", function load(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ info("skipping spurious load event");
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", load, true);
+
+ // Observe page setup
+ let win = gBrowser.contentWindow;
+ if (win.gSearch.currentEngineName ==
+ Services.search.currentEngine.name) {
+ doSearch(win.document);
+ }
+ else {
+ info("Waiting for newtab search init");
+ win.addEventListener("ContentSearchService", function done(event) {
+ info("Got newtab search event " + event.detail.type);
+ if (event.detail.type == "State") {
+ win.removeEventListener("ContentSearchService", done);
+ // Let gSearch respond to the event before continuing.
+ executeSoon(() => doSearch(win.document));
+ }
+ });
+ }
+ }, true);
+ }
+ }
+ ];
+
+ function nextTest() {
+ if (gTests.length) {
+ gCurrTest = gTests.shift();
+ info("Running : " + gCurrTest.name);
+ executeSoon(gCurrTest.run);
+ } else {
+ finish();
+ }
+ }
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let listener = {
+ onStateChange: function onStateChange(webProgress, req, flags, status) {
+ info("onStateChange");
+ // Only care about top-level document starts
+ let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_START;
+ if (!(flags & docStart) || !webProgress.isTopLevel)
+ return;
+
+ if (req.originalURI.spec == "about:blank")
+ return;
+
+ info("received document start");
+
+ ok(req instanceof Ci.nsIChannel, "req is a channel");
+ is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
+ info("Actual URI: " + req.URI.spec);
+
+ req.cancel(Components.results.NS_ERROR_FAILURE);
+
+ executeSoon(nextTest);
+ }
+ }
+
+ registerCleanupFunction(function () {
+ engine.alias = undefined;
+ gBrowser.removeProgressListener(listener);
+ gBrowser.removeTab(tab);
+ Services.search.currentEngine = previouslySelectedEngine;
+ });
+
+ tab.linkedBrowser.addEventListener("load", function load() {
+ tab.linkedBrowser.removeEventListener("load", load, true);
+ gBrowser.addProgressListener(listener);
+ nextTest();
+ }, true);
+}
diff --git a/browser/components/search/test/browser_bing.js b/browser/components/search/test/browser_bing.js
new file mode 100644
index 000000000..3a41ae0ac
--- /dev/null
+++ b/browser/components/search/test/browser_bing.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Bing search plugin URLs
+ */
+
+"use strict";
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+function test() {
+ let engine = Services.search.getEngineByName("Bing");
+ ok(engine, "Bing");
+
+ let base = "https://www.bing.com/search?q=foo&pc=MOZI";
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base + "&form=MOZSBR", "Check search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "contextmenu").uri.spec;
+ is(url, base + "&form=MOZCON", "Check context menu search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "keyword").uri.spec;
+ is(url, base + "&form=MOZLBR", "Check keyword search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "searchbar").uri.spec;
+ is(url, base + "&form=MOZSBR", "Check search bar search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "homepage").uri.spec;
+ is(url, base + "&form=MOZSPG", "Check homepage search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "newtab").uri.spec;
+ is(url, base + "&form=MOZTSB", "Check newtab search URL for 'foo'");
+
+ // Check search suggestion URL.
+ url = engine.getSubmission("foo", "application/x-suggestions+json").uri.spec;
+ is(url, "https://www.bing.com/osjson.aspx?query=foo&form=OSDJAS&language=" + getLocale(), "Check search suggestion URL for 'foo'");
+
+ // Check all other engine properties.
+ const EXPECTED_ENGINE = {
+ name: "Bing",
+ alias: null,
+ description: "Bing. Search by Microsoft.",
+ searchForm: "https://www.bing.com/search?q=&pc=MOZI&form=MOZSBR",
+ hidden: false,
+ wrappedJSObject: {
+ queryCharset: "UTF-8",
+ "_iconURL": "data:image/x-icon;base64,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",
+ _urls : [
+ {
+ type: "application/x-suggestions+json",
+ method: "GET",
+ template: "https://www.bing.com/osjson.aspx",
+ params: [
+ {
+ name: "query",
+ value: "{searchTerms}",
+ purpose: undefined,
+ },
+ {
+ name: "form",
+ value: "OSDJAS",
+ purpose: undefined,
+ },
+ {
+ name: "language",
+ value: "{moz:locale}",
+ purpose: undefined,
+ },
+ ],
+ },
+ {
+ type: "text/html",
+ method: "GET",
+ template: "https://www.bing.com/search",
+ params: [
+ {
+ name: "q",
+ value: "{searchTerms}",
+ purpose: undefined,
+ },
+ {
+ name: "pc",
+ value: "MOZI",
+ purpose: undefined,
+ },
+ {
+ name: "form",
+ value: "MOZCON",
+ purpose: "contextmenu",
+ },
+ {
+ name: "form",
+ value: "MOZSBR",
+ purpose: "searchbar",
+ },
+ {
+ name: "form",
+ value: "MOZSPG",
+ purpose: "homepage",
+ },
+ {
+ name: "form",
+ value: "MOZLBR",
+ purpose:"keyword",
+ },
+ {
+ name: "form",
+ value: "MOZTSB",
+ purpose: "newtab",
+ },
+ ],
+ mozparams: {},
+ },
+ ],
+ },
+ };
+
+ isSubObjectOf(EXPECTED_ENGINE, engine, "Bing");
+}
diff --git a/browser/components/search/test/browser_bing_behavior.js b/browser/components/search/test/browser_bing_behavior.js
new file mode 100644
index 000000000..bc9b187ec
--- /dev/null
+++ b/browser/components/search/test/browser_bing_behavior.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Bing search plugin URLs
+ */
+
+"use strict";
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+
+function test() {
+ let engine = Services.search.getEngineByName("Bing");
+ ok(engine, "Bing is installed");
+
+ let previouslySelectedEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+ engine.alias = "b";
+
+ let base = "https://www.bing.com/search?q=foo&pc=MOZI";
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base + "&form=MOZSBR", "Check search URL for 'foo'");
+
+ waitForExplicitFinish();
+
+ var gCurrTest;
+ var gTests = [
+ {
+ name: "context menu search",
+ searchURL: base + "&form=MOZCON",
+ run: function () {
+ // Simulate a contextmenu search
+ // FIXME: This is a bit "low-level"...
+ BrowserSearch.loadSearch("foo", false, "contextmenu");
+ }
+ },
+ {
+ name: "keyword search",
+ searchURL: base + "&form=MOZLBR",
+ run: function () {
+ gURLBar.value = "? foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "keyword search with alias",
+ searchURL: base + "&form=MOZLBR",
+ run: function () {
+ gURLBar.value = "b foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "search bar search",
+ searchURL: base + "&form=MOZSBR",
+ run: function () {
+ let sb = BrowserSearch.searchBar;
+ sb.focus();
+ sb.value = "foo";
+ registerCleanupFunction(function () {
+ sb.value = "";
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "new tab search",
+ searchURL: base + "&form=MOZTSB",
+ run: function () {
+ function doSearch(doc) {
+ // Re-add the listener, and perform a search
+ gBrowser.addProgressListener(listener);
+ doc.getElementById("newtab-search-text").value = "foo";
+ doc.getElementById("newtab-search-submit").click();
+ }
+
+ // load about:newtab, but remove the listener first so it doesn't
+ // get in the way
+ gBrowser.removeProgressListener(listener);
+ gBrowser.loadURI("about:newtab");
+ info("Waiting for about:newtab load");
+ tab.linkedBrowser.addEventListener("load", function load(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ info("skipping spurious load event");
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", load, true);
+
+ // Observe page setup
+ let win = gBrowser.contentWindow;
+ if (win.gSearch.currentEngineName ==
+ Services.search.currentEngine.name) {
+ doSearch(win.document);
+ }
+ else {
+ info("Waiting for newtab search init");
+ win.addEventListener("ContentSearchService", function done(event) {
+ info("Got newtab search event " + event.detail.type);
+ if (event.detail.type == "State") {
+ win.removeEventListener("ContentSearchService", done);
+ // Let gSearch respond to the event before continuing.
+ executeSoon(() => doSearch(win.document));
+ }
+ });
+ }
+ }, true);
+ }
+ }
+ ];
+
+ function nextTest() {
+ if (gTests.length) {
+ gCurrTest = gTests.shift();
+ info("Running : " + gCurrTest.name);
+ executeSoon(gCurrTest.run);
+ } else {
+ finish();
+ }
+ }
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let listener = {
+ onStateChange: function onStateChange(webProgress, req, flags, status) {
+ info("onStateChange");
+ // Only care about top-level document starts
+ let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_START;
+ if (!(flags & docStart) || !webProgress.isTopLevel)
+ return;
+
+ if (req.originalURI.spec == "about:blank")
+ return;
+
+ info("received document start");
+
+ ok(req instanceof Ci.nsIChannel, "req is a channel");
+ is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
+ info("Actual URI: " + req.URI.spec);
+
+ req.cancel(Components.results.NS_ERROR_FAILURE);
+
+ executeSoon(nextTest);
+ }
+ }
+
+ registerCleanupFunction(function () {
+ engine.alias = undefined;
+ gBrowser.removeProgressListener(listener);
+ gBrowser.removeTab(tab);
+ Services.search.currentEngine = previouslySelectedEngine;
+ });
+
+ tab.linkedBrowser.addEventListener("load", function load() {
+ tab.linkedBrowser.removeEventListener("load", load, true);
+ gBrowser.addProgressListener(listener);
+ nextTest();
+ }, true);
+}
diff --git a/browser/components/search/test/browser_contextSearchTabPosition.js b/browser/components/search/test/browser_contextSearchTabPosition.js
new file mode 100644
index 000000000..21a8c1130
--- /dev/null
+++ b/browser/components/search/test/browser_contextSearchTabPosition.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+add_task(function* test() {
+ yield SpecialPowers.pushPrefEnv({set: [["toolkit.telemetry.enabled", true]]});
+ let engine = yield promiseNewEngine("testEngine.xml");
+ let histogramKey = "other-" + engine.name + ".contextmenu";
+ let numSearchesBefore = 0;
+
+ try {
+ let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
+ if (histogramKey in hs) {
+ numSearchesBefore = hs[histogramKey].sum;
+ }
+ } catch (ex) {
+ // No searches performed yet, not a problem, |numSearchesBefore| is 0.
+ }
+
+ let tabs = [];
+ let tabsLoadedDeferred = new Deferred();
+
+ function tabAdded(event) {
+ let tab = event.target;
+ tabs.push(tab);
+
+ // We wait for the blank tab and the two context searches tabs to open.
+ if (tabs.length == 3) {
+ tabsLoadedDeferred.resolve();
+ }
+ }
+
+ let container = gBrowser.tabContainer;
+ container.addEventListener("TabOpen", tabAdded, false);
+
+ gBrowser.addTab("about:blank");
+ BrowserSearch.loadSearchFromContext("mozilla");
+ BrowserSearch.loadSearchFromContext("firefox");
+
+ // Wait for all the tabs to open.
+ yield tabsLoadedDeferred.promise;
+
+ is(tabs[0], gBrowser.tabs[3], "blank tab has been pushed to the end");
+ is(tabs[1], gBrowser.tabs[1], "first search tab opens next to the current tab");
+ is(tabs[2], gBrowser.tabs[2], "second search tab opens next to the first search tab");
+
+ container.removeEventListener("TabOpen", tabAdded, false);
+ tabs.forEach(gBrowser.removeTab, gBrowser);
+
+ // Make sure that the context searches are correctly recorded.
+ let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
+ Assert.ok(histogramKey in hs, "The histogram must contain the correct key");
+ Assert.equal(hs[histogramKey].sum, numSearchesBefore + 2,
+ "The histogram must contain the correct search count");
+});
+
+function Deferred() {
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+}
diff --git a/browser/components/search/test/browser_contextmenu.js b/browser/components/search/test/browser_contextmenu.js
new file mode 100644
index 000000000..c485242b4
--- /dev/null
+++ b/browser/components/search/test/browser_contextmenu.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * * http://creativecommons.org/publicdomain/zero/1.0/ */
+/*
+ * Test searching for the selected text using the context menu
+ */
+
+add_task(function* () {
+ const ss = Services.search;
+ const ENGINE_NAME = "Foo";
+ var contextMenu;
+
+ // We want select events to be fired.
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({"set": [["dom.select_events.enabled", true]]}, resolve));
+
+ let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ let originalValue = envService.get("XPCSHELL_TEST_PROFILE_DIR");
+ envService.set("XPCSHELL_TEST_PROFILE_DIR", "1");
+
+ let url = "chrome://mochitests/content/browser/browser/components/search/test/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ let originalSubstitution = resProt.getSubstitution("search-plugins");
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ let searchDonePromise;
+ yield new Promise(resolve => {
+ function observer(aSub, aTopic, aData) {
+ switch (aData) {
+ case "engine-added":
+ var engine = ss.getEngineByName(ENGINE_NAME);
+ ok(engine, "Engine was added.");
+ ss.currentEngine = engine;
+ envService.set("XPCSHELL_TEST_PROFILE_DIR", originalValue);
+ resProt.setSubstitution("search-plugins", originalSubstitution);
+ break;
+ case "engine-current":
+ is(ss.currentEngine.name, ENGINE_NAME, "currentEngine set");
+ resolve();
+ break;
+ case "engine-removed":
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ if (searchDonePromise) {
+ searchDonePromise();
+ }
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+ ss.addEngine("resource://search-plugins/testEngine_mozsearch.xml",
+ null, "data:image/x-icon,%00", false);
+ });
+
+ contextMenu = document.getElementById("contentAreaContextMenu");
+ ok(contextMenu, "Got context menu XUL");
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/plain;charset=utf8,test%20search");
+
+ yield ContentTask.spawn(tab.linkedBrowser, "", function*() {
+ return new Promise(resolve => {
+ content.document.addEventListener("selectionchange", function selectionChanged() {
+ content.document.removeEventListener("selectionchange", selectionChanged);
+ resolve();
+ });
+ content.document.getSelection().selectAllChildren(content.document.body);
+ });
+ });
+
+ var eventDetails = { type: "contextmenu", button: 2 };
+
+ let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter("body", eventDetails, gBrowser.selectedBrowser);
+ yield popupPromise;
+
+ info("checkContextMenu");
+ var searchItem = contextMenu.getElementsByAttribute("id", "context-searchselect")[0];
+ ok(searchItem, "Got search context menu item");
+ is(searchItem.label, 'Search ' + ENGINE_NAME + ' for \u201ctest search\u201d', "Check context menu label");
+ is(searchItem.disabled, false, "Check that search context menu item is enabled");
+
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ searchItem.click();
+ });
+
+ is(gBrowser.currentURI.spec,
+ "http://mochi.test:8888/browser/browser/components/search/test/?test=test+search&ie=utf-8&channel=contextsearch",
+ "Checking context menu search URL");
+
+ contextMenu.hidePopup();
+
+ // Remove the tab opened by the search
+ gBrowser.removeCurrentTab();
+
+ yield new Promise(resolve => {
+ searchDonePromise = resolve;
+ ss.removeEngine(ss.currentEngine);
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/search/test/browser_google.js b/browser/components/search/test/browser_google.js
new file mode 100644
index 000000000..2b0cabea7
--- /dev/null
+++ b/browser/components/search/test/browser_google.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Google search plugin URLs
+ */
+
+"use strict";
+
+function test() {
+ let engine = Services.search.getEngineByName("Google");
+ ok(engine, "Google");
+
+ let base = "https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&client=firefox-b";
+ let keywordBase = base + "-ab";
+
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base, "Check search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "contextmenu").uri.spec;
+ is(url, base, "Check context menu search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "keyword").uri.spec;
+ is(url, keywordBase, "Check keyword search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "searchbar").uri.spec;
+ is(url, base, "Check search bar search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "homepage").uri.spec;
+ is(url, base, "Check homepage search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "newtab").uri.spec;
+ is(url, base, "Check newtab search URL for 'foo'");
+
+ // Check search suggestion URL.
+ url = engine.getSubmission("foo", "application/x-suggestions+json").uri.spec;
+ is(url, "https://www.google.com/complete/search?client=firefox&q=foo", "Check search suggestion URL for 'foo'");
+
+ // Check result parsing and alternate domains.
+ let alternateBase = base.replace("www.google.com", "www.google.fr");
+ is(Services.search.parseSubmissionURL(base).terms, "foo",
+ "Check result parsing");
+ is(Services.search.parseSubmissionURL(alternateBase).terms, "foo",
+ "Check alternate domain");
+
+ // Check all other engine properties.
+ const EXPECTED_ENGINE = {
+ name: "Google",
+ alias: null,
+ description: "Google Search",
+ searchForm: "https://www.google.com/search?q=&ie=utf-8&oe=utf-8&client=firefox-b",
+ hidden: false,
+ wrappedJSObject: {
+ queryCharset: "UTF-8",
+ "_iconURL": "data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///zD9/f2W/f392P39/fn9/f35/f391/39/ZT+/v4uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7+Cf39/Zn///////////////////////////////////////////39/ZX///8IAAAAAAAAAAAAAAAA/v7+Cf39/cH/////+v35/7TZp/92ul3/WKs6/1iqOv9yuFn/rNWd//j79v///////f39v////wgAAAAAAAAAAP39/Zn/////7PXp/3G3WP9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP+Or1j//vDo///////9/f2VAAAAAP///zD/////+vz5/3G3V/9TqDT/WKo6/6LQkf/U6cz/1urO/6rUm/+Zo0r/8IZB//adZ////v7///////7+/i79/f2Y/////4nWzf9Lqkj/Vqo4/9Xqzv///////////////////////ebY//SHRv/0hUL//NjD///////9/f2U/f392v////8sxPH/Ebzt/43RsP/////////////////////////////////4roL/9IVC//i1jf///////f391/39/fr/////Cr37/wW8+/+16/7/////////////////9IVC//SFQv/0hUL/9IVC//SFQv/3pnX///////39/fn9/f36/////wu++/8FvPv/tuz+//////////////////SFQv/0hUL/9IVC//SFQv/0hUL/96p7///////9/f35/f392/////81yfz/CrL5/2uk9v///////////////////////////////////////////////////////f392P39/Zn/////ks/7/zdS7P84Rur/0NT6///////////////////////9/f////////////////////////39/Zb+/v4y//////n5/v9WYu3/NUPq/ztJ6/+VnPT/z9L6/9HU+v+WnfT/Ul7t/+Hj/P////////////////////8wAAAAAP39/Z3/////6Or9/1hj7v81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v9sdvD////////////9/f2YAAAAAAAAAAD///8K/f39w//////5+f7/paz2/11p7v88Suv/Okfq/1pm7v+iqfX/+fn+///////9/f3B/v7+CQAAAAAAAAAAAAAAAP///wr9/f2d///////////////////////////////////////////9/f2Z/v7+CQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/jL9/f2Z/f392/39/fr9/f36/f392v39/Zj///8wAAAAAAAAAAAAAAAAAAAAAPAPAADAAwAAgAEAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABAACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/g3+/v5X/f39mf39/cj9/f3q/f39+f39/fn9/f3q/f39yP39/Zn+/v5W////DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/iT9/f2c/f399f/////////////////////////////////////////////////////9/f31/f39mv7+/iMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/gn9/f2K/f39+////////////////////////////////////////////////////////////////////////////f39+v39/Yf///8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+/v4k/f390v////////////////////////////////////////////////////////////////////////////////////////////////39/dD///8iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////MP39/er//////////////////////////+r05v+v16H/gsBs/2WxSf9Wqjj/Vqk3/2OwRv99vWX/pdKV/97u2P////////////////////////////39/ej+/v4vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/iT9/f3q/////////////////////+v15/+Pxnv/VKk2/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/36+Z//d7tf///////////////////////39/ej///8iAAAAAAAAAAAAAAAAAAAAAAAAAAD///8K/f390//////////////////////E4bn/XKw+/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/1apN/+x0pv///////////////////////39/dD///8IAAAAAAAAAAAAAAAAAAAAAP39/Yv/////////////////////sdij/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP9TqDT/YKU1/8qOPv/5wZ////////////////////////39/YcAAAAAAAAAAAAAAAD+/v4l/f39+////////////////8Lgt/9TqDT/U6g0/1OoNP9TqDT/U6g0/1OoNP9utlT/n86N/7faqv+426v/pdKV/3u8ZP9UqDX/U6g0/3egN//jiUH/9IVC//SFQv/82MP//////////////////f39+v7+/iMAAAAAAAAAAP39/Z3////////////////q9Ob/W6w+/1OoNP9TqDT/U6g0/1OoNP9nskz/zOXC/////////////////////////////////+Dv2v+osWP/8YVC//SFQv/0hUL/9IVC//WQVP/++fb//////////////////f39mgAAAAD+/v4O/f399v///////////////4LHj/9TqDT/U6g0/1OoNP9TqDT/dblc//L58P/////////////////////////////////////////////8+v/3p3f/9IVC//SFQv/0hUL/9IVC//rIqf/////////////////9/f31////DP7+/ln////////////////f9v7/Cbz2/zOwhv9TqDT/U6g0/2KwRv/v9+z///////////////////////////////////////////////////////738//1kFT/9IVC//SFQv/0hUL/9plg///////////////////////+/v5W/f39nP///////////////4jf/f8FvPv/Bbz7/yG1s/9QqDz/vN2w//////////////////////////////////////////////////////////////////rHqP/0hUL/9IVC//SFQv/0hUL//vDn//////////////////39/Zn9/f3L////////////////R878/wW8+/8FvPv/Bbz7/y7C5P/7/fr//////////////////////////////////////////////////////////////////ere//SFQv/0hUL/9IVC//SFQv/718H//////////////////f39yP39/ez///////////////8cwvv/Bbz7/wW8+/8FvPv/WNL8///////////////////////////////////////0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//rIqv/////////////////9/f3q/f39+v///////////////we9+/8FvPv/Bbz7/wW8+/993P3///////////////////////////////////////SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/+cGf//////////////////39/fn9/f36////////////////B737/wW8+/8FvPv/Bbz7/33c/f//////////////////////////////////////9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/6xaX//////////////////f39+f39/e3///////////////8cwvv/Bbz7/wW8+/8FvPv/WdP8///////////////////////////////////////0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//vVv//////////////////9/f3q/f39y////////////////0bN/P8FvPv/Bbz7/wW8+/8hrvn/+/v///////////////////////////////////////////////////////////////////////////////////////////////////////////////////39/cj9/f2c////////////////ht/9/wW8+/8FvPv/FZP1/zRJ6/+zuPf//////////////////////////////////////////////////////////////////////////////////////////////////////////////////f39mf7+/lr////////////////d9v7/B7n7/yB38f81Q+r/NUPq/0hV7P/u8P3////////////////////////////////////////////////////////////////////////////////////////////////////////////+/v5X////D/39/ff///////////////9tkPT/NUPq/zVD6v81Q+r/NUPq/2Fs7//y8v7////////////////////////////////////////////09f7//////////////////////////////////////////////////f399f7+/g0AAAAA/f39n////////////////+Tm/P89Suv/NUPq/zVD6v81Q+r/NUPq/1Bc7f/IzPn/////////////////////////////////x8v5/0xY7P+MlPP////////////////////////////////////////////9/f2cAAAAAAAAAAD+/v4n/f39/P///////////////7W69/81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v9ZZe7/k5v0/6609/+vtff/lJv0/1pm7v81Q+r/NUPq/zVD6v+GjvL//v7//////////////////////////////f39+/7+/iQAAAAAAAAAAAAAAAD9/f2N/////////////////////6Cn9f81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v+BivL////////////////////////////9/f2KAAAAAAAAAAAAAAAAAAAAAP7+/gv9/f3V/////////////////////7W69/8+S+v/NUPq/zVD6v81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v81Q+r/P0zr/7q/+P///////////////////////f390v7+/gkAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/ib9/f3r/////////////////////+Xn/P94gfH/NkTq/zVD6v81Q+r/NUPq/zVD6v81Q+r/NUPq/zVD6v81Q+r/NkTq/3Z/8f/l5/z///////////////////////39/er+/v4kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/jL9/f3r///////////////////////////k5vz/nqX1/2p08P9IVez/OEbq/zdF6v9GU+z/aHLv/5qh9f/i5Pz////////////////////////////9/f3q////MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7+/ib9/f3V/////////////////////////////////////////////////////////////////////////////////////////////////f390v7+/iQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wr9/f2N/f39/P///////////////////////////////////////////////////////////////////////////f39+/39/Yv+/v4JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+/v4n/f39n/39/ff//////////////////////////////////////////////////////f399v39/Z3+/v4lAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7+Dv7+/lr9/f2c/f39y/39/e39/f36/f39+v39/ez9/f3L/f39nP7+/ln+/v4OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AA///AAD//AAAP/gAAB/wAAAP4AAAB8AAAAPAAAADgAAAAYAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABgAAAAcAAAAPAAAAD4AAAB/AAAA/4AAAf/AAAP/8AAP//wAP/",
+ _urls : [
+ {
+ type: "application/x-suggestions+json",
+ method: "GET",
+ template: "https://www.google.com/complete/search?client=firefox&q={searchTerms}",
+ params: "",
+ },
+ {
+ type: "text/html",
+ method: "GET",
+ template: "https://www.google.com/search",
+ params: [
+ {
+ "name": "q",
+ "value": "{searchTerms}",
+ "purpose": undefined,
+ },
+ {
+ "name": "ie",
+ "value": "utf-8",
+ "purpose": undefined,
+ },
+ {
+ "name": "oe",
+ "value": "utf-8",
+ "purpose": undefined,
+ },
+ {
+ "name": "client",
+ "value": "firefox-b-ab",
+ "purpose": "keyword",
+ },
+ {
+ "name": "client",
+ "value": "firefox-b",
+ "purpose": "searchbar",
+ },
+ ],
+ mozparams: {
+ },
+ },
+ ],
+ },
+ };
+
+ isSubObjectOf(EXPECTED_ENGINE, engine, "Google");
+}
diff --git a/browser/components/search/test/browser_google_behavior.js b/browser/components/search/test/browser_google_behavior.js
new file mode 100644
index 000000000..55405bb29
--- /dev/null
+++ b/browser/components/search/test/browser_google_behavior.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Google search plugin URLs
+ */
+
+"use strict";
+
+function test() {
+ let engine = Services.search.getEngineByName("Google");
+ ok(engine, "Google is installed");
+
+ let previouslySelectedEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+ engine.alias = "g";
+
+ let base = "https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&client=firefox-b";
+ let keywordBase = base + "-ab";
+
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base, "Check search URL for 'foo'");
+
+ waitForExplicitFinish();
+
+ var gCurrTest;
+ var gTests = [
+ {
+ name: "context menu search",
+ searchURL: base,
+ run: function () {
+ // Simulate a contextmenu search
+ // FIXME: This is a bit "low-level"...
+ BrowserSearch.loadSearch("foo", false, "contextmenu");
+ }
+ },
+ {
+ name: "keyword search",
+ searchURL: keywordBase,
+ run: function () {
+ gURLBar.value = "? foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "keyword search",
+ searchURL: keywordBase,
+ run: function () {
+ gURLBar.value = "g foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "search bar search",
+ searchURL: base,
+ run: function () {
+ let sb = BrowserSearch.searchBar;
+ sb.focus();
+ sb.value = "foo";
+ registerCleanupFunction(function () {
+ sb.value = "";
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "new tab search",
+ searchURL: base,
+ run: function () {
+ function doSearch(doc) {
+ // Re-add the listener, and perform a search
+ gBrowser.addProgressListener(listener);
+ doc.getElementById("newtab-search-text").value = "foo";
+ doc.getElementById("newtab-search-submit").click();
+ }
+
+ // load about:newtab, but remove the listener first so it doesn't
+ // get in the way
+ gBrowser.removeProgressListener(listener);
+ gBrowser.loadURI("about:newtab");
+ info("Waiting for about:newtab load");
+ tab.linkedBrowser.addEventListener("load", function load(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ info("skipping spurious load event");
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", load, true);
+
+ // Observe page setup
+ let win = gBrowser.contentWindow;
+ if (win.gSearch.currentEngineName ==
+ Services.search.currentEngine.name) {
+ doSearch(win.document);
+ }
+ else {
+ info("Waiting for newtab search init");
+ win.addEventListener("ContentSearchService", function done(event) {
+ info("Got newtab search event " + event.detail.type);
+ if (event.detail.type == "State") {
+ win.removeEventListener("ContentSearchService", done);
+ // Let gSearch respond to the event before continuing.
+ executeSoon(() => doSearch(win.document));
+ }
+ });
+ }
+ }, true);
+ }
+ }
+ ];
+
+ function nextTest() {
+ if (gTests.length) {
+ gCurrTest = gTests.shift();
+ info("Running : " + gCurrTest.name);
+ executeSoon(gCurrTest.run);
+ } else {
+ finish();
+ }
+ }
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let listener = {
+ onStateChange: function onStateChange(webProgress, req, flags, status) {
+ info("onStateChange");
+ // Only care about top-level document starts
+ let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_START;
+ if (!(flags & docStart) || !webProgress.isTopLevel)
+ return;
+
+ if (req.originalURI.spec == "about:blank")
+ return;
+
+ info("received document start");
+
+ ok(req instanceof Ci.nsIChannel, "req is a channel");
+ is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
+ info("Actual URI: " + req.URI.spec);
+
+ req.cancel(Components.results.NS_ERROR_FAILURE);
+
+ executeSoon(nextTest);
+ }
+ }
+
+ registerCleanupFunction(function () {
+ engine.alias = undefined;
+ gBrowser.removeProgressListener(listener);
+ gBrowser.removeTab(tab);
+ Services.search.currentEngine = previouslySelectedEngine;
+ });
+
+ tab.linkedBrowser.addEventListener("load", function load() {
+ tab.linkedBrowser.removeEventListener("load", load, true);
+ gBrowser.addProgressListener(listener);
+ nextTest();
+ }, true);
+}
diff --git a/browser/components/search/test/browser_google_codes.js b/browser/components/search/test/browser_google_codes.js
new file mode 100644
index 000000000..e166b6868
--- /dev/null
+++ b/browser/components/search/test/browser_google_codes.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kUrlPref = "geoSpecificDefaults.url";
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+var originalGeoURL;
+
+/**
+ * Clean the profile of any cache file left from a previous run.
+ * Returns a boolean indicating if the cache file existed.
+ */
+function removeCacheFile()
+{
+ const CACHE_FILENAME = "search.json.mozlz4";
+
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(CACHE_FILENAME);
+ if (file.exists()) {
+ file.remove(false);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Returns a promise that is resolved when an observer notification from the
+ * search service fires with the specified data.
+ *
+ * @param aExpectedData
+ * The value the observer notification sends that causes us to resolve
+ * the promise.
+ */
+function waitForSearchNotification(aExpectedData, aCallback) {
+ const SEARCH_SERVICE_TOPIC = "browser-search-service";
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ if (aData != aExpectedData)
+ return;
+
+ Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
+ aCallback();
+ }, SEARCH_SERVICE_TOPIC, false);
+}
+
+function asyncInit() {
+ return new Promise(resolve => {
+ Services.search.init(function() {
+ ok(Services.search.isInitialized, "search service should be initialized");
+ resolve();
+ });
+ });
+}
+
+function asyncReInit() {
+ const kLocalePref = "general.useragent.locale";
+
+ let promise = new Promise(resolve => {
+ waitForSearchNotification("reinit-complete", resolve);
+ });
+
+ Services.search.QueryInterface(Ci.nsIObserver)
+ .observe(null, "nsPref:changed", kLocalePref);
+
+ return promise;
+}
+
+let gEngineCount;
+
+add_task(function* preparation() {
+ // ContentSearch is interferring with our async re-initializations of the
+ // search service: once _initServicePromise has resolved, it will access
+ // the search service, thus causing unpredictable behavior due to
+ // synchronous initializations of the service.
+ let originalContentSearchPromise = ContentSearch._initServicePromise;
+ ContentSearch._initServicePromise = new Promise(resolve => {
+ registerCleanupFunction(() => {
+ ContentSearch._initServicePromise = originalContentSearchPromise;
+ resolve();
+ });
+ });
+
+ yield asyncInit();
+ gEngineCount = Services.search.getVisibleEngines().length;
+
+ waitForSearchNotification("uninit-complete", () => {
+ // Verify search service is not initialized
+ is(Services.search.isInitialized, false, "Search service should NOT be initialized");
+
+ removeCacheFile();
+
+ // Geo specific defaults won't be fetched if there's no country code.
+ Services.prefs.setCharPref("browser.search.geoip.url",
+ 'data:application/json,{"country_code": "US"}');
+
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
+
+ // Make the new Google the only engine
+ originalGeoURL = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + kUrlPref);
+ let geoUrl = 'data:application/json,{"interval": 31536000, "settings": {"searchDefault": "Google", "visibleDefaultEngines": ["google"]}}';
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, geoUrl);
+ });
+
+ yield asyncReInit();
+
+ yield new Promise(resolve => {
+ waitForSearchNotification("write-cache-to-disk-complete", resolve);
+ });
+});
+
+add_task(function* tests() {
+ let engines = Services.search.getEngines();
+ is(Services.search.currentEngine.name, "Google", "Search engine should be Google");
+ is(engines.length, 1, "There should only be one engine");
+
+ let engine = Services.search.getEngineByName("Google");
+ ok(engine, "Google");
+
+ let base = "https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&client=firefox-b";
+
+ // Keyword uses a slightly different code
+ let keywordBase = base + "-ab";
+
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo", null, "contextmenu").uri.spec;
+ is(url, base, "Check context menu search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "keyword").uri.spec;
+ is(url, keywordBase, "Check keyword search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "searchbar").uri.spec;
+ is(url, base, "Check search bar search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "homepage").uri.spec;
+ is(url, base, "Check homepage search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "newtab").uri.spec;
+ is(url, base, "Check newtab search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "system").uri.spec;
+ is(url, base, "Check system search URL for 'foo'");
+});
+
+
+add_task(function* cleanup() {
+ waitForSearchNotification("uninit-complete", () => {
+ // Verify search service is not initialized
+ is(Services.search.isInitialized, false,
+ "Search service should NOT be initialized");
+ removeCacheFile();
+
+ Services.prefs.clearUserPref("browser.search.geoip.url");
+
+ // We can't clear the pref because it's set to false by testing/profiles/prefs_general.js
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, originalGeoURL);
+ });
+
+ yield asyncReInit();
+ is(gEngineCount, Services.search.getVisibleEngines().length,
+ "correct engine count after cleanup");
+});
diff --git a/browser/components/search/test/browser_healthreport.js b/browser/components/search/test/browser_healthreport.js
new file mode 100644
index 000000000..c68ad174c
--- /dev/null
+++ b/browser/components/search/test/browser_healthreport.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+
+function test() {
+ waitForExplicitFinish();
+ resetPreferences();
+
+ function testTelemetry() {
+ // Find the right bucket for the "Foo" engine.
+ let engine = Services.search.getEngineByName("Foo");
+ let histogramKey = (engine.identifier || "other-Foo") + ".searchbar";
+ let numSearchesBefore = 0;
+ try {
+ let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
+ if (histogramKey in hs) {
+ numSearchesBefore = hs[histogramKey].sum;
+ }
+ } catch (ex) {
+ // No searches performed yet, not a problem, |numSearchesBefore| is 0.
+ }
+
+ // Now perform a search and ensure the count is incremented.
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+ let searchBar = BrowserSearch.searchBar;
+
+ searchBar.value = "firefox health report";
+ searchBar.focus();
+
+ function afterSearch() {
+ searchBar.value = "";
+ gBrowser.removeTab(tab);
+
+ // Make sure that the context searches are correctly recorded.
+ let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
+ Assert.ok(histogramKey in hs, "The histogram must contain the correct key");
+ Assert.equal(hs[histogramKey].sum, numSearchesBefore + 1,
+ "Performing a search increments the related SEARCH_COUNTS key by 1.");
+
+ let engine = Services.search.getEngineByName("Foo");
+ Services.search.removeEngine(engine);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ executeSoon(() => executeSoon(afterSearch));
+ }
+
+ function observer(subject, topic, data) {
+ switch (data) {
+ case "engine-added":
+ let engine = Services.search.getEngineByName("Foo");
+ ok(engine, "Engine was added.");
+ Services.search.currentEngine = engine;
+ break;
+
+ case "engine-current":
+ is(Services.search.currentEngine.name, "Foo", "Current engine is Foo");
+ testTelemetry();
+ break;
+
+ case "engine-removed":
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ finish();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+ SpecialPowers.pushPrefEnv({set: [["toolkit.telemetry.enabled", true]]}).then(function() {
+ Services.search.addEngine("http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml",
+ null, "data:image/x-icon,%00", false);
+ });
+}
+
+function resetPreferences() {
+ Preferences.resetBranch("datareporting.policy.");
+ Preferences.set("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
+}
diff --git a/browser/components/search/test/browser_hiddenOneOffs_cleanup.js b/browser/components/search/test/browser_hiddenOneOffs_cleanup.js
new file mode 100644
index 000000000..9a584feb6
--- /dev/null
+++ b/browser/components/search/test/browser_hiddenOneOffs_cleanup.js
@@ -0,0 +1,99 @@
+/* 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/. */
+const testPref = "Foo,FooDupe";
+
+function promiseNewEngine(basename) {
+ return new Promise((resolve, reject) => {
+ info("Waiting for engine to be added: " + basename);
+ Services.search.init({
+ onInitComplete: function() {
+ let url = getRootDirectory(gTestPath) + basename;
+ Services.search.addEngine(url, null, "", false, {
+ onSuccess: function (engine) {
+ info("Search engine added: " + basename);
+ resolve(engine);
+ },
+ onError: function (errCode) {
+ ok(false, "addEngine failed with error code " + errCode);
+ reject();
+ }
+ });
+ }
+ });
+ });
+}
+
+add_task(function* test_remove() {
+ yield promiseNewEngine("testEngine_dupe.xml");
+ yield promiseNewEngine("testEngine.xml");
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", testPref);
+
+ info("Removing testEngine_dupe.xml");
+ Services.search.removeEngine(Services.search.getEngineByName("FooDupe"));
+
+ let hiddenOneOffs =
+ Services.prefs.getCharPref("browser.search.hiddenOneOffs").split(",");
+
+ is(hiddenOneOffs.length, 1,
+ "hiddenOneOffs has the correct engine count post removal.");
+ is(hiddenOneOffs.some(x => x == "FooDupe"), false,
+ "Removed Engine is not in hiddenOneOffs after removal");
+ is(hiddenOneOffs.some(x => x == "Foo"), true,
+ "Current hidden engine is not affected by removal.");
+
+ info("Removing testEngine.xml");
+ Services.search.removeEngine(Services.search.getEngineByName("Foo"));
+
+ is(Services.prefs.getCharPref("browser.search.hiddenOneOffs"), "",
+ "hiddenOneOffs is empty after removing all hidden engines.");
+});
+
+add_task(function* test_add() {
+ yield promiseNewEngine("testEngine.xml");
+ info("setting prefs to " + testPref);
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", testPref);
+ yield promiseNewEngine("testEngine_dupe.xml");
+
+ let hiddenOneOffs =
+ Services.prefs.getCharPref("browser.search.hiddenOneOffs").split(",");
+
+ is(hiddenOneOffs.length, 1,
+ "hiddenOneOffs has the correct number of hidden engines present post add.");
+ is(hiddenOneOffs.some(x => x == "FooDupe"), false,
+ "Added engine is not present in hidden list.");
+ is(hiddenOneOffs.some(x => x == "Foo"), true,
+ "Adding an engine does not remove engines from hidden list.");
+});
+
+add_task(function* test_diacritics() {
+ const diacritic_engine = "Foo \u2661";
+ let Preferences =
+ Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+
+ Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
+ yield promiseNewEngine("testEngine_diacritics.xml");
+
+ let hiddenOneOffs =
+ Preferences.get("browser.search.hiddenOneOffs").split(",");
+ is(hiddenOneOffs.some(x => x == diacritic_engine), false,
+ "Observer cleans up added hidden engines that include a diacritic.");
+
+ Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
+
+ info("Removing testEngine_diacritics.xml");
+ Services.search.removeEngine(Services.search.getEngineByName(diacritic_engine));
+
+ hiddenOneOffs =
+ Preferences.get("browser.search.hiddenOneOffs").split(",");
+ is(hiddenOneOffs.some(x => x == diacritic_engine), false,
+ "Observer cleans up removed hidden engines that include a diacritic.");
+});
+
+registerCleanupFunction(() => {
+ info("Removing testEngine.xml");
+ Services.search.removeEngine(Services.search.getEngineByName("Foo"));
+ info("Removing testEngine_dupe.xml");
+ Services.search.removeEngine(Services.search.getEngineByName("FooDupe"));
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+});
diff --git a/browser/components/search/test/browser_hiddenOneOffs_diacritics.js b/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
new file mode 100644
index 000000000..db24c7192
--- /dev/null
+++ b/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
@@ -0,0 +1,59 @@
+/* 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/. */
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchbar = document.getElementById("searchbar");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid",
+ "searchbar-search-button");
+
+const diacritic_engine = "Foo \u2661";
+
+var Preferences =
+ Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+
+add_task(function* init() {
+ let currentEngine = Services.search.currentEngine;
+ yield promiseNewEngine("testEngine_diacritics.xml", {setAsCurrent: false});
+ registerCleanupFunction(() => {
+ Services.search.currentEngine = currentEngine;
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+ });
+});
+
+add_task(function* test_hidden() {
+ Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ yield promise;
+
+ ok(!getOneOffs().some(x => x.getAttribute("tooltiptext") == diacritic_engine),
+ "Search engines with diacritics are hidden when added to hiddenOneOffs preference.");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ info("Closing search panel");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+});
+
+add_task(function* test_shown() {
+ Preferences.set("browser.search.hiddenOneOffs", "");
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ yield promise;
+
+ ok(getOneOffs().some(x => x.getAttribute("tooltiptext") == diacritic_engine),
+ "Search engines with diacritics are shown when removed from hiddenOneOffs preference.");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+});
diff --git a/browser/components/search/test/browser_oneOffContextMenu.js b/browser/components/search/test/browser_oneOffContextMenu.js
new file mode 100644
index 000000000..69207923b
--- /dev/null
+++ b/browser/components/search/test/browser_oneOffContextMenu.js
@@ -0,0 +1,105 @@
+"use strict";
+
+const TEST_ENGINE_NAME = "Foo";
+const TEST_ENGINE_BASENAME = "testEngine.xml";
+
+const searchbar = document.getElementById("searchbar");
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const searchIcon = document.getAnonymousElementByAttribute(
+ searchbar, "anonid", "searchbar-search-button"
+);
+const oneOffBinding = document.getAnonymousElementByAttribute(
+ searchPopup, "anonid", "search-one-off-buttons"
+);
+const contextMenu = document.getAnonymousElementByAttribute(
+ oneOffBinding, "anonid", "search-one-offs-context-menu"
+);
+const oneOffButtons = document.getAnonymousElementByAttribute(
+ oneOffBinding, "anonid", "search-panel-one-offs"
+);
+const searchInNewTabMenuItem = document.getAnonymousElementByAttribute(
+ oneOffBinding, "anonid", "search-one-offs-context-open-in-new-tab"
+);
+
+add_task(function* init() {
+ yield promiseNewEngine(TEST_ENGINE_BASENAME, {
+ setAsCurrent: false,
+ });
+});
+
+add_task(function* extendedTelemetryDisabled() {
+ yield SpecialPowers.pushPrefEnv({set: [["toolkit.telemetry.enabled", false]]});
+ yield doTest();
+ checkTelemetry("other");
+});
+
+add_task(function* extendedTelemetryEnabled() {
+ yield SpecialPowers.pushPrefEnv({set: [["toolkit.telemetry.enabled", true]]});
+ yield doTest();
+ checkTelemetry("other-" + TEST_ENGINE_NAME);
+});
+
+function* doTest() {
+ // Open the popup.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ yield promise;
+
+ // Get the one-off button for the test engine.
+ let oneOffButton;
+ for (let node of oneOffButtons.childNodes) {
+ if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
+ oneOffButton = node;
+ break;
+ }
+ }
+ Assert.notEqual(oneOffButton, undefined,
+ "One-off for test engine should exist");
+
+ // Open the context menu on the one-off.
+ promise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(oneOffButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ yield promise;
+
+ // Click the Search in New Tab menu item.
+ promise = BrowserTestUtils.waitForNewTab(gBrowser);
+ EventUtils.synthesizeMouseAtCenter(searchInNewTabMenuItem, {});
+ let tab = yield promise;
+
+ // By default the search will open in the background and the popup will stay open:
+ promise = promiseEvent(searchPopup, "popuphidden");
+ info("Closing search panel");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+
+ // Check the loaded tab.
+ Assert.equal(tab.linkedBrowser.currentURI.spec,
+ "http://mochi.test:8888/browser/browser/components/search/test/",
+ "Expected search tab should have loaded");
+
+ yield BrowserTestUtils.removeTab(tab);
+
+ // Move the cursor out of the panel area to avoid messing with other tests.
+ yield EventUtils.synthesizeNativeMouseMove(searchbar);
+}
+
+function checkTelemetry(expectedEngineName) {
+ let propertyPath = [
+ "countableEvents",
+ "__DEFAULT__",
+ "search-oneoff",
+ expectedEngineName + ".oneoff-context-searchbar",
+ "unknown",
+ "tab-background",
+ ];
+ let telem = BrowserUITelemetry.getToolbarMeasures();
+ for (let prop of propertyPath) {
+ Assert.ok(prop in telem, "Property " + prop + " should be in the telemetry");
+ telem = telem[prop];
+ }
+ Assert.equal(telem, 1, "Click count");
+}
diff --git a/browser/components/search/test/browser_oneOffContextMenu_setDefault.js b/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
new file mode 100644
index 000000000..ff49cb0c6
--- /dev/null
+++ b/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
@@ -0,0 +1,195 @@
+"use strict";
+
+const TEST_ENGINE_NAME = "Foo";
+const TEST_ENGINE_BASENAME = "testEngine.xml";
+const SEARCHBAR_BASE_ID = "searchbar-engine-one-off-item-";
+const URLBAR_BASE_ID = "urlbar-engine-one-off-item-";
+const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
+
+const searchbar = document.getElementById("searchbar");
+const urlbar = document.getElementById("urlbar");
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const urlbarPopup = document.getElementById("PopupAutoCompleteRichResult");
+const searchIcon = document.getAnonymousElementByAttribute(
+ searchbar, "anonid", "searchbar-search-button"
+);
+const searchOneOffBinding = document.getAnonymousElementByAttribute(
+ searchPopup, "anonid", "search-one-off-buttons"
+);
+const urlBarOneOffBinding = document.getAnonymousElementByAttribute(
+ urlbarPopup, "anonid", "one-off-search-buttons"
+);
+
+let originalEngine = Services.search.currentEngine;
+
+function resetEngine() {
+ Services.search.currentEngine = originalEngine;
+}
+
+registerCleanupFunction(resetEngine);
+
+add_task(function* init() {
+ yield promiseNewEngine(TEST_ENGINE_BASENAME, {
+ setAsCurrent: false,
+ });
+});
+
+add_task(function* test_searchBarChangeEngine() {
+ let oneOffButton = yield openPopupAndGetEngineButton(true, searchPopup,
+ searchOneOffBinding,
+ SEARCHBAR_BASE_ID);
+
+ const setDefaultEngineMenuItem = document.getAnonymousElementByAttribute(
+ searchOneOffBinding, "anonid", "search-one-offs-context-set-default"
+ );
+
+ // Click the set default engine menu item.
+ let promise = promiseCurrentEngineChanged();
+ EventUtils.synthesizeMouseAtCenter(setDefaultEngineMenuItem, {});
+
+ // This also checks the engine correctly changed.
+ yield promise;
+
+ Assert.equal(oneOffButton.id, SEARCHBAR_BASE_ID + originalEngine.name,
+ "Should now have the original engine's id for the button");
+ Assert.equal(oneOffButton.getAttribute("tooltiptext"), originalEngine.name,
+ "Should now have the original engine's name for the tooltip");
+ Assert.equal(oneOffButton.image, originalEngine.iconURI.spec,
+ "Should now have the original engine's uri for the image");
+
+ yield promiseClosePopup(searchPopup);
+});
+
+add_task(function* test_urlBarChangeEngine() {
+ Services.prefs.setBoolPref(ONEOFF_URLBAR_PREF, true);
+ registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
+ });
+
+ // Ensure the engine is reset.
+ resetEngine();
+
+ let oneOffButton = yield openPopupAndGetEngineButton(false, urlbarPopup,
+ urlBarOneOffBinding,
+ URLBAR_BASE_ID);
+
+ const setDefaultEngineMenuItem = document.getAnonymousElementByAttribute(
+ urlBarOneOffBinding, "anonid", "search-one-offs-context-set-default"
+ );
+
+ // Click the set default engine menu item.
+ let promise = promiseCurrentEngineChanged();
+ EventUtils.synthesizeMouseAtCenter(setDefaultEngineMenuItem, {});
+
+ // This also checks the engine correctly changed.
+ yield promise;
+
+ let currentEngine = Services.search.currentEngine;
+
+ // For the urlbar, we should keep the new engine's icon.
+ Assert.equal(oneOffButton.id, URLBAR_BASE_ID + currentEngine.name,
+ "Should now have the original engine's id for the button");
+ Assert.equal(oneOffButton.getAttribute("tooltiptext"), currentEngine.name,
+ "Should now have the original engine's name for the tooltip");
+ Assert.equal(oneOffButton.image, currentEngine.iconURI.spec,
+ "Should now have the original engine's uri for the image");
+
+ yield promiseClosePopup(urlbarPopup);
+});
+
+/**
+ * Promises that an engine change has happened for the current engine, which
+ * has resulted in the test engine now being the current engine.
+ *
+ * @return {Promise} Resolved once the test engine is set as the current engine.
+ */
+function promiseCurrentEngineChanged() {
+ return new Promise(resolve => {
+ function observer(aSub, aTopic, aData) {
+ if (aData == "engine-current") {
+ Assert.ok(Services.search.currentEngine.name, TEST_ENGINE_NAME, "currentEngine set");
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ resolve();
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified", false);
+ });
+}
+
+/**
+ * Opens the specified urlbar/search popup and gets the test engine from the
+ * one-off buttons.
+ *
+ * @param {Boolean} isSearch true if the search popup should be opened; false
+ * for the urlbar popup.
+ * @param {Object} popup The expected popup.
+ * @param {Object} oneOffBinding The expected one-off-binding for the popup.
+ * @param {String} baseId The expected string for the id of the current
+ * engine button, without the engine name.
+ * @return {Object} Returns an object that represents the one off button for the
+ * test engine.
+ */
+function* openPopupAndGetEngineButton(isSearch, popup, oneOffBinding, baseId) {
+ // Open the popup.
+ let promise = promiseEvent(popup, "popupshown");
+ info("Opening panel");
+
+ // We have to open the popups in differnt ways.
+ if (isSearch) {
+ // Use the search icon to avoid hitting the network.
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ } else {
+ // There's no history at this stage, so we need to press a key.
+ urlbar.focus();
+ EventUtils.synthesizeKey("a", {});
+ }
+ yield promise;
+
+ const contextMenu = document.getAnonymousElementByAttribute(
+ oneOffBinding, "anonid", "search-one-offs-context-menu"
+ );
+ const oneOffButtons = document.getAnonymousElementByAttribute(
+ oneOffBinding, "anonid", "search-panel-one-offs"
+ );
+
+ // Get the one-off button for the test engine.
+ let oneOffButton;
+ for (let node of oneOffButtons.childNodes) {
+ if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
+ oneOffButton = node;
+ break;
+ }
+ }
+ Assert.notEqual(oneOffButton, undefined,
+ "One-off for test engine should exist");
+ Assert.equal(oneOffButton.getAttribute("tooltiptext"), TEST_ENGINE_NAME,
+ "One-off should have the tooltip set to the engine name");
+ Assert.equal(oneOffButton.id, baseId + TEST_ENGINE_NAME,
+ "Should have the correct id");
+
+ // Open the context menu on the one-off.
+ promise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(oneOffButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ yield promise;
+
+ return oneOffButton;
+}
+
+/**
+ * Closes the popup and moves the mouse away from it.
+ *
+ * @param {Button} popup The popup to close.
+ */
+function* promiseClosePopup(popup) {
+ // close the panel using the escape key.
+ let promise = promiseEvent(popup, "popuphidden");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+
+ // Move the cursor out of the panel area to avoid messing with other tests.
+ yield EventUtils.synthesizeNativeMouseMove(popup);
+}
diff --git a/browser/components/search/test/browser_oneOffHeader.js b/browser/components/search/test/browser_oneOffHeader.js
new file mode 100644
index 000000000..3a209bf56
--- /dev/null
+++ b/browser/components/search/test/browser_oneOffHeader.js
@@ -0,0 +1,142 @@
+/* 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/. */
+// Tests that keyboard navigation in the search panel works as designed.
+
+const isMac = ("nsILocalFileMac" in Ci);
+
+const searchbar = document.getElementById("searchbar");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid",
+ "searchbar-search-button");
+
+const oneOffsContainer =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
+const searchSettings =
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
+ "search-settings");
+var header =
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
+ "search-panel-one-offs-header");
+function getHeaderText() {
+ let headerChild = header.selectedPanel;
+ while (headerChild.hasChildNodes()) {
+ headerChild = headerChild.firstChild;
+ }
+ let headerStrings = [];
+ for (let label = headerChild; label; label = label.nextSibling) {
+ headerStrings.push(label.value);
+ }
+ return headerStrings.join("");
+}
+
+const msg = isMac ? 5 : 1;
+const utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+const scale = utils.screenPixelsPerCSSPixel;
+function* synthesizeNativeMouseMove(aElement) {
+ let rect = aElement.getBoundingClientRect();
+ let win = aElement.ownerGlobal;
+ let x = win.mozInnerScreenX + (rect.left + rect.right) / 2;
+ let y = win.mozInnerScreenY + (rect.top + rect.bottom) / 2;
+
+ // Wait for the mouseup event to occur before continuing.
+ return new Promise((resolve, reject) => {
+ function eventOccurred(e)
+ {
+ aElement.removeEventListener("mouseover", eventOccurred, true);
+ resolve();
+ }
+
+ aElement.addEventListener("mouseover", eventOccurred, true);
+
+ utils.sendNativeMouseEvent(x * scale, y * scale, msg, 0, null);
+ });
+}
+
+
+add_task(function* init() {
+ yield promiseNewEngine("testEngine.xml");
+});
+
+add_task(function* test_notext() {
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ yield promise;
+
+ is(header.getAttribute("selectedIndex"), 0,
+ "Header has the correct index selected with no search terms.");
+
+ is(getHeaderText(), "Search with:",
+ "Search header string is correct when no search terms have been entered");
+
+ yield synthesizeNativeMouseMove(searchSettings);
+ is(header.getAttribute("selectedIndex"), 0,
+ "Header has the correct index when no search terms have been entered and the Change Search Settings button is selected.");
+ is(getHeaderText(), "Search with:",
+ "Header has the correct text when no search terms have been entered and the Change Search Settings button is selected.");
+
+ let buttons = getOneOffs();
+ yield synthesizeNativeMouseMove(buttons[0]);
+ is(header.getAttribute("selectedIndex"), 2,
+ "Header has the correct index selected when a search engine has been selected");
+ is(getHeaderText(), "Search " + buttons[0].engine.name,
+ "Is the header text correct when a search engine is selected and no terms have been entered.");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ info("Closing search panel");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+});
+
+add_task(function* test_text() {
+ textbox.value = "foo";
+ registerCleanupFunction(() => {
+ textbox.value = "";
+ });
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ yield promise;
+
+ is(header.getAttribute("selectedIndex"), 1,
+ "Header has the correct index selected with a search term.");
+ is(getHeaderText(), "Search for foo with:",
+ "Search header string is correct when a search term has been entered");
+
+ let buttons = getOneOffs();
+ yield synthesizeNativeMouseMove(buttons[0]);
+ is(header.getAttribute("selectedIndex"), 2,
+ "Header has the correct index selected when a search engine has been selected");
+ is(getHeaderText(), "Search " + buttons[0].engine.name,
+ "Is the header text correct when search terms are entered after a search engine has been selected.");
+
+ yield synthesizeNativeMouseMove(searchSettings);
+ is(header.getAttribute("selectedIndex"), 1,
+ "Header has the correct index selected when search terms have been entered and the Change Search Settings button is selected.");
+ is(getHeaderText(), "Search for foo with:",
+ "Header has the correct text when search terms have been entered and the Change Search Settings button is selected.");
+
+ // Click the "Foo Search" header at the top of the popup and make sure it
+ // loads the search results.
+ let searchbarEngine =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "searchbar-engine");
+
+ yield synthesizeNativeMouseMove(searchbarEngine);
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchbarEngine, {});
+ });
+
+ let url = Services.search.currentEngine.getSubmission(textbox.value).uri.spec;
+ yield promiseTabLoadEvent(gBrowser.selectedTab, url);
+
+ // Move the cursor out of the panel area to avoid messing with other tests.
+ yield synthesizeNativeMouseMove(searchbar);
+});
diff --git a/browser/components/search/test/browser_private_search_perwindowpb.js b/browser/components/search/test/browser_private_search_perwindowpb.js
new file mode 100644
index 000000000..c0410371b
--- /dev/null
+++ b/browser/components/search/test/browser_private_search_perwindowpb.js
@@ -0,0 +1,76 @@
+// This test performs a search in a public window, then a different
+// search in a private window, and then checks in the public window
+// whether there is an autocomplete entry for the private search.
+
+add_task(function* () {
+ // Don't use about:home as the homepage for new windows
+ Services.prefs.setIntPref("browser.startup.page", 0);
+ registerCleanupFunction(() => Services.prefs.clearUserPref("browser.startup.page"));
+
+ let windowsToClose = [];
+
+ function performSearch(aWin, aIsPrivate) {
+ let searchBar = aWin.BrowserSearch.searchBar;
+ ok(searchBar, "got search bar");
+
+ let loadPromise = BrowserTestUtils.browserLoaded(aWin.gBrowser.selectedBrowser);
+
+ searchBar.value = aIsPrivate ? "private test" : "public test";
+ searchBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, aWin);
+
+ return loadPromise;
+ }
+
+ function* testOnWindow(aIsPrivate) {
+ let win = yield BrowserTestUtils.openNewBrowserWindow({ private: aIsPrivate });
+ yield SimpleTest.promiseFocus(win);
+ windowsToClose.push(win);
+ return win;
+ }
+
+ yield promiseNewEngine("426329.xml", { iconURL: "data:image/x-icon,%00" });
+
+ let newWindow = yield* testOnWindow(false);
+ yield performSearch(newWindow, false);
+
+ newWindow = yield* testOnWindow(true);
+ yield performSearch(newWindow, true);
+
+ newWindow = yield* testOnWindow(false);
+
+ let searchBar = newWindow.BrowserSearch.searchBar;
+ searchBar.value = "p";
+ searchBar.focus();
+
+ let popup = searchBar.textbox.popup;
+ let popupPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ searchBar.textbox.showHistoryPopup();
+ yield popupPromise;
+
+ let entries = getMenuEntries(searchBar);
+ for (let i = 0; i < entries.length; i++) {
+ isnot(entries[i], "private test",
+ "shouldn't see private autocomplete entries");
+ }
+
+ searchBar.textbox.toggleHistoryPopup();
+ searchBar.value = "";
+
+ windowsToClose.forEach(function(win) {
+ win.close();
+ });
+});
+
+function getMenuEntries(searchBar) {
+ let entries = [];
+ let autocompleteMenu = searchBar.textbox.popup;
+ // Could perhaps pull values directly from the controller, but it seems
+ // more reliable to test the values that are actually in the tree?
+ let column = autocompleteMenu.tree.columns[0];
+ let numRows = autocompleteMenu.tree.view.rowCount;
+ for (let i = 0; i < numRows; i++) {
+ entries.push(autocompleteMenu.tree.view.getValueAt(i, column));
+ }
+ return entries;
+}
diff --git a/browser/components/search/test/browser_searchbar_keyboard_navigation.js b/browser/components/search/test/browser_searchbar_keyboard_navigation.js
new file mode 100644
index 000000000..d395dfdc2
--- /dev/null
+++ b/browser/components/search/test/browser_searchbar_keyboard_navigation.js
@@ -0,0 +1,425 @@
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchbar = document.getElementById("searchbar");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
+
+const kValues = ["foo1", "foo2", "foo3"];
+const kUserValue = "foo";
+
+function getOpenSearchItems() {
+ let os = [];
+
+ let addEngineList =
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
+ "add-engines");
+ for (let item = addEngineList.firstChild; item; item = item.nextSibling)
+ os.push(item);
+
+ return os;
+}
+
+add_task(function* init() {
+ yield promiseNewEngine("testEngine.xml");
+
+ // First cleanup the form history in case other tests left things there.
+ yield new Promise((resolve, reject) => {
+ info("cleanup the search history");
+ searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
+ {handleCompletion: resolve,
+ handleError: reject});
+ });
+
+ yield new Promise((resolve, reject) => {
+ info("adding search history values: " + kValues);
+ let ops = kValues.map(value => { return {op: "add",
+ fieldname: "searchbar-history",
+ value: value}
+ });
+ searchbar.FormHistory.update(ops, {
+ handleCompletion: function() {
+ registerCleanupFunction(() => {
+ info("removing search history values: " + kValues);
+ let ops =
+ kValues.map(value => { return {op: "remove",
+ fieldname: "searchbar-history",
+ value: value}
+ });
+ searchbar.FormHistory.update(ops);
+ });
+ resolve();
+ },
+ handleError: reject
+ });
+ });
+
+ textbox.value = kUserValue;
+ registerCleanupFunction(() => { textbox.value = ""; });
+});
+
+
+add_task(function* test_arrows() {
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ yield promise;
+ is(textbox.mController.searchString, kUserValue, "The search string should be 'foo'");
+
+ // Check the initial state of the panel before sending keyboard events.
+ is(searchPopup.view.rowCount, kValues.length, "There should be 3 suggestions");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // The tests will be less meaningful if the first, second, last, and
+ // before-last one-off buttons aren't different. We should always have more
+ // than 4 default engines, but it's safer to check this assumption.
+ let oneOffs = getOneOffs();
+ ok(oneOffs.length >= 4, "we have at least 4 one-off buttons displayed")
+
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // The down arrow should first go through the suggestions.
+ for (let i = 0; i < kValues.length; ++i) {
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(searchPopup.selectedIndex, i,
+ "the suggestion at index " + i + " should be selected");
+ is(textbox.value, kValues[i],
+ "the textfield value should be " + kValues[i]);
+ }
+
+ // Pressing down again should remove suggestion selection and change the text
+ // field value back to what the user typed, and select the first one-off.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue,
+ "the textfield value should be back to initial value");
+
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ }
+
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ // We should now be back to the initial situation.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ info("now test the up arrow key");
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // cycle through the one-off items, the first one is already selected.
+ for (let i = oneOffs.length; i; --i) {
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton, oneOffs[i - 1],
+ "the one-off button #" + i + " should be selected");
+ }
+
+ // Another press on up should clear the one-off selection and select the
+ // last suggestion.
+ EventUtils.synthesizeKey("VK_UP", {});
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ for (let i = kValues.length - 1; i >= 0; --i) {
+ is(searchPopup.selectedIndex, i,
+ "the suggestion at index " + i + " should be selected");
+ is(textbox.value, kValues[i],
+ "the textfield value should be " + kValues[i]);
+ EventUtils.synthesizeKey("VK_UP", {});
+ }
+
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue,
+ "the textfield value should be back to initial value");
+});
+
+add_task(function* test_typing_clears_button_selection() {
+ is(Services.focus.focusedElement, textbox.inputField,
+ "the search bar should be focused"); // from the previous test.
+ ok(!textbox.selectedButton, "no button should be selected");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // Type a character.
+ EventUtils.synthesizeKey("a", {});
+ ok(!textbox.selectedButton, "the settings item should be de-selected");
+
+ // Remove the character.
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+});
+
+add_task(function* test_tab() {
+ is(Services.focus.focusedElement, textbox.inputField,
+ "the search bar should be focused"); // from the previous test.
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Pressing tab should select the first one-off without selecting suggestions.
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // One more <tab> selects the settings button.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // Pressing tab again should close the panel...
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ yield promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(Services.focus.focusedElement, textbox.inputField,
+ "the search bar no longer be focused");
+});
+
+add_task(function* test_shift_tab() {
+ // First reopen the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ yield promise;
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Press up once to select the last button.
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // Press up again to select the last one-off button.
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ // Pressing shift+tab should cycle through the one-off items.
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ if (i)
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // Pressing shift+tab again should close the panel...
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+ yield promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(Services.focus.focusedElement, textbox.inputField,
+ "the search bar no longer be focused");
+});
+
+add_task(function* test_alt_down() {
+ // First refocus the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ yield promise;
+
+ // close the panel using the escape key.
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+
+ // check that alt+down opens the panel...
+ promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+ yield promise;
+
+ // ... and does nothing else.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // Pressing alt+down should select the first one-off without selecting suggestions
+ // and cycle through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the first one-off should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should be selected");
+});
+
+add_task(function* test_alt_up() {
+ // close the panel using the escape key.
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+
+ // check that alt+up opens the panel...
+ promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ yield promise;
+
+ // ... and does nothing else.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // Pressing alt+up should select the last one-off without selecting suggestions
+ // and cycle up through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the last one-off should be selected.
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ is(textbox.selectedButton, oneOffs[oneOffs.length - 1],
+ "the last one-off button should be selected");
+
+ // Cleanup for the next test.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ ok(!textbox.selectedButton, "no one-off should be selected anymore");
+});
+
+add_task(function* test_tab_and_arrows() {
+ // Check the initial state is as expected.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // After pressing down, the first sugggestion should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(searchPopup.selectedIndex, 0, "first suggestion should be selected");
+ is(textbox.value, kValues[0], "the textfield value should have changed");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // After pressing tab, the first one-off should be selected,
+ // and the first suggestion still selected.
+ let oneOffs = getOneOffs();
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should be selected");
+ is(searchPopup.selectedIndex, 0, "first suggestion should still be selected");
+
+ // After pressing down, the second suggestion should be selected,
+ // and the first one-off still selected.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should still be selected");
+ is(searchPopup.selectedIndex, 1, "second suggestion should be selected");
+
+ // After pressing up, the first suggestion should be selected again,
+ // and the first one-off still selected.
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should still be selected");
+ is(searchPopup.selectedIndex, 0, "second suggestion should be selected again");
+
+ // After pressing up again, we should have no suggestion selected anymore,
+ // the textfield value back to the user-typed value, and still the first one-off
+ // selected.
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue,
+ "the textfield value should be back to user typed value");
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should still be selected");
+
+ // Now pressing down should select the second one-off.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton, oneOffs[1],
+ "the second one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "there should still be no selected suggestion");
+
+ // Finally close the panel.
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+});
+
+add_task(function* test_open_search() {
+ let rootDir = getRootDirectory(gTestPath);
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, rootDir + "opensearch.html");
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ yield promise;
+
+ let engines = getOpenSearchItems();
+ is(engines.length, 2, "the opensearch.html page exposes 2 engines")
+
+ // Check that there's initially no selection.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no button should be selected");
+
+ // Pressing up once selects the setting button...
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // ...and then pressing up selects open search engines.
+ for (let i = engines.length; i; --i) {
+ EventUtils.synthesizeKey("VK_UP", {});
+ let selectedButton = textbox.selectedButton;
+ is(selectedButton, engines[i - 1],
+ "the engine #" + i + " should be selected");
+ ok(selectedButton.classList.contains("addengine-item"),
+ "the button is themed as an engine item");
+ }
+
+ // Pressing up again should select the last one-off button.
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton, getOneOffs().pop(),
+ "the last one-off button should be selected");
+
+ info("now check that the down key navigates open search items as expected");
+ for (let i = 0; i < engines.length; ++i) {
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton, engines[i],
+ "the engine #" + (i + 1) + " should be selected");
+ }
+
+ // Pressing down on the last engine item selects the settings button.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/search/test/browser_searchbar_openpopup.js b/browser/components/search/test/browser_searchbar_openpopup.js
new file mode 100644
index 000000000..befc8f142
--- /dev/null
+++ b/browser/components/search/test/browser_searchbar_openpopup.js
@@ -0,0 +1,521 @@
+// Tests that the suggestion popup appears at the right times in response to
+// focus and user events (mouse, keyboard, drop).
+
+// Instead of loading EventUtils.js into the test scope in browser-test.js for all tests,
+// we only need EventUtils.js for a few files which is why we are using loadSubScript.
+var EventUtils = {};
+this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+const searchbar = document.getElementById("searchbar");
+const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid", "searchbar-search-button");
+const goButton = document.getAnonymousElementByAttribute(searchbar, "anonid", "search-go-button");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const kValues = ["long text", "long text 2", "long text 3"];
+
+const isWindows = Services.appinfo.OS == "WINNT";
+const mouseDown = isWindows ? 2 : 1;
+const mouseUp = isWindows ? 4 : 2;
+const utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+const scale = utils.screenPixelsPerCSSPixel;
+
+function* synthesizeNativeMouseClick(aElement) {
+ let rect = aElement.getBoundingClientRect();
+ let win = aElement.ownerGlobal;
+ let x = win.mozInnerScreenX + (rect.left + rect.right) / 2;
+ let y = win.mozInnerScreenY + (rect.top + rect.bottom) / 2;
+
+ // Wait for the mouseup event to occur before continuing.
+ return new Promise((resolve, reject) => {
+ function eventOccurred(e)
+ {
+ aElement.removeEventListener("mouseup", eventOccurred, true);
+ resolve();
+ }
+
+ aElement.addEventListener("mouseup", eventOccurred, true);
+
+ utils.sendNativeMouseEvent(x * scale, y * scale, mouseDown, 0, null);
+ utils.sendNativeMouseEvent(x * scale, y * scale, mouseUp, 0, null);
+ });
+}
+
+add_task(function* init() {
+ yield promiseNewEngine("testEngine.xml");
+
+ // First cleanup the form history in case other tests left things there.
+ yield new Promise((resolve, reject) => {
+ info("cleanup the search history");
+ searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
+ {handleCompletion: resolve,
+ handleError: reject});
+ });
+
+ yield new Promise((resolve, reject) => {
+ info("adding search history values: " + kValues);
+ let ops = kValues.map(value => { return {op: "add",
+ fieldname: "searchbar-history",
+ value: value}
+ });
+ searchbar.FormHistory.update(ops, {
+ handleCompletion: function() {
+ registerCleanupFunction(() => {
+ info("removing search history values: " + kValues);
+ let ops =
+ kValues.map(value => { return {op: "remove",
+ fieldname: "searchbar-history",
+ value: value}
+ });
+ searchbar.FormHistory.update(ops);
+ });
+ resolve();
+ },
+ handleError: reject
+ });
+ });
+});
+
+// Adds a task that shouldn't show the search suggestions popup.
+function add_no_popup_task(task) {
+ add_task(function*() {
+ let sawPopup = false;
+ function listener() {
+ sawPopup = true;
+ }
+
+ info("Entering test " + task.name);
+ searchPopup.addEventListener("popupshowing", listener, false);
+ yield Task.spawn(task);
+ searchPopup.removeEventListener("popupshowing", listener, false);
+ ok(!sawPopup, "Shouldn't have seen the suggestions popup");
+ info("Leaving test " + task.name);
+ });
+}
+
+// Simulates the full set of events for a context click
+function context_click(target) {
+ for (let event of ["mousedown", "contextmenu", "mouseup"])
+ EventUtils.synthesizeMouseAtCenter(target, { type: event, button: 2 });
+}
+
+// Right clicking the icon should not open the popup.
+add_no_popup_task(function* open_icon_context() {
+ gURLBar.focus();
+ let toolbarPopup = document.getElementById("toolbar-context-menu");
+
+ let promise = promiseEvent(toolbarPopup, "popupshown");
+ context_click(searchIcon);
+ yield promise;
+
+ promise = promiseEvent(toolbarPopup, "popuphidden");
+ toolbarPopup.hidePopup();
+ yield promise;
+});
+
+// With no text in the search box left clicking the icon should open the popup.
+// Clicking the icon again should hide the popup and not show it again.
+add_task(function* open_empty() {
+ gURLBar.focus();
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Clicking icon");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ yield promise;
+ is(searchPopup.getAttribute("showonlysettings"), "true", "Should only show the settings");
+ is(textbox.mController.searchString, "", "Should be an empty search string");
+
+ // By giving the textbox some text any next attempt to open the search popup
+ // from the click handler will try to search for this text.
+ textbox.value = "foo";
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+
+ info("Hiding popup");
+ yield synthesizeNativeMouseClick(searchIcon);
+ yield promise;
+
+ is(textbox.mController.searchString, "", "Should not have started to search for the new text");
+
+ // Cancel the search if it started.
+ if (textbox.mController.searchString != "") {
+ textbox.mController.stopSearch();
+ }
+
+ textbox.value = "";
+});
+
+// With no text in the search box left clicking it should not open the popup.
+add_no_popup_task(function* click_doesnt_open_popup() {
+ gURLBar.focus();
+
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 0, "Should have selected all of the text");
+});
+
+// Left clicking in a non-empty search box when unfocused should focus it and open the popup.
+add_task(function* click_opens_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+
+ textbox.value = "";
+});
+
+// Right clicking in a non-empty search box when unfocused should open the edit context menu.
+add_no_popup_task(function* right_click_doesnt_open_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let contextPopup = document.getAnonymousElementByAttribute(textbox.inputField.parentNode, "anonid", "input-box-contextmenu");
+ let promise = promiseEvent(contextPopup, "popupshown");
+ context_click(textbox);
+ yield promise;
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(contextPopup, "popuphidden");
+ contextPopup.hidePopup();
+ yield promise;
+
+ textbox.value = "";
+});
+
+// Moving focus away from the search box should close the popup
+add_task(function* focus_change_closes_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let promise2 = promiseEvent(searchbar, "blur");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ yield promise;
+ yield promise2;
+
+ textbox.value = "";
+});
+
+// Moving focus away from the search box should close the small popup
+add_task(function* focus_change_closes_small_popup() {
+ gURLBar.focus();
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ // For some reason sending the mouse event immediately doesn't open the popup.
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ yield promise;
+ is(searchPopup.getAttribute("showonlysettings"), "true", "Should show the small popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let promise2 = promiseEvent(searchbar, "blur");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ yield promise;
+ yield promise2;
+});
+
+// Pressing escape should close the popup.
+add_task(function* escape_closes_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+
+ textbox.value = "";
+});
+
+// Pressing contextmenu should close the popup.
+add_task(function* contextmenu_closes_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+
+ // synthesizeKey does not work with VK_CONTEXT_MENU (bug 1127368)
+ EventUtils.synthesizeMouseAtCenter(textbox, { type: "contextmenu", button: null });
+
+ yield promise;
+
+ let contextPopup =
+ document.getAnonymousElementByAttribute(textbox.inputField.parentNode,
+ "anonid", "input-box-contextmenu");
+ promise = promiseEvent(contextPopup, "popuphidden");
+ contextPopup.hidePopup();
+ yield promise;
+
+ textbox.value = "";
+});
+
+// Tabbing to the search box should open the popup if it contains text.
+add_task(function* tab_opens_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+
+ textbox.value = "";
+});
+
+// Tabbing to the search box should not open the popup if it doesn't contain text.
+add_no_popup_task(function* tab_doesnt_open_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ EventUtils.synthesizeKey("VK_TAB", {});
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ textbox.value = "";
+});
+
+// Switching back to the window when the search box has focus from mouse should not open the popup.
+add_task(function* refocus_window_doesnt_open_popup_mouse() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(searchbar, {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let newWin = OpenBrowserWindow();
+ yield new Promise(resolve => waitForFocus(resolve, newWin));
+ yield promise;
+
+ function listener() {
+ ok(false, "Should not have shown the popup.");
+ }
+ searchPopup.addEventListener("popupshowing", listener, false);
+
+ promise = promiseEvent(searchbar, "focus");
+ newWin.close();
+ yield promise;
+
+ // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
+ yield new Promise(resolve => executeSoon(resolve));
+ yield new Promise(resolve => executeSoon(resolve));
+ yield new Promise(resolve => executeSoon(resolve));
+
+ searchPopup.removeEventListener("popupshowing", listener, false);
+ textbox.value = "";
+});
+
+// Switching back to the window when the search box has focus from keyboard should not open the popup.
+add_task(function* refocus_window_doesnt_open_popup_keyboard() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let newWin = OpenBrowserWindow();
+ yield new Promise(resolve => waitForFocus(resolve, newWin));
+ yield promise;
+
+ function listener() {
+ ok(false, "Should not have shown the popup.");
+ }
+ searchPopup.addEventListener("popupshowing", listener, false);
+
+ promise = promiseEvent(searchbar, "focus");
+ newWin.close();
+ yield promise;
+
+ // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
+ yield new Promise(resolve => executeSoon(resolve));
+ yield new Promise(resolve => executeSoon(resolve));
+ yield new Promise(resolve => executeSoon(resolve));
+
+ searchPopup.removeEventListener("popupshowing", listener, false);
+ textbox.value = "";
+});
+
+// Clicking the search go button shouldn't open the popup
+add_no_popup_task(function* search_go_doesnt_open_popup() {
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ gURLBar.focus();
+ textbox.value = "foo";
+ searchbar.updateGoButtonVisibility();
+
+ let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeMouseAtCenter(goButton, {});
+ yield promise;
+
+ textbox.value = "";
+ gBrowser.removeCurrentTab();
+});
+
+// Clicks outside the search popup should close the popup but not consume the click.
+add_task(function* dont_consume_clicks() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ yield promise;
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ yield synthesizeNativeMouseClick(gURLBar);
+ yield promise;
+
+ is(Services.focus.focusedElement, gURLBar.inputField, "Should have focused the URL bar");
+
+ textbox.value = "";
+});
+
+// Dropping text to the searchbar should open the popup
+add_task(function* drop_opens_popup() {
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeDrop(searchIcon, textbox.inputField, [[ {type: "text/plain", data: "foo" } ]], "move", window);
+ yield promise;
+
+ isnot(searchPopup.getAttribute("showonlysettings"), "true", "Should show the full popup");
+ is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+
+ textbox.value = "";
+});
+
+// Moving the caret using the cursor keys should not close the popup.
+add_task(function* dont_rollup_oncaretmove() {
+ gURLBar.focus();
+ textbox.value = "long text";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ yield promise;
+
+ // Deselect the text
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ is(textbox.selectionStart, 9, "Should have moved the caret (selectionStart after deselect right)");
+ is(textbox.selectionEnd, 9, "Should have moved the caret (selectionEnd after deselect right)");
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("VK_LEFT", {});
+ is(textbox.selectionStart, 8, "Should have moved the caret (selectionStart after left)");
+ is(textbox.selectionEnd, 8, "Should have moved the caret (selectionEnd after left)");
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ is(textbox.selectionStart, 9, "Should have moved the caret (selectionStart after right)");
+ is(textbox.selectionEnd, 9, "Should have moved the caret (selectionEnd after right)");
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ // Ensure caret movement works while a suggestion is selected.
+ is(textbox.popup.selectedIndex, -1, "No selected item in list");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.popup.selectedIndex, 0, "Selected item in list");
+ is(textbox.selectionStart, 9, "Should have moved the caret to the end (selectionStart after selection)");
+ is(textbox.selectionEnd, 9, "Should have moved the caret to the end (selectionEnd after selection)");
+
+ EventUtils.synthesizeKey("VK_LEFT", {});
+ is(textbox.selectionStart, 8, "Should have moved the caret again (selectionStart after left)");
+ is(textbox.selectionEnd, 8, "Should have moved the caret again (selectionEnd after left)");
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("VK_LEFT", {});
+ is(textbox.selectionStart, 7, "Should have moved the caret (selectionStart after left)");
+ is(textbox.selectionEnd, 7, "Should have moved the caret (selectionEnd after left)");
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ is(textbox.selectionStart, 8, "Should have moved the caret (selectionStart after right)");
+ is(textbox.selectionEnd, 8, "Should have moved the caret (selectionEnd after right)");
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ if (navigator.platform.indexOf("Mac") == -1) {
+ EventUtils.synthesizeKey("VK_HOME", {});
+ is(textbox.selectionStart, 0, "Should have moved the caret (selectionStart after home)");
+ is(textbox.selectionEnd, 0, "Should have moved the caret (selectionEnd after home)");
+ is(searchPopup.state, "open", "Popup should still be open");
+ }
+
+ // Close the popup again
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promise;
+
+ textbox.value = "";
+});
diff --git a/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js b/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
new file mode 100644
index 000000000..37ca32cf2
--- /dev/null
+++ b/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -0,0 +1,354 @@
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchbar = document.getElementById("searchbar");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
+const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid",
+ "searchbar-search-button");
+
+const kValues = ["foo1", "foo2", "foo3"];
+
+function getOpenSearchItems() {
+ let os = [];
+
+ let addEngineList =
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
+ "add-engines");
+ for (let item = addEngineList.firstChild; item; item = item.nextSibling)
+ os.push(item);
+
+ return os;
+}
+
+add_task(function* init() {
+ yield promiseNewEngine("testEngine.xml");
+
+ // First cleanup the form history in case other tests left things there.
+ yield new Promise((resolve, reject) => {
+ info("cleanup the search history");
+ searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
+ {handleCompletion: resolve,
+ handleError: reject});
+ });
+
+ yield new Promise((resolve, reject) => {
+ info("adding search history values: " + kValues);
+ let ops = kValues.map(value => { return {op: "add",
+ fieldname: "searchbar-history",
+ value: value}
+ });
+ searchbar.FormHistory.update(ops, {
+ handleCompletion: function() {
+ registerCleanupFunction(() => {
+ info("removing search history values: " + kValues);
+ let ops =
+ kValues.map(value => { return {op: "remove",
+ fieldname: "searchbar-history",
+ value: value}
+ });
+ searchbar.FormHistory.update(ops);
+ });
+ resolve();
+ },
+ handleError: reject
+ });
+ });
+});
+
+
+add_task(function* test_arrows() {
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ yield promise;
+info("textbox.mController.searchString = " + textbox.mController.searchString);
+ is(textbox.mController.searchString, "", "The search string should be empty");
+
+ // Check the initial state of the panel before sending keyboard events.
+ is(searchPopup.getAttribute("showonlysettings"), "true", "Should show the small popup");
+ // Having suggestions populated (but hidden) is important, because if there
+ // are none we can't ensure the keyboard events don't reach them.
+ is(searchPopup.view.rowCount, kValues.length, "There should be 3 suggestions");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // The tests will be less meaningful if the first, second, last, and
+ // before-last one-off buttons aren't different. We should always have more
+ // than 4 default engines, but it's safer to check this assumption.
+ let oneOffs = getOneOffs();
+ ok(oneOffs.length >= 4, "we have at least 4 one-off buttons displayed")
+
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Pressing should select the first one-off.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ }
+
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ // We should now be back to the initial situation.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ info("now test the up arrow key");
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // cycle through the one-off items, the first one is already selected.
+ for (let i = oneOffs.length; i; --i) {
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton, oneOffs[i - 1],
+ "the one-off button #" + i + " should be selected");
+ }
+
+ // Another press on up should clear the one-off selection.
+ EventUtils.synthesizeKey("VK_UP", {});
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+});
+
+add_task(function* test_tab() {
+ is(Services.focus.focusedElement, textbox.inputField,
+ "the search bar should be focused"); // from the previous test.
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Pressing tab should select the first one-off without selecting suggestions.
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // One more <tab> selects the settings button.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // Pressing tab again should close the panel...
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ yield promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(Services.focus.focusedElement, textbox.inputField,
+ "the search bar no longer be focused");
+});
+
+add_task(function* test_shift_tab() {
+ // First reopen the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ yield promise;
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.getAttribute("showonlysettings"), "true", "Should show the small popup");
+
+ // Press up once to select the last button.
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // Press up again to select the last one-off button.
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ // Pressing shift+tab should cycle through the one-off items.
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ if (i)
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // Pressing shift+tab again should close the panel...
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+ yield promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(Services.focus.focusedElement, textbox.inputField,
+ "the search bar no longer be focused");
+});
+
+add_task(function* test_alt_down() {
+ // First reopen the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ yield promise;
+
+ // and check it's in a correct initial state.
+ is(searchPopup.getAttribute("showonlysettings"), "true", "Should show the small popup");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // Pressing alt+down should select the first one-off without selecting suggestions
+ // and cycle through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the first one-off should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should be selected");
+
+ // Clear the selection with an alt+up keypress
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+});
+
+add_task(function* test_alt_up() {
+ // Check the initial state of the panel
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // Pressing alt+up should select the last one-off without selecting suggestions
+ // and cycle up through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ is(textbox.selectedButton, oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the last one-off should be selected.
+ EventUtils.synthesizeKey("VK_UP", {altKey: true});
+ is(textbox.selectedButton, oneOffs[oneOffs.length - 1],
+ "the last one-off button should be selected");
+
+ // Cleanup for the next test.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ ok(!textbox.selectedButton, "no one-off should be selected anymore");
+});
+
+add_task(function* test_tab_and_arrows() {
+ // Check the initial state is as expected.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // After pressing down, the first one-off should be selected.
+ let oneOffs = getOneOffs();
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing tab, the second one-off should be selected.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(textbox.selectedButton, oneOffs[1],
+ "the second one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing up, the first one-off should be selected again.
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton, oneOffs[0],
+ "the first one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // Finally close the panel.
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+});
+
+add_task(function* test_open_search() {
+ let rootDir = getRootDirectory(gTestPath);
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, rootDir + "opensearch.html");
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ yield promise;
+ is(searchPopup.getAttribute("showonlysettings"), "true", "Should show the small popup");
+
+ let engines = getOpenSearchItems();
+ is(engines.length, 2, "the opensearch.html page exposes 2 engines")
+
+ // Check that there's initially no selection.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no button should be selected");
+
+ // Pressing up once selects the setting button...
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ // ...and then pressing up selects open search engines.
+ for (let i = engines.length; i; --i) {
+ EventUtils.synthesizeKey("VK_UP", {});
+ let selectedButton = textbox.selectedButton;
+ is(selectedButton, engines[i - 1],
+ "the engine #" + i + " should be selected");
+ ok(selectedButton.classList.contains("addengine-item"),
+ "the button is themed as an engine item");
+ }
+
+ // Pressing up again should select the last one-off button.
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(textbox.selectedButton, getOneOffs().pop(),
+ "the last one-off button should be selected");
+
+ info("now check that the down key navigates open search items as expected");
+ for (let i = 0; i < engines.length; ++i) {
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton, engines[i],
+ "the engine #" + (i + 1) + " should be selected");
+ }
+
+ // Pressing down on the last engine item selects the settings button.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
+ "the settings item should be selected");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ yield promise;
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/search/test/browser_webapi.js b/browser/components/search/test/browser_webapi.js
new file mode 100644
index 000000000..d8161ffbe
--- /dev/null
+++ b/browser/components/search/test/browser_webapi.js
@@ -0,0 +1,92 @@
+var ROOT = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+const searchBundle = Services.strings.createBundle("chrome://global/locale/search/search.properties");
+const brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+const brandName = brandBundle.GetStringFromName("brandShortName");
+
+function getString(key, ...params) {
+ return searchBundle.formatStringFromName(key, params, params.length);
+}
+
+function AddSearchProvider(...args) {
+ return gBrowser.addTab(ROOT + "webapi.html?" + encodeURIComponent(JSON.stringify(args)));
+}
+
+function promiseDialogOpened() {
+ return new Promise((resolve, reject) => {
+ Services.wm.addListener({
+ onOpenWindow: function(xulWin) {
+ Services.wm.removeListener(this);
+
+ let win = xulWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ waitForFocus(() => {
+ if (win.location == "chrome://global/content/commonDialog.xul")
+ resolve(win)
+ else
+ reject();
+ }, win);
+ }
+ });
+ });
+}
+
+add_task(function* test_working() {
+ gBrowser.selectedTab = AddSearchProvider(ROOT + "testEngine.xml");
+
+ let dialog = yield promiseDialogOpened();
+ is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
+ is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
+ "Should have seen the right install message");
+ dialog.document.documentElement.cancelDialog();
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* test_HTTP() {
+ gBrowser.selectedTab = AddSearchProvider(ROOT.replace("http:", "HTTP:") + "testEngine.xml");
+
+ let dialog = yield promiseDialogOpened();
+ is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
+ is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
+ "Should have seen the right install message");
+ dialog.document.documentElement.cancelDialog();
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* test_relative() {
+ gBrowser.selectedTab = AddSearchProvider("testEngine.xml");
+
+ let dialog = yield promiseDialogOpened();
+ is(dialog.args.promptType, "confirmEx", "Should see the confirmation dialog.");
+ is(dialog.args.text, getString("addEngineConfirmation", "Foo", "example.com"),
+ "Should have seen the right install message");
+ dialog.document.documentElement.cancelDialog();
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* test_invalid() {
+ gBrowser.selectedTab = AddSearchProvider("z://foobar");
+
+ let dialog = yield promiseDialogOpened();
+ is(dialog.args.promptType, "alert", "Should see the alert dialog.");
+ is(dialog.args.text, getString("error_invalid_engine_msg", brandName),
+ "Should have seen the right error message")
+ dialog.document.documentElement.acceptDialog();
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* test_missing() {
+ let url = ROOT + "foobar.xml";
+ gBrowser.selectedTab = AddSearchProvider(url);
+
+ let dialog = yield promiseDialogOpened();
+ is(dialog.args.promptType, "alert", "Should see the alert dialog.");
+ is(dialog.args.text, getString("error_loading_engine_msg2", brandName, url),
+ "Should have seen the right error message")
+ dialog.document.documentElement.acceptDialog();
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/search/test/browser_yahoo.js b/browser/components/search/test/browser_yahoo.js
new file mode 100644
index 000000000..f45b47d0c
--- /dev/null
+++ b/browser/components/search/test/browser_yahoo.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Yahoo search plugin URLs
+ */
+
+"use strict";
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+function test() {
+ let engine = Services.search.getEngineByName("Yahoo");
+ ok(engine, "Yahoo");
+
+ let base = "https://search.yahoo.com/yhs/search?p=foo&ei=UTF-8&hspart=mozilla";
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base + "&hsimp=yhs-001", "Check search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "searchbar").uri.spec;
+ is(url, base + "&hsimp=yhs-001", "Check search bar search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "keyword").uri.spec;
+ is(url, base + "&hsimp=yhs-002", "Check keyword search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "homepage").uri.spec;
+ is(url, base + "&hsimp=yhs-003", "Check homepage search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "newtab").uri.spec;
+ is(url, base + "&hsimp=yhs-004", "Check newtab search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "contextmenu").uri.spec;
+ is(url, base + "&hsimp=yhs-005", "Check context menu search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "system").uri.spec;
+ is(url, base + "&hsimp=yhs-007", "Check system search URL for 'foo'");
+ url = engine.getSubmission("foo", null, "invalid").uri.spec;
+ is(url, base + "&hsimp=yhs-001", "Check invalid URL for 'foo'");
+
+ // Check search suggestion URL.
+ url = engine.getSubmission("foo", "application/x-suggestions+json").uri.spec;
+ is(url, "https://search.yahoo.com/sugg/ff?output=fxjson&appid=ffd&command=foo", "Check search suggestion URL for 'foo'");
+
+ // Check all other engine properties.
+ const EXPECTED_ENGINE = {
+ name: "Yahoo",
+ alias: null,
+ description: "Yahoo Search",
+ searchForm: "https://search.yahoo.com/yhs/search?p=&ei=UTF-8&hspart=mozilla&hsimp=yhs-001",
+ hidden: false,
+ wrappedJSObject: {
+ queryCharset: "UTF-8",
+ "_iconURL": "data:image/x-icon;base64,AAABAAIAEBAAAAEACAA8DQAAJgAAACAgAAABAAgAowsAAGINAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAAAJcEhZcwAACxMAAAsTAQCanBgAAApPaUNDUFBob3Rvc2hvcCBJQ0MgcHJvZmlsZQAAeNqdU2dUU+kWPffe9EJLiICUS29SFQggUkKLgBSRJiohCRBKiCGh2RVRwRFFRQQbyKCIA46OgIwVUSwMigrYB+Qhoo6Do4iKyvvhe6Nr1rz35s3+tdc+56zznbPPB8AIDJZIM1E1gAypQh4R4IPHxMbh5C5AgQokcAAQCLNkIXP9IwEA+H48PCsiwAe+AAF40wsIAMBNm8AwHIf/D+pCmVwBgIQBwHSROEsIgBQAQHqOQqYAQEYBgJ2YJlMAoAQAYMtjYuMAUC0AYCd/5tMAgJ34mXsBAFuUIRUBoJEAIBNliEQAaDsArM9WikUAWDAAFGZLxDkA2C0AMElXZkgAsLcAwM4QC7IACAwAMFGIhSkABHsAYMgjI3gAhJkAFEbyVzzxK64Q5yoAAHiZsjy5JDlFgVsILXEHV1cuHijOSRcrFDZhAmGaQC7CeZkZMoE0D+DzzAAAoJEVEeCD8/14zg6uzs42jrYOXy3qvwb/ImJi4/7lz6twQAAA4XR+0f4sL7MagDsGgG3+oiXuBGheC6B194tmsg9AtQCg6dpX83D4fjw8RaGQudnZ5eTk2ErEQlthyld9/mfCX8BX/Wz5fjz89/XgvuIkgTJdgUcE+ODCzPRMpRzPkgmEYtzmj0f8twv//B3TIsRJYrlYKhTjURJxjkSajPMypSKJQpIpxSXS/2Ti3yz7Az7fNQCwaj4Be5EtqF1jA/ZLJxBYdMDi9wAA8rtvwdQoCAOAaIPhz3f/7z/9R6AlAIBmSZJxAABeRCQuVMqzP8cIAABEoIEqsEEb9MEYLMAGHMEF3MEL/GA2hEIkxMJCEEIKZIAccmAprIJCKIbNsB0qYC/UQB00wFFohpNwDi7CVbgOPXAP+mEInsEovIEJBEHICBNhIdqIAWKKWCOOCBeZhfghwUgEEoskIMmIFFEiS5E1SDFSilQgVUgd8j1yAjmHXEa6kTvIADKC/Ia8RzGUgbJRPdQMtUO5qDcahEaiC9BkdDGajxagm9BytBo9jDah59CraA/ajz5DxzDA6BgHM8RsMC7Gw0KxOCwJk2PLsSKsDKvGGrBWrAO7ifVjz7F3BBKBRcAJNgR3QiBhHkFIWExYTthIqCAcJDQR2gk3CQOEUcInIpOoS7QmuhH5xBhiMjGHWEgsI9YSjxMvEHuIQ8Q3JBKJQzInuZACSbGkVNIS0kbSblIj6SypmzRIGiOTydpka7IHOZQsICvIheSd5MPkM+Qb5CHyWwqdYkBxpPhT4ihSympKGeUQ5TTlBmWYMkFVo5pS3aihVBE1j1pCraG2Uq9Rh6gTNHWaOc2DFklLpa2ildMaaBdo92mv6HS6Ed2VHk6X0FfSy+lH6JfoA/R3DA2GFYPHiGcoGZsYBxhnGXcYr5hMphnTixnHVDA3MeuY55kPmW9VWCq2KnwVkcoKlUqVJpUbKi9Uqaqmqt6qC1XzVctUj6leU32uRlUzU+OpCdSWq1WqnVDrUxtTZ6k7qIeqZ6hvVD+kfln9iQZZw0zDT0OkUaCxX+O8xiALYxmzeCwhaw2rhnWBNcQmsc3ZfHYqu5j9HbuLPaqpoTlDM0ozV7NS85RmPwfjmHH4nHROCecop5fzforeFO8p4ikbpjRMuTFlXGuqlpeWWKtIq1GrR+u9Nq7tp52mvUW7WfuBDkHHSidcJ0dnj84FnedT2VPdpwqnFk09OvWuLqprpRuhu0R3v26n7pievl6Ankxvp955vef6HH0v/VT9bfqn9UcMWAazDCQG2wzOGDzFNXFvPB0vx9vxUUNdw0BDpWGVYZfhhJG50Tyj1UaNRg+MacZc4yTjbcZtxqMmBiYhJktN6k3umlJNuaYppjtMO0zHzczNos3WmTWbPTHXMueb55vXm9+3YFp4Wiy2qLa4ZUmy5FqmWe62vG6FWjlZpVhVWl2zRq2drSXWu627pxGnuU6TTque1mfDsPG2ybaptxmw5dgG2662bbZ9YWdiF2e3xa7D7pO9k326fY39PQcNh9kOqx1aHX5ztHIUOlY63prOnO4/fcX0lukvZ1jPEM/YM+O2E8spxGmdU5vTR2cXZ7lzg/OIi4lLgssulz4umxvG3ci95Ep09XFd4XrS9Z2bs5vC7ajbr+427mnuh9yfzDSfKZ5ZM3PQw8hD4FHl0T8Ln5Uwa9+sfk9DT4FntecjL2MvkVet17C3pXeq92HvFz72PnKf4z7jPDfeMt5ZX8w3wLfIt8tPw2+eX4XfQ38j/2T/ev/RAKeAJQFnA4mBQYFbAvv4enwhv44/Ottl9rLZ7UGMoLlBFUGPgq2C5cGtIWjI7JCtIffnmM6RzmkOhVB+6NbQB2HmYYvDfgwnhYeFV4Y/jnCIWBrRMZc1d9HcQ3PfRPpElkTem2cxTzmvLUo1Kj6qLmo82je6NLo/xi5mWczVWJ1YSWxLHDkuKq42bmy+3/zt84fineIL43sXmC/IXXB5oc7C9IWnFqkuEiw6lkBMiE44lPBBECqoFowl8hN3JY4KecIdwmciL9E20YjYQ1wqHk7ySCpNepLskbw1eSTFM6Us5bmEJ6mQvEwNTN2bOp4WmnYgbTI9Or0xg5KRkHFCqiFNk7Zn6mfmZnbLrGWFsv7Fbou3Lx6VB8lrs5CsBVktCrZCpuhUWijXKgeyZ2VXZr/Nico5lqueK83tzLPK25A3nO+f/+0SwhLhkralhktXLR1Y5r2sajmyPHF52wrjFQUrhlYGrDy4irYqbdVPq+1Xl65+vSZ6TWuBXsHKgsG1AWvrC1UK5YV969zX7V1PWC9Z37Vh+oadGz4ViYquFNsXlxV/2CjceOUbh2/Kv5nclLSpq8S5ZM9m0mbp5t4tnlsOlqqX5pcObg3Z2rQN31a07fX2Rdsvl80o27uDtkO5o788uLxlp8nOzTs/VKRU9FT6VDbu0t21Ydf4btHuG3u89jTs1dtbvPf9Psm+21UBVU3VZtVl+0n7s/c/romq6fiW+21drU5tce3HA9ID/QcjDrbXudTVHdI9VFKP1ivrRw7HH77+ne93LQ02DVWNnMbiI3BEeeTp9wnf9x4NOtp2jHus4QfTH3YdZx0vakKa8ppGm1Oa+1tiW7pPzD7R1ureevxH2x8PnDQ8WXlK81TJadrpgtOTZ/LPjJ2VnX1+LvncYNuitnvnY87fag9v77oQdOHSRf+L5zu8O85c8rh08rLb5RNXuFearzpfbep06jz+k9NPx7ucu5quuVxrue56vbV7ZvfpG543zt30vXnxFv/W1Z45Pd2983pv98X39d8W3X5yJ/3Oy7vZdyfurbxPvF/0QO1B2UPdh9U/W/7c2O/cf2rAd6Dz0dxH9waFg8/+kfWPD0MFj5mPy4YNhuueOD45OeI/cv3p/KdDz2TPJp4X/qL+y64XFi9++NXr187RmNGhl/KXk79tfKX96sDrGa/bxsLGHr7JeDMxXvRW++3Bd9x3He+j3w9P5Hwgfyj/aPmx9VPQp/uTGZOT/wQDmPP8YzMt2wAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAACZ0lEQVR42mzSP4icZRTF4ee+38xOkp2sG5cQxVJIIaaKkICxTkqJjQhpJFYiop2F1YKFQqoUVpEoCBYSS7dfOxVFWGIsokUE/0TEye7OzPe977XYNWk83b0cDoffvXHWGxkKYjt0N1fi+FaJIzNIFSJ0kDXn0z5nF1O9Sp5PzaizamLD2NELo5W4sOwXqqX/04o1R2wg9PYs/GXUmTjqpGNxwvWdFzz19Akvjj+XUkYTggylFLfml93due+tZ7+y577BrkJnbNWke8yHmzvgi/4lq+WU1XjCsThl2p1ya3GZ4KNrt03KuhXH0SkkkbTOL5+u2PnuZ/D8axtGMTaKsbOvrINP3v/W3Y9XhCJjQCrUWRedVpaq3nvn7oHXrz8jD8PfvnEGbL0716LXytIoxqizkups4R/VwhB7hpi7sXkbXNo86bkrazK5sXnbEHND7BvMLcykOotz3vlxvZw+faRb08VEiVC64rPdSw/pZ/Ly9EutNi3TkHOLOvN3u3OnHNx7MFio5qq5Ifdce/WHhwEfXPnekPuq/UPPQhrAKOV0MFdyRFQFRefr7Z9wRrb0zfYd1aCpGmr2BvtSTkcp1wZLnX0tx4oQjeHX+UF97P75QGspM7VMqTfopVwb0aY1F4ZWlFK1SCVDHQKUEvphj0ztkEdrvZoLtOkoNS2XlkHJIlroIky7Jw8atDSJdQ/aPTUdtJBaLqVmlJpqQataCZKhY/L4HwcEI/Qbv1v8tivbIdVG1UtNnPVmFmPEoT9l/Dc9Ujp42Mx4uGl6I5pmgdjGzaLbopsdJqZHWZnqtKkXcZU8D/8OAPAMQ4kD8KK1AAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAByVJREFUWAm1l1uIldcVx9d3ruMZZzRaay+pCjFJH6LSRqxQqA1NH0pBiH3Qp774kEAg4EOkxKdQSCjUFvpm6YsNVNoSaGjFtmga2yZgCIIawdv04g2kM7Uz6lzO+c758v/t/9lzTB/61Oxhn7332muv9V+3vb8pnooDVRkzZ4oY/LmK6mQZa05frX6yFJ9Ae7x4qd2IuV1FFM9WMfhaI9Z+pQBAL+aiEZ0QgNBm2YuZmxHF9VZMXqmivFaLweUyuteWYvHGVPWr2f+F7YvF/ola9DZGVJsHUXs8YvBEK1ZrXt9URDwqxY1BdGMQvWjGqkgA+iLUtazHuADUoowHYugKTilaR7SIpZjWqOMRfY090RbasS4JglpFtzWIcqwZa+pSqnWVcLLXijXpZCFpvbgb/VhMe8huMLPylWkci8/oSD8xJq7hj4WUWvXrlbqVrUyKtBYdpX3Bh9YbzsdErwRgbZKyFP+KdqxPssu4l2hDAOOxIj6bCHigKWRNCcpMCHHHB4TJLc+TXxKHnC51Ct+Qgxl/TZ0qE5Be/EdWTwjqQuJJAPIB8qAZk4kZoXJnvHH+27Hq0+0YX12PH+w7E3/8zbWkitN2M8pS7kCKZ761OV55c2fcm+nG7J1e7N/+e3m2nbyKQcAhnHWZLC86B1rxiFRvSIkIgJHFVWzZ+qk4fG5HEr4wV8buVb+Vuv5QeVZsi/HeW//eHZ1HbNfLT5+Jc2dndBav9KXugfqc+pLsv6Xxvk6kVheumnpDnXlTVMZWfHh+Li6cdOKvmGzEC69+WTskzwr1SfUJ9ZWp7z/0pWXlF9+ejQtnUdCWnAxQ+al5Tdz80lIVEP8x9eZQWCQwOTAhNc34Re+rUW8U0S+r2Ns8nWzBKgONBOeX3V3RaCpPRN7XeFcO7yYl+InML2U3VdBVHszHzbSXYLBJkuTSQzBuphoYZ7X/u8O30gFAHHxzi+Yop8ETcfDXW5JyKMd/fFuO9l3mYuwLAl5gbMg8QuKdYQg4Zjcxo7HikMeIn37vcizes9Ide9bGhs9NLPN9YX0ndnzHpbZ4vx9HXr6kc6Sobo2hIkuzOnIh0xMFRlvc0waWL+p3UePCQ/Myjjx/JSnl59CJbUkJgl75g+ZD/D978Yrc7EuMPe4ESo6OYsaasiiX7tADAyny5cGtyMHsDxzFnP0Tx6Z0SfsW27B1PHZ+c13seGZdbNo2Lo6Iu7e7cfznfxc/8ggNQBhZI9dSs2c5k+rFaHBXmZhd32xTGdlZPvzDvefj9XddlgeObYVpuf1o3zkpyrEnCJwBDjlmr9i7XP3jgrYkDamhEqRA8UOBxZ53tcOtBbgyzr53M65f8DU6sVZ1o067cfFBvP+XGzrDOa5s+JkTShIc+dBtlLOLlRpqAUDc+yqQMnViNq81edDVnPixno/vP/dXjn2svbbnPa1RiqXEHVkYQ06RWygnFEtpbZDLAJws2X1OHgfCv+hiRkZU8Y+pmbjwzjTE1D48PR1TV+5IMErgsjex2A8TJrqCHH9Cw6U0BGBkPUWrKTZnPq4L9WqIOFvEO8ml+vbRvyUB/Jw6OiUa9GydM58qQl6lTrNHyiENrwyTkOvXLziVkMlOOsesVKyIFtZB1zfDAGvdyj4xtkD7yHQ8Ynn4hCrwvYA+DOJCSlXAZl3MjNQobNzVPK7gJm0AiPsQyEg0c6s1cbEB5X08AmDz1TTLucApzHHyJgADvUqVysJMKOSicLRQl+emOIvbnaw+ot2pSTzl5zzJVjPaZ6ix7zCSN4E1shOAWnqbyYH8bOqd1h9AGJ0qtl6LRBubcBKxbo6xh60kWlbLjgG4NJ2ETkwqbl7SeUXVSCq+BF1C2bWEgEO4CxBGvOydGmu3ooXv7AEogLFqn2JtWKO8yc9xAmDxjhGiWMOQXe63zCvHtIjOpGOIwvGJlhRQepyzaiu0MQ4MnFhuT7CiJQC+sUg4jtOYO+1IH9OdCwgBSmOkP2r60CarHeXMjxw3PGyvOBnN670EgOPOc1yEYgDYCxbqTPDXki1srChi4R6lpQ+uDmVFDtkA5GH1qJEvQFgacqCFT37pyP+Y+DMJs0Y54NgbiIVn61jhEUrNARuNIi3vOQf8iUeQuNzILe4b/jFZ7RDYJhTbVRaJTxyWh8PgO93hQJCBsSa2GQyyoLlBzWDxgnm9l0JgADgNgVxElCH22xs4NCsaieSUyzWXaSTLDAPlGQB0Kt6JaqpzYjkJQT9id60aNwqZjVqlz9Kqp+JcfDjOAqhirNoCI6MelpVPAjZ/CbFv45Y9YNcicqDMKm/Xo/FPJdMlqZ9SIK7qSrrci9mbl6q3/DGQ5f7XuK347rgKeuMgiicEfLPmT0rGY1K5SdI/ryritlMbJrr/PZ8+I8qf9PF8qhMrT39QHfHLkhj/fz/bi+eb83F/VxX1b6jWvt6KdTs/AvvCmqXE235jAAAAAElFTkSuQmCC",
+ _urls : [
+ {
+ type: "application/x-suggestions+json",
+ method: "GET",
+ template: "https://search.yahoo.com/sugg/ff",
+ params: [
+ {
+ name: "output",
+ value: "fxjson",
+ purpose: undefined,
+ },
+ {
+ name: "appid",
+ value: "ffd",
+ purpose: undefined,
+ },
+ {
+ name: "command",
+ value: "{searchTerms}",
+ purpose: undefined,
+ },
+ ],
+ },
+ {
+ type: "text/html",
+ method: "GET",
+ template: "https://search.yahoo.com/yhs/search",
+ params: [
+ {
+ name: "p",
+ value: "{searchTerms}",
+ purpose: undefined,
+ },
+ {
+ name: "ei",
+ value: "UTF-8",
+ purpose: undefined,
+ },
+ {
+ name: "hspart",
+ value: "mozilla",
+ purpose: undefined,
+ },
+ {
+ name: "hsimp",
+ value: "yhs-001",
+ purpose: "searchbar",
+ },
+ {
+ name: "hsimp",
+ value: "yhs-002",
+ purpose: "keyword",
+ },
+ {
+ name: "hsimp",
+ value: "yhs-003",
+ purpose: "homepage",
+ },
+ {
+ name: "hsimp",
+ value: "yhs-004",
+ purpose: "newtab",
+ },
+ {
+ name: "hsimp",
+ value: "yhs-005",
+ purpose: "contextmenu",
+ },
+ {
+ name: "hsimp",
+ value: "yhs-007",
+ purpose: "system",
+ },
+ ],
+ mozparams: {},
+ },
+ ],
+ },
+ };
+
+ isSubObjectOf(EXPECTED_ENGINE, engine, "Yahoo");
+}
diff --git a/browser/components/search/test/browser_yahoo_behavior.js b/browser/components/search/test/browser_yahoo_behavior.js
new file mode 100644
index 000000000..5b2d61422
--- /dev/null
+++ b/browser/components/search/test/browser_yahoo_behavior.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Yahoo search plugin URLs
+ */
+
+"use strict";
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+
+
+function test() {
+ let engine = Services.search.getEngineByName("Yahoo");
+ ok(engine, "Yahoo is installed");
+
+ let previouslySelectedEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+ engine.alias = "y";
+
+ let base = "https://search.yahoo.com/yhs/search?p=foo&ei=UTF-8&hspart=mozilla";
+ let url;
+
+ // Test search URLs (including purposes).
+ url = engine.getSubmission("foo").uri.spec;
+ is(url, base + "&hsimp=yhs-001", "Check search URL for 'foo'");
+
+ waitForExplicitFinish();
+
+ var gCurrTest;
+ var gTests = [
+ {
+ name: "context menu search",
+ searchURL: base + "&hsimp=yhs-005",
+ run: function () {
+ // Simulate a contextmenu search
+ // FIXME: This is a bit "low-level"...
+ BrowserSearch.loadSearch("foo", false, "contextmenu");
+ }
+ },
+ {
+ name: "keyword search",
+ searchURL: base + "&hsimp=yhs-002",
+ run: function () {
+ gURLBar.value = "? foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "keyword search with alias",
+ searchURL: base + "&hsimp=yhs-002",
+ run: function () {
+ gURLBar.value = "y foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "search bar search",
+ searchURL: base + "&hsimp=yhs-001",
+ run: function () {
+ let sb = BrowserSearch.searchBar;
+ sb.focus();
+ sb.value = "foo";
+ registerCleanupFunction(function () {
+ sb.value = "";
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }
+ },
+ {
+ name: "new tab search",
+ searchURL: base + "&hsimp=yhs-004",
+ run: function () {
+ function doSearch(doc) {
+ // Re-add the listener, and perform a search
+ gBrowser.addProgressListener(listener);
+ doc.getElementById("newtab-search-text").value = "foo";
+ doc.getElementById("newtab-search-submit").click();
+ }
+
+ // load about:newtab, but remove the listener first so it doesn't
+ // get in the way
+ gBrowser.removeProgressListener(listener);
+ gBrowser.loadURI("about:newtab");
+ info("Waiting for about:newtab load");
+ tab.linkedBrowser.addEventListener("load", function load(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ info("skipping spurious load event");
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", load, true);
+
+ // Observe page setup
+ let win = gBrowser.contentWindow;
+ if (win.gSearch.currentEngineName ==
+ Services.search.currentEngine.name) {
+ doSearch(win.document);
+ }
+ else {
+ info("Waiting for newtab search init");
+ win.addEventListener("ContentSearchService", function done(event) {
+ info("Got newtab search event " + event.detail.type);
+ if (event.detail.type == "State") {
+ win.removeEventListener("ContentSearchService", done);
+ // Let gSearch respond to the event before continuing.
+ executeSoon(() => doSearch(win.document));
+ }
+ });
+ }
+ }, true);
+ }
+ }
+ ];
+
+ function nextTest() {
+ if (gTests.length) {
+ gCurrTest = gTests.shift();
+ info("Running : " + gCurrTest.name);
+ executeSoon(gCurrTest.run);
+ } else {
+ finish();
+ }
+ }
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let listener = {
+ onStateChange: function onStateChange(webProgress, req, flags, status) {
+ info("onStateChange");
+ // Only care about top-level document starts
+ let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_START;
+ if (!(flags & docStart) || !webProgress.isTopLevel)
+ return;
+
+ if (req.originalURI.spec == "about:blank")
+ return;
+
+ info("received document start");
+
+ ok(req instanceof Ci.nsIChannel, "req is a channel");
+ is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
+ info("Actual URI: " + req.URI.spec);
+
+ req.cancel(Components.results.NS_ERROR_FAILURE);
+
+ executeSoon(nextTest);
+ }
+ }
+
+ registerCleanupFunction(function () {
+ engine.alias = undefined;
+ gBrowser.removeProgressListener(listener);
+ gBrowser.removeTab(tab);
+ Services.search.currentEngine = previouslySelectedEngine;
+ });
+
+ tab.linkedBrowser.addEventListener("load", function load() {
+ tab.linkedBrowser.removeEventListener("load", load, true);
+ gBrowser.addProgressListener(listener);
+ nextTest();
+ }, true);
+}
diff --git a/browser/components/search/test/head.js b/browser/components/search/test/head.js
new file mode 100644
index 000000000..de27b5e1e
--- /dev/null
+++ b/browser/components/search/test/head.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Promise.jsm");
+
+/**
+ * Recursively compare two objects and check that every property of expectedObj has the same value
+ * on actualObj.
+ */
+function isSubObjectOf(expectedObj, actualObj, name) {
+ for (let prop in expectedObj) {
+ if (typeof expectedObj[prop] == 'function')
+ continue;
+ if (expectedObj[prop] instanceof Object) {
+ is(actualObj[prop].length, expectedObj[prop].length, name + "[" + prop + "]");
+ isSubObjectOf(expectedObj[prop], actualObj[prop], name + "[" + prop + "]");
+ } else {
+ is(actualObj[prop], expectedObj[prop], name + "[" + prop + "]");
+ }
+ }
+}
+
+function getLocale() {
+ const localePref = "general.useragent.locale";
+ return getLocalizedPref(localePref, Services.prefs.getCharPref(localePref));
+}
+
+/**
+ * Wrapper for nsIPrefBranch::getComplexValue.
+ * @param aPrefName
+ * The name of the pref to get.
+ * @returns aDefault if the requested pref doesn't exist.
+ */
+function getLocalizedPref(aPrefName, aDefault) {
+ try {
+ return Services.prefs.getComplexValue(aPrefName, Ci.nsIPrefLocalizedString).data;
+ } catch (ex) {
+ return aDefault;
+ }
+}
+
+function promiseEvent(aTarget, aEventName, aPreventDefault) {
+ function cancelEvent(event) {
+ if (aPreventDefault) {
+ event.preventDefault();
+ }
+
+ return true;
+ }
+
+ return BrowserTestUtils.waitForEvent(aTarget, aEventName, false, cancelEvent);
+}
+
+/**
+ * Adds a new search engine to the search service and confirms it completes.
+ *
+ * @param {String} basename The file to load that contains the search engine
+ * details.
+ * @param {Object} [options] Options for the test:
+ * - {String} [iconURL] The icon to use for the search engine.
+ * - {Boolean} [setAsCurrent] Whether to set the new engine to be the
+ * current engine or not.
+ * - {String} [testPath] Used to override the current test path if this
+ * file is used from a different directory.
+ * @returns {Promise} The promise is resolved once the engine is added, or
+ * rejected if the addition failed.
+ */
+function promiseNewEngine(basename, options = {}) {
+ return new Promise((resolve, reject) => {
+ // Default the setAsCurrent option to true.
+ let setAsCurrent =
+ options.setAsCurrent == undefined ? true : options.setAsCurrent;
+ info("Waiting for engine to be added: " + basename);
+ Services.search.init({
+ onInitComplete: function() {
+ let url = getRootDirectory(options.testPath || gTestPath) + basename;
+ let current = Services.search.currentEngine;
+ Services.search.addEngine(url, null, options.iconURL || "", false, {
+ onSuccess: function (engine) {
+ info("Search engine added: " + basename);
+ if (setAsCurrent) {
+ Services.search.currentEngine = engine;
+ }
+ registerCleanupFunction(() => {
+ if (setAsCurrent) {
+ Services.search.currentEngine = current;
+ }
+ Services.search.removeEngine(engine);
+ info("Search engine removed: " + basename);
+ });
+ resolve(engine);
+ },
+ onError: function (errCode) {
+ ok(false, "addEngine failed with error code " + errCode);
+ reject();
+ }
+ });
+ }
+ });
+ });
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url)
+{
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url)
+ BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+
+ return loaded;
+}
+
+// Get an array of the one-off buttons.
+function getOneOffs() {
+ let oneOffs = [];
+ let searchPopup = document.getElementById("PopupSearchAutoComplete");
+ let oneOffsContainer =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
+ let oneOff =
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
+ "search-panel-one-offs");
+ for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+ if (oneOff.nodeType == Node.ELEMENT_NODE) {
+ if (oneOff.classList.contains("dummy") ||
+ oneOff.classList.contains("search-setting-button-compact"))
+ break;
+ oneOffs.push(oneOff);
+ }
+ }
+ return oneOffs;
+}
diff --git a/browser/components/search/test/opensearch.html b/browser/components/search/test/opensearch.html
new file mode 100644
index 000000000..f4c0cc98e
--- /dev/null
+++ b/browser/components/search/test/opensearch.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/testEngine_mozsearch.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/components/search/test/test.html b/browser/components/search/test/test.html
new file mode 100644
index 000000000..a39bece4f
--- /dev/null
+++ b/browser/components/search/test/test.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Bug 426329</title>
+</head>
+<body></body>
+</html>
diff --git a/browser/components/search/test/testEngine.xml b/browser/components/search/test/testEngine.xml
new file mode 100644
index 000000000..21ddc4b9a
--- /dev/null
+++ b/browser/components/search/test/testEngine.xml
@@ -0,0 +1,12 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Foo</ShortName>
+ <Description>Foo Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</moz:SearchForm>
+ <moz:Alias>fooalias</moz:Alias>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/testEngine_diacritics.xml b/browser/components/search/test/testEngine_diacritics.xml
new file mode 100644
index 000000000..0744921eb
--- /dev/null
+++ b/browser/components/search/test/testEngine_diacritics.xml
@@ -0,0 +1,12 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Foo &#9825;</ShortName>
+ <Description>Engine whose ShortName contains non-BMP Unicode characters</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</moz:SearchForm>
+ <moz:Alias>diacriticalias</moz:Alias>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/testEngine_dupe.xml b/browser/components/search/test/testEngine_dupe.xml
new file mode 100644
index 000000000..d2db580c4
--- /dev/null
+++ b/browser/components/search/test/testEngine_dupe.xml
@@ -0,0 +1,12 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>FooDupe</ShortName>
+ <Description>Second Engine Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</moz:SearchForm>
+ <moz:Alias>secondalias</moz:Alias>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/testEngine_mozsearch.xml b/browser/components/search/test/testEngine_mozsearch.xml
new file mode 100644
index 000000000..9b4c02a0c
--- /dev/null
+++ b/browser/components/search/test/testEngine_mozsearch.xml
@@ -0,0 +1,14 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Foo</ShortName>
+ <Description>Foo Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?suggestions&amp;locale={moz:locale}&amp;test={searchTerms}"/>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/">
+ <Param name="test" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="keywordsearch"/>
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="contextsearch"/>
+ </Url>
+ <SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</SearchForm>
+</SearchPlugin>
diff --git a/browser/components/search/test/webapi.html b/browser/components/search/test/webapi.html
new file mode 100644
index 000000000..1ef38b895
--- /dev/null
+++ b/browser/components/search/test/webapi.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+<script>
+function installEngine() {
+ var query = window.location.search.substring(1);
+ var args = JSON.parse(decodeURIComponent(query));
+
+ window.external.AddSearchProvider(...args);
+}
+</script>
+</head>
+<body onload="installEngine()">
+</body>
+</html>