gecko/testing/marionette/marionette-actions.js
Wes Kocher 9359b09a77 Backed out 15 changesets (bug 1107706) for marionette bustage CLOSED TREE
Backed out changeset 3c25064e24da (bug 1107706)
Backed out changeset 3b7cdf06f4b9 (bug 1107706)
Backed out changeset ec2b1317d3c6 (bug 1107706)
Backed out changeset 91b35cb3308b (bug 1107706)
Backed out changeset 43c58b21251f (bug 1107706)
Backed out changeset e3ddaf8aae39 (bug 1107706)
Backed out changeset 0cd696bfc3b0 (bug 1107706)
Backed out changeset eeb3d39874b1 (bug 1107706)
Backed out changeset 7bc309f733fa (bug 1107706)
Backed out changeset 69669d0e6ddc (bug 1107706)
Backed out changeset 7f506cdb77b8 (bug 1107706)
Backed out changeset 7abef4010b30 (bug 1107706)
Backed out changeset b0d00faceef4 (bug 1107706)
Backed out changeset 0c074cdc434e (bug 1107706)
Backed out changeset 3b449f8dd470 (bug 1107706)
2015-03-23 18:48:07 -07:00

383 lines
13 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/. */
/**
* 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, frame, 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, frame);
let {onSuccess, onError} = callbacks;
this.onSuccess = onSuccess;
this.onError = onError;
this.frame = frame;
if (touchId == null) {
touchId = this.nextTouchId++;
}
if (!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) {
this.onError(e.message, e.code, e.stack);
this.resetValues();
}
},
/**
* This function emit mouse event
* @param: doc is the current document
* type is the type of event to dispatch
* clickCount is the number of clicks, button notes the mouse button
* elClientX and elClientY are the coordinates of the mouse relative to the viewport
* modifiers is an object of modifier keys present
*/
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.
*/
resetValues: function () {
this.onSuccess = null;
this.onError = null;
this.frame = 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.
*/
actions: function (chain, touchId, i, keyModifiers) {
if (i == chain.length) {
this.onSuccess({value: touchId});
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.onError("Element has not been pressed", 500, null);
this.resetValues();
return;
}
}
switch(command) {
case 'keyDown':
this.utils.sendKeyDown(pack[1], keyModifiers, this.frame);
this.actions(chain, touchId, i, keyModifiers);
break;
case 'keyUp':
this.utils.sendKeyUp(pack[1], keyModifiers, this.frame);
this.actions(chain, touchId, i, keyModifiers);
break;
case 'click':
el = this.elementManager.getKnownElement(pack[1], this.frame);
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.onError("Invalid Command: press cannot follow an active touch event", 500, null);
this.resetValues();
return;
}
// 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.frame);
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.frame);
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 'x', and 'y' are the relative to the target.
* If they are not specified, then the center of the target is used.
*/
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 ]
*/
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
];
},
//x and y are coordinates relative to the viewport
generateEvents: function (type, x, y, touchId, target, keyModifiers) {
this.lastCoordinates = [x, y];
let doc = this.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.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 {message:"Unknown event type: " + type, code: 500, stack:null};
}
this.checkForInterrupted();
},
mouseTap: function (doc, x, y, button, clickCount, keyModifiers) {
this.emitMouseEvent(doc, 'mousemove', x, y, button, clickCount, keyModifiers);
this.emitMouseEvent(doc, 'mousedown', x, y, button, clickCount, keyModifiers);
this.emitMouseEvent(doc, 'mouseup', x, y, button, clickCount, keyModifiers);
},
}