diff options
Diffstat (limited to 'browser/components/search')
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,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", + _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 ♡</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&locale={moz:locale}&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> |