Bug 1135846 - Expose marionette's actions code to chrome scope where applicable. r=dburns

This commit is contained in:
Chris Manchester 2015-03-19 18:41:19 -07:00
parent 1b6f431698
commit 63bae57fab
7 changed files with 578 additions and 404 deletions

View File

@ -32,72 +32,3 @@ class TestClick(MarionetteTestCase):
with self.assertRaises(ElementNotVisibleException):
self.marionette.find_element(By.ID, 'child').click()
class TestClickAction(MarionetteTestCase):
def setUp(self):
MarionetteTestCase.setUp(self)
if self.marionette.session_capabilities['platformName'] == 'DARWIN':
self.mod_key = Keys.META
else:
self.mod_key = Keys.CONTROL
self.action = Actions(self.marionette)
def test_click_action(self):
test_html = self.marionette.absolute_url("test.html")
self.marionette.navigate(test_html)
link = self.marionette.find_element(By.ID, "mozLink")
self.action.click(link).perform()
self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;"))
def test_clicking_element_out_of_view_succeeds(self):
# The action based click doesn't check for visibility.
test_html = self.marionette.absolute_url('hidden.html')
self.marionette.navigate(test_html)
el = self.marionette.find_element(By.ID, 'child')
self.action.click(el).perform()
def test_double_click_action(self):
test_html = self.marionette.absolute_url("javascriptPage.html")
self.marionette.navigate(test_html)
el = self.marionette.find_element(By.ID, 'displayed')
# The first click just brings the element into view so text selection
# works as expected. (A different test page could be used to isolate
# this element and make sure it's always in view)
el.click()
self.action.double_click(el).perform()
el.send_keys(self.mod_key + 'c')
rel = self.marionette.find_element("id", "keyReporter")
rel.send_keys(self.mod_key + 'v')
self.assertEqual(rel.get_attribute('value'), 'Displayed')
def test_context_click_action(self):
test_html = self.marionette.absolute_url("javascriptPage.html")
self.marionette.navigate(test_html)
click_el = self.marionette.find_element(By.ID, 'resultContainer')
def context_menu_state():
with self.marionette.using_context('chrome'):
cm_el = self.marionette.find_element(By.ID, 'contentAreaContextMenu')
return cm_el.get_attribute('state')
self.assertEqual('closed', context_menu_state())
self.action.context_click(click_el).perform()
self.wait_for_condition(lambda _: context_menu_state() == 'open')
with self.marionette.using_context('chrome'):
(self.marionette.find_element(By.ID, 'main-window')
.send_keys(Keys.ESCAPE))
self.wait_for_condition(lambda _: context_menu_state() == 'closed')
def test_middle_click_action(self):
test_html = self.marionette.absolute_url("clicks.html")
self.marionette.navigate(test_html)
self.marionette.find_element(By.ID, "addbuttonlistener").click()
el = self.marionette.find_element(By.ID, "showbutton")
self.action.middle_click(el).perform()
self.wait_for_condition(
lambda _: el.get_attribute('innerHTML') == '1')

View File

