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 the Mozilla Foundation.
17 * Portions created by the Initial Developer are Copyright (C) 2010
18 * the Initial Developer. All Rights Reserved.
21 * Henrik Skupin <hskupin@mozilla.com>
22 * Adrian Kalla <akalla@aviary.pl>
24 * Alternatively, the contents of this file may be used under the terms of
25 * either the GNU General Public License Version 2 or later (the "GPL"), or
26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 * in which case the provisions of the GPL or the LGPL are applicable instead
28 * of those above. If you wish to allow use of your version of this file only
29 * under the terms of either the GPL or the LGPL, and not to allow others to
30 * use your version of this file under the terms of the MPL, indicate your
31 * decision by deleting the provisions above and replace them with the notice
32 * and other provisions required by the GPL or the LGPL. If you do not delete
33 * the provisions above, a recipient may use your version of this file under
34 * the terms of any one of the MPL, the GPL or the LGPL.
36 * ***** END LICENSE BLOCK ***** */
38 // Include required modules
39 var modalDialog = require("modal-dialog");
40 var utils = require("utils");
44 * Unwraps a node which is wrapped into a XPCNativeWrapper or XrayWrapper
46 * @param {DOMnode} Wrapped DOM node
47 * @returns {DOMNode} Unwrapped DOM node
49 function unwrapNode(aNode) {
53 // unwrap is not available on older branches (3.5 and 3.6) - Bug 533596
54 if ("unwrap" in XPCNativeWrapper) {
55 node = XPCNativeWrapper.unwrap(node);
57 else if ("wrappedJSObject" in node) {
58 node = node.wrappedJSObject;
67 * DOMWalker Constructor
69 * @param {MozMillController} controller
70 * MozMill controller of the window to operate on.
71 * @param {Function} callbackFilter
72 * callback-method to filter nodes
73 * @param {Function} callbackNodeTest
74 * callback-method to test accepted nodes
75 * @param {Function} callbackResults
76 * callback-method to process the results
77 * [optional - default: undefined]
79 function DOMWalker(controller, callbackFilter, callbackNodeTest,
82 this._controller = controller;
83 this._callbackFilter = callbackFilter;
84 this._callbackNodeTest = callbackNodeTest;
85 this._callbackResults = callbackResults;
88 DOMWalker.FILTER_ACCEPT = 1;
89 DOMWalker.FILTER_REJECT = 2;
90 DOMWalker.FILTER_SKIP = 3;
92 DOMWalker.GET_BY_ID = "id";
93 DOMWalker.GET_BY_SELECTOR = "selector";
95 DOMWalker.WINDOW_CURRENT = 1;
96 DOMWalker.WINDOW_MODAL = 2;
97 DOMWalker.WINDOW_NEW = 4;
99 DOMWalker.prototype = {
101 * Returns the filter-callback
105 get callbackFilter() {
106 return this._callbackFilter;
110 * Returns the node-testing-callback
114 get callbackNodeTest() {
115 return this._callbackNodeTest;
119 * Returns the results-callback
123 get callbackResults() {
124 return this._callbackResults;
128 * Returns the MozMill controller
130 * @returns Mozmill controller
131 * @type {MozMillController}
134 return this._controller;
138 * The main DOMWalker function.
140 * It start's the _walk-method for a given window or other dialog, runs
141 * a callback to process the results for that window/dialog.
142 * After that switches to provided new windows/dialogs.
144 * @param {array of objects} ids
145 * Contains informations on the elements to open while
146 * Object-elements: getBy - attribute-name of the attribute
147 * containing the identification
148 * information for the opener-element
149 * subContent - array of ids of the opener-elements
150 * in the window with the value of
151 * the above getBy-attribute
152 * target - information, where the new
153 * elements will be opened
155 * title - title of the opened dialog/window
156 * waitFunction - The function used as an argument
157 * for MozmillController.waitFor to
158 * wait before starting the walk.
159 * [optional - default: no waiting]
160 * windowHandler - Window instance
161 * [only needed for some tests]
164 * Node to start testing from
165 * [optional - default: this._controller.window.document.documentElement]
166 * @param {Function} waitFunction
167 * The function used as an argument for MozmillController.waitFor to
168 * wait before starting the walk.
169 * [optional - default: no waiting]
171 walk : function DOMWalker_walk(ids, root, waitFunction) {
172 if (typeof waitFunction == 'function')
173 this._controller.waitFor(waitFunction());
176 root = this._controller.window.document.documentElement;
178 var resultsArray = this._walk(root);
180 if (typeof this._callbackResults == 'function')
181 this._callbackResults(this._controller, resultsArray);
184 this._prepareTargetWindows(ids);
188 * DOMWalker_filter filters a given node by submitting it to the
189 * this._callbackFilter method to decide, if it should be submitted to
190 * a provided this._callbackNodeTest method for testing (that hapens in case
192 * In case of FILTER_ACCEPT and FILTER_SKIP, the children of such a node
193 * will be filtered recursively.
194 * Nodes with the nodeStatus "FILTER_REJECT" and their descendants will be
195 * completetly ignored.
199 * @param {array of elements} collectedResults
200 * An array with gathered all results from testing a given element
201 * @returns An array with gathered all results from testing a given element
202 * @type {array of elements}
204 _filter : function DOMWalker_filter(node, collectedResults) {
205 var nodeStatus = this._callbackFilter(node);
207 var nodeTestResults = [];
209 switch (nodeStatus) {
210 case DOMWalker.FILTER_ACCEPT:
211 nodeTestResults = this._callbackNodeTest(node);
212 collectedResults = collectedResults.concat(nodeTestResults);
213 // no break here as we have to perform the _walk below too
214 case DOMWalker.FILTER_SKIP:
215 nodeTestResults = this._walk(node);
221 collectedResults = collectedResults.concat(nodeTestResults);
223 return collectedResults;
227 * Retrieves and returns a wanted node based on the provided identification
230 * @param {array of objects} idSet
231 * Contains informations on the elements to open while
232 * Object-elements: getBy - attribute-name of the attribute
233 * containing the identification
234 * information for the opener-element
235 * subContent - array of ids of the opener-elements
236 * in the window with the value of
237 * the above getBy-attribute
238 * target - information, where the new
239 * elements will be opened
241 * title - title of the opened dialog/window
242 * waitFunction - The function used as an argument
243 * for MozmillController.waitFor to
244 * wait before starting the walk.
245 * [optional - default: no waiting]
246 * windowHandler - Window instance
247 * [only needed for some tests]
252 _getNode : function DOMWalker_getNode(idSet) {
253 var doc = this._controller.window.document;
255 // QuerySelector seems to be unusuale for id's in this case:
256 // https://developer.mozilla.org/En/Code_snippets/QuerySelector
257 switch (idSet.getBy) {
258 case DOMWalker.GET_BY_ID:
259 return doc.getElementById(idSet[idSet.getBy]);
260 case DOMWalker.GET_BY_SELECTOR:
261 return doc.querySelector(idSet[idSet.getBy]);
263 throw new Error("Not supported getBy-attribute: " + idSet.getBy);
268 * Main entry point to open new elements like windows, tabpanels, prefpanes,
271 * @param {array of objects} ids
272 * Contains informations on the elements to open while
273 * Object-elements: getBy - attribute-name of the attribute
274 * containing the identification
275 * information for the opener-element
276 * subContent - array of ids of the opener-elements
277 * in the window with the value of
278 * the above getBy-attribute
279 * target - information, where the new
280 * elements will be opened
282 * title - title of the opened dialog/window
283 * waitFunction - The function used as an argument
284 * for MozmillController.waitFor to
285 * wait before starting the walk.
286 * [optional - default: no waiting]
287 * windowHandler - Window instance
288 * [only needed for some tests]
290 _prepareTargetWindows : function DOMWalker_prepareTargetWindows(ids) {
291 var doc = this._controller.window.document;
293 // Go through all the provided ids
294 for (var i = 0; i < ids.length; i++) {
295 var node = this._getNode(ids[i]);
297 // Go further only, if the needed element exists
301 // Decide if what we want to open is a new normal/modal window or if it
302 // will be opened in the current window.
303 switch (idSet.target) {
304 case DOMWalker.WINDOW_CURRENT:
305 this._processNode(node, idSet);
307 case DOMWalker.WINDOW_MODAL:
308 // Modal windows have to be able to access that informations
309 var modalInfos = {ids : idSet.subContent,
310 callbackFilter : this._callbackFilter,
311 callbackNodeTest : this._callbackNodeTest,
312 callbackResults : this._callbackResults,
313 waitFunction : idSet.waitFunction}
314 persisted.modalInfos = modalInfos;
316 var md = new modalDialog.modalDialog(this._controller.window);
317 md.start(this._modalWindowHelper);
319 this._processNode(node, idSet);
322 case DOMWalker.WINDOW_NEW:
323 this._processNode(node, idSet);
325 // Get the new non-modal window controller
326 var controller = utils.handleWindow('title', idSet.title,
329 // Start a new DOMWalker instance
330 let domWalker = new DOMWalker(controller, this._callbackFilter,
331 this._callbackNodeTest,
332 this._callbackResults);
333 domWalker.walk(idSet.subContent,
334 controller.window.document.documentElement,
338 controller.window.close();
341 throw new Error("Node does not exist: " + ids[i][ids[i].getBy]);
348 * Opens new windows/dialog and starts the DOMWalker.walk() in case of dialogs
349 * in existing windows.
351 * @param {Node} activeNode
352 * Node that holds the information which way
353 * to open the new window/dialog
354 * @param {object} idSet
355 * ID set for the element to open
357 _processNode: function DOMWalker_processNode(activeNode, idSet) {
358 var doc = this._controller.window.document;
359 var nodeToProcess = this._getNode(idSet);
361 // Opens a new window/dialog through a menulist and runs DOMWalker.walk()
363 // If the wanted window/dialog is already selected, just run this function
364 // recursively for it's descendants.
365 if (activeNode.localName == "menulist") {
366 if (nodeToProcess.value != idSet.value) {
367 var dropDown = new elementslib.Elem(nodeToProcess);
368 this._controller.waitForElement(dropDown);
370 this._controller.select(dropDown, null, null, idSet.value);
372 this._controller.waitFor(function() {
373 return nodeToProcess.value == idSet.value;
374 }, "The menu item did not load in time: " + idSet.value);
376 // If the target is a new modal/non-modal window, this.walk() has to be
377 // started by the method opening that window. If not, we do it here.
378 if (idSet.target == DOMWalker.WINDOW_CURRENT)
379 this.walk(idSet.subContent, null, idSet.waitFunction);
380 } else if (nodeToProcess.selected && idSet.subContent &&
381 idSet.subContent.length > 0) {
382 this._prepareTargetWindows(idSet.subContent);
386 // Opens a new prefpane using a provided windowHandler object
387 // and runs DOMWalker.walk() for it.
388 // If the wanted prefpane is already selected, just run this function
389 // recursively for it's descendants.
390 else if (activeNode.localName == "prefpane") {
391 var windowHandler = idSet.windowHandler;
393 if (windowHandler.paneId != idSet.id) {
394 windowHandler.paneId = idSet.id;
396 // Wait for the pane's content to load and to be fully displayed
397 this._controller.waitFor(function() {
398 return (nodeToProcess.loaded &&
400 nodeToProcess.style.opacity == 1 ||
401 nodeToProcess.style.opacity == null));
402 }, "The pane did not load in time: " + idSet.id);
404 // If the target is a new modal/non-modal window, this.walk() has to be
405 // started by the method opening that window. If not, we do it here.
406 if (idSet.target == DOMWalker.WINDOW_CURRENT)
407 this.walk(idSet.subContent, null, idSet.waitFunction);
408 } else if (windowHandler.paneId == idSet.id && idSet.subContent &&
409 idSet.subContent.length > 0) {
410 this._prepareTargetWindows(idSet.subContent);
414 // Switches to another tab and runs DOMWalker.walk() for it.
415 // If the wanted tabpanel is already selected, just run this function
416 // recursively for it's descendants.
417 else if (activeNode.localName == "tab") {
418 if (nodeToProcess.selected != true) {
419 this._controller.click(new elementslib.Elem(nodeToProcess));
421 // If the target is a new modal/non-modal window, this.walk() has to be
422 // started by the method opening that window. If not, we do it here.
423 if (idSet.target == DOMWalker.WINDOW_CURRENT)
424 this.walk(idSet.subContent, null, idSet.waitFunction);
425 } else if (nodeToProcess.selected && idSet.subContent
426 && idSet.subContent.length > 0) {
427 this._prepareTargetWindows(idSet.subContent);
431 // Opens a new dialog/window by clicking on an object and runs
432 // DOMWalker.walk() for it.
434 this._controller.click(new elementslib.Elem(nodeToProcess));
436 // If the target is a new modal/non-modal window, this.walk() has to be
437 // started by the method opening that window. If not, we do it here.
438 if (idSet.target == DOMWalker.WINDOW_CURRENT)
439 this.walk(idSet.subContent, null, idSet.waitFunction);
444 * DOMWalker_walk goes recursively through the DOM, starting with a provided
445 * root-node and filters the nodes using the this._filter method.
448 * Node to start testing from
449 * [optional - default: this._controller.window.document.documentElement]
450 * @returns An array with gathered all results from testing a given element
451 * @type {array of elements}
453 _walk : function DOMWalker__walk(root) {
454 if (!root.childNodes)
455 throw new Error("root.childNodes does not exist");
457 var collectedResults = [];
459 // There seems to be no other way to get to the nodes hidden in the
460 // "_buttons" object (see Bug 614949)
462 for each (button in root._buttons) {
463 collectedResults = this._filter(button, collectedResults);
467 for (var i = 0; i < root.childNodes.length; i++) {
468 collectedResults = this._filter(root.childNodes[i], collectedResults);
471 return collectedResults;
475 * Callback function to handle new windows
477 * @param {MozMillController} controller
478 * MozMill controller of the new window to operate on.
480 _modalWindowHelper: function DOMWalker_modalWindowHelper(controller) {
481 let domWalker = new DOMWalker(controller,
482 persisted.modalInfos.callbackFilter,
483 persisted.modalInfos.callbackNodeTest,
484 persisted.modalInfos.callbackResults);
485 domWalker.walk(persisted.modalInfos.ids,
486 controller.window.document.documentElement,
487 persisted.modalInfos.waitFunction);
489 delete persisted.modalInfos;
491 controller.window.close();
496 * Default constructor
498 * @param {object} aRoot
499 * Root node in the DOM to use as parent
501 function nodeCollector(aRoot) {
502 this._root = aRoot.wrappedJSObject ? aRoot.wrappedJSObject : aRoot;
503 this._document = this._root.ownerDocument ? this._root.ownerDocument : this._root;
508 * Node collector class
510 nodeCollector.prototype = {
512 * Converts current nodes to elements
514 * @returns List of elements
515 * @type {array of ElemBase}
520 Array.forEach(this._nodes, function(element) {
521 elements.push(new elementslib.Elem(element));
528 * Get the current list of DOM nodes
530 * @returns List of nodes
531 * @type {array of object}
538 * Sets current nodes to entries from the node list
540 * @param {array of objects} aNodeList
541 * List of DOM nodes to set
543 set nodes(aNodeList) {
547 Array.forEach(aNodeList, function(node) {
548 this._nodes.push(node);
554 * Get the root node used as parent for a node collection
556 * @returns Current root node
564 * Sets root node to the specified DOM node
566 * @param {object} aRoot
567 * DOM node to use as root for node collection
577 * Filter nodes given by the specified callback function
579 * @param {function} aCallback
580 * Function to test each element of the array.
581 * Elements: node, index (optional) , array (optional)
582 * @param {object} aThisObject
583 * Object to use as 'this' when executing callback.
584 * [optional - default: function scope]
586 * @returns The class instance
589 filter : function nodeCollector_filter(aCallback, aThisObject) {
591 throw new Error(arguments.callee.name + ": No callback specified");
593 this.nodes = Array.filter(this.nodes, aCallback, aThisObject);
599 * Filter nodes by DOM property and its value
601 * @param {string} aProperty
602 * Property to filter for
603 * @param {string} aValue
604 * Expected value of the DOM property
605 * [optional - default: n/a]
607 * @returns The class instance
610 filterByDOMProperty : function nodeCollector_filterByDOMProperty(aProperty, aValue) {
611 return this.filter(function(node) {
612 if (aProperty && aValue)
613 return node.getAttribute(aProperty) == aValue;
615 return node.hasAttribute(aProperty);
622 * Filter nodes by JS property and its value
624 * @param {string} aProperty
625 * Property to filter for
626 * @param {string} aValue
627 * Expected value of the JS property
628 * [optional - default: n/a]
630 * @returns The class instance
633 filterByJSProperty : function nodeCollector_filterByJSProperty(aProperty, aValue) {
634 return this.filter(function(node) {
635 if (aProperty && aValue)
636 return node.aProperty == aValue;
638 return node.aProperty !== undefined;
645 * Find anonymouse nodes with the specified attribute and value
647 * @param {string} aAttribute
648 * DOM attribute of the wanted node
649 * @param {string} aValue
650 * Value of the DOM attribute
652 * @returns The class instance
655 queryAnonymousNodes : function nodeCollector_queryAnonymousNodes(aAttribute, aValue) {
656 var node = this._document.getAnonymousElementByAttribute(this._root,
659 this.nodes = node ? [node] : [ ];
665 * Find nodes with the specified selector
667 * @param {string} aSelector
668 * jQuery like element selector string
670 * @returns The class instance
673 queryNodes : function nodeCollector_queryNodes(aSelector) {
674 this.nodes = this._root.querySelectorAll(aSelector);
680 // Exports of functions
681 exports.unwrapNode = unwrapNode;
683 // Exports of classes
684 exports.DOMWalker = DOMWalker;
685 exports.nodeCollector = nodeCollector;