Bug 1107706: Part 16: Fix rebase of action chains for chrome space

--HG--
extra : rebase_source : 82f95f1fd1e7eda093148bd4d0f7bdc1525cc8d2
This commit is contained in:
Andreas Tolfsen 2015-03-24 15:35:58 +00:00
Родитель 2bff29ff8d
Коммит 53cbc0626e
3 изменённых файлов: 380 добавлений и 244 удалений

Просмотреть файл

@ -2,13 +2,15 @@
* 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.ActionChain = function(utils, checkForInterrupted) {
// for assigning unique ids to all touches
this.nextTouchId = 1000;
// Keep track of active Touches
// keep track of active Touches
this.touchIds = {};
// last touch for each fingerId
this.lastCoordinates = null;
@ -17,9 +19,9 @@ this.ActionChain = function (utils, checkForInterrupted) {
// whether to send mouse event
this.mouseEventsOnly = false;
this.checkTimer = Components.classes["@mozilla.org/timer;1"]
.createInstance(Components.interfaces.nsITimer);
.createInstance(Components.interfaces.nsITimer);
// Callbacks for command completion.
// callbacks for command completion
this.onSuccess = null;
this.onError = null;
if (typeof checkForInterrupted == "function") {
@ -28,355 +30,463 @@ this.ActionChain = function (utils, checkForInterrupted) {
this.checkForInterrupted = () => {};
}
// Determines if we create touch events.
// determines if we create touch events
this.inputSource = null;
// Test utilities providing some event synthesis code.
// test utilities providing some event synthesis code
this.utils = utils;
}
};
ActionChain.prototype = {
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;
}
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);
this.onSuccess = callbacks.onSuccess;
this.onError = callbacks.onError;
this.frame = frame;
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 (touchId == null) {
touchId = this.nextTouchId++;
}
if (!frame.document.createTouch) {
this.mouseEventsOnly = true;
}
if (!frame.document.createTouch) {
this.mouseEventsOnly = true;
}
let keyModifiers = {
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false
};
let keyModifiers = {
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false
};
try {
this.actions(commandArray, touchId, 0, keyModifiers);
} catch (e) {
this.onError(e);
this.resetValues();
}
};
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 {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);
/**
* 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)
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);
let mods;
if (typeof modifiers != "undefined") {
mods = this.utils._parseModifiers(modifiers);
} else {
mods = 0;
}
},
/**
* 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;
},
domUtils.sendMouseEvent(
type,
elClientX,
elClientY,
button || 0,
clickCount || 1,
mods,
false,
0,
this.inputSource);
}
};
/**
* 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) {
/**
* Reset any persisted values after a command completes.
*/
ActionChain.prototype.resetValues = function() {
this.onSuccess = null;
this.onError = null;
this.frame = null;
this.elementManager = null;
this.touchProvider = null;
this.mouseEventsOnly = false;
};
if (i == chain.length) {
this.onSuccess({value: touchId});
/**
* 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({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.resetValues();
return;
throw new WebDriverError("Element has not been pressed");
}
}
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':
switch(command) {
case "keyDown":
this.utils.sendKeyDown(pack[1], keyModifiers, this.frame);
this.actions(chain, touchId, i, keyModifiers);
break;
case 'keyUp':
case "keyUp":
this.utils.sendKeyUp(pack[1], keyModifiers, this.frame);
this.actions(chain, touchId, i, keyModifiers);
break;
case 'click':
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);
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.emitMouseEvent(el.ownerDocument, "contextmenu", c.x, c.y,
button, clickCount, keyModifiers);
}
this.actions(chain, touchId, i, keyModifiers);
break;
case 'press':
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.generateEvents(
"cancel",
this.lastCoordinates[0],
this.lastCoordinates[1],
touchId,
null,
keyModifiers);
this.resetValues();
return;
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.frame);
c = this.coordinates(el, pack[2], pack[3]);
touchId = this.generateEvents('press', c.x, c.y, null, el, keyModifiers);
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);
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':
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.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);
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;
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){}
} catch (e) {}
if (time >= standard && this.isTap) {
chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]);
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.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);
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);
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
];
},
/**
* 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;
};
//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':
/**
* 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.frame.document;
switch (type) {
case "tap":
if (this.mouseEventsOnly) {
this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY,
null, null, keyModifiers);
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.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':
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 {
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.touchProvider.emitTouchEvent("touchstart", touch);
this.touchIds[touchId] = touch;
return touchId;
}
break;
case 'release':
case "release":
if (this.mouseEventsOnly) {
let [x, y] = this.lastCoordinates;
this.emitMouseEvent(doc, 'mouseup', x, y,
null, null, keyModifiers);
}
else {
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);
this.touchProvider.emitTouchEvent("touchend", touch);
if (this.isTap) {
this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY,
null, null, keyModifiers);
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':
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]);
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':
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.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);
this.touchProvider.emitTouchEvent("touchmove", touch);
}
break;
case 'contextmenu':
case "contextmenu":
this.isTap = false;
let event = this.frame.document.createEvent('MouseEvents');
let event = this.frame.document.createEvent("MouseEvents");
if (this.mouseEventsOnly) {
target = doc.elementFromPoint(this.lastCoordinates[0], this.lastCoordinates[1]);
}
else {
} 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);
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);
},
}
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);
};

Просмотреть файл

@ -22,6 +22,7 @@ this.DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
XPCOMUtils.defineLazyServiceGetter(
this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager");
Cu.import("chrome://marionette/content/actions.js");
Cu.import("chrome://marionette/content/elements.js");
Cu.import("chrome://marionette/content/emulator.js");
Cu.import("chrome://marionette/content/error.js");
@ -239,6 +240,7 @@ this.GeckoDriver = function(appName, device, emulator) {
this.oopFrameId = null;
this.observing = null;
this._browserIds = new WeakMap();
this.actions = new ActionChain(utils);
this.sessionCapabilities = {
// Mandated capabilities
@ -1879,14 +1881,29 @@ GeckoDriver.prototype.singleTap = function(cmd, resp) {
* Last touch ID.
*/
GeckoDriver.prototype.actionChain = function(cmd, resp) {
let {chain, nextId} = cmd.parameters;
switch (this.context) {
case Context.CHROME:
throw new WebDriverError("Command 'actionChain' is not available in chrome context");
if (this.appName != "Firefox") {
// be conservative until this has a use case and is established
// to work as expected on b2g/fennec
throw new WebDriverError(
"Command 'actionChain' is not available in chrome context");
}
let cbs = {};
cbs.onSuccess = val => resp.value = val;
cbs.onError = err => { throw err };
let win = this.getCurrentWindow();
let elm = this.curBrowser.elementManager;
this.actions.dispatchActions(chain, nextId, win, elm, cbs);
break;
case Context.CONTENT:
this.addFrameCloseListener("action chain");
resp.value = yield this.listener.actionChain(
{chain: cmd.parameters.chain, nextId: cmd.parameters.nextId});
resp.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
break;
}
};

Просмотреть файл

@ -969,8 +969,17 @@ function actionChain(msg) {
touchProvider.createATouch = createATouch;
touchProvider.emitTouchEvent = emitTouchEvent;
actions.dispatchActions(args, touchId, curFrame, elementManager, callbacks,
touchProvider);
try {
actions.dispatchActions(
args,
touchId,
curFrame,
elementManager,
callbacks,
touchProvider);
} catch (e) {
sendError(e.message, e.code, e.stack, command_id);
}
}
/**