@ -0,0 +1,117 @@
# 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/.
from marionette import MarionetteTestCase
from marionette_driver.marionette import Actions
from marionette_driver.keys import Keys
from marionette_driver.by import By
class TestMouseAction(MarionetteTestCase):
def setUp(self):
MarionetteTestCase.setUp(self)
if self.marionette.session_capabilities['platformName'] == 'DARWIN':
self.mod_key = Keys.META
else:
self.mod_key = Keys.CONTROL
self.action = Actions(self.marionette)
def test_click_action(self):
test_html = self.marionette.absolute_url("test.html")
self.marionette.navigate(test_html)
link = self.marionette.find_element(By.ID, "mozLink")
self.action.click(link).perform()
self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;"))
def test_clicking_element_out_of_view_succeeds(self):
# The action based click doesn't check for visibility.
test_html = self.marionette.absolute_url('hidden.html')
self.marionette.navigate(test_html)
el = self.marionette.find_element(By.ID, 'child')
self.action.click(el).perform()
def test_double_click_action(self):
test_html = self.marionette.absolute_url("javascriptPage.html")
self.marionette.navigate(test_html)
el = self.marionette.find_element(By.ID, 'displayed')
# The first click just brings the element into view so text selection
# works as expected. (A different test page could be used to isolate
# this element and make sure it's always in view)
el.click()
self.action.double_click(el).perform()
el.send_keys(self.mod_key + 'c')
rel = self.marionette.find_element("id", "keyReporter")
rel.send_keys(self.mod_key + 'v')
self.assertEqual(rel.get_attribute('value'), 'Displayed')
def test_context_click_action(self):
test_html = self.marionette.absolute_url("javascriptPage.html")
self.marionette.navigate(test_html)
click_el = self.marionette.find_element(By.ID, 'resultContainer')
def context_menu_state():
with self.marionette.using_context('chrome'):
cm_el = self.marionette.find_element(By.ID, 'contentAreaContextMenu')
return cm_el.get_attribute('state')
self.assertEqual('closed', context_menu_state())
self.action.context_click(click_el).perform()
self.wait_for_condition(lambda _: context_menu_state() == 'open')
with self.marionette.using_context('chrome'):
(self.marionette.find_element(By.ID, 'main-window')
.send_keys(Keys.ESCAPE))
self.wait_for_condition(lambda _: context_menu_state() == 'closed')
def test_middle_click_action(self):
test_html = self.marionette.absolute_url("clicks.html")
self.marionette.navigate(test_html)
self.marionette.find_element(By.ID, "addbuttonlistener").click()
el = self.marionette.find_element(By.ID, "showbutton")
self.action.middle_click(el).perform()
self.wait_for_condition(
lambda _: el.get_attribute('innerHTML') == '1')
def test_chrome_click(self):
self.marionette.navigate("about:blank")
data_uri = "data:text/html,<html></html>"
with self.marionette.using_context('chrome'):
urlbar = self.marionette.find_element(By.ID, "urlbar")
urlbar.send_keys(data_uri)
go_button = self.marionette.find_element(By.ID, "urlbar-go-button")
self.action.click(go_button).perform()
self.wait_for_condition(lambda mn: mn.get_url() == data_uri)
def test_chrome_double_click(self):
self.marionette.navigate("about:blank")
test_word = "quux"
with self.marionette.using_context('chrome'):
urlbar = self.marionette.find_element(By.ID, "urlbar")
self.assertEqual(urlbar.get_attribute('value'), '')
urlbar.send_keys(test_word)
self.assertEqual(urlbar.get_attribute('value'), test_word)
(self.action.double_click(urlbar).perform()
.key_down(self.mod_key)
.key_down('x').perform())
self.assertEqual(urlbar.get_attribute('value'), '')
def test_chrome_context_click_action(self):
self.marionette.set_context('chrome')
def context_menu_state():
cm_el = self.marionette.find_element(By.ID, 'tabContextMenu')
return cm_el.get_attribute('state')
currtab = self.marionette.execute_script("return gBrowser.selectedTab")
self.assertEqual('closed', context_menu_state())
self.action.context_click(currtab).perform()
self.wait_for_condition(lambda _: context_menu_state() == 'open')
(self.marionette.find_element(By.ID, 'main-window')
.send_keys(Keys.ESCAPE))
self.wait_for_condition(lambda _: context_menu_state() == 'closed')

View File

@ -148,4 +148,6 @@ skip-if = os == "linux" # Bug 1085717
[test_modal_dialogs.py]
b2g = false
[test_key_actions.py]
[test_mouse_action.py]
b2g = false
[test_teardown_context_preserved.py]

View File

@ -9,6 +9,7 @@ marionette.jar:
content/marionette-elements.js (marionette-elements.js)
content/marionette-sendkeys.js (marionette-sendkeys.js)
content/marionette-common.js (marionette-common.js)
content/marionette-actions.js (marionette-actions.js)
content/marionette-simpletest.js (marionette-simpletest.js)
content/marionette-frame-manager.js (marionette-frame-manager.js)
content/EventUtils.js (EventUtils.js)

