/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is MozMill Test code. * * The Initial Developer of the Original Code is Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2009 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Henrik Skupin * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ /** * @fileoverview * The SearchAPI adds support for search related functions like the search bar. */ const MODULE_NAME = 'SearchAPI'; // Include necessary modules const RELATIVE_ROOT = '.'; const MODULE_REQUIRES = ['ModalDialogAPI', 'UtilsAPI']; const TIMEOUT = 5000; // Helper lookup constants for the engine manager elements const MANAGER_BUTTONS = '/id("engineManager")/anon({"anonid":"buttons"})'; // Helper lookup constants for the search bar elements const NAV_BAR = '/id("main-window")/id("tab-view-deck")/{"flex":"1"}' + '/id("navigator-toolbox")/id("nav-bar")'; const SEARCH_BAR = NAV_BAR + '/id("search-container")/id("searchbar")'; const SEARCH_TEXTBOX = SEARCH_BAR + '/anon({"anonid":"searchbar-textbox"})'; const SEARCH_DROPDOWN = SEARCH_TEXTBOX + '/[0]/anon({"anonid":"searchbar-engine-button"})'; const SEARCH_POPUP = SEARCH_DROPDOWN + '/anon({"anonid":"searchbar-popup"})'; const SEARCH_INPUT = SEARCH_TEXTBOX + '/anon({"class":"autocomplete-textbox-container"})' + '/anon({"anonid":"textbox-input-box"})' + '/anon({"anonid":"input"})'; const SEARCH_CONTEXT = SEARCH_TEXTBOX + '/anon({"anonid":"textbox-input-box"})' + '/anon({"anonid":"input-box-contextmenu"})'; const SEARCH_GO_BUTTON = SEARCH_TEXTBOX + '/anon({"class":"search-go-container"})' + '/anon({"class":"search-go-button"})'; const SEARCH_AUTOCOMPLETE = '/id("main-window")/id("mainPopupSet")/id("PopupAutoComplete")'; /** * Constructor * * @param {MozMillController} controller * MozMillController of the engine manager */ function engineManager(controller) { this._controller = controller; this._ModalDialogAPI = collector.getModule('ModalDialogAPI'); this._WidgetsAPI = collector.getModule('WidgetsAPI'); } /** * Search Manager class */ engineManager.prototype = { /** * Get the controller of the associated engine manager dialog * * @returns Controller of the browser window * @type MozMillController */ get controller() { return this._controller; }, /** * Gets the list of search engines * * @returns List of engines * @type object */ get engines() { var engines = [ ]; var tree = this.getElement({type: "engine_list"}).getNode(); for (var ii = 0; ii < tree.view.rowCount; ii ++) { engines.push({name: tree.view.getCellText(ii, tree.columns.getColumnAt(0)), keyword: tree.view.getCellText(ii, tree.columns.getColumnAt(1))}); } return engines; }, /** * Gets the name of the selected search engine * * @returns Name of the selected search engine * @type string */ get selectedEngine() { var treeNode = this.getElement({type: "engine_list"}).getNode(); if(this.selectedIndex != -1) { return treeNode.view.getCellText(this.selectedIndex, treeNode.columns.getColumnAt(0)); } else { return null; } }, /** * Select the engine with the given name * * @param {string} name * Name of the search engine to select */ set selectedEngine(name) { var treeNode = this.getElement({type: "engine_list"}).getNode(); for (var ii = 0; ii < treeNode.view.rowCount; ii ++) { if (name == treeNode.view.getCellText(ii, treeNode.columns.getColumnAt(0))) { this.selectedIndex = ii; break; } } }, /** * Gets the index of the selected search engine * * @returns Index of the selected search engine * @type number */ get selectedIndex() { var tree = this.getElement({type: "engine_list"}); var treeNode = tree.getNode(); return treeNode.view.selection.currentIndex; }, /** * Select the engine with the given index * * @param {number} index * Index of the search engine to select */ set selectedIndex(index) { var tree = this.getElement({type: "engine_list"}); var treeNode = tree.getNode(); if (index < treeNode.view.rowCount) { this._WidgetsAPI.clickTreeCell(this._controller, tree, index, 0, {}); } this._controller.waitForEval("subject.manager.selectedIndex == subject.newIndex", TIMEOUT, 100, {manager: this, newIndex: index}); }, /** * Gets the suggestions enabled state */ get suggestionsEnabled() { var checkbox = this.getElement({type: "suggest"}); return checkbox.getNode().checked; }, /** * Sets the suggestions enabled state */ set suggestionsEnabled(state) { var checkbox = this.getElement({type: "suggest"}); this._controller.check(checkbox, state); }, /** * Close the engine manager * * @param {MozMillController} controller * MozMillController of the window to operate on * @param {boolean} saveChanges * (Optional) If true the OK button is clicked otherwise Cancel */ close : function preferencesDialog_close(saveChanges) { saveChanges = (saveChanges == undefined) ? false : saveChanges; var button = this.getElement({type: "button", subtype: (saveChanges ? "accept" : "cancel")}); this._controller.click(button); }, /** * Edit the keyword associated to a search engine * * @param {string} name * Name of the engine to remove * @param {function} handler * Callback function for Engine Manager */ editKeyword : function engineManager_editKeyword(name, handler) { if (!handler) throw new Error(arguments.callee.name + ": No callback handler specified."); // Select the search engine this.selectedEngine = name; // Setup the modal dialog handler md = new this._ModalDialogAPI.modalDialog(handler); md.start(200); var button = this.getElement({type: "engine_button", subtype: "edit"}); this._controller.click(button); // XXX: We have to wait a bit more, so the modal dialog handler can kick in. Otherwise // we continue executing remaining tests too early. this._controller.sleep(400); }, /** * Gets all the needed external DTD urls as an array * * @returns Array of external DTD urls * @type [string] */ getDtds : function engineManager_getDtds() { var dtds = ["chrome://browser/locale/engineManager.dtd"]; return dtds; }, /** * Retrieve an UI element based on the given spec * * @param {object} spec * Information of the UI element which should be retrieved * type: General type information * subtype: Specific element or property * value: Value of the element or property * @returns Element which has been created * @type ElemBase */ getElement : function engineManager_getElement(spec) { var elem = null; switch(spec.type) { /** * subtype: subtype to match * value: value to match */ case "more_engines": elem = new elementslib.ID(this._controller.window.document, "addEngines"); break; case "button": elem = new elementslib.Lookup(this._controller.window.document, MANAGER_BUTTONS + '/{"dlgtype":"' + spec.subtype + '"}'); break; case "engine_button": switch(spec.subtype) { case "down": elem = new elementslib.ID(this._controller.window.document, "dn"); break; case "edit": elem = new elementslib.ID(this._controller.window.document, "edit"); break; case "remove": elem = new elementslib.ID(this._controller.window.document, "remove"); break; case "up": elem = new elementslib.ID(this._controller.window.document, "up"); break; } break; case "engine_list": elem = new elementslib.ID(this._controller.window.document, "engineList"); break; case "suggest": elem = new elementslib.ID(this._controller.window.document, "enableSuggest"); break; default: throw new Error(arguments.callee.name + ": Unknown element type - " + spec.type); } return elem; }, /** * Clicks the "Get more search engines..." link */ getMoreSearchEngines : function engineManager_getMoreSearchEngines() { var link = this.getElement({type: "more_engines"}); this._controller.click(link); }, /** * Move down the engine with the given name * * @param {string} name * Name of the engine to remove */ moveDownEngine : function engineManager_moveDownEngine(name) { this.selectedEngine = name; var index = this.selectedIndex; var button = this.getElement({type: "engine_button", subtype: "down"}); this._controller.click(button); this._controller.waitForEval("subject.manager.selectedIndex == subject.oldIndex + 1", TIMEOUT, 100, {manager: this, oldIndex: index}); }, /** * Move up the engine with the given name * * @param {string} name * Name of the engine to remove */ moveUpEngine : function engineManager_moveUpEngine(name) { this.selectedEngine = name; var index = this.selectedIndex; var button = this.getElement({type: "engine_button", subtype: "up"}); this._controller.click(button); this._controller.waitForEval("subject.manager.selectedIndex == subject.oldIndex - 1", TIMEOUT, 100, {manager: this, oldIndex: index}); }, /** * Remove the engine with the given name * * @param {string} name * Name of the engine to remove */ removeEngine : function engineManager_removeEngine(name) { this.selectedEngine = name; var button = this.getElement({type: "engine_button", subtype: "remove"}); this._controller.click(button); this._controller.waitForEval("subject.manager.selectedEngine != subject.removedEngine", TIMEOUT, 100, {manager: this, removedEngine: name}); }, /** * Restores the defaults for search engines */ restoreDefaults : function engineManager_restoreDefaults() { var button = this.getElement({type: "button", subtype: "extra2"}); this._controller.click(button); } }; /** * Constructor * * @param {MozMillController} controller * MozMillController of the browser window to operate on */ function searchBar(controller) { this._controller = controller; this._bss = Cc["@mozilla.org/browser/search-service;1"] .getService(Ci.nsIBrowserSearchService); this._ModalDialogAPI = collector.getModule('ModalDialogAPI'); this._utilsAPI = collector.getModule('UtilsAPI'); } /** * Search Manager class */ searchBar.prototype = { /** * Get the controller of the associated browser window * * @returns Controller of the browser window * @type MozMillController */ get controller() { return this._controller; }, /** * Get the names of all installed engines */ get engines() { var engines = [ ]; var popup = this.getElement({type: "searchBar_dropDownPopup"}); for (var ii = 0; ii < popup.getNode().childNodes.length; ii++) { var entry = popup.getNode().childNodes[ii]; if (entry.className.indexOf("searchbar-engine") != -1) { engines.push({name: entry.id, selected: entry.selected, tooltipText: entry.getAttribute('tooltiptext') }); } } return engines; }, /** * Get the search engines drop down open state */ get enginesDropDownOpen() { var popup = this.getElement({type: "searchBar_dropDownPopup"}); return popup.getNode().state != "closed"; }, /** * Set the search engines drop down open state */ set enginesDropDownOpen(newState) { if (this.enginesDropDownOpen != newState) { var button = this.getElement({type: "searchBar_dropDown"}); this._controller.click(button); this._controller.waitForEval("subject.searchBar.enginesDropDownOpen == subject.newState", TIMEOUT, 100, {searchBar: this, newState: newState }); this._controller.sleep(0); } }, /** * Get the names of all installable engines */ get installableEngines() { var engines = [ ]; var popup = this.getElement({type: "searchBar_dropDownPopup"}); for (var ii = 0; ii < popup.getNode().childNodes.length; ii++) { var entry = popup.getNode().childNodes[ii]; if (entry.className.indexOf("addengine-item") != -1) { engines.push({name: entry.getAttribute('title'), selected: entry.selected, tooltipText: entry.getAttribute('tooltiptext') }); } } return engines; }, /** * Returns the currently selected search engine * * @return Name of the currently selected engine * @type string */ get selectedEngine() { // Open drop down which updates the list of search engines var state = this.enginesDropDownOpen; this.enginesDropDownOpen = true; var engine = this.getElement({type: "engine", subtype: "selected", value: "true"}); this._controller.waitForElement(engine, TIMEOUT); this.enginesDropDownOpen = state; return engine.getNode().id; }, /** * Select the search engine with the given name * * @param {string} name * Name of the search engine to select */ set selectedEngine(name) { // Open drop down and click on search engine this.enginesDropDownOpen = true; var engine = this.getElement({type: "engine", subtype: "id", value: name}); this._controller.waitThenClick(engine, TIMEOUT); // Wait until the drop down has been closed this._controller.waitForEval("subject.searchBar.enginesDropDownOpen == false", TIMEOUT, 100, {searchBar: this}); this._controller.waitForEval("subject.searchBar.selectedEngine == subject.newEngine", TIMEOUT, 100, {searchBar: this, newEngine: name}); }, /** * Returns all the visible search engines (API call) */ get visibleEngines() { return this._bss.getVisibleEngines({}); }, /** * Checks if the correct target URL has been opened for the search * * @param {string} searchTerm * Text which should be checked for */ checkSearchResultPage : function searchBar_checkSearchResultPage(searchTerm) { // Retrieve the URL which is used for the currently selected search engine var targetUrl = this._bss.currentEngine.getSubmission(searchTerm, null).uri; var currentUrl = this._controller.tabs.activeTabWindow.document.location.href; // Check if pure domain names are identical var domainName = targetUrl.host.replace(/.+\.(\w+)\.\w+$/gi, "$1"); var index = currentUrl.indexOf(domainName); this._controller.assertJS("subject.URLContainsDomain == true", {URLContainsDomain: currentUrl.indexOf(domainName) != -1}); // Check if search term is listed in URL this._controller.assertJS("subject.URLContainsText == true", {URLContainsText: currentUrl.toLowerCase().indexOf(searchTerm.toLowerCase()) != -1}); }, /** * Clear the search field */ clear : function searchBar_clear() { var activeElement = this._controller.window.document.activeElement; var searchInput = this.getElement({type: "searchBar_input"}); var cmdKey = this._utilsAPI.getEntity(this.getDtds(), "selectAllCmd.key"); this._controller.keypress(searchInput, cmdKey, {accelKey: true}); this._controller.keypress(searchInput, 'VK_DELETE', {}); if (activeElement) activeElement.focus(); }, /** * Focus the search bar text field * * @param {object} event * Specifies the event which has to be used to focus the search bar */ focus : function searchBar_focus(event) { var input = this.getElement({type: "searchBar_input"}); switch (event.type) { case "click": this._controller.click(input); break; case "shortcut": if (mozmill.isLinux) { var cmdKey = this._utilsAPI.getEntity(this.getDtds(), "searchFocusUnix.commandkey"); } else { var cmdKey = this._utilsAPI.getEntity(this.getDtds(), "searchFocus.commandkey"); } this._controller.keypress(null, cmdKey, {accelKey: true}); break; default: throw new Error(arguments.callee.name + ": Unknown element type - " + event.type); } // Check if the search bar has the focus var activeElement = this._controller.window.document.activeElement; this._controller.assertJS("subject.isFocused == true", {isFocused: input.getNode() == activeElement}); }, /** * Gets all the needed external DTD urls as an array * * @returns Array of external DTD urls * @type [string] */ getDtds : function searchBar_getDtds() { var dtds = ["chrome://browser/locale/browser.dtd"]; return dtds; }, /** * Retrieve an UI element based on the given spec * * @param {object} spec * Information of the UI element which should be retrieved * type: General type information * subtype: Specific element or property * value: Value of the element or property * @returns Element which has been created * @type ElemBase */ getElement : function searchBar_getElement(spec) { var elem = null; switch(spec.type) { /** * subtype: subtype to match * value: value to match */ case "engine": // XXX: bug 555938 - Mozmill can't fetch the element via a lookup here. // That means we have to grab it temporarily by iterating through all childs. var popup = this.getElement({type: "searchBar_dropDownPopup"}).getNode(); for (var ii = 0; ii < popup.childNodes.length; ii++) { var entry = popup.childNodes[ii]; if (entry.getAttribute(spec.subtype) == spec.value) { elem = new elementslib.Elem(entry); break; } } //elem = new elementslib.Lookup(this._controller.window.document, SEARCH_POPUP + // '/anon({"' + spec.subtype + '":"' + spec.value + '"})'); break; case "engine_manager": // XXX: bug 555938 - Mozmill can't fetch the element via a lookup here. // That means we have to grab it temporarily by iterating through all childs. var popup = this.getElement({type: "searchBar_dropDownPopup"}).getNode(); for (var ii = popup.childNodes.length - 1; ii >= 0; ii--) { var entry = popup.childNodes[ii]; if (entry.className == "open-engine-manager") { elem = new elementslib.Elem(entry); break; } } //elem = new elementslib.Lookup(this._controller.window.document, SEARCH_POPUP + // '/anon({"anonid":"open-engine-manager"})'); break; case "searchBar": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_BAR); break; case "searchBar_autoCompletePopup": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_AUTOCOMPLETE); break; case "searchBar_contextMenu": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_CONTEXT); break; case "searchBar_dropDown": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_DROPDOWN); break; case "searchBar_dropDownPopup": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_POPUP); break; case "searchBar_goButton": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_GO_BUTTON); break; case "searchBar_input": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_INPUT); break; case "searchBar_suggestions": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_AUTOCOMPLETE + '/anon({"anonid":"tree"})'); break; case "searchBar_textBox": elem = new elementslib.Lookup(this._controller.window.document, SEARCH_TEXTBOX); break; default: throw new Error(arguments.callee.name + ": Unknown element type - " + spec.type); } return elem; }, /** * Returns the search suggestions for the search term */ getSuggestions : function(searchTerm) { var suggestions = [ ]; var popup = this.getElement({type: "searchBar_autoCompletePopup"}); var treeElem = this.getElement({type: "searchBar_suggestions"}); // Enter search term and wait for the popup this.type(searchTerm); this._controller.waitForEval("subject.popup.state == 'open'", TIMEOUT, 100, {popup: popup.getNode()}); this._controller.waitForElement(treeElem, TIMEOUT); // Get all suggestions var tree = treeElem.getNode(); this._controller.waitForEval("subject.tree.view != null", TIMEOUT, 100, {tree: tree}); for (var i = 0; i < tree.view.rowCount; i ++) { suggestions.push(tree.view.getCellText(i, tree.columns.getColumnAt(0))); } // Close auto-complete popup this._controller.keypress(popup, "VK_ESCAPE", {}); this._controller.waitForEval("subject.popup.state == 'closed'", TIMEOUT, 100, {popup: popup.getNode()}); return suggestions; }, /** * Check if a search engine is installed (API call) * * @param {string} name * Name of the search engine to check */ isEngineInstalled : function searchBar_isEngineInstalled(name) { var engine = this._bss.getEngineByName(name); return (engine != null); }, /** * Open the Engine Manager * * @param {function} handler * Callback function for Engine Manager */ openEngineManager : function searchBar_openEngineManager(handler) { if (!handler) throw new Error(arguments.callee.name + ": No callback handler specified."); this.enginesDropDownOpen = true; var engineManager = this.getElement({type: "engine_manager"}); // Setup the modal dialog handler md = new this._ModalDialogAPI.modalDialog(handler); md.start(); // XXX: Bug 555347 - Process any outstanding events before clicking the entry this._controller.sleep(0); this._controller.click(engineManager); // Wait until the drop down has been closed this._controller.waitForEval("subject.search.enginesDropDownOpen == false", TIMEOUT, 100, {search: this}); // XXX: We have to wait a bit more, so the modal dialog handler can kick in. Otherwise // we continue executing remaining tests too early. this._controller.sleep(200); }, /** * Remove the search engine with the given name (API call) * * @param {string} name * Name of the search engine to remove */ removeEngine : function searchBar_removeEngine(name) { if (this.isEngineInstalled(name)) { var engine = this._bss.getEngineByName(name); this._bss.removeEngine(engine); } }, /** * Restore the default set of search engines (API call) */ restoreDefaultEngines : function searchBar_restoreDefaults() { // XXX: Bug 556477 - Restore default sorting this.openEngineManager(function(controller) { var manager = new engineManager(controller); // We have to do any action so the restore button gets enabled manager.moveDownEngine(manager.engines[0].name); manager.restoreDefaults(); manager.close(true); }); // Update the visibility status for each engine and reset the default engine this._bss.restoreDefaultEngines(); this._bss.currentEngine = this._bss.defaultEngine; // Clear any entered search term this.clear(); }, /** * Start a search with the given search term and check if the resulting URL * contains the search term. * * @param {object} data * Object which contains the search term and the action type */ search : function searchBar_search(data) { var searchBar = this.getElement({type: "searchBar"}); this.type(data.text); switch (data.action) { case "returnKey": this._controller.keypress(searchBar, 'VK_RETURN', {}); break; case "goButton": default: this._controller.click(this.getElement({type: "searchBar_goButton"})); break; } this._controller.waitForPageLoad(); this.checkSearchResultPage(data.text); }, /** * Enter a search term into the search bar * * @param {string} searchTerm * Text which should be searched for */ type : function searchBar_type(searchTerm) { var searchBar = this.getElement({type: "searchBar"}); this._controller.type(searchBar, searchTerm); } };