Wed, 31 Dec 2014 07:53:36 +0100
Correct small whitespace inconsistency, lost while renaming variables.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
5 var EXPORTED_SYMBOLS = ["Elem", "Selector", "ID", "Link", "XPath", "Name", "Lookup",
6 "MozMillElement", "MozMillCheckBox", "MozMillRadio", "MozMillDropList",
7 "MozMillTextBox", "subclasses"
8 ];
10 const NAMESPACE_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
12 const Cc = Components.classes;
13 const Ci = Components.interfaces;
14 const Cu = Components.utils;
16 var EventUtils = {}; Cu.import('resource://mozmill/stdlib/EventUtils.js', EventUtils);
18 var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
19 var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
20 var elementslib = {}; Cu.import('resource://mozmill/driver/elementslib.js', elementslib);
21 var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
23 var assert = new assertions.Assert();
25 // A list of all the subclasses available. Shared modules can push their own subclasses onto this list
26 var subclasses = [MozMillCheckBox, MozMillRadio, MozMillDropList, MozMillTextBox];
28 /**
29 * createInstance()
30 *
31 * Returns an new instance of a MozMillElement
32 * The type of the element is automatically determined
33 */
34 function createInstance(locatorType, locator, elem, document) {
35 var args = { "document": document, "element": elem };
37 // If we already have an element lets determine the best MozMillElement type
38 if (elem) {
39 for (var i = 0; i < subclasses.length; ++i) {
40 if (subclasses[i].isType(elem)) {
41 return new subclasses[i](locatorType, locator, args);
42 }
43 }
44 }
46 // By default we create a base MozMillElement
47 if (MozMillElement.isType(elem)) {
48 return new MozMillElement(locatorType, locator, args);
49 }
51 throw new Error("Unsupported element type " + locatorType + ": " + locator);
52 }
54 var Elem = function (node) {
55 return createInstance("Elem", node, node);
56 };
58 var Selector = function (document, selector, index) {
59 return createInstance("Selector", selector, elementslib.Selector(document, selector, index), document);
60 };
62 var ID = function (document, nodeID) {
63 return createInstance("ID", nodeID, elementslib.ID(document, nodeID), document);
64 };
66 var Link = function (document, linkName) {
67 return createInstance("Link", linkName, elementslib.Link(document, linkName), document);
68 };
70 var XPath = function (document, expr) {
71 return createInstance("XPath", expr, elementslib.XPath(document, expr), document);
72 };
74 var Name = function (document, nName) {
75 return createInstance("Name", nName, elementslib.Name(document, nName), document);
76 };
78 var Lookup = function (document, expression) {
79 var elem = createInstance("Lookup", expression, elementslib.Lookup(document, expression), document);
81 // Bug 864268 - Expose the expression property to maintain backwards compatibility
82 elem.expression = elem._locator;
84 return elem;
85 };
87 /**
88 * MozMillElement
89 * The base class for all mozmill elements
90 */
91 function MozMillElement(locatorType, locator, args) {
92 args = args || {};
93 this._locatorType = locatorType;
94 this._locator = locator;
95 this._element = args["element"];
96 this._owner = args["owner"];
98 this._document = this._element ? this._element.ownerDocument : args["document"];
99 this._defaultView = this._document ? this._document.defaultView : null;
101 // Used to maintain backwards compatibility with controller.js
102 this.isElement = true;
103 }
105 // Static method that returns true if node is of this element type
106 MozMillElement.isType = function (node) {
107 return true;
108 };
110 // This getter is the magic behind lazy loading (note distinction between _element and element)
111 MozMillElement.prototype.__defineGetter__("element", function () {
112 // If the document is invalid (e.g. reload of the page), invalidate the cached
113 // element and update the document cache
114 if (this._defaultView && this._defaultView.document !== this._document) {
115 this._document = this._defaultView.document;
116 this._element = undefined;
117 }
119 if (this._element == undefined) {
120 if (elementslib[this._locatorType]) {
121 this._element = elementslib[this._locatorType](this._document, this._locator);
122 } else if (this._locatorType == "Elem") {
123 this._element = this._locator;
124 } else {
125 throw new Error("Unknown locator type: " + this._locatorType);
126 }
127 }
129 return this._element;
130 });
132 /**
133 * Drag an element to the specified offset on another element, firing mouse and
134 * drag events. Adapted from ChromeUtils.js synthesizeDrop()
135 *
136 * By default it will drag the source element over the destination's element
137 * center with a "move" dropEffect.
138 *
139 * @param {MozElement} aElement
140 * Destination element over which the drop occurs
141 * @param {Number} [aOffsetX=aElement.width/2]
142 * Relative x offset for dropping on aElement
143 * @param {Number} [aOffsetY=aElement.height/2]
144 * Relative y offset for dropping on aElement
145 * @param {DOMWindow} [aSourceWindow=this.element.ownerDocument.defaultView]
146 * Custom source Window to be used.
147 * @param {DOMWindow} [aDestWindow=aElement.getNode().ownerDocument.defaultView]
148 * Custom destination Window to be used.
149 * @param {String} [aDropEffect="move"]
150 * Possible values: copy, move, link, none
151 * @param {Object[]} [aDragData]
152 * An array holding custom drag data to be used during the drag event
153 * Format: [{ type: "text/plain", "Text to drag"}, ...]
154 *
155 * @returns {String} the captured dropEffect
156 */
157 MozMillElement.prototype.dragToElement = function(aElement, aOffsetX, aOffsetY,
158 aSourceWindow, aDestWindow,
159 aDropEffect, aDragData) {
160 if (!this.element) {
161 throw new Error("Could not find element " + this.getInfo());
162 }
163 if (!aElement) {
164 throw new Error("Missing destination element");
165 }
167 var srcNode = this.element;
168 var destNode = aElement.getNode();
169 var srcWindow = aSourceWindow ||
170 (srcNode.ownerDocument ? srcNode.ownerDocument.defaultView
171 : srcNode);
172 var destWindow = aDestWindow ||
173 (destNode.ownerDocument ? destNode.ownerDocument.defaultView
174 : destNode);
176 var srcRect = srcNode.getBoundingClientRect();
177 var srcCoords = {
178 x: srcRect.width / 2,
179 y: srcRect.height / 2
180 };
181 var destRect = destNode.getBoundingClientRect();
182 var destCoords = {
183 x: (!aOffsetX || isNaN(aOffsetX)) ? (destRect.width / 2) : aOffsetX,
184 y: (!aOffsetY || isNaN(aOffsetY)) ? (destRect.height / 2) : aOffsetY
185 };
187 var windowUtils = destWindow.QueryInterface(Ci.nsIInterfaceRequestor)
188 .getInterface(Ci.nsIDOMWindowUtils);
189 var ds = Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService);
191 var dataTransfer;
192 var trapDrag = function (event) {
193 srcWindow.removeEventListener("dragstart", trapDrag, true);
194 dataTransfer = event.dataTransfer;
196 if (!aDragData) {
197 return;
198 }
200 for (var i = 0; i < aDragData.length; i++) {
201 var item = aDragData[i];
202 for (var j = 0; j < item.length; j++) {
203 dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
204 }
205 }
207 dataTransfer.dropEffect = aDropEffect || "move";
208 event.preventDefault();
209 event.stopPropagation();
210 }
212 ds.startDragSession();
214 try {
215 srcWindow.addEventListener("dragstart", trapDrag, true);
216 EventUtils.synthesizeMouse(srcNode, srcCoords.x, srcCoords.y,
217 { type: "mousedown" }, srcWindow);
218 EventUtils.synthesizeMouse(destNode, destCoords.x, destCoords.y,
219 { type: "mousemove" }, destWindow);
221 var event = destWindow.document.createEvent("DragEvents");
222 event.initDragEvent("dragenter", true, true, destWindow, 0, 0, 0, 0, 0,
223 false, false, false, false, 0, null, dataTransfer);
224 event.initDragEvent("dragover", true, true, destWindow, 0, 0, 0, 0, 0,
225 false, false, false, false, 0, null, dataTransfer);
226 event.initDragEvent("drop", true, true, destWindow, 0, 0, 0, 0, 0,
227 false, false, false, false, 0, null, dataTransfer);
228 windowUtils.dispatchDOMEventViaPresShell(destNode, event, true);
230 EventUtils.synthesizeMouse(destNode, destCoords.x, destCoords.y,
231 { type: "mouseup" }, destWindow);
233 return dataTransfer.dropEffect;
234 } finally {
235 ds.endDragSession(true);
236 }
238 };
240 // Returns the actual wrapped DOM node
241 MozMillElement.prototype.getNode = function () {
242 return this.element;
243 };
245 MozMillElement.prototype.getInfo = function () {
246 return this._locatorType + ": " + this._locator;
247 };
249 /**
250 * Sometimes an element which once existed will no longer exist in the DOM
251 * This function re-searches for the element
252 */
253 MozMillElement.prototype.exists = function () {
254 this._element = undefined;
255 if (this.element) {
256 return true;
257 }
259 return false;
260 };
262 /**
263 * Synthesize a keypress event on the given element
264 *
265 * @param {string} aKey
266 * Key to use for synthesizing the keypress event. It can be a simple
267 * character like "k" or a string like "VK_ESCAPE" for command keys
268 * @param {object} aModifiers
269 * Information about the modifier keys to send
270 * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
271 * [optional - default: false]
272 * altKey - Hold down the alt key
273 * [optional - default: false]
274 * ctrlKey - Hold down the ctrl key
275 * [optional - default: false]
276 * metaKey - Hold down the meta key (command key on Mac)
277 * [optional - default: false]
278 * shiftKey - Hold down the shift key
279 * [optional - default: false]
280 * @param {object} aExpectedEvent
281 * Information about the expected event to occur
282 * Elements: target - Element which should receive the event
283 * [optional - default: current element]
284 * type - Type of the expected key event
285 */
286 MozMillElement.prototype.keypress = function (aKey, aModifiers, aExpectedEvent) {
287 if (!this.element) {
288 throw new Error("Could not find element " + this.getInfo());
289 }
291 var win = this.element.ownerDocument ? this.element.ownerDocument.defaultView
292 : this.element;
293 this.element.focus();
295 if (aExpectedEvent) {
296 if (!aExpectedEvent.type) {
297 throw new Error(arguments.callee.name + ": Expected event type not specified");
298 }
300 var target = aExpectedEvent.target ? aExpectedEvent.target.getNode()
301 : this.element;
302 EventUtils.synthesizeKeyExpectEvent(aKey, aModifiers || {}, target, aExpectedEvent.type,
303 "MozMillElement.keypress()", win);
304 } else {
305 EventUtils.synthesizeKey(aKey, aModifiers || {}, win);
306 }
308 broker.pass({'function':'MozMillElement.keypress()'});
310 return true;
311 };
314 /**
315 * Synthesize a general mouse event on the given element
316 *
317 * @param {number} aOffsetX
318 * Relative x offset in the elements bounds to click on
319 * @param {number} aOffsetY
320 * Relative y offset in the elements bounds to click on
321 * @param {object} aEvent
322 * Information about the event to send
323 * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
324 * [optional - default: false]
325 * altKey - Hold down the alt key
326 * [optional - default: false]
327 * button - Mouse button to use
328 * [optional - default: 0]
329 * clickCount - Number of counts to click
330 * [optional - default: 1]
331 * ctrlKey - Hold down the ctrl key
332 * [optional - default: false]
333 * metaKey - Hold down the meta key (command key on Mac)
334 * [optional - default: false]
335 * shiftKey - Hold down the shift key
336 * [optional - default: false]
337 * type - Type of the mouse event ('click', 'mousedown',
338 * 'mouseup', 'mouseover', 'mouseout')
339 * [optional - default: 'mousedown' + 'mouseup']
340 * @param {object} aExpectedEvent
341 * Information about the expected event to occur
342 * Elements: target - Element which should receive the event
343 * [optional - default: current element]
344 * type - Type of the expected mouse event
345 */
346 MozMillElement.prototype.mouseEvent = function (aOffsetX, aOffsetY, aEvent, aExpectedEvent) {
347 if (!this.element) {
348 throw new Error(arguments.callee.name + ": could not find element " + this.getInfo());
349 }
351 if ("document" in this.element) {
352 throw new Error("A window cannot be a target for mouse events.");
353 }
355 var rect = this.element.getBoundingClientRect();
357 if (!aOffsetX || isNaN(aOffsetX)) {
358 aOffsetX = rect.width / 2;
359 }
361 if (!aOffsetY || isNaN(aOffsetY)) {
362 aOffsetY = rect.height / 2;
363 }
365 // Scroll element into view otherwise the click will fail
366 if ("scrollIntoView" in this.element)
367 this.element.scrollIntoView();
369 if (aExpectedEvent) {
370 // The expected event type has to be set
371 if (!aExpectedEvent.type) {
372 throw new Error(arguments.callee.name + ": Expected event type not specified");
373 }
375 // If no target has been specified use the specified element
376 var target = aExpectedEvent.target ? aExpectedEvent.target.getNode()
377 : this.element;
378 if (!target) {
379 throw new Error(arguments.callee.name + ": could not find element " +
380 aExpectedEvent.target.getInfo());
381 }
383 EventUtils.synthesizeMouseExpectEvent(this.element, aOffsetX, aOffsetY, aEvent,
384 target, aExpectedEvent.type,
385 "MozMillElement.mouseEvent()",
386 this.element.ownerDocument.defaultView);
387 } else {
388 EventUtils.synthesizeMouse(this.element, aOffsetX, aOffsetY, aEvent,
389 this.element.ownerDocument.defaultView);
390 }
392 // Bug 555347
393 // We don't know why this sleep is necessary but more investigation is needed
394 // before it can be removed
395 utils.sleep(0);
397 return true;
398 };
400 /**
401 * Synthesize a mouse click event on the given element
402 */
403 MozMillElement.prototype.click = function (aOffsetX, aOffsetY, aExpectedEvent) {
404 // Handle menu items differently
405 if (this.element && this.element.tagName == "menuitem") {
406 this.element.click();
407 } else {
408 this.mouseEvent(aOffsetX, aOffsetY, {}, aExpectedEvent);
409 }
411 broker.pass({'function':'MozMillElement.click()'});
413 return true;
414 };
416 /**
417 * Synthesize a double click on the given element
418 */
419 MozMillElement.prototype.doubleClick = function (aOffsetX, aOffsetY, aExpectedEvent) {
420 this.mouseEvent(aOffsetX, aOffsetY, {clickCount: 2}, aExpectedEvent);
422 broker.pass({'function':'MozMillElement.doubleClick()'});
424 return true;
425 };
427 /**
428 * Synthesize a mouse down event on the given element
429 */
430 MozMillElement.prototype.mouseDown = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
431 this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mousedown"}, aExpectedEvent);
433 broker.pass({'function':'MozMillElement.mouseDown()'});
435 return true;
436 };
438 /**
439 * Synthesize a mouse out event on the given element
440 */
441 MozMillElement.prototype.mouseOut = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
442 this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseout"}, aExpectedEvent);
444 broker.pass({'function':'MozMillElement.mouseOut()'});
446 return true;
447 };
449 /**
450 * Synthesize a mouse over event on the given element
451 */
452 MozMillElement.prototype.mouseOver = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
453 this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseover"}, aExpectedEvent);
455 broker.pass({'function':'MozMillElement.mouseOver()'});
457 return true;
458 };
460 /**
461 * Synthesize a mouse up event on the given element
462 */
463 MozMillElement.prototype.mouseUp = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) {
464 this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseup"}, aExpectedEvent);
466 broker.pass({'function':'MozMillElement.mouseUp()'});
468 return true;
469 };
471 /**
472 * Synthesize a mouse middle click event on the given element
473 */
474 MozMillElement.prototype.middleClick = function (aOffsetX, aOffsetY, aExpectedEvent) {
475 this.mouseEvent(aOffsetX, aOffsetY, {button: 1}, aExpectedEvent);
477 broker.pass({'function':'MozMillElement.middleClick()'});
479 return true;
480 };
482 /**
483 * Synthesize a mouse right click event on the given element
484 */
485 MozMillElement.prototype.rightClick = function (aOffsetX, aOffsetY, aExpectedEvent) {
486 this.mouseEvent(aOffsetX, aOffsetY, {type : "contextmenu", button: 2 }, aExpectedEvent);
488 broker.pass({'function':'MozMillElement.rightClick()'});
490 return true;
491 };
493 /**
494 * Synthesize a general touch event on the given element
495 *
496 * @param {Number} [aOffsetX=aElement.width / 2]
497 * Relative x offset in the elements bounds to click on
498 * @param {Number} [aOffsetY=aElement.height / 2]
499 * Relative y offset in the elements bounds to click on
500 * @param {Object} [aEvent]
501 * Information about the event to send
502 * @param {Boolean} [aEvent.altKey=false]
503 * A Boolean value indicating whether or not the alt key was down when
504 * the touch event was fired
505 * @param {Number} [aEvent.angle=0]
506 * The angle (in degrees) that the ellipse described by rx and
507 * ry must be rotated, clockwise, to most accurately cover the area
508 * of contact between the user and the surface.
509 * @param {Touch[]} [aEvent.changedTouches]
510 * A TouchList of all the Touch objects representing individual points of
511 * contact whose states changed between the previous touch event and
512 * this one
513 * @param {Boolean} [aEvent.ctrlKey]
514 * A Boolean value indicating whether or not the control key was down
515 * when the touch event was fired
516 * @param {Number} [aEvent.force=1]
517 * The amount of pressure being applied to the surface by the user, as a
518 * float between 0.0 (no pressure) and 1.0 (maximum pressure)
519 * @param {Number} [aEvent.id=0]
520 * A unique identifier for this Touch object. A given touch (say, by a
521 * finger) will have the same identifier for the duration of its movement
522 * around the surface. This lets you ensure that you're tracking the same
523 * touch all the time
524 * @param {Boolean} [aEvent.metaKey]
525 * A Boolean value indicating whether or not the meta key was down when
526 * the touch event was fired.
527 * @param {Number} [aEvent.rx=1]
528 * The X radius of the ellipse that most closely circumscribes the area
529 * of contact with the screen.
530 * @param {Number} [aEvent.ry=1]
531 * The Y radius of the ellipse that most closely circumscribes the area
532 * of contact with the screen.
533 * @param {Boolean} [aEvent.shiftKey]
534 * A Boolean value indicating whether or not the shift key was down when
535 * the touch event was fired
536 * @param {Touch[]} [aEvent.targetTouches]
537 * A TouchList of all the Touch objects that are both currently in
538 * contact with the touch surface and were also started on the same
539 * element that is the target of the event
540 * @param {Touch[]} [aEvent.touches]
541 * A TouchList of all the Touch objects representing all current points
542 * of contact with the surface, regardless of target or changed status
543 * @param {Number} [aEvent.type=*|touchstart|touchend|touchmove|touchenter|touchleave|touchcancel]
544 * The type of touch event that occurred
545 * @param {Element} [aEvent.target]
546 * The target of the touches associated with this event. This target
547 * corresponds to the target of all the touches in the targetTouches
548 * attribute, but note that other touches in this event may have a
549 * different target. To be careful, you should use the target associated
550 * with individual touches
551 */
552 MozMillElement.prototype.touchEvent = function (aOffsetX, aOffsetY, aEvent) {
553 if (!this.element) {
554 throw new Error(arguments.callee.name + ": could not find element " + this.getInfo());
555 }
557 if ("document" in this.element) {
558 throw new Error("A window cannot be a target for touch events.");
559 }
561 var rect = this.element.getBoundingClientRect();
563 if (!aOffsetX || isNaN(aOffsetX)) {
564 aOffsetX = rect.width / 2;
565 }
567 if (!aOffsetY || isNaN(aOffsetY)) {
568 aOffsetY = rect.height / 2;
569 }
571 // Scroll element into view otherwise the click will fail
572 if ("scrollIntoView" in this.element) {
573 this.element.scrollIntoView();
574 }
576 EventUtils.synthesizeTouch(this.element, aOffsetX, aOffsetY, aEvent,
577 this.element.ownerDocument.defaultView);
579 return true;
580 };
582 /**
583 * Synthesize a touch tap event on the given element
584 *
585 * @param {Number} [aOffsetX=aElement.width / 2]
586 * Left offset in px where the event is triggered
587 * @param {Number} [aOffsetY=aElement.height / 2]
588 * Top offset in px where the event is triggered
589 * @param {Object} [aExpectedEvent]
590 * Information about the expected event to occur
591 * @param {MozMillElement} [aExpectedEvent.target=this.element]
592 * Element which should receive the event
593 * @param {MozMillElement} [aExpectedEvent.type]
594 * Type of the expected mouse event
595 */
596 MozMillElement.prototype.tap = function (aOffsetX, aOffsetY, aExpectedEvent) {
597 this.mouseEvent(aOffsetX, aOffsetY, {
598 clickCount: 1,
599 inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
600 }, aExpectedEvent);
602 broker.pass({'function':'MozMillElement.tap()'});
604 return true;
605 };
607 /**
608 * Synthesize a double tap on the given element
609 *
610 * @param {Number} [aOffsetX=aElement.width / 2]
611 * Left offset in px where the event is triggered
612 * @param {Number} [aOffsetY=aElement.height / 2]
613 * Top offset in px where the event is triggered
614 * @param {Object} [aExpectedEvent]
615 * Information about the expected event to occur
616 * @param {MozMillElement} [aExpectedEvent.target=this.element]
617 * Element which should receive the event
618 * @param {MozMillElement} [aExpectedEvent.type]
619 * Type of the expected mouse event
620 */
621 MozMillElement.prototype.doubleTap = function (aOffsetX, aOffsetY, aExpectedEvent) {
622 this.mouseEvent(aOffsetX, aOffsetY, {
623 clickCount: 2,
624 inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
625 }, aExpectedEvent);
627 broker.pass({'function':'MozMillElement.doubleTap()'});
629 return true;
630 };
632 /**
633 * Synthesize a long press
634 *
635 * @param {Number} aOffsetX
636 * Left offset in px where the event is triggered
637 * @param {Number} aOffsetY
638 * Top offset in px where the event is triggered
639 * @param {Number} [aTime=1000]
640 * Duration of the "press" event in ms
641 */
642 MozMillElement.prototype.longPress = function (aOffsetX, aOffsetY, aTime) {
643 var time = aTime || 1000;
645 this.touchStart(aOffsetX, aOffsetY);
646 utils.sleep(time);
647 this.touchEnd(aOffsetX, aOffsetY);
649 broker.pass({'function':'MozMillElement.longPress()'});
651 return true;
652 };
654 /**
655 * Synthesize a touch & drag event on the given element
656 *
657 * @param {Number} aOffsetX1
658 * Left offset of the start position
659 * @param {Number} aOffsetY1
660 * Top offset of the start position
661 * @param {Number} aOffsetX2
662 * Left offset of the end position
663 * @param {Number} aOffsetY2
664 * Top offset of the end position
665 */
666 MozMillElement.prototype.touchDrag = function (aOffsetX1, aOffsetY1, aOffsetX2, aOffsetY2) {
667 this.touchStart(aOffsetX1, aOffsetY1);
668 this.touchMove(aOffsetX2, aOffsetY2);
669 this.touchEnd(aOffsetX2, aOffsetY2);
671 broker.pass({'function':'MozMillElement.move()'});
673 return true;
674 };
676 /**
677 * Synthesize a press / touchstart event on the given element
678 *
679 * @param {Number} aOffsetX
680 * Left offset where the event is triggered
681 * @param {Number} aOffsetY
682 * Top offset where the event is triggered
683 */
684 MozMillElement.prototype.touchStart = function (aOffsetX, aOffsetY) {
685 this.touchEvent(aOffsetX, aOffsetY, { type: "touchstart" });
687 broker.pass({'function':'MozMillElement.touchStart()'});
689 return true;
690 };
692 /**
693 * Synthesize a release / touchend event on the given element
694 *
695 * @param {Number} aOffsetX
696 * Left offset where the event is triggered
697 * @param {Number} aOffsetY
698 * Top offset where the event is triggered
699 */
700 MozMillElement.prototype.touchEnd = function (aOffsetX, aOffsetY) {
701 this.touchEvent(aOffsetX, aOffsetY, { type: "touchend" });
703 broker.pass({'function':'MozMillElement.touchEnd()'});
705 return true;
706 };
708 /**
709 * Synthesize a touchMove event on the given element
710 *
711 * @param {Number} aOffsetX
712 * Left offset where the event is triggered
713 * @param {Number} aOffsetY
714 * Top offset where the event is triggered
715 */
716 MozMillElement.prototype.touchMove = function (aOffsetX, aOffsetY) {
717 this.touchEvent(aOffsetX, aOffsetY, { type: "touchmove" });
719 broker.pass({'function':'MozMillElement.touchMove()'});
721 return true;
722 };
724 MozMillElement.prototype.waitForElement = function (timeout, interval) {
725 var elem = this;
727 assert.waitFor(function () {
728 return elem.exists();
729 }, "Element.waitForElement(): Element '" + this.getInfo() +
730 "' has been found", timeout, interval);
732 broker.pass({'function':'MozMillElement.waitForElement()'});
733 };
735 MozMillElement.prototype.waitForElementNotPresent = function (timeout, interval) {
736 var elem = this;
738 assert.waitFor(function () {
739 return !elem.exists();
740 }, "Element.waitForElementNotPresent(): Element '" + this.getInfo() +
741 "' has not been found", timeout, interval);
743 broker.pass({'function':'MozMillElement.waitForElementNotPresent()'});
744 };
746 MozMillElement.prototype.waitThenClick = function (timeout, interval,
747 aOffsetX, aOffsetY, aExpectedEvent) {
748 this.waitForElement(timeout, interval);
749 this.click(aOffsetX, aOffsetY, aExpectedEvent);
750 };
752 /**
753 * Waits for the element to be available in the DOM, then trigger a tap event
754 *
755 * @param {Number} [aTimeout=5000]
756 * Time to wait for the element to be available
757 * @param {Number} [aInterval=100]
758 * Interval to check for availability
759 * @param {Number} [aOffsetX=aElement.width / 2]
760 * Left offset where the event is triggered
761 * @param {Number} [aOffsetY=aElement.height / 2]
762 * Top offset where the event is triggered
763 * @param {Object} [aExpectedEvent]
764 * Information about the expected event to occur
765 * @param {MozMillElement} [aExpectedEvent.target=this.element]
766 * Element which should receive the event
767 * @param {MozMillElement} [aExpectedEvent.type]
768 * Type of the expected mouse event
769 */
770 MozMillElement.prototype.waitThenTap = function (aTimeout, aInterval,
771 aOffsetX, aOffsetY, aExpectedEvent) {
772 this.waitForElement(aTimeout, aInterval);
773 this.tap(aOffsetX, aOffsetY, aExpectedEvent);
774 };
776 // Dispatches an HTMLEvent
777 MozMillElement.prototype.dispatchEvent = function (eventType, canBubble, modifiers) {
778 canBubble = canBubble || true;
779 modifiers = modifiers || { };
781 let document = 'ownerDocument' in this.element ? this.element.ownerDocument
782 : this.element.document;
784 let evt = document.createEvent('HTMLEvents');
785 evt.shiftKey = modifiers["shift"];
786 evt.metaKey = modifiers["meta"];
787 evt.altKey = modifiers["alt"];
788 evt.ctrlKey = modifiers["ctrl"];
789 evt.initEvent(eventType, canBubble, true);
791 this.element.dispatchEvent(evt);
792 };
795 /**
796 * MozMillCheckBox, which inherits from MozMillElement
797 */
798 function MozMillCheckBox(locatorType, locator, args) {
799 MozMillElement.call(this, locatorType, locator, args);
800 }
803 MozMillCheckBox.prototype = Object.create(MozMillElement.prototype, {
804 check : {
805 /**
806 * Enable/Disable a checkbox depending on the target state
807 *
808 * @param {boolean} state State to set
809 * @return {boolean} Success state
810 */
811 value : function MMCB_check(state) {
812 var result = false;
814 if (!this.element) {
815 throw new Error("could not find element " + this.getInfo());
816 }
818 // If we have a XUL element, unwrap its XPCNativeWrapper
819 if (this.element.namespaceURI == NAMESPACE_XUL) {
820 this.element = utils.unwrapNode(this.element);
821 }
823 state = (typeof(state) == "boolean") ? state : false;
824 if (state != this.element.checked) {
825 this.click();
826 var element = this.element;
828 assert.waitFor(function () {
829 return element.checked == state;
830 }, "CheckBox.check(): Checkbox " + this.getInfo() + " could not be checked/unchecked", 500);
832 result = true;
833 }
835 broker.pass({'function':'MozMillCheckBox.check(' + this.getInfo() +
836 ', state: ' + state + ')'});
838 return result;
839 }
840 }
841 });
844 /**
845 * Returns true if node is of type MozMillCheckBox
846 *
847 * @static
848 * @param {DOMNode} node Node to check for its type
849 * @return {boolean} True if node is of type checkbox
850 */
851 MozMillCheckBox.isType = function MMCB_isType(node) {
852 return ((node.localName.toLowerCase() == "input" && node.getAttribute("type") == "checkbox") ||
853 (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'checkbox') ||
854 (node.localName.toLowerCase() == 'checkbox'));
855 };
858 /**
859 * MozMillRadio, which inherits from MozMillElement
860 */
861 function MozMillRadio(locatorType, locator, args) {
862 MozMillElement.call(this, locatorType, locator, args);
863 }
866 MozMillRadio.prototype = Object.create(MozMillElement.prototype, {
867 select : {
868 /**
869 * Select the given radio button
870 *
871 * @param {number} [index=0]
872 * Specifies which radio button in the group to select (only
873 * applicable to radiogroup elements)
874 * @return {boolean} Success state
875 */
876 value : function MMR_select(index) {
877 if (!this.element) {
878 throw new Error("could not find element " + this.getInfo());
879 }
881 if (this.element.localName.toLowerCase() == "radiogroup") {
882 var element = this.element.getElementsByTagName("radio")[index || 0];
883 new MozMillRadio("Elem", element).click();
884 } else {
885 var element = this.element;
886 this.click();
887 }
889 assert.waitFor(function () {
890 // If we have a XUL element, unwrap its XPCNativeWrapper
891 if (element.namespaceURI == NAMESPACE_XUL) {
892 element = utils.unwrapNode(element);
893 return element.selected == true;
894 }
896 return element.checked == true;
897 }, "Radio.select(): Radio button " + this.getInfo() + " has been selected", 500);
899 broker.pass({'function':'MozMillRadio.select(' + this.getInfo() + ')'});
901 return true;
902 }
903 }
904 });
907 /**
908 * Returns true if node is of type MozMillRadio
909 *
910 * @static
911 * @param {DOMNode} node Node to check for its type
912 * @return {boolean} True if node is of type radio
913 */
914 MozMillRadio.isType = function MMR_isType(node) {
915 return ((node.localName.toLowerCase() == 'input' && node.getAttribute('type') == 'radio') ||
916 (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'radio') ||
917 (node.localName.toLowerCase() == 'radio') ||
918 (node.localName.toLowerCase() == 'radiogroup'));
919 };
922 /**
923 * MozMillDropList, which inherits from MozMillElement
924 */
925 function MozMillDropList(locatorType, locator, args) {
926 MozMillElement.call(this, locatorType, locator, args);
927 }
930 MozMillDropList.prototype = Object.create(MozMillElement.prototype, {
931 select : {
932 /**
933 * Select the specified option and trigger the relevant events of the element
934 * @return {boolean}
935 */
936 value : function MMDL_select(index, option, value) {
937 if (!this.element){
938 throw new Error("Could not find element " + this.getInfo());
939 }
941 //if we have a select drop down
942 if (this.element.localName.toLowerCase() == "select"){
943 var item = null;
945 // The selected item should be set via its index
946 if (index != undefined) {
947 // Resetting a menulist has to be handled separately
948 if (index == -1) {
949 this.dispatchEvent('focus', false);
950 this.element.selectedIndex = index;
951 this.dispatchEvent('change', true);
953 broker.pass({'function':'MozMillDropList.select()'});
955 return true;
956 } else {
957 item = this.element.options.item(index);
958 }
959 } else {
960 for (var i = 0; i < this.element.options.length; i++) {
961 var entry = this.element.options.item(i);
962 if (option != undefined && entry.innerHTML == option ||
963 value != undefined && entry.value == value) {
964 item = entry;
965 break;
966 }
967 }
968 }
970 // Click the item
971 try {
972 // EventUtils.synthesizeMouse doesn't work.
973 this.dispatchEvent('focus', false);
974 item.selected = true;
975 this.dispatchEvent('change', true);
977 var self = this;
978 var selected = index || option || value;
979 assert.waitFor(function () {
980 switch (selected) {
981 case index:
982 return selected === self.element.selectedIndex;
983 break;
984 case option:
985 return selected === item.label;
986 break;
987 case value:
988 return selected === item.value;
989 break;
990 }
991 }, "DropList.select(): The correct item has been selected");
993 broker.pass({'function':'MozMillDropList.select()'});
995 return true;
996 } catch (e) {
997 throw new Error("No item selected for element " + this.getInfo());
998 }
999 }
1000 //if we have a xul menupopup select accordingly
1001 else if (this.element.namespaceURI.toLowerCase() == NAMESPACE_XUL) {
1002 var ownerDoc = this.element.ownerDocument;
1003 // Unwrap the XUL element's XPCNativeWrapper
1004 this.element = utils.unwrapNode(this.element);
1005 // Get the list of menuitems
1006 var menuitems = this.element.
1007 getElementsByTagNameNS(NAMESPACE_XUL, "menupopup")[0].
1008 getElementsByTagNameNS(NAMESPACE_XUL, "menuitem");
1010 var item = null;
1012 if (index != undefined) {
1013 if (index == -1) {
1014 this.dispatchEvent('focus', false);
1015 this.element.boxObject.QueryInterface(Ci.nsIMenuBoxObject).activeChild = null;
1016 this.dispatchEvent('change', true);
1018 broker.pass({'function':'MozMillDropList.select()'});
1020 return true;
1021 } else {
1022 item = menuitems[index];
1023 }
1024 } else {
1025 for (var i = 0; i < menuitems.length; i++) {
1026 var entry = menuitems[i];
1027 if (option != undefined && entry.label == option ||
1028 value != undefined && entry.value == value) {
1029 item = entry;
1030 break;
1031 }
1032 }
1033 }
1035 // Click the item
1036 try {
1037 item.click();
1039 var self = this;
1040 var selected = index || option || value;
1041 assert.waitFor(function () {
1042 switch (selected) {
1043 case index:
1044 return selected === self.element.selectedIndex;
1045 break;
1046 case option:
1047 return selected === self.element.label;
1048 break;
1049 case value:
1050 return selected === self.element.value;
1051 break;
1052 }
1053 }, "DropList.select(): The correct item has been selected");
1055 broker.pass({'function':'MozMillDropList.select()'});
1057 return true;
1058 } catch (e) {
1059 throw new Error('No item selected for element ' + this.getInfo());
1060 }
1061 }
1062 }
1063 }
1064 });
1067 /**
1068 * Returns true if node is of type MozMillDropList
1069 *
1070 * @static
1071 * @param {DOMNode} node Node to check for its type
1072 * @return {boolean} True if node is of type dropdown list
1073 */
1074 MozMillDropList.isType = function MMR_isType(node) {
1075 return ((node.localName.toLowerCase() == 'toolbarbutton' &&
1076 (node.getAttribute('type') == 'menu' || node.getAttribute('type') == 'menu-button')) ||
1077 (node.localName.toLowerCase() == 'menu') ||
1078 (node.localName.toLowerCase() == 'menulist') ||
1079 (node.localName.toLowerCase() == 'select' ));
1080 };
1083 /**
1084 * MozMillTextBox, which inherits from MozMillElement
1085 */
1086 function MozMillTextBox(locatorType, locator, args) {
1087 MozMillElement.call(this, locatorType, locator, args);
1088 }
1091 MozMillTextBox.prototype = Object.create(MozMillElement.prototype, {
1092 sendKeys : {
1093 /**
1094 * Synthesize keypress events for each character on the given element
1095 *
1096 * @param {string} aText
1097 * The text to send as single keypress events
1098 * @param {object} aModifiers
1099 * Information about the modifier keys to send
1100 * Elements: accelKey - Hold down the accelerator key (ctrl/meta)
1101 * [optional - default: false]
1102 * altKey - Hold down the alt key
1103 * [optional - default: false]
1104 * ctrlKey - Hold down the ctrl key
1105 * [optional - default: false]
1106 * metaKey - Hold down the meta key (command key on Mac)
1107 * [optional - default: false]
1108 * shiftKey - Hold down the shift key
1109 * [optional - default: false]
1110 * @param {object} aExpectedEvent
1111 * Information about the expected event to occur
1112 * Elements: target - Element which should receive the event
1113 * [optional - default: current element]
1114 * type - Type of the expected key event
1115 * @return {boolean} Success state
1116 */
1117 value : function MMTB_sendKeys(aText, aModifiers, aExpectedEvent) {
1118 if (!this.element) {
1119 throw new Error("could not find element " + this.getInfo());
1120 }
1122 var element = this.element;
1123 Array.forEach(aText, function (letter) {
1124 var win = element.ownerDocument ? element.ownerDocument.defaultView
1125 : element;
1126 element.focus();
1128 if (aExpectedEvent) {
1129 if (!aExpectedEvent.type) {
1130 throw new Error(arguments.callee.name + ": Expected event type not specified");
1131 }
1133 var target = aExpectedEvent.target ? aExpectedEvent.target.getNode()
1134 : element;
1135 EventUtils.synthesizeKeyExpectEvent(letter, aModifiers || {}, target,
1136 aExpectedEvent.type,
1137 "MozMillTextBox.sendKeys()", win);
1138 } else {
1139 EventUtils.synthesizeKey(letter, aModifiers || {}, win);
1140 }
1141 });
1143 broker.pass({'function':'MozMillTextBox.type()'});
1145 return true;
1146 }
1147 }
1148 });
1151 /**
1152 * Returns true if node is of type MozMillTextBox
1153 *
1154 * @static
1155 * @param {DOMNode} node Node to check for its type
1156 * @return {boolean} True if node is of type textbox
1157 */
1158 MozMillTextBox.isType = function MMR_isType(node) {
1159 return ((node.localName.toLowerCase() == 'input' &&
1160 (node.getAttribute('type') == 'text' || node.getAttribute('type') == 'search')) ||
1161 (node.localName.toLowerCase() == 'textarea') ||
1162 (node.localName.toLowerCase() == 'textbox'));
1163 };