View File

@ -0,0 +1,382 @@
/* 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);
},
}

View File

@ -13,6 +13,7 @@ let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js");
loader.loadSubScript("chrome://marionette/content/marionette-common.js");
loader.loadSubScript("chrome://marionette/content/marionette-actions.js");
Cu.import("chrome://marionette/content/marionette-elements.js");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
@ -40,8 +41,8 @@ let curFrame = content;
let previousFrame = null;
let elementManager = new ElementManager([]);
let accessibility = new Accessibility();
let actions = new ActionChain(utils, checkForInterrupted);
let importedScripts = null;
let inputSource = null;
// The sandbox we execute test scripts in. Gets lazily created in
// createExecuteContentSandbox().
@ -68,17 +69,8 @@ let navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let onDOMContentLoaded;
// Send move events about this often
let EVENT_INTERVAL = 30; // milliseconds
// For assigning unique ids to all touches
let nextTouchId = 1000;
//Keep track of active Touches
let touchIds = {};
// last touch for each fingerId
let multiLast = {};
let lastCoordinates = null;
let isTap = false;
let scrolling = false;
// whether to send mouse event
let mouseEventsOnly = false;
Cu.import("resource://gre/modules/Log.jsm");
let logger = Log.repository.getLogger("Marionette");
@ -124,7 +116,7 @@ function registerSelf() {
function emitTouchEventForIFrame(message) {
message = message.json;
let identifier = nextTouchId;
let identifier = actions.nextTouchId;
let domWindowUtils = curFrame.
QueryInterface(Components.interfaces.nsIInterfaceRequestor).
@ -245,7 +237,7 @@ function newSession(msg) {
// events being the result of a physical mouse action.
// This is especially important for the touch event shim,
// in order to prevent creating touch event for these fake mouse events.
inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
actions.inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
}
}
@ -326,7 +318,7 @@ function deleteSession(msg) {
// reset frame to the top-most frame
curFrame = content;
curFrame.focus();
touchIds = {};
actions.touchIds = {};
}
/*
@ -378,7 +370,7 @@ function sendError(message, status, trace, command_id) {
function resetValues() {
sandbox = null;
curFrame = content;
mouseEventsOnly = false;
actions.mouseEventsOnly = false;
}
/**
@ -404,6 +396,22 @@ function wasInterrupted() {
return sendSyncMessage("MarionetteFrame:getInterruptedState", {})[0].value;
}
function checkForInterrupted() {
if (wasInterrupted()) {
if (previousFrame) {
//if previousFrame is set, then we're in a single process environment
cuFrame = actions.frame = previousFrame;
previousFrame = null;
sandbox = null;
}
else {
//else we're in OOP environment, so we'll switch to the original OOP frame
sendSyncMessage("Marionette:switchToModalOrigin");
}
sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
}
}
/*
* Marionette Methods
*/
@ -731,7 +739,7 @@ function emitTouchEvent(type, touch) {
QueryInterface(Components.interfaces.nsIInterfaceRequestor).
getInterface(Components.interfaces.nsIWebNavigation).
QueryInterface(Components.interfaces.nsIDocShell);
if (docShell.asyncPanZoomEnabled && scrolling) {
if (docShell.asyncPanZoomEnabled && actions.scrolling) {
// if we're in APZ and we're scrolling, we must use injectTouchEvent to dispatch our touchmove events
let index = sendSyncMessage("MarionetteFrame:getCurrentFrameId");
// only call emitTouchEventForIFrame if we're inside an iframe.
@ -758,53 +766,6 @@ function emitTouchEvent(type, touch) {
}
}
/**
* 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
*/
function emitMouseEvent(doc, type, elClientX, elClientY, button, clickCount, modifiers) {
if (!wasInterrupted()) {
let loggingInfo = "emitting Mouse event of type " + type +
" at coordinates (" + elClientX + ", " + elClientY +
") relative to the viewport\n" +
" button: " + button + "\n" +
" clickCount: " + clickCount + "\n";
dumpLog(loggingInfo);
/*
Disabled per bug 888303
marionetteLogObj.log(loggingInfo, "TRACE");
sendSyncMessage("Marionette:shareData",
{log: elementManager.wrapValue(marionetteLogObj.getLogs())});
marionetteLogObj.clearLogs();
*/
let win = doc.defaultView;
let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
let mods;
if (typeof modifiers != "undefined") {
mods = utils._parseModifiers(modifiers);
} else {
mods = 0;
}
domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1,
mods, false, 0, inputSource);
}
}
/**
* Helper function that perform a mouse tap
*/
function mousetap(doc, x, y, keyModifiers) {
emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers);
emitMouseEvent(doc, 'mouseup', x, y, null, null, keyModifiers);
}
/**
* 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.
@ -825,6 +786,7 @@ function coordinates(target, x, y) {
return coords;
}
/**
* This function returns true if the given coordinates are in the viewport.
* @param 'x', and 'y' are the coordinates relative to the target.
@ -876,113 +838,6 @@ function checkVisible(el, x, y) {
return true;
}
//x and y are coordinates relative to the viewport
function generateEvents(type, x, y, touchId, target, keyModifiers) {
lastCoordinates = [x, y];
let doc = curFrame.document;
switch (type) {
case 'tap':
if (mouseEventsOnly) {
mousetap(target.ownerDocument, x, y);
}
else {
let touchId = nextTouchId++;
let touch = createATouch(target, x, y, touchId);
emitTouchEvent('touchstart', touch);
emitTouchEvent('touchend', touch);
mousetap(target.ownerDocument, x, y);
}
lastCoordinates = null;
break;
case 'press':
isTap = true;
if (mouseEventsOnly) {
emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers);
}
else {
let touchId = nextTouchId++;
let touch = createATouch(target, x, y, touchId);
emitTouchEvent('touchstart', touch);
touchIds[touchId] = touch;
return touchId;
}
break;
case 'release':
if (mouseEventsOnly) {
emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1],
null, null, keyModifiers);
}
else {
let touch = touchIds[touchId];
touch = createATouch(touch.target, lastCoordinates[0], lastCoordinates[1], touchId);
emitTouchEvent('touchend', touch);
if (isTap) {
mousetap(touch.target.ownerDocument, touch.clientX, touch.clientY, keyModifiers);
}
delete touchIds[touchId];
}
isTap = false;
lastCoordinates = null;
break;
case 'cancel':
isTap = false;
if (mouseEventsOnly) {
emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1],
null, null, keyModifiers);
}
else {
emitTouchEvent('touchcancel', touchIds[touchId]);
delete touchIds[touchId];
}
lastCoordinates = null;
break;
case 'move':
isTap = false;
if (mouseEventsOnly) {
emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
}
else {
touch = createATouch(touchIds[touchId].target, x, y, touchId);
touchIds[touchId] = touch;
emitTouchEvent('touchmove', touch);
}
break;
case 'contextmenu':
isTap = false;
let event = curFrame.document.createEvent('MouseEvents');
if (mouseEventsOnly) {
target = doc.elementFromPoint(lastCoordinates[0], lastCoordinates[1]);
}
else {
target = touchIds[touchId].target;
}
let [ clientX, clientY,
pageX, pageY,
screenX, screenY ] = 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};
}
if (wasInterrupted()) {
if (previousFrame) {
//if previousFrame is set, then we're in a single process environment
curFrame = previousFrame;
previousFrame = null;
sandbox = null;
}
else {
//else we're in OOP environment, so we'll switch to the original OOP frame
sendSyncMessage("Marionette:switchToModalOrigin");
}
sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
}
}
/**
* Function that perform a single tap
@ -1001,10 +856,16 @@ function singleTap(msg) {
}
checkActionableAccessibility(acc);
if (!curFrame.document.createTouch) {
mouseEventsOnly = true;
actions.mouseEventsOnly = true;
}
c = coordinates(el, msg.json.corx, msg.json.cory);
generateEvents('tap', c.x, c.y, null, el);
let c = coordinates(el, msg.json.corx, msg.json.cory);
if (!actions.mouseEventsOnly) {
let touchId = actions.nextTouchId++;
let touch = createATouch(el, c.x, c.y, touchId);
emitTouchEvent('touchstart', touch);
emitTouchEvent('touchend', touch);
}
actions.mouseTap(el.ownerDocument, c.x, c.y);
sendOk(msg.json.command_id);
}
catch (e) {
@ -1070,20 +931,6 @@ function checkActionableAccessibility(accesible) {
accessibility.handleErrorMessage(message);
}
/**
* Given an element and a pair of coordinates, returns an array of the form
* [ clientX, clientY, pageX, pageY, screenX, screenY ]
*/
function getCoordinateInfo(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
];
}
/**
* Function to create a touch based on the element
@ -1093,138 +940,11 @@ function createATouch(el, corx, cory, touchId) {
let doc = el.ownerDocument;
let win = doc.defaultView;
let [clientX, clientY, pageX, pageY, screenX, screenY] =
getCoordinateInfo(el, corx, cory);
actions.getCoordinateInfo(el, corx, cory);
let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY);
return atouch;
}
/**
* 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.
*/
function actions(chain, touchId, command_id, i, keyModifiers) {
if (typeof i === "undefined") {
i = 0;
}
if (typeof keyModifiers === "undefined") {
keyModifiers = {
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false
};
}
if (i == chain.length) {
sendResponse({value: touchId}, command_id);
return;
}
let pack = chain[i];
let command = pack[0];
let el;
let c;
i++;
if (['press', 'wait', 'keyDown', 'keyUp'].indexOf(command) == -1) {
//if mouseEventsOnly, then touchIds isn't used
if (!(touchId in touchIds) && !mouseEventsOnly) {
sendError("Element has not been pressed", 500, null, command_id);
return;
}
}
switch(command) {
case 'keyDown':
utils.sendKeyDown(pack[1], keyModifiers, curFrame);
actions(chain, touchId, command_id, i, keyModifiers);
break;
case 'keyUp':
utils.sendKeyUp(pack[1], keyModifiers, curFrame);
actions(chain, touchId, command_id, i, keyModifiers);
break;
case 'click':
el = elementManager.getKnownElement(pack[1], curFrame);
let button = pack[2];
let clickCount = pack[3];
c = coordinates(el, null, null);
emitMouseEvent(el.ownerDocument, 'mousemove', c.x, c.y, button, clickCount,
keyModifiers);
emitMouseEvent(el.ownerDocument, 'mousedown', c.x, c.y, button, clickCount,
keyModifiers);
emitMouseEvent(el.ownerDocument, 'mouseup', c.x, c.y, button, clickCount,
keyModifiers);
if (button == 2) {
emitMouseEvent(el.ownerDocument, 'contextmenu', c.x, c.y, button, clickCount,
keyModifiers);
}
actions(chain, touchId, command_id, i, keyModifiers);
break;
case 'press':
if (lastCoordinates) {
generateEvents('cancel', lastCoordinates[0], lastCoordinates[1],
touchId, null, keyModifiers);
sendError("Invalid Command: press cannot follow an active touch event", 500, null, command_id);
return;
}
// look ahead to check if we're scrolling. Needed for APZ touch dispatching.
if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) {
scrolling = true;
}
el = elementManager.getKnownElement(pack[1], curFrame);
c = coordinates(el, pack[2], pack[3]);
touchId = generateEvents('press', c.x, c.y, null, el, keyModifiers);
actions(chain, touchId, command_id, i, keyModifiers);
break;
case 'release':
generateEvents('release', lastCoordinates[0], lastCoordinates[1],
touchId, null, keyModifiers);
actions(chain, null, command_id, i, keyModifiers);
scrolling = false;
break;
case 'move':
el = elementManager.getKnownElement(pack[1], curFrame);
c = coordinates(el);
generateEvents('move', c.x, c.y, touchId, null, keyModifiers);
actions(chain, touchId, command_id, i, keyModifiers);
break;
case 'moveByOffset':
generateEvents('move', lastCoordinates[0] + pack[1], lastCoordinates[1] + pack[2],
touchId, null, keyModifiers);
actions(chain, touchId, command_id, 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 && isTap) {
chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]);
time = standard;
}
checkTimer.initWithCallback(function() {
actions(chain, touchId, command_id, i, keyModifiers);
}, time, Ci.nsITimer.TYPE_ONE_SHOT);
}
else {
actions(chain, touchId, command_id, i, keyModifiers);
}
break;
case 'cancel':
generateEvents('cancel', lastCoordinates[0], lastCoordinates[1],
touchId, null, keyModifiers);
actions(chain, touchId, command_id, i, keyModifiers);
scrolling = false;
break;
case 'longPress':
generateEvents('contextmenu', lastCoordinates[0], lastCoordinates[1],
touchId, null, keyModifiers);
actions(chain, touchId, command_id, i, keyModifiers);
break;
}
}
/**
* Function to start action chain on one finger
*/
@ -1232,20 +952,21 @@ function actionChain(msg) {
let command_id = msg.json.command_id;
let args = msg.json.chain;
let touchId = msg.json.nextId;
try {
let commandArray = elementManager.convertWrappedArguments(args, curFrame);
// loop the action array [ ['press', id], ['move', id], ['release', id] ]
if (touchId == null) {
touchId = nextTouchId++;
}
if (!curFrame.document.createTouch) {
mouseEventsOnly = true;
}
actions(commandArray, touchId, command_id);
}
catch (e) {
sendError(e.message, e.code, e.stack, msg.json.command_id);
}
let callbacks = {};
callbacks.onSuccess = (value) => {
sendResponse(value, command_id);
};
callbacks.onError = (message, code, trace) => {
sendError(message, code, trace, msg.json.command_id);
};
let touchProvider = {};
touchProvider.createATouch = createATouch;
touchProvider.emitTouchEvent = emitTouchEvent;
actions.dispatchActions(args, touchId, curFrame, elementManager, callbacks,
touchProvider);
}
/**

View File

@ -17,6 +17,7 @@ let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js");
loader.loadSubScript("chrome://marionette/content/marionette-common.js");
loader.loadSubScript("chrome://marionette/content/marionette-actions.js");
Cu.import("resource://gre/modules/Services.jsm");
loader.loadSubScript("chrome://marionette/content/marionette-frame-manager.js");
Cu.import("chrome://marionette/content/marionette-elements.js");
@ -190,6 +191,7 @@ function MarionetteServerConnection(aPrefix, aTransport, aServer)
this.observing = null;
this._browserIds = new WeakMap();
this.quitFlags = null;
this.actions = new ActionChain(utils);
}
MarionetteServerConnection.prototype = {
@ -1878,18 +1880,36 @@ MarionetteServerConnection.prototype = {
* 'value' represents a nested array: inner array represents each event; outer array represents collection of events
*/
actionChain: function MDA_actionChain(aRequest) {
this.command_id = this.getCommandId();
let command_id = this.command_id = this.getCommandId();
let chain = aRequest.parameters.chain;
let nextId = aRequest.parameters.nextId;
if (this.context == "chrome") {
this.sendError("Command 'actionChain' is not available in chrome context", 500, null, this.command_id);
}
else {
if (appName != 'Firefox') {
// Be conservative until this has a use case and is established to work as
// expected on b2g/fennec.
this.sendError("Command 'actionChain' is not available in chrome context",
500, null, this.command_id);
}
let callbacks = {};
callbacks.onSuccess = (value) => {
this.sendResponse(value, command_id);
};
callbacks.onError = (message, code, trace) => {
this.sendError(message, code, trace, command_id);
};
let currWin = this.getCurrentWindow();
let elementManager = this.curBrowser.elementManager;
this.actions.dispatchActions(chain, nextId, currWin, elementManager, callbacks);
} else {
this.addFrameCloseListener("action chain");
this.sendAsync("actionChain",
{
chain: aRequest.parameters.chain,
nextId: aRequest.parameters.nextId
chain: chain,
nextId: nextId
},
this.command_id);
command_id);
}
},