/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ this.EXPORTED_SYMBOLS = ["ActionChain"]; /** * Functionality for (single finger) action chains. */ this.ActionChain = function(utils, checkForInterrupted) { // for assigning unique ids to all touches this.nextTouchId = 1000; // keep track of active Touches this.touchIds = {}; // last touch for each fingerId this.lastCoordinates = null; this.isTap = false; this.scrolling = false; // whether to send mouse event this.mouseEventsOnly = false; this.checkTimer = Components.classes["@mozilla.org/timer;1"] .createInstance(Components.interfaces.nsITimer); // callbacks for command completion this.onSuccess = null; this.onError = null; if (typeof checkForInterrupted == "function") { this.checkForInterrupted = checkForInterrupted; } else { this.checkForInterrupted = () => {}; } // determines if we create touch events this.inputSource = null; // test utilities providing some event synthesis code this.utils = utils; }; ActionChain.prototype.dispatchActions = function( args, touchId, container, elementManager, callbacks, touchProvider) { // Some touch events code in the listener needs to do ipc, so we can't // share this code across chrome/content. if (touchProvider) { this.touchProvider = touchProvider; } this.elementManager = elementManager; let commandArray = elementManager.convertWrappedArguments(args, container); this.onSuccess = callbacks.onSuccess; this.onError = callbacks.onError; this.container = container; if (touchId == null) { touchId = this.nextTouchId++; } if (!container.frame.document.createTouch) { this.mouseEventsOnly = true; } let keyModifiers = { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }; try { this.actions(commandArray, touchId, 0, keyModifiers); } catch (e) { callbacks.onError(e); this.resetValues(); } }; /** * This function emit mouse event. * * @param {Document} doc * Current document. * @param {string} type * Type of event to dispatch. * @param {number} clickCount * Number of clicks, button notes the mouse button. * @param {number} elClientX * X coordinate of the mouse relative to the viewport. * @param {number} elClientY * Y coordinate of the mouse relative to the viewport. * @param {Object} modifiers * An object of modifier keys present. */ ActionChain.prototype.emitMouseEvent = function( doc, type, elClientX, elClientY, button, clickCount, modifiers) { if (!this.checkForInterrupted()) { let loggingInfo = "emitting Mouse event of type " + type + " at coordinates (" + elClientX + ", " + elClientY + ") relative to the viewport\n" + " button: " + button + "\n" + " clickCount: " + clickCount + "\n"; dump(Date.now() + " Marionette: " + loggingInfo); let win = doc.defaultView; let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsIDOMWindowUtils); let mods; if (typeof modifiers != "undefined") { mods = this.utils._parseModifiers(modifiers); } else { mods = 0; } domUtils.sendMouseEvent( type, elClientX, elClientY, button || 0, clickCount || 1, mods, false, 0, this.inputSource); } }; /** * Reset any persisted values after a command completes. */ ActionChain.prototype.resetValues = function() { this.onSuccess = null; this.onError = null; this.container = null; this.elementManager = null; this.touchProvider = null; this.mouseEventsOnly = false; }; /** * Function to emit touch events for each finger. e.g. * finger=[['press', id], ['wait', 5], ['release']] touchId represents * the finger id, i keeps track of the current action of the chain * keyModifiers is an object keeping track keyDown/keyUp pairs through * an action chain. */ ActionChain.prototype.actions = function(chain, touchId, i, keyModifiers) { if (i == chain.length) { this.onSuccess(touchId || null); this.resetValues(); return; } let pack = chain[i]; let command = pack[0]; let el; let c; i++; if (["press", "wait", "keyDown", "keyUp", "click"].indexOf(command) == -1) { // if mouseEventsOnly, then touchIds isn't used if (!(touchId in this.touchIds) && !this.mouseEventsOnly) { this.resetValues(); throw new WebDriverError("Element has not been pressed"); } } switch(command) { case "keyDown": this.utils.sendKeyDown(pack[1], keyModifiers, this.container.frame); this.actions(chain, touchId, i, keyModifiers); break; case "keyUp": this.utils.sendKeyUp(pack[1], keyModifiers, this.container.frame); this.actions(chain, touchId, i, keyModifiers); break; case "click": el = this.elementManager.getKnownElement(pack[1], this.container); let button = pack[2]; let clickCount = pack[3]; c = this.coordinates(el, null, null); this.mouseTap(el.ownerDocument, c.x, c.y, button, clickCount, keyModifiers); if (button == 2) { this.emitMouseEvent(el.ownerDocument, "contextmenu", c.x, c.y, button, clickCount, keyModifiers); } this.actions(chain, touchId, i, keyModifiers); break; case "press": if (this.lastCoordinates) { this.generateEvents( "cancel", this.lastCoordinates[0], this.lastCoordinates[1], touchId, null, keyModifiers); this.resetValues(); throw new WebDriverError( "Invalid Command: press cannot follow an active touch event"); } // look ahead to check if we're scrolling. Needed for APZ touch dispatching. if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) { this.scrolling = true; } el = this.elementManager.getKnownElement(pack[1], this.container); c = this.coordinates(el, pack[2], pack[3]); touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers); this.actions(chain, touchId, i, keyModifiers); break; case "release": this.generateEvents( "release", this.lastCoordinates[0], this.lastCoordinates[1], touchId, null, keyModifiers); this.actions(chain, null, i, keyModifiers); this.scrolling = false; break; case "move": el = this.elementManager.getKnownElement(pack[1], this.container); c = this.coordinates(el); this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers); this.actions(chain, touchId, i, keyModifiers); break; case "moveByOffset": this.generateEvents( "move", this.lastCoordinates[0] + pack[1], this.lastCoordinates[1] + pack[2], touchId, null, keyModifiers); this.actions(chain, touchId, i, keyModifiers); break; case "wait": if (pack[1] != null) { let time = pack[1] * 1000; // standard waiting time to fire contextmenu let standard = 750; try { standard = Services.prefs.getIntPref("ui.click_hold_context_menus.delay"); } catch (e) {} if (time >= standard && this.isTap) { chain.splice(i, 0, ["longPress"], ["wait", (time - standard) / 1000]); time = standard; } this.checkTimer.initWithCallback( () => this.actions(chain, touchId, i, keyModifiers), time, Components.interfaces.nsITimer.TYPE_ONE_SHOT); } else { this.actions(chain, touchId, i, keyModifiers); } break; case "cancel": this.generateEvents( "cancel", this.lastCoordinates[0], this.lastCoordinates[1], touchId, null, keyModifiers); this.actions(chain, touchId, i, keyModifiers); this.scrolling = false; break; case "longPress": this.generateEvents( "contextmenu", this.lastCoordinates[0], this.lastCoordinates[1], touchId, null, keyModifiers); this.actions(chain, touchId, i, keyModifiers); break; } }; /** * This function generates a pair of coordinates relative to the viewport given a * target element and coordinates relative to that element's top-left corner. * * @param {DOMElement} target * The target to calculate coordinates of. * @param {number} x * X coordinate relative to target. If unspecified, the centre of * the target is used. * @param {number} y * Y coordinate relative to target. If unspecified, the centre of * the target is used. */ ActionChain.prototype.coordinates = function(target, x, y) { let box = target.getBoundingClientRect(); if (x == null) { x = box.width / 2; } if (y == null) { y = box.height / 2; } let coords = {}; coords.x = box.left + x; coords.y = box.top + y; return coords; }; /** * Given an element and a pair of coordinates, returns an array of the * form [clientX, clientY, pageX, pageY, screenX, screenY]. */ ActionChain.prototype.getCoordinateInfo = function(el, corx, cory) { let win = el.ownerDocument.defaultView; return [ corx, // clientX cory, // clientY corx + win.pageXOffset, // pageX cory + win.pageYOffset, // pageY corx + win.mozInnerScreenX, // screenX cory + win.mozInnerScreenY // screenY ]; }; /** * @param {number} x * X coordinate of the location to generate the event that is relative * to the viewport. * @param {number} y * Y coordinate of the location to generate the event that is relative * to the viewport. */ ActionChain.prototype.generateEvents = function( type, x, y, touchId, target, keyModifiers) { this.lastCoordinates = [x, y]; let doc = this.container.frame.document; switch (type) { case "tap": if (this.mouseEventsOnly) { this.mouseTap( touch.target.ownerDocument, touch.clientX, touch.clientY, null, null, keyModifiers); } else { touchId = this.nextTouchId++; let touch = this.touchProvider.createATouch(target, x, y, touchId); this.touchProvider.emitTouchEvent("touchstart", touch); this.touchProvider.emitTouchEvent("touchend", touch); this.mouseTap( touch.target.ownerDocument, touch.clientX, touch.clientY, null, null, keyModifiers); } this.lastCoordinates = null; break; case "press": this.isTap = true; if (this.mouseEventsOnly) { this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers); this.emitMouseEvent(doc, "mousedown", x, y, null, null, keyModifiers); } else { touchId = this.nextTouchId++; let touch = this.touchProvider.createATouch(target, x, y, touchId); this.touchProvider.emitTouchEvent("touchstart", touch); this.touchIds[touchId] = touch; return touchId; } break; case "release": if (this.mouseEventsOnly) { let [x, y] = this.lastCoordinates; this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers); } else { let touch = this.touchIds[touchId]; let [x, y] = this.lastCoordinates; touch = this.touchProvider.createATouch(touch.target, x, y, touchId); this.touchProvider.emitTouchEvent("touchend", touch); if (this.isTap) { this.mouseTap( touch.target.ownerDocument, touch.clientX, touch.clientY, null, null, keyModifiers); } delete this.touchIds[touchId]; } this.isTap = false; this.lastCoordinates = null; break; case "cancel": this.isTap = false; if (this.mouseEventsOnly) { let [x, y] = this.lastCoordinates; this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers); } else { this.touchProvider.emitTouchEvent("touchcancel", this.touchIds[touchId]); delete this.touchIds[touchId]; } this.lastCoordinates = null; break; case "move": this.isTap = false; if (this.mouseEventsOnly) { this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers); } else { let touch = this.touchProvider.createATouch( this.touchIds[touchId].target, x, y, touchId); this.touchIds[touchId] = touch; this.touchProvider.emitTouchEvent("touchmove", touch); } break; case "contextmenu": this.isTap = false; let event = this.container.frame.document.createEvent("MouseEvents"); if (this.mouseEventsOnly) { target = doc.elementFromPoint(this.lastCoordinates[0], this.lastCoordinates[1]); } else { target = this.touchIds[touchId].target; } let [clientX, clientY, pageX, pageY, screenX, screenY] = this.getCoordinateInfo(target, x, y); event.initMouseEvent( "contextmenu", true, true, target.ownerDocument.defaultView, 1, screenX, screenY, clientX, clientY, false, false, false, false, 0, null); target.dispatchEvent(event); break; default: throw new WebDriverError("Unknown event type: " + type); } this.checkForInterrupted(); }; ActionChain.prototype.mouseTap = function(doc, x, y, button, count, mod) { this.emitMouseEvent(doc, "mousemove", x, y, button, count, mod); this.emitMouseEvent(doc, "mousedown", x, y, button, count, mod); this.emitMouseEvent(doc, "mouseup", x, y, button, count, mod); };