]> git.donarmstrong.com Git - dactyl.git/blob - common/tests/functional/shared-modules/dom-utils.js
Initial import of 1.0~b6
[dactyl.git] / common / tests / functional / shared-modules / dom-utils.js
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
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/
8  *
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
12  * License.
13  *
14  * The Original Code is MozMill Test code.
15  *
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.
19  *
20  * Contributor(s):
21  *   Henrik Skupin <hskupin@mozilla.com>
22  *   Adrian Kalla <akalla@aviary.pl>
23  *
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.
35  *
36  * ***** END LICENSE BLOCK ***** */
37
38 // Include required modules
39 var modalDialog = require("modal-dialog");
40 var utils = require("utils");
41
42
43 /**
44  * Unwraps a node which is wrapped into a XPCNativeWrapper or XrayWrapper
45  *
46  * @param {DOMnode} Wrapped DOM node
47  * @returns {DOMNode} Unwrapped DOM node
48  */
49 function unwrapNode(aNode) {
50   var node = aNode;
51
52   if (node) {
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);
56     }
57     else if ("wrappedJSObject" in node) {
58       node = node.wrappedJSObject;
59     }
60   }
61
62   return node;
63 }
64
65
66 /**
67  * DOMWalker Constructor
68  *
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]
78  */
79 function DOMWalker(controller, callbackFilter, callbackNodeTest,
80                    callbackResults) {
81
82   this._controller = controller;
83   this._callbackFilter = callbackFilter;
84   this._callbackNodeTest = callbackNodeTest;
85   this._callbackResults = callbackResults;
86 }
87
88 DOMWalker.FILTER_ACCEPT = 1;
89 DOMWalker.FILTER_REJECT = 2;
90 DOMWalker.FILTER_SKIP = 3;
91
92 DOMWalker.GET_BY_ID = "id";
93 DOMWalker.GET_BY_SELECTOR = "selector";
94
95 DOMWalker.WINDOW_CURRENT = 1;
96 DOMWalker.WINDOW_MODAL = 2;
97 DOMWalker.WINDOW_NEW = 4;
98
99 DOMWalker.prototype = {
100   /**
101    * Returns the filter-callback
102    *
103    * @returns Function
104    */
105   get callbackFilter() {
106     return this._callbackFilter;
107   },
108
109   /**
110    * Returns the node-testing-callback
111    *
112    * @returns Function
113    */
114   get callbackNodeTest() {
115     return this._callbackNodeTest;
116   },
117
118   /**
119    * Returns the results-callback
120    *
121    * @returns Function
122    */
123   get callbackResults() {
124     return this._callbackResults;
125   },
126
127   /**
128    * Returns the MozMill controller
129    *
130    * @returns Mozmill controller
131    * @type {MozMillController}
132    */
133   get controller() {
134     return this._controller;
135   },
136
137   /**
138    * The main DOMWalker function.
139    *
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.
143    *
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
154    *                                          [1|2|4]
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]
162    *
163    * @param {Node} root
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]
170    */
171   walk : function DOMWalker_walk(ids, root, waitFunction) {
172     if (typeof waitFunction == 'function')
173       this._controller.waitFor(waitFunction());
174
175     if (!root)
176       root = this._controller.window.document.documentElement;
177
178     var resultsArray = this._walk(root);
179
180     if (typeof this._callbackResults == 'function')
181       this._callbackResults(this._controller, resultsArray);
182
183     if (ids)
184       this._prepareTargetWindows(ids);
185   },
186
187   /**
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
191    * of FILTER_ACCEPT).
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.
196    *
197    * @param {Node} node
198    *        Node to filter
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}
203    */
204   _filter : function DOMWalker_filter(node, collectedResults) {
205     var nodeStatus = this._callbackFilter(node);
206
207     var nodeTestResults = [];
208
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);
216         break;
217       default:
218         break;
219     }
220
221     collectedResults = collectedResults.concat(nodeTestResults);
222
223     return collectedResults;
224   },
225
226   /**
227    * Retrieves and returns a wanted node based on the provided identification
228    * set.
229    *
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
240    *                                          [1|2|4]
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]
248    *
249    * @returns Node
250    * @type {Node}
251    */
252   _getNode : function DOMWalker_getNode(idSet) {
253     var doc = this._controller.window.document;
254
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]);
262       default:
263         throw new Error("Not supported getBy-attribute: " + idSet.getBy);
264     }
265   },
266
267   /**
268    * Main entry point to open new elements like windows, tabpanels, prefpanes,
269    * dialogs
270    *
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
281    *                                          [1|2|4]
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]
289    */
290   _prepareTargetWindows : function DOMWalker_prepareTargetWindows(ids) {
291     var doc = this._controller.window.document;
292
293     // Go through all the provided ids
294     for (var i = 0; i < ids.length; i++) {
295       var node = this._getNode(ids[i]);
296
297       // Go further only, if the needed element exists
298       if (node) {
299         var idSet = ids[i];
300
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);
306             break;
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;
315
316             var md = new modalDialog.modalDialog(this._controller.window);
317             md.start(this._modalWindowHelper);
318
319             this._processNode(node, idSet);
320             md.waitForDialog();
321             break;
322           case DOMWalker.WINDOW_NEW:
323             this._processNode(node, idSet);
324
325             // Get the new non-modal window controller
326             var controller = utils.handleWindow('title', idSet.title,
327                                                 undefined, false);
328
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,
335                            idSet.waitFunction);
336
337             // Close the window
338             controller.window.close();
339             break;
340           default:
341             throw new Error("Node does not exist: " + ids[i][ids[i].getBy]);
342         }
343       }
344     }
345   },
346
347   /**
348    * Opens new windows/dialog and starts the DOMWalker.walk() in case of dialogs
349    * in existing windows.
350    *
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
356    */
357   _processNode: function DOMWalker_processNode(activeNode, idSet) {
358     var doc = this._controller.window.document;
359     var nodeToProcess = this._getNode(idSet);
360
361     // Opens a new window/dialog through a menulist and runs DOMWalker.walk()
362     // for it.
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);
369
370         this._controller.select(dropDown, null, null, idSet.value);
371
372         this._controller.waitFor(function() {
373           return nodeToProcess.value == idSet.value;
374         }, "The menu item did not load in time: " + idSet.value);
375
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);
383       }
384     }
385
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;
392
393       if (windowHandler.paneId != idSet.id) {
394         windowHandler.paneId = idSet.id;
395
396         // Wait for the pane's content to load and to be fully displayed
397         this._controller.waitFor(function() {
398           return (nodeToProcess.loaded &&
399                   (!mozmill.isMac ||
400                    nodeToProcess.style.opacity == 1 ||
401                    nodeToProcess.style.opacity == null));
402         }, "The pane did not load in time: " + idSet.id);
403
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);
411       }
412     }
413
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));
420
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);
428       }
429     }
430
431     // Opens a new dialog/window by clicking on an object and runs
432     // DOMWalker.walk() for it.
433     else {
434       this._controller.click(new elementslib.Elem(nodeToProcess));
435
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);
440     }
441   },
442
443   /**
444    * DOMWalker_walk goes recursively through the DOM, starting with a provided
445    * root-node and filters the nodes using the this._filter method.
446    *
447    * @param {Node} root
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}
452    */
453   _walk : function DOMWalker__walk(root) {
454     if (!root.childNodes)
455       throw new Error("root.childNodes does not exist");
456
457     var collectedResults = [];
458
459     // There seems to be no other way to get to the nodes hidden in the
460     // "_buttons" object (see Bug 614949)
461     if (root._buttons) {
462       for each (button in root._buttons) {
463         collectedResults = this._filter(button, collectedResults);
464       }
465     }
466
467     for (var i = 0; i < root.childNodes.length; i++) {
468       collectedResults = this._filter(root.childNodes[i], collectedResults);
469     }
470
471     return collectedResults;
472   },
473
474   /**
475    * Callback function to handle new windows
476    *
477    * @param {MozMillController} controller
478    *        MozMill controller of the new window to operate on.
479    */
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);
488
489     delete persisted.modalInfos;
490
491     controller.window.close();
492   }
493 }
494
495 /**
496  * Default constructor
497  *
498  * @param {object} aRoot
499  *        Root node in the DOM to use as parent
500  */
501 function nodeCollector(aRoot) {
502   this._root = aRoot.wrappedJSObject ? aRoot.wrappedJSObject : aRoot;
503   this._document = this._root.ownerDocument ? this._root.ownerDocument : this._root;
504   this._nodes = [ ];
505 }
506
507 /**
508  * Node collector class
509  */
510 nodeCollector.prototype = {
511   /**
512    * Converts current nodes to elements
513    *
514    * @returns List of elements
515    * @type {array of ElemBase}
516    */
517   get elements() {
518     var elements = [ ];
519
520     Array.forEach(this._nodes, function(element) {
521       elements.push(new elementslib.Elem(element));
522     });
523
524     return elements;
525   },
526
527   /**
528    * Get the current list of DOM nodes
529    *
530    * @returns List of nodes
531    * @type {array of object}
532    */
533   get nodes() {
534     return this._nodes;
535   },
536
537   /**
538    * Sets current nodes to entries from the node list
539    *
540    * @param {array of objects} aNodeList
541    *        List of DOM nodes to set
542    */
543   set nodes(aNodeList) {
544     if (aNodeList) {
545       this._nodes = [ ];
546
547       Array.forEach(aNodeList, function(node) {
548         this._nodes.push(node);
549       }, this);
550     }
551   },
552
553   /**
554    * Get the root node used as parent for a node collection
555    *
556    * @returns Current root node
557    * @type {object}
558    */
559   get root() {
560     return this._root;
561   },
562
563   /**
564    * Sets root node to the specified DOM node
565    *
566    * @param {object} aRoot
567    *        DOM node to use as root for node collection
568    */
569   set root(aRoot) {
570     if (aRoot) {
571       this._root = aRoot;
572       this._nodes = [ ];
573     }
574   },
575
576   /**
577    * Filter nodes given by the specified callback function
578    *
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]
585    *
586    * @returns The class instance
587    * @type {object}
588    */
589   filter : function nodeCollector_filter(aCallback, aThisObject) {
590     if (!aCallback)
591       throw new Error(arguments.callee.name + ": No callback specified");
592
593     this.nodes = Array.filter(this.nodes, aCallback, aThisObject);
594
595     return this;
596   },
597
598   /**
599    * Filter nodes by DOM property and its value
600    *
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]
606    *
607    * @returns The class instance
608    * @type {object}
609    */
610   filterByDOMProperty : function nodeCollector_filterByDOMProperty(aProperty, aValue) {
611     return this.filter(function(node) {
612       if (aProperty && aValue)
613         return node.getAttribute(aProperty) == aValue;
614       else if (aProperty)
615         return node.hasAttribute(aProperty);
616       else
617         return true;
618     });
619   },
620
621   /**
622    * Filter nodes by JS property and its value
623    *
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]
629    *
630    * @returns The class instance
631    * @type {object}
632    */
633   filterByJSProperty : function nodeCollector_filterByJSProperty(aProperty, aValue) {
634     return this.filter(function(node) {
635       if (aProperty && aValue)
636         return node.aProperty == aValue;
637       else if (aProperty)
638         return node.aProperty !== undefined;
639       else
640         return true;
641     });
642   },
643
644   /**
645    * Find anonymouse nodes with the specified attribute and value
646    *
647    * @param {string} aAttribute
648    *        DOM attribute of the wanted node
649    * @param {string} aValue
650    *        Value of the DOM attribute
651    *
652    * @returns The class instance
653    * @type {object}
654    */
655   queryAnonymousNodes : function nodeCollector_queryAnonymousNodes(aAttribute, aValue) {
656     var node = this._document.getAnonymousElementByAttribute(this._root,
657                                                              aAttribute,
658                                                              aValue);
659     this.nodes = node ? [node] : [ ];
660
661     return this;
662   },
663
664   /**
665    * Find nodes with the specified selector
666    *
667    * @param {string} aSelector
668    *        jQuery like element selector string
669    *
670    * @returns The class instance
671    * @type {object}
672    */
673   queryNodes : function nodeCollector_queryNodes(aSelector) {
674     this.nodes = this._root.querySelectorAll(aSelector);
675
676     return this;
677   }
678 }
679
680 // Exports of functions
681 exports.unwrapNode = unwrapNode;
682
683 // Exports of classes
684 exports.DOMWalker = DOMWalker;
685 exports.nodeCollector = nodeCollector;