1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
14 * The Original Code is MozMill Test code.
16 * The Initial Developer of the Original Code is Mozilla Foundation.
17 * Portions created by the Initial Developer are Copyright (C) 2009
18 * the Initial Developer. All Rights Reserved.
21 * Henrik Skupin <hskupin@mozilla.com>
23 * Alternatively, the contents of this file may be used under the terms of
24 * either the GNU General Public License Version 2 or later (the "GPL"), or
25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 * in which case the provisions of the GPL or the LGPL are applicable instead
27 * of those above. If you wish to allow use of your version of this file only
28 * under the terms of either the GPL or the LGPL, and not to allow others to
29 * use your version of this file under the terms of the MPL, indicate your
30 * decision by deleting the provisions above and replace them with the notice
31 * and other provisions required by the GPL or the LGPL. If you do not delete
32 * the provisions above, a recipient may use your version of this file under
33 * the terms of any one of the MPL, the GPL or the LGPL.
35 * ***** END LICENSE BLOCK ***** */
39 * The SearchAPI adds support for search related functions like the search bar.
42 // Include required modules
43 var modalDialog = require("modal-dialog");
44 var utils = require("utils");
45 var widgets = require("widgets");
48 const TIMEOUT_REQUEST_SUGGESTIONS = 750;
50 // Helper lookup constants for the engine manager elements
51 const MANAGER_BUTTONS = '/id("engineManager")/anon({"anonid":"buttons"})';
53 // Helper lookup constants for the search bar elements
54 const NAV_BAR = '/id("main-window")/id("tab-view-deck")/{"flex":"1"}' +
55 '/id("navigator-toolbox")/id("nav-bar")';
56 const SEARCH_BAR = NAV_BAR + '/id("search-container")/id("searchbar")';
57 const SEARCH_TEXTBOX = SEARCH_BAR + '/anon({"anonid":"searchbar-textbox"})';
58 const SEARCH_DROPDOWN = SEARCH_TEXTBOX + '/[0]/anon({"anonid":"searchbar-engine-button"})';
59 const SEARCH_POPUP = SEARCH_DROPDOWN + '/anon({"anonid":"searchbar-popup"})';
60 const SEARCH_INPUT = SEARCH_TEXTBOX + '/anon({"class":"autocomplete-textbox-container"})' +
61 '/anon({"anonid":"textbox-input-box"})' +
62 '/anon({"anonid":"input"})';
63 const SEARCH_CONTEXT = SEARCH_TEXTBOX + '/anon({"anonid":"textbox-input-box"})' +
64 '/anon({"anonid":"input-box-contextmenu"})';
65 const SEARCH_GO_BUTTON = SEARCH_TEXTBOX + '/anon({"class":"search-go-container"})' +
66 '/anon({"class":"search-go-button"})';
67 const SEARCH_AUTOCOMPLETE = '/id("main-window")/id("mainPopupSet")/id("PopupAutoComplete")';
72 * @param {MozMillController} controller
73 * MozMillController of the engine manager
75 function engineManager(controller)
77 this._controller = controller;
81 * Search Manager class
83 engineManager.prototype = {
85 * Get the controller of the associated engine manager dialog
87 * @returns Controller of the browser window
88 * @type MozMillController
92 return this._controller;
96 * Gets the list of search engines
98 * @returns List of engines
103 var tree = this.getElement({type: "engine_list"}).getNode();
105 for (var ii = 0; ii < tree.view.rowCount; ii ++) {
106 engines.push({name: tree.view.getCellText(ii, tree.columns.getColumnAt(0)),
107 keyword: tree.view.getCellText(ii, tree.columns.getColumnAt(1))});
114 * Gets the name of the selected search engine
116 * @returns Name of the selected search engine
119 get selectedEngine() {
120 var treeNode = this.getElement({type: "engine_list"}).getNode();
122 if(this.selectedIndex != -1) {
123 return treeNode.view.getCellText(this.selectedIndex,
124 treeNode.columns.getColumnAt(0));
131 * Select the engine with the given name
133 * @param {string} name
134 * Name of the search engine to select
136 set selectedEngine(name) {
137 var treeNode = this.getElement({type: "engine_list"}).getNode();
139 for (var ii = 0; ii < treeNode.view.rowCount; ii ++) {
140 if (name == treeNode.view.getCellText(ii, treeNode.columns.getColumnAt(0))) {
141 this.selectedIndex = ii;
148 * Gets the index of the selected search engine
150 * @returns Index of the selected search engine
153 get selectedIndex() {
154 var tree = this.getElement({type: "engine_list"});
155 var treeNode = tree.getNode();
157 return treeNode.view.selection.currentIndex;
161 * Select the engine with the given index
163 * @param {number} index
164 * Index of the search engine to select
166 set selectedIndex(index) {
167 var tree = this.getElement({type: "engine_list"});
168 var treeNode = tree.getNode();
170 if (index < treeNode.view.rowCount) {
171 widgets.clickTreeCell(this._controller, tree, index, 0, {});
174 this._controller.waitForEval("subject.manager.selectedIndex == subject.newIndex", TIMEOUT, 100,
175 {manager: this, newIndex: index});
179 * Gets the suggestions enabled state
181 get suggestionsEnabled() {
182 var checkbox = this.getElement({type: "suggest"});
184 return checkbox.getNode().checked;
188 * Sets the suggestions enabled state
190 set suggestionsEnabled(state) {
191 var checkbox = this.getElement({type: "suggest"});
192 this._controller.check(checkbox, state);
196 * Close the engine manager
198 * @param {MozMillController} controller
199 * MozMillController of the window to operate on
200 * @param {boolean} saveChanges
201 * (Optional) If true the OK button is clicked otherwise Cancel
203 close : function preferencesDialog_close(saveChanges) {
204 saveChanges = (saveChanges == undefined) ? false : saveChanges;
206 var button = this.getElement({type: "button", subtype: (saveChanges ? "accept" : "cancel")});
207 this._controller.click(button);
211 * Edit the keyword associated to a search engine
213 * @param {string} name
214 * Name of the engine to remove
215 * @param {function} handler
216 * Callback function for Engine Manager
218 editKeyword : function engineManager_editKeyword(name, handler)
220 // Select the search engine
221 this.selectedEngine = name;
223 // Setup the modal dialog handler
224 md = new modalDialog.modalDialog(this._controller.window);
227 var button = this.getElement({type: "engine_button", subtype: "edit"});
228 this._controller.click(button);
233 * Gets all the needed external DTD urls as an array
235 * @returns Array of external DTD urls
238 getDtds : function engineManager_getDtds() {
239 var dtds = ["chrome://browser/locale/engineManager.dtd"];
244 * Retrieve an UI element based on the given spec
246 * @param {object} spec
247 * Information of the UI element which should be retrieved
248 * type: General type information
249 * subtype: Specific element or property
250 * value: Value of the element or property
251 * @returns Element which has been created
254 getElement : function engineManager_getElement(spec) {
259 * subtype: subtype to match
260 * value: value to match
263 elem = new elementslib.ID(this._controller.window.document, "addEngines");
266 elem = new elementslib.Lookup(this._controller.window.document, MANAGER_BUTTONS +
267 '/{"dlgtype":"' + spec.subtype + '"}');
269 case "engine_button":
270 switch(spec.subtype) {
272 elem = new elementslib.ID(this._controller.window.document, "dn");
275 elem = new elementslib.ID(this._controller.window.document, "edit");
278 elem = new elementslib.ID(this._controller.window.document, "remove");
281 elem = new elementslib.ID(this._controller.window.document, "up");
286 elem = new elementslib.ID(this._controller.window.document, "engineList");
289 elem = new elementslib.ID(this._controller.window.document, "enableSuggest");
292 throw new Error(arguments.callee.name + ": Unknown element type - " + spec.type);
299 * Clicks the "Get more search engines..." link
301 getMoreSearchEngines : function engineManager_getMoreSearchEngines() {
302 var link = this.getElement({type: "more_engines"});
303 this._controller.click(link);
307 * Move down the engine with the given name
309 * @param {string} name
310 * Name of the engine to remove
312 moveDownEngine : function engineManager_moveDownEngine(name) {
313 this.selectedEngine = name;
314 var index = this.selectedIndex;
316 var button = this.getElement({type: "engine_button", subtype: "down"});
317 this._controller.click(button);
319 this._controller.waitForEval("subject.manager.selectedIndex == subject.oldIndex + 1", TIMEOUT, 100,
320 {manager: this, oldIndex: index});
324 * Move up the engine with the given name
326 * @param {string} name
327 * Name of the engine to remove
329 moveUpEngine : function engineManager_moveUpEngine(name) {
330 this.selectedEngine = name;
331 var index = this.selectedIndex;
333 var button = this.getElement({type: "engine_button", subtype: "up"});
334 this._controller.click(button);
336 this._controller.waitForEval("subject.manager.selectedIndex == subject.oldIndex - 1", TIMEOUT, 100,
337 {manager: this, oldIndex: index});
341 * Remove the engine with the given name
343 * @param {string} name
344 * Name of the engine to remove
346 removeEngine : function engineManager_removeEngine(name) {
347 this.selectedEngine = name;
349 var button = this.getElement({type: "engine_button", subtype: "remove"});
350 this._controller.click(button);
352 this._controller.waitForEval("subject.manager.selectedEngine != subject.removedEngine", TIMEOUT, 100,
353 {manager: this, removedEngine: name});
357 * Restores the defaults for search engines
359 restoreDefaults : function engineManager_restoreDefaults() {
360 var button = this.getElement({type: "button", subtype: "extra2"});
361 this._controller.click(button);
368 * @param {MozMillController} controller
369 * MozMillController of the browser window to operate on
371 function searchBar(controller)
373 this._controller = controller;
374 this._bss = Cc["@mozilla.org/browser/search-service;1"]
375 .getService(Ci.nsIBrowserSearchService);
379 * Search Manager class
381 searchBar.prototype = {
383 * Get the controller of the associated browser window
385 * @returns Controller of the browser window
386 * @type MozMillController
390 return this._controller;
394 * Get the names of all installed engines
399 var popup = this.getElement({type: "searchBar_dropDownPopup"});
401 for (var ii = 0; ii < popup.getNode().childNodes.length; ii++) {
402 var entry = popup.getNode().childNodes[ii];
403 if (entry.className.indexOf("searchbar-engine") != -1) {
404 engines.push({name: entry.id,
405 selected: entry.selected,
406 tooltipText: entry.getAttribute('tooltiptext')
415 * Get the search engines drop down open state
417 get enginesDropDownOpen()
419 var popup = this.getElement({type: "searchBar_dropDownPopup"});
420 return popup.getNode().state != "closed";
424 * Set the search engines drop down open state
426 set enginesDropDownOpen(newState)
428 if (this.enginesDropDownOpen != newState) {
429 var button = this.getElement({type: "searchBar_dropDown"});
430 this._controller.click(button);
432 this._controller.waitForEval("subject.searchBar.enginesDropDownOpen == subject.newState", TIMEOUT, 100,
433 {searchBar: this, newState: newState });
434 this._controller.sleep(0);
439 * Get the names of all installable engines
441 get installableEngines()
444 var popup = this.getElement({type: "searchBar_dropDownPopup"});
446 for (var ii = 0; ii < popup.getNode().childNodes.length; ii++) {
447 var entry = popup.getNode().childNodes[ii];
448 if (entry.className.indexOf("addengine-item") != -1) {
449 engines.push({name: entry.getAttribute('title'),
450 selected: entry.selected,
451 tooltipText: entry.getAttribute('tooltiptext')
460 * Returns the currently selected search engine
462 * @return Name of the currently selected engine
467 // Open drop down which updates the list of search engines
468 var state = this.enginesDropDownOpen;
469 this.enginesDropDownOpen = true;
471 var engine = this.getElement({type: "engine", subtype: "selected", value: "true"});
472 this._controller.waitForElement(engine, TIMEOUT);
474 this.enginesDropDownOpen = state;
476 return engine.getNode().id;
480 * Select the search engine with the given name
482 * @param {string} name
483 * Name of the search engine to select
485 set selectedEngine(name) {
486 // Open drop down and click on search engine
487 this.enginesDropDownOpen = true;
489 var engine = this.getElement({type: "engine", subtype: "id", value: name});
490 this._controller.waitThenClick(engine, TIMEOUT);
492 // Wait until the drop down has been closed
493 this._controller.waitForEval("subject.searchBar.enginesDropDownOpen == false", TIMEOUT, 100,
496 this._controller.waitForEval("subject.searchBar.selectedEngine == subject.newEngine", TIMEOUT, 100,
497 {searchBar: this, newEngine: name});
501 * Returns all the visible search engines (API call)
505 return this._bss.getVisibleEngines({});
509 * Checks if the correct target URL has been opened for the search
511 * @param {string} searchTerm
512 * Text which should be checked for
514 checkSearchResultPage : function searchBar_checkSearchResultPage(searchTerm) {
515 // Retrieve the URL which is used for the currently selected search engine
516 var targetUrl = this._bss.currentEngine.getSubmission(searchTerm, null).uri;
517 var currentUrl = this._controller.tabs.activeTabWindow.document.location;
519 var domainRegex = /[^\.]+\.([^\.]+)\..+$/gi;
520 var targetDomainName = targetUrl.host.replace(domainRegex, "$1");
521 var currentDomainName = currentUrl.host.replace(domainRegex, "$1");
523 this._controller.assert(function () {
524 return currentDomainName === targetDomainName;
525 }, "Current domain name matches target domain name - got '" +
526 currentDomainName + "', expected '" + targetDomainName + "'");
528 // Check if search term is listed in URL
529 this._controller.assert(function () {
530 return currentUrl.href.toLowerCase().indexOf(searchTerm.toLowerCase()) != -1;
531 }, "Current URL contains the search term - got '" +
532 currentUrl.href.toLowerCase() + "', expected '" + searchTerm.toLowerCase() + "'");
537 * Clear the search field
539 clear : function searchBar_clear()
541 var activeElement = this._controller.window.document.activeElement;
543 var searchInput = this.getElement({type: "searchBar_input"});
544 var cmdKey = utils.getEntity(this.getDtds(), "selectAllCmd.key");
545 this._controller.keypress(searchInput, cmdKey, {accelKey: true});
546 this._controller.keypress(searchInput, 'VK_DELETE', {});
549 activeElement.focus();
553 * Focus the search bar text field
555 * @param {object} event
556 * Specifies the event which has to be used to focus the search bar
558 focus : function searchBar_focus(event)
560 var input = this.getElement({type: "searchBar_input"});
562 switch (event.type) {
564 this._controller.click(input);
567 if (mozmill.isLinux) {
568 var cmdKey = utils.getEntity(this.getDtds(), "searchFocusUnix.commandkey");
570 var cmdKey = utils.getEntity(this.getDtds(), "searchFocus.commandkey");
572 this._controller.keypress(null, cmdKey, {accelKey: true});
575 throw new Error(arguments.callee.name + ": Unknown element type - " + event.type);
578 // Check if the search bar has the focus
579 var activeElement = this._controller.window.document.activeElement;
580 this._controller.assertJS("subject.isFocused == true",
581 {isFocused: input.getNode() == activeElement});
585 * Gets all the needed external DTD urls as an array
587 * @returns Array of external DTD urls
590 getDtds : function searchBar_getDtds() {
591 var dtds = ["chrome://browser/locale/browser.dtd"];
596 * Retrieve an UI element based on the given spec
598 * @param {object} spec
599 * Information of the UI element which should be retrieved
600 * type: General type information
601 * subtype: Specific element or property
602 * value: Value of the element or property
603 * @returns Element which has been created
606 getElement : function searchBar_getElement(spec) {
611 * subtype: subtype to match
612 * value: value to match
615 // XXX: bug 555938 - Mozmill can't fetch the element via a lookup here.
616 // That means we have to grab it temporarily by iterating through all childs.
617 var popup = this.getElement({type: "searchBar_dropDownPopup"}).getNode();
618 for (var ii = 0; ii < popup.childNodes.length; ii++) {
619 var entry = popup.childNodes[ii];
620 if (entry.getAttribute(spec.subtype) == spec.value) {
621 elem = new elementslib.Elem(entry);
625 //elem = new elementslib.Lookup(this._controller.window.document, SEARCH_POPUP +
626 // '/anon({"' + spec.subtype + '":"' + spec.value + '"})');
628 case "engine_manager":
629 // XXX: bug 555938 - Mozmill can't fetch the element via a lookup here.
630 // That means we have to grab it temporarily by iterating through all childs.
631 var popup = this.getElement({type: "searchBar_dropDownPopup"}).getNode();
632 for (var ii = popup.childNodes.length - 1; ii >= 0; ii--) {
633 var entry = popup.childNodes[ii];
634 if (entry.className == "open-engine-manager") {
635 elem = new elementslib.Elem(entry);
639 //elem = new elementslib.Lookup(this._controller.window.document, SEARCH_POPUP +
640 // '/anon({"anonid":"open-engine-manager"})');
643 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_BAR);
645 case "searchBar_autoCompletePopup":
646 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_AUTOCOMPLETE);
648 case "searchBar_contextMenu":
649 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_CONTEXT);
651 case "searchBar_dropDown":
652 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_DROPDOWN);
654 case "searchBar_dropDownPopup":
655 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_POPUP);
657 case "searchBar_goButton":
658 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_GO_BUTTON);
660 case "searchBar_input":
661 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_INPUT);
663 case "searchBar_suggestions":
664 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_AUTOCOMPLETE +
665 '/anon({"anonid":"tree"})');
667 case "searchBar_textBox":
668 elem = new elementslib.Lookup(this._controller.window.document, SEARCH_TEXTBOX);
671 throw new Error(arguments.callee.name + ": Unknown element type - " + spec.type);
678 * Returns the search suggestions for the search term
680 getSuggestions : function(searchTerm) {
681 var suggestions = [ ];
682 var popup = this.getElement({type: "searchBar_autoCompletePopup"});
683 var treeElem = this.getElement({type: "searchBar_suggestions"});
685 // XXX Bug 542990, Bug 392633
686 // Typing too fast can cause several issue like the suggestions not to appear.
687 // Lets type the letters one by one and wait for the popup or the timeout
688 for (var i = 0; i < searchTerm.length; i++) {
690 this.type(searchTerm[i]);
691 this._controller.waitFor(function () {
692 return popup.getNode().state === 'open';
693 }, "", TIMEOUT_REQUEST_SUGGESTIONS);
696 // We are not interested in handling the timeout for now
700 // After entering the search term the suggestions have to be visible
701 this._controller.assert(function () {
702 return popup.getNode().state === 'open';
703 }, "Search suggestions are visible");
704 this._controller.waitForElement(treeElem, TIMEOUT);
706 // Get all suggestions
707 var tree = treeElem.getNode();
708 this._controller.waitForEval("subject.tree.view != null", TIMEOUT, 100,
710 for (var i = 0; i < tree.view.rowCount; i ++) {
711 suggestions.push(tree.view.getCellText(i, tree.columns.getColumnAt(0)));
714 // Close auto-complete popup
715 this._controller.keypress(popup, "VK_ESCAPE", {});
716 this._controller.waitForEval("subject.popup.state == 'closed'", TIMEOUT, 100,
717 {popup: popup.getNode()});
723 * Check if a search engine is installed (API call)
725 * @param {string} name
726 * Name of the search engine to check
728 isEngineInstalled : function searchBar_isEngineInstalled(name)
730 var engine = this._bss.getEngineByName(name);
731 return (engine != null);
735 * Open the Engine Manager
737 * @param {function} handler
738 * Callback function for Engine Manager
740 openEngineManager : function searchBar_openEngineManager(handler)
742 this.enginesDropDownOpen = true;
743 var engineManager = this.getElement({type: "engine_manager"});
745 // Setup the modal dialog handler
746 md = new modalDialog.modalDialog(this._controller.window);
749 // XXX: Bug 555347 - Process any outstanding events before clicking the entry
750 this._controller.sleep(0);
751 this._controller.click(engineManager);
754 this._controller.assert(function () {
755 return this.enginesDropDownOpen == false;
756 }, "The search engine drop down menu has been closed", this);
760 * Remove the search engine with the given name (API call)
762 * @param {string} name
763 * Name of the search engine to remove
765 removeEngine : function searchBar_removeEngine(name)
767 if (this.isEngineInstalled(name)) {
768 var engine = this._bss.getEngineByName(name);
769 this._bss.removeEngine(engine);
774 * Restore the default set of search engines (API call)
776 restoreDefaultEngines : function searchBar_restoreDefaults()
778 // XXX: Bug 556477 - Restore default sorting
779 this.openEngineManager(function(controller) {
780 var manager = new engineManager(controller);
782 // We have to do any action so the restore button gets enabled
783 manager.moveDownEngine(manager.engines[0].name);
784 manager.restoreDefaults();
788 // Update the visibility status for each engine and reset the default engine
789 this._bss.restoreDefaultEngines();
790 this._bss.currentEngine = this._bss.defaultEngine;
792 // Clear any entered search term
797 * Start a search with the given search term and check if the resulting URL
798 * contains the search term.
800 * @param {object} data
801 * Object which contains the search term and the action type
803 search : function searchBar_search(data)
805 var searchBar = this.getElement({type: "searchBar"});
806 this.type(data.text);
808 switch (data.action) {
810 this._controller.keypress(searchBar, 'VK_RETURN', {});
814 this._controller.click(this.getElement({type: "searchBar_goButton"}));
818 this._controller.waitForPageLoad();
819 this.checkSearchResultPage(data.text);
823 * Enter a search term into the search bar
825 * @param {string} searchTerm
826 * Text which should be searched for
828 type : function searchBar_type(searchTerm) {
829 var searchBar = this.getElement({type: "searchBar"});
830 this._controller.type(searchBar, searchTerm);
835 exports.engineManager = engineManager;
836 exports.searchBar = searchBar;