gecko-dev/testing/marionette/legacyaction.js

541 строка
14 KiB
JavaScript

/* 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 { Preferences } = ChromeUtils.import(
"resource://gre/modules/Preferences.jsm"
);
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { WebDriverError } = ChromeUtils.import(
"chrome://marionette/content/error.js"
);
const { element, WebElement } = ChromeUtils.import(
"chrome://marionette/content/element.js"
);
const { evaluate } = ChromeUtils.import(
"chrome://marionette/content/evaluate.js"
);
const { event } = ChromeUtils.import("chrome://marionette/content/event.js");
const { Log } = ChromeUtils.import("chrome://marionette/content/log.js");
XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
const CONTEXT_MENU_DELAY_PREF = "ui.click_hold_context_menus.delay";
const DEFAULT_CONTEXT_MENU_DELAY = 750; // ms
this.EXPORTED_SYMBOLS = ["legacyaction"];
/* global action */
/** @namespace */
this.legacyaction = this.action = {};
/**
* Functionality for (single finger) action chains.
*/
action.Chain = function() {
// 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);
// 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 = evaluate.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.bind(this));
};
/**
* 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
) {
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.windowUtils;
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 webEl;
let el;
let c;
i++;
if (!["press", "wait", "keyDown", "keyUp", "click"].includes(command)) {
// 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":
webEl = WebElement.fromUUID(pack[1], "content");
el = this.seenEls.get(webEl);
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].includes("move")) {
this.scrolling = true;
}
webEl = WebElement.fromUUID(pack[1], "content");
el = this.seenEls.get(webEl);
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":
webEl = WebElement.fromUUID(pack[1], "content");
el = this.seenEls.get(webEl);
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.ownerGlobal;
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) {
let touch = this.touchProvider.createATouch(target, x, y, touchId);
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, , , screenX, screenY] = this.getCoordinateInfo(
target,
x,
y
);
event.initMouseEvent(
"contextmenu",
true,
true,
target.ownerGlobal,
1,
screenX,
screenY,
clientX,
clientY,
false,
false,
false,
false,
0,
null
);
target.dispatchEvent(event);
break;
default:
throw new WebDriverError("Unknown event type: " + type);
}
return null;
};
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);
};