--- a/testing/marionette/action.js
+++ b/testing/marionette/action.js
@@ -335,22 +335,25 @@ const KEY_CODE_LOOKUP = {
};
/** Represents possible values for a pointer-move origin. */
action.PointerOrigin = {
Viewport: "viewport",
Pointer: "pointer",
};
+/** Flag for WebDriver spec conforming pointer origin calculation. */
+action.specCompatPointerOrigin = true;
+
/**
* Look up a PointerOrigin.
*
* @param {(string|Element)=} obj
* Origin for a <code>pointerMove</code> action. Must be one of
- * "viewport" (default), "pointer", or a DOM element or a DOM element.
+ * "viewport" (default), "pointer", or a DOM element.
*
* @return {action.PointerOrigin}
* Pointer origin.
*
* @throws {InvalidArgumentError}
* If <var>obj</var> is not a valid origin.
*/
action.PointerOrigin.get = function(obj) {
@@ -966,21 +969,28 @@ action.Mouse = class {
* tick's actions are not dispatched until the Promise for the current
* tick is resolved.
*
* @param {action.Chain} chain
* Actions grouped by tick; each element in |chain| is a sequence of
* actions for one tick.
* @param {WindowProxy} window
* Current window global.
+ * @param {boolean=} [specCompatPointerOrigin=true] specCompatPointerOrigin
+ * Flag to turn off the WebDriver spec conforming pointer origin
+ * calculation. It has to be kept until all Selenium bindings can
+ * successfully handle the WebDriver spec conforming Pointer Origin
+ * calculation. See https://bugzilla.mozilla.org/show_bug.cgi?id=1429338.
*
* @return {Promise}
* Promise for dispatching all actions in |chain|.
*/
-action.dispatch = function(chain, window) {
+action.dispatch = function(chain, window, specCompatPointerOrigin = true) {
+ action.specCompatPointerOrigin = specCompatPointerOrigin;
+
let chainEvents = (async () => {
for (let tickActions of chain) {
await action.dispatchTickActions(
tickActions,
action.computeTickDuration(tickActions),
window);
}
})();
@@ -1003,18 +1013,17 @@ action.dispatch = function(chain, window
* @param {number} tickDuration
* Duration in milliseconds of this tick.
* @param {WindowProxy} window
* Current window global.
*
* @return {Promise}
* Promise for dispatching all tick-actions and pending DOM events.
*/
-action.dispatchTickActions = function(
- tickActions, tickDuration, window) {
+action.dispatchTickActions = function(tickActions, tickDuration, window) {
let pendingEvents = tickActions.map(toEvents(tickDuration, window));
return Promise.all(pendingEvents);
};
/**
* Compute tick duration in milliseconds for a collection of actions.
*
* @param {Array.<action.Action>} tickActions
@@ -1424,12 +1433,15 @@ function inViewPort(x, y, win) {
assert.number(x, `Expected x to be finite number`);
assert.number(y, `Expected y to be finite number`);
// Viewport includes scrollbars if rendered.
return !(x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight);
}
function getElementCenter(el, window) {
if (element.isDOMElement(el)) {
- return element.getInViewCentrePoint(el.getClientRects()[0], window);
+ if (action.specCompatPointerOrigin) {
+ return element.getInViewCentrePoint(el.getClientRects()[0], window);
+ }
+ return element.coordinates(el);
}
return {};
}
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -596,16 +596,20 @@ GeckoDriver.prototype.listeningPromise =
* <dd>Describes the timeouts imposed on certian session operations.
*
* <dt><code>proxy</code> (Proxy object)
* <dd>Defines the proxy configuration.
*
* <dt><code>moz:accessibilityChecks</code> (boolean)
* <dd>Run a11y checks when clicking elements.
*
+ * <dt><code>moz:useNonSpecCompliantPointerOrigin</code> (boolean)
+ * <dd>Use the not WebDriver conforming calculation of the pointer origin
+ * when the origin is an element, and the element center point is used.
+ *
* <dt><code>moz:webdriverClick</code> (boolean)
* <dd>Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
* </dl>
*
* <h4>Timeouts object</h4>
*
* <dl>
* <dt><code>script</code> (number)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
@@ -63,24 +63,34 @@ class TestCapabilities(MarionetteTestCas
current_profile = self.marionette.instance.runner.device.app_ctx.remote_profile
else:
current_profile = convert_path(self.marionette.instance.runner.profile.profile)
self.assertEqual(convert_path(str(self.caps["moz:profile"])), current_profile)
self.assertEqual(convert_path(str(self.marionette.profile)), current_profile)
self.assertIn("moz:accessibilityChecks", self.caps)
self.assertFalse(self.caps["moz:accessibilityChecks"])
+
+ self.assertIn("moz:useNonSpecCompliantPointerOrigin", self.caps)
+ self.assertFalse(self.caps["moz:useNonSpecCompliantPointerOrigin"])
+
self.assertIn("moz:webdriverClick", self.caps)
- self.assertEqual(self.caps["moz:webdriverClick"], True)
+ self.assertTrue(self.caps["moz:webdriverClick"])
def test_disable_webdriver_click(self):
self.marionette.delete_session()
self.marionette.start_session({"moz:webdriverClick": False})
caps = self.marionette.session_capabilities
- self.assertEqual(False, caps["moz:webdriverClick"])
+ self.assertFalse(caps["moz:webdriverClick"])
+
+ def test_use_non_spec_compliant_pointer_origin(self):
+ self.marionette.delete_session()
+ self.marionette.start_session({"moz:useNonSpecCompliantPointerOrigin": True})
+ caps = self.marionette.session_capabilities
+ self.assertTrue(caps["moz:useNonSpecCompliantPointerOrigin"])
def test_we_get_valid_uuid4_when_creating_a_session(self):
self.assertNotIn("{", self.marionette.session_id,
"Session ID has {{}} in it: {}".format(
self.marionette.session_id))
class TestCapabilityMatching(MarionetteTestCase):
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_mouse_action.py
@@ -0,0 +1,145 @@
+# 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 __future__ import absolute_import
+
+import urllib
+
+from marionette_driver import By, errors, Wait
+from marionette_driver.keys import Keys
+from marionette_driver.marionette import W3C_WEBELEMENT_KEY
+
+from marionette_harness import MarionetteTestCase
+
+
+def inline(doc):
+ return "data:text/html;charset=utf-8,{}".format(urllib.quote(doc))
+
+
+class Actions(object):
+ """Temporary class until Marionette client supports the WebDriver actions."""
+
+ def __init__(self, marionette):
+ self.marionette = marionette
+
+ self.action_chain = []
+
+ def perform(self):
+ params = {"actions": [{
+ "actions": self.action_chain,
+ "id": "mouse",
+ "parameters": {
+ "pointerType": "mouse"
+ },
+ "type": "pointer"
+ }]}
+
+ return self.marionette._send_message("performActions", params=params)
+
+ def move(self, element, x=0, y=0, duration=250):
+ self.action_chain.append({
+ "duration": duration,
+ "origin": {
+ W3C_WEBELEMENT_KEY: element.id
+ },
+ "type": "pointerMove",
+ "x": x,
+ "y": y,
+ })
+
+ return self
+
+ def click(self):
+ self.action_chain.extend([{
+ "button": 0,
+ "type": "pointerDown"
+ }, {
+ "button": 0,
+ "type": "pointerUp"
+ }])
+
+ return self
+
+
+class BaseMouseAction(MarionetteTestCase):
+
+ def setUp(self):
+ super(BaseMouseAction, self).setUp()
+
+ if self.marionette.session_capabilities["platformName"] == "darwin":
+ self.mod_key = Keys.META
+ else:
+ self.mod_key = Keys.CONTROL
+
+ self.action = Actions(self.marionette)
+
+ def tearDown(self):
+ super(BaseMouseAction, self).tearDown()
+
+ @property
+ def click_position(self):
+ return self.marionette.execute_script("""
+ if (window.click_x && window.click_y) {
+ return {x: window.click_x, y: window.click_y};
+ }
+ """, sandbox=None)
+
+ def get_element_center_point(self, elem):
+ return {
+ "x": elem.location["x"] + elem.size["width"] / 2,
+ "y": elem.location["y"] + elem.size["height"] / 2
+ }
+
+
+class TestNonSpecCompliantPointerOrigin(BaseMouseAction):
+
+ def setUp(self):
+ super(TestNonSpecCompliantPointerOrigin, self).setUp()
+
+ self.marionette.delete_session()
+ self.marionette.start_session({"moz:useNonSpecCompliantPointerOrigin": True})
+
+ def tearDown(self):
+ self.marionette.delete_session()
+ self.marionette.start_session()
+
+ super(TestNonSpecCompliantPointerOrigin, self).tearDown()
+
+ def test_click_element_smaller_than_viewport(self):
+ self.marionette.navigate(inline("""
+ <div id="div" style="width: 10vw; height: 10vh; background: green;"
+ onclick="window.click_x = event.clientX; window.click_y = event.clientY" />
+ """))
+ elem = self.marionette.find_element(By.ID, "div")
+ elem_center_point = self.get_element_center_point(elem)
+
+ self.action.move(elem).click().perform()
+ click_position = Wait(self.marionette).until(lambda _: self.click_position,
+ message="No click event has been detected")
+ self.assertAlmostEqual(click_position["x"], elem_center_point["x"], delta=1)
+ self.assertAlmostEqual(click_position["y"], elem_center_point["y"], delta=1)
+
+ def test_click_element_larger_than_viewport_with_center_point_inside(self):
+ self.marionette.navigate(inline("""
+ <div id="div" style="width: 150vw; height: 150vh; background: green;"
+ onclick="window.click_x = event.clientX; window.click_y = event.clientY" />
+ """))
+ elem = self.marionette.find_element(By.ID, "div")
+ elem_center_point = self.get_element_center_point(elem)
+
+ self.action.move(elem).click().perform()
+ click_position = Wait(self.marionette).until(lambda _: self.click_position,
+ message="No click event has been detected")
+ self.assertAlmostEqual(click_position["x"], elem_center_point["x"], delta=1)
+ self.assertAlmostEqual(click_position["y"], elem_center_point["y"], delta=1)
+
+ def test_click_element_larger_than_viewport_with_center_point_outside(self):
+ self.marionette.navigate(inline("""
+ <div id="div" style="width: 300vw; height: 300vh; background: green;"
+ onclick="window.click_x = event.clientX; window.click_y = event.clientY" />
+ """))
+ elem = self.marionette.find_element(By.ID, "div")
+
+ with self.assertRaises(errors.MoveTargetOutOfBoundsException):
+ self.action.move(elem).click().perform()
--- a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
@@ -98,16 +98,18 @@ skip-if = manage_instance == false || ap
skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921
[test_with_using_context.py]
[test_modal_dialogs.py]
skip-if = appname == 'fennec' # Bug 1325738
[test_key_actions.py]
[test_legacy_mouse_action.py]
skip-if = appname == 'fennec'
+[test_mouse_action.py]
+skip-if = appname == 'fennec'
[test_teardown_context_preserved.py]
[test_file_upload.py]
skip-if = appname == 'fennec' || os == "win" # http://bugs.python.org/issue14574
[test_execute_sandboxes.py]
[test_prefs.py]
[test_prefs_enforce.py]
skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -782,17 +782,19 @@ function createATouch(el, corx, cory, to
* Perform a series of grouped actions at the specified points in time.
*
* @param {obj} msg
* Object with an |actions| attribute that is an Array of objects
* each of which represents an action sequence.
*/
async function performActions(msg) {
let chain = action.Chain.fromJSON(msg.actions);
- await action.dispatch(chain, curContainer.frame);
+ await action.dispatch(chain, curContainer.frame,
+ !capabilities.get("moz:useNonSpecCompliantPointerOrigin"),
+ );
}
/**
* The release actions command is used to release all the keys and pointer
* buttons that are currently depressed. This causes events to be fired
* as if the state was released by an explicit series of actions. It also
* clears all the internal state of the virtual devices.
*/
--- a/testing/marionette/session.js
+++ b/testing/marionette/session.js
@@ -373,16 +373,17 @@ session.Capabilities = class extends Map
// features
["rotatable", appinfo.name == "B2G"],
// proprietary
["moz:accessibilityChecks", false],
["moz:headless", Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless],
["moz:processID", Services.appinfo.processID],
["moz:profile", maybeProfile()],
+ ["moz:useNonSpecCompliantPointerOrigin", false],
["moz:webdriverClick", true],
]);
}
/**
* @param {string} key
* Capability key.
* @param {(string|number|boolean)} value
@@ -460,25 +461,30 @@ session.Capabilities = class extends Map
matched.set("proxy", proxy);
break;
case "timeouts":
let timeouts = session.Timeouts.fromJSON(v);
matched.set("timeouts", timeouts);
break;
+ case "moz:accessibilityChecks":
+ assert.boolean(v);
+ matched.set("moz:accessibilityChecks", v);
+ break;
+
+ case "moz:useNonSpecCompliantPointerOrigin":
+ assert.boolean(v);
+ matched.set("moz:useNonSpecCompliantPointerOrigin", v);
+ break;
+
case "moz:webdriverClick":
assert.boolean(v);
matched.set("moz:webdriverClick", v);
break;
-
- case "moz:accessibilityChecks":
- assert.boolean(v);
- matched.set("moz:accessibilityChecks", v);
- break;
}
}
return matched;
}
};
// Specialisation of |JSON.stringify| that produces JSON-safe object
--- a/testing/marionette/test_session.js
+++ b/testing/marionette/test_session.js
@@ -374,16 +374,17 @@ add_test(function test_Capabilities_ctor
ok(caps.get("timeouts") instanceof session.Timeouts);
ok(caps.get("proxy") instanceof session.Proxy);
ok(caps.has("rotatable"));
equal(false, caps.get("moz:accessibilityChecks"));
ok(caps.has("moz:processID"));
ok(caps.has("moz:profile"));
+ equal(false, caps.get("moz:useNonSpecCompliantPointerOrigin"));
equal(true, caps.get("moz:webdriverClick"));
run_next_test();
});
add_test(function test_Capabilities_toString() {
equal("[object session.Capabilities]", new session.Capabilities().toString());
@@ -403,16 +404,18 @@ add_test(function test_Capabilities_toJS
deepEqual(caps.get("timeouts").toJSON(), json.timeouts);
equal(undefined, json.proxy);
equal(caps.get("rotatable"), json.rotatable);
equal(caps.get("moz:accessibilityChecks"), json["moz:accessibilityChecks"]);
equal(caps.get("moz:processID"), json["moz:processID"]);
equal(caps.get("moz:profile"), json["moz:profile"]);
+ equal(caps.get("moz:useNonSpecCompliantPointerOrigin"),
+ json["moz:useNonSpecCompliantPointerOrigin"]);
equal(caps.get("moz:webdriverClick"), json["moz:webdriverClick"]);
run_next_test();
});
add_test(function test_Capabilities_fromJSON() {
const {fromJSON} = session.Capabilities;
@@ -442,27 +445,36 @@ add_test(function test_Capabilities_from
let proxyConfig = {proxyType: "manual"};
caps = fromJSON({proxy: proxyConfig});
equal("manual", caps.get("proxy").proxyType);
let timeoutsConfig = {implicit: 123};
caps = fromJSON({timeouts: timeoutsConfig});
equal(123, caps.get("timeouts").implicit);
- equal(true, caps.get("moz:webdriverClick"));
- caps = fromJSON({"moz:webdriverClick": true});
- equal(true, caps.get("moz:webdriverClick"));
- Assert.throws(() => fromJSON({"moz:webdriverClick": "foo"}));
- Assert.throws(() => fromJSON({"moz:webdriverClick": 1}));
-
caps = fromJSON({"moz:accessibilityChecks": true});
equal(true, caps.get("moz:accessibilityChecks"));
caps = fromJSON({"moz:accessibilityChecks": false});
equal(false, caps.get("moz:accessibilityChecks"));
Assert.throws(() => fromJSON({"moz:accessibilityChecks": "foo"}));
+ Assert.throws(() => fromJSON({"moz:accessibilityChecks": 1}));
+
+ caps = fromJSON({"moz:useNonSpecCompliantPointerOrigin": false});
+ equal(false, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ caps = fromJSON({"moz:useNonSpecCompliantPointerOrigin": true});
+ equal(true, caps.get("moz:useNonSpecCompliantPointerOrigin"));
+ Assert.throws(() => fromJSON({"moz:useNonSpecCompliantPointerOrigin": "foo"}));
+ Assert.throws(() => fromJSON({"moz:useNonSpecCompliantPointerOrigin": 1}));
+
+ caps = fromJSON({"moz:webdriverClick": true});
+ equal(true, caps.get("moz:webdriverClick"));
+ caps = fromJSON({"moz:webdriverClick": false});
+ equal(false, caps.get("moz:webdriverClick"));
+ Assert.throws(() => fromJSON({"moz:webdriverClick": "foo"}));
+ Assert.throws(() => fromJSON({"moz:webdriverClick": 1}));
run_next_test();
});
// use session.Proxy.toJSON to test marshal
add_test(function test_marshal() {
let proxy = new session.Proxy();
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -384919,16 +384919,22 @@
]
],
"webdriver/tests/actions/mouse_dblclick.py": [
[
"/webdriver/tests/actions/mouse_dblclick.py",
{}
]
],
+ "webdriver/tests/actions/pointer_origin.py": [
+ [
+ "/webdriver/tests/actions/pointer_origin.py",
+ {}
+ ]
+ ],
"webdriver/tests/actions/sequence.py": [
[
"/webdriver/tests/actions/sequence.py",
{
"timeout": "long"
}
]
],
@@ -538719,17 +538725,17 @@
"bef45b2ff4f474982a84d80d3e37ccae0d459f4b",
"testharness"
],
"domparsing/OWNERS": [
"da58846d7ccfd175f043332a58c73feab9394e63",
"support"
],
"domparsing/XMLSerializer-serializeToString.html": [
- "121db7defe0663346fd83ba832e09d20ce6d41be",
+ "71314752b8552c19b0951647594b9c5c85ca01b6",
"testharness"
],
"domparsing/createContextualFragment.html": [
"e3d576cab516b137b3c758f4ab7528303a28744a",
"testharness"
],
"domparsing/innerhtml-01.xhtml": [
"a8db52434a64ea628f168583c5bef0557c79254a",
@@ -592219,25 +592225,29 @@
"d589b53f0096893600e696b43ec19ca84e5ee2ab",
"wdspec"
],
"webdriver/tests/actions/key_shortcuts.py": [
"dbe27dd0b1625169fc8cc2055f8fb49d5a4a78d2",
"wdspec"
],
"webdriver/tests/actions/modifier_click.py": [
- "88a384182fdd9df1515b9d8cfda8f56aed138ec7",
+ "56df38086ef05cd8bff1437038efb598ab63f1e3",
"wdspec"
],
"webdriver/tests/actions/mouse.py": [
- "708373af0d50f2a0a9776743848482c939f90ec8",
+ "2fb4c47335f144a2dd6f16db4c20239116f20fed",
"wdspec"
],
"webdriver/tests/actions/mouse_dblclick.py": [
- "154d595a3d4466a44c5217c54bb3c717d9a2b9ec",
+ "932b053eef5e052d53ab2007540428d68b758ad4",
+ "wdspec"
+ ],
+ "webdriver/tests/actions/pointer_origin.py": [
+ "da2a9f21018582c8cd52d206d172841f71fd19f3",
"wdspec"
],
"webdriver/tests/actions/sequence.py": [
"d43caf0f8607a76c3baed7806664b686bde21fda",
"wdspec"
],
"webdriver/tests/actions/special_keys.py": [
"64eb2401664b71d68f7b53e236a947eec6d651cc",
@@ -592255,17 +592265,17 @@
"208a1c4fbc0d5c542d17de7f6474d477ce1feb45",
"support"
],
"webdriver/tests/actions/support/refine.py": [
"0d244bffe67ef57be68aad99f1cbc7440ff80e27",
"support"
],
"webdriver/tests/actions/support/test_actions_wdspec.html": [
- "95203777fcc012ab64465287737a89a4ba2c31dc",
+ "34f99c46ac9c52e5902477c26a3d16a89a29235a",
"support"
],
"webdriver/tests/conftest.py": [
"c812269d034c9ca1b8c4f136dd5d0cea52f4d0f0",
"support"
],
"webdriver/tests/contexts/json_serialize_windowproxy.py": [
"d29c82c48b3bd1e2b07c40798a774eb77d6178a5",
--- a/testing/web-platform/tests/webdriver/tests/actions/mouse.py
+++ b/testing/web-platform/tests/webdriver/tests/actions/mouse.py
@@ -1,27 +1,22 @@
import pytest
-from tests.actions.support.mouse import get_center
+from tests.actions.support.mouse import get_inview_center, get_viewport_rect
from tests.actions.support.refine import get_events, filter_dict
from tests.support.asserts import assert_move_to_coordinates
from tests.support.inline import inline
from tests.support.wait import wait
def link_doc(dest):
content = "<a href=\"{}\" id=\"link\">destination</a>".format(dest)
return inline(content)
-# TODO use pytest.approx once we upgrade to pytest > 3.0
-def approx(n, m, tolerance=1):
- return abs(n - m) <= tolerance
-
-
def test_click_at_coordinates(session, test_actions_page, mouse_chain):
div_point = {
"x": 82,
"y": 187,
}
mouse_chain \
.pointer_move(div_point["x"], div_point["y"], duration=1000) \
.click() \
@@ -30,17 +25,17 @@ def test_click_at_coordinates(session, t
assert len(events) == 4
assert_move_to_coordinates(div_point, "outer", events)
for e in events:
if e["type"] != "mousedown":
assert e["buttons"] == 0
assert e["button"] == 0
expected = [
{"type": "mousedown", "buttons": 1},
- {"type": "mouseup", "buttons": 0},
+ {"type": "mouseup", "buttons": 0},
{"type": "click", "buttons": 0},
]
filtered_events = [filter_dict(e, expected[0]) for e in events]
assert expected == filtered_events[1:]
def test_context_menu_at_coordinates(session, test_actions_page, mouse_chain):
div_point = {
@@ -50,39 +45,39 @@ def test_context_menu_at_coordinates(ses
mouse_chain \
.pointer_move(div_point["x"], div_point["y"]) \
.pointer_down(button=2) \
.pointer_up(button=2) \
.perform()
events = get_events(session)
expected = [
{"type": "mousedown", "button": 2},
- {"type": "contextmenu", "button": 2},
+ {"type": "contextmenu", "button": 2},
]
assert len(events) == 4
filtered_events = [filter_dict(e, expected[0]) for e in events]
mousedown_contextmenu_events = [
x for x in filtered_events
if x["type"] in ["mousedown", "contextmenu"]
]
assert expected == mousedown_contextmenu_events
def test_click_element_center(session, test_actions_page, mouse_chain):
outer = session.find.css("#outer", all=False)
- center = get_center(outer.rect)
+ center = get_inview_center(outer.rect, get_viewport_rect(session))
mouse_chain.click(element=outer).perform()
events = get_events(session)
assert len(events) == 4
event_types = [e["type"] for e in events]
assert ["mousemove", "mousedown", "mouseup", "click"] == event_types
for e in events:
if e["type"] != "mousemove":
- assert approx(e["pageX"], center["x"])
- assert approx(e["pageY"], center["y"])
+ assert pytest.approx(e["pageX"], center["x"])
+ assert pytest.approx(e["pageY"], center["y"])
assert e["target"] == "outer"
def test_click_navigation(session, url, release_actions):
destination = url("/webdriver/tests/actions/support/test_actions_wdspec.html")
start = link_doc(destination)
def click(link):
@@ -97,37 +92,38 @@ def test_click_navigation(session, url,
wait(session, lambda s: s.url == destination, error_message)
# repeat steps to check behaviour after document unload
session.url = start
click(session.find.css("#link", all=False))
wait(session, lambda s: s.url == destination, error_message)
@pytest.mark.parametrize("drag_duration", [0, 300, 800])
-@pytest.mark.parametrize("dx, dy",
- [(20, 0), (0, 15), (10, 15), (-20, 0), (10, -15), (-10, -15)])
+@pytest.mark.parametrize("dx, dy", [
+ (20, 0), (0, 15), (10, 15), (-20, 0), (10, -15), (-10, -15)
+])
def test_drag_and_drop(session,
test_actions_page,
mouse_chain,
dx,
dy,
drag_duration):
drag_target = session.find.css("#dragTarget", all=False)
initial_rect = drag_target.rect
- initial_center = get_center(initial_rect)
+ initial_center = get_inview_center(initial_rect, get_viewport_rect(session))
# Conclude chain with extra move to allow time for last queued
# coordinate-update of drag_target and to test that drag_target is "dropped".
mouse_chain \
.pointer_move(0, 0, origin=drag_target) \
.pointer_down() \
.pointer_move(dx, dy, duration=drag_duration, origin="pointer") \
.pointer_up() \
.pointer_move(80, 50, duration=100, origin="pointer") \
.perform()
# mouseup that ends the drag is at the expected destination
e = get_events(session)[1]
assert e["type"] == "mouseup"
- assert approx(e["pageX"], initial_center["x"] + dx)
- assert approx(e["pageY"], initial_center["y"] + dy)
+ assert pytest.approx(e["pageX"], initial_center["x"] + dx)
+ assert pytest.approx(e["pageY"], initial_center["y"] + dy)
# check resulting location of the dragged element
final_rect = drag_target.rect
assert initial_rect["x"] + dx == final_rect["x"]
assert initial_rect["y"] + dy == final_rect["y"]
--- a/testing/web-platform/tests/webdriver/tests/actions/mouse_dblclick.py
+++ b/testing/web-platform/tests/webdriver/tests/actions/mouse_dblclick.py
@@ -1,11 +1,11 @@
import pytest
-from tests.actions.support.mouse import get_center
+from tests.actions.support.mouse import get_inview_center, get_viewport_rect
from tests.actions.support.refine import get_events, filter_dict
from tests.support.asserts import assert_move_to_coordinates
_DBLCLICK_INTERVAL = 640
# Using local fixtures because we want to start a new session between
@@ -58,17 +58,17 @@ def test_dblclick_at_coordinates(dblclic
]
assert len(events) == 8
filtered_events = [filter_dict(e, expected[0]) for e in events]
assert expected == filtered_events[1:]
def test_dblclick_with_pause_after_second_pointerdown(dblclick_session, mouse_chain):
outer = dblclick_session.find.css("#outer", all=False)
- center = get_center(outer.rect)
+ center = get_inview_center(outer.rect, get_viewport_rect(dblclick_session))
mouse_chain \
.pointer_move(int(center["x"]), int(center["y"])) \
.click() \
.pointer_down() \
.pause(_DBLCLICK_INTERVAL + 10) \
.pointer_up() \
.perform()
events = get_events(dblclick_session)
@@ -83,17 +83,17 @@ def test_dblclick_with_pause_after_secon
]
assert len(events) == 8
filtered_events = [filter_dict(e, expected[0]) for e in events]
assert expected == filtered_events[1:]
def test_no_dblclick(dblclick_session, mouse_chain):
outer = dblclick_session.find.css("#outer", all=False)
- center = get_center(outer.rect)
+ center = get_inview_center(outer.rect, get_viewport_rect(dblclick_session))
mouse_chain \
.pointer_move(int(center["x"]), int(center["y"])) \
.click() \
.pause(_DBLCLICK_INTERVAL + 10) \
.click() \
.perform()
events = get_events(dblclick_session)
expected = [
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/actions/pointer_origin.py
@@ -0,0 +1,129 @@
+import pytest
+
+from webdriver import MoveTargetOutOfBoundsException
+
+from tests.actions.support.mouse import get_inview_center, get_viewport_rect
+from tests.support.inline import inline
+
+
+def origin_doc(inner_style, outer_style=""):
+ return inline("""
+ <div id="outer" style="{1}"
+ onmousemove="window.coords = {{x: event.clientX, y: event.clientY}}">
+ <div id="inner" style="{0}"></div>
+ </div>
+ """.format(inner_style, outer_style))
+
+
+def get_click_coordinates(session):
+ return session.execute_script("return window.coords;")
+
+
+def test_viewport_inside(session, mouse_chain):
+ point = {"x": 50, "y": 50}
+
+ session.url = origin_doc("width: 100px; height: 50px; background: green;")
+ mouse_chain \
+ .pointer_move(point["x"], point["y"], origin="viewport") \
+ .perform()
+
+ click_coords = session.execute_script("return window.coords;")
+ assert pytest.approx(click_coords["x"], point["x"])
+ assert pytest.approx(click_coords["y"], point["y"])
+
+
+def test_viewport_outside(session, mouse_chain):
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ mouse_chain \
+ .pointer_move(-50, -50, origin="viewport") \
+ .perform()
+
+
+def test_pointer_inside(session, mouse_chain):
+ start_point = {"x": 50, "y": 50}
+ offset = {"x": 10, "y": 5}
+
+ session.url = origin_doc("width: 100px; height: 50px; background: green;")
+ mouse_chain \
+ .pointer_move(start_point["x"], start_point["y"]) \
+ .pointer_move(offset["x"], offset["y"], origin="pointer") \
+ .perform()
+
+ click_coords = session.execute_script("return window.coords;")
+ assert pytest.approx(click_coords["x"], start_point["x"] + offset["x"])
+ assert pytest.approx(click_coords["y"], start_point["y"] + offset["y"])
+
+
+def test_pointer_outside(session, mouse_chain):
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ mouse_chain \
+ .pointer_move(-50, -50, origin="pointer") \
+ .perform()
+
+
+def test_element_center_point(session, mouse_chain):
+ session.url = origin_doc("width: 100px; height: 50px; background: green;")
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain \
+ .pointer_move(0, 0, origin=elem) \
+ .perform()
+
+ click_coords = get_click_coordinates(session)
+ assert pytest.approx(click_coords["x"], center["x"])
+ assert pytest.approx(click_coords["y"], center["y"])
+
+
+def test_element_center_point_with_offset(session, mouse_chain):
+ session.url = origin_doc("width: 100px; height: 50px; background: green;")
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain \
+ .pointer_move(10, 15, origin=elem) \
+ .perform()
+
+ click_coords = get_click_coordinates(session)
+ assert pytest.approx(click_coords["x"], center["x"] + 10)
+ assert pytest.approx(click_coords["y"], center["y"] + 15)
+
+
+def test_element_in_view_center_point_partly_visible(session, mouse_chain):
+ session.url = origin_doc("""width: 100px; height: 50px; background: green;
+ position: relative; left: -50px; top: -25px;""")
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain \
+ .pointer_move(0, 0, origin=elem) \
+ .perform()
+
+ click_coords = get_click_coordinates(session)
+ assert pytest.approx(click_coords["x"], center["x"])
+ assert pytest.approx(click_coords["y"], center["y"])
+
+
+def test_element_larger_than_viewport(session, mouse_chain):
+ session.url = origin_doc("width: 300vw; height: 300vh; background: green;")
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain \
+ .pointer_move(0, 0, origin=elem) \
+ .perform()
+
+ click_coords = get_click_coordinates(session)
+ assert pytest.approx(click_coords["x"], center["x"])
+ assert pytest.approx(click_coords["y"], center["y"])
+
+
+def test_element_outside_of_view_port(session, mouse_chain):
+ session.url = origin_doc("""width: 100px; height: 50px; background: green;
+ position: relative; left: -200px; top: -100px;""")
+ elem = session.find.css("#inner", all=False)
+
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ mouse_chain \
+ .pointer_move(0, 0, origin=elem) \
+ .perform()
--- a/testing/web-platform/tests/webdriver/tests/actions/support/mouse.py
+++ b/testing/web-platform/tests/webdriver/tests/actions/support/mouse.py
@@ -1,5 +1,26 @@
-def get_center(rect):
+def get_viewport_rect(session):
+ return session.execute_script("""
+ return {
+ height: window.innerHeight || document.documentElement.clientHeight,
+ width: window.innerWidth || document.documentElement.clientWidth,
+ };
+ """)
+
+
+def get_inview_center(elem_rect, viewport_rect):
+ x = {
+ "left": max(0, min(elem_rect["x"], elem_rect["x"] + elem_rect["width"])),
+ "right": min(viewport_rect["width"], max(elem_rect["x"],
+ elem_rect["x"] + elem_rect["width"])),
+ }
+
+ y = {
+ "top": max(0, min(elem_rect["y"], elem_rect["y"] + elem_rect["height"])),
+ "bottom": min(viewport_rect["height"], max(elem_rect["y"],
+ elem_rect["y"] + elem_rect["height"])),
+ }
+
return {
- "x": rect["width"] / 2 + rect["x"],
- "y": rect["height"] / 2 + rect["y"],
+ "x": (x["left"] + x["right"]) / 2,
+ "y": (y["top"] + y["bottom"]) / 2,
}