123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- /* 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/. */
- const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
- Cu.import("resource://gre/modules/Log.jsm");
- Cu.import("resource://gre/modules/Preferences.jsm");
- Cu.import("chrome://marionette/content/element.js");
- Cu.import("chrome://marionette/content/event.js");
- const CONTEXT_MENU_DELAY_PREF = "ui.click_hold_context_menus.delay";
- const DEFAULT_CONTEXT_MENU_DELAY = 750; // ms
- this.EXPORTED_SYMBOLS = ["legacyaction"];
- const logger = Log.repository.getLogger("Marionette");
- this.legacyaction = this.action = {};
- /**
- * Functionality for (single finger) action chains.
- */
- action.Chain = function (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 = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- if (typeof checkForInterrupted == "function") {
- this.checkForInterrupted = checkForInterrupted;
- } else {
- this.checkForInterrupted = () => {};
- }
- // determines if we create touch events
- this.inputSource = null;
- };
- action.Chain.prototype.dispatchActions = function (
- args,
- touchId,
- container,
- seenEls,
- 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.seenEls = seenEls;
- this.container = container;
- let commandArray = element.fromJson(
- args, seenEls, container.frame, container.shadowRoot);
- if (touchId == null) {
- touchId = this.nextTouchId++;
- }
- if (!container.frame.document.createTouch) {
- this.mouseEventsOnly = true;
- }
- let keyModifiers = {
- shiftKey: false,
- ctrlKey: false,
- altKey: false,
- metaKey: false,
- };
- return new Promise(resolve => {
- this.actions(commandArray, touchId, 0, keyModifiers, resolve);
- }).catch(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.
- */
- action.Chain.prototype.emitMouseEvent = function (
- doc,
- type,
- elClientX,
- elClientY,
- button,
- clickCount,
- modifiers) {
- if (!this.checkForInterrupted()) {
- logger.debug(`Emitting ${type} mouse event ` +
- `at coordinates (${elClientX}, ${elClientY}) ` +
- `relative to the viewport, ` +
- `button: ${button}, ` +
- `clickCount: ${clickCount}`);
- let win = doc.defaultView;
- let domUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
- let mods;
- if (typeof modifiers != "undefined") {
- mods = event.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.
- */
- action.Chain.prototype.resetValues = function() {
- this.container = null;
- this.seenEls = null;
- this.touchProvider = null;
- this.mouseEventsOnly = false;
- };
- /**
- * Emit events for each action in the provided chain.
- *
- * To emit touch events for each finger, one might send a [["press", id],
- * ["wait", 5], ["release"]] chain.
- *
- * @param {Array.<Array<?>>} chain
- * A multi-dimensional array of actions.
- * @param {Object.<string, number>} touchId
- * Represents the finger ID.
- * @param {number} i
- * Keeps track of the current action of the chain.
- * @param {Object.<string, boolean>} keyModifiers
- * Keeps track of keyDown/keyUp pairs through an action chain.
- * @param {function(?)} cb
- * Called on success.
- *
- * @return {Object.<string, number>}
- * Last finger ID, or an empty object.
- */
- action.Chain.prototype.actions = function (chain, touchId, i, keyModifiers, cb) {
- if (i == chain.length) {
- cb(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":
- event.sendKeyDown(pack[1], keyModifiers, this.container.frame);
- this.actions(chain, touchId, i, keyModifiers, cb);
- break;
- case "keyUp":
- event.sendKeyUp(pack[1], keyModifiers, this.container.frame);
- this.actions(chain, touchId, i, keyModifiers, cb);
- break;
- case "click":
- el = this.seenEls.get(pack[1], this.container);
- let button = pack[2];
- let clickCount = pack[3];
- c = element.coordinates(el);
- 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, cb);
- 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.seenEls.get(pack[1], this.container);
- c = element.coordinates(el, pack[2], pack[3]);
- touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers);
- this.actions(chain, touchId, i, keyModifiers, cb);
- break;
- case "release":
- this.generateEvents(
- "release",
- this.lastCoordinates[0],
- this.lastCoordinates[1],
- touchId,
- null,
- keyModifiers);
- this.actions(chain, null, i, keyModifiers, cb);
- this.scrolling = false;
- break;
- case "move":
- el = this.seenEls.get(pack[1], this.container);
- c = element.coordinates(el);
- this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers);
- this.actions(chain, touchId, i, keyModifiers, cb);
- 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, cb);
- break;
- case "wait":
- if (pack[1] != null) {
- let time = pack[1] * 1000;
- // standard waiting time to fire contextmenu
- let standard = Preferences.get(
- CONTEXT_MENU_DELAY_PREF,
- DEFAULT_CONTEXT_MENU_DELAY);
- 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, cb),
- time, Ci.nsITimer.TYPE_ONE_SHOT);
- } else {
- this.actions(chain, touchId, i, keyModifiers, cb);
- }
- break;
- case "cancel":
- this.generateEvents(
- "cancel",
- this.lastCoordinates[0],
- this.lastCoordinates[1],
- touchId,
- null,
- keyModifiers);
- this.actions(chain, touchId, i, keyModifiers, cb);
- this.scrolling = false;
- break;
- case "longPress":
- this.generateEvents(
- "contextmenu",
- this.lastCoordinates[0],
- this.lastCoordinates[1],
- touchId,
- null,
- keyModifiers);
- this.actions(chain, touchId, i, keyModifiers, cb);
- break;
- }
- };
- /**
- * Given an element and a pair of coordinates, returns an array of the
- * form [clientX, clientY, pageX, pageY, screenX, screenY].
- */
- action.Chain.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.
- */
- action.Chain.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();
- };
- action.Chain.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);
- };
|