mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 1414322 - Use WebDriver conformant interactability checks for sendKeysToElement. r=ato
Enables webdriver spec keyboard interactability tests for 'Element Send Keys' by default by re-using the same capability 'moz:webdriverClick' from 'Element Click'. It can be disabled by turning off this preference. Also various webplatform tests for webdriver spec have been added which cover both the scroll into view action, and keyboard interactability check. Existing Marionette unit tests will be run in both modes, until we can get rid of the legacy mode. MozReview-Commit-ID: dFB8sQ6CN5 --HG-- extra : rebase_source : 9f6a3c3e42f779f039f61d0b239f9f5925ecdcf5
This commit is contained in:
parent
88c43fb94d
commit
cb6d8c72b5
@ -284,12 +284,14 @@ moz:webdriverClick
|
||||
------------------
|
||||
|
||||
A boolean value to indicate which kind of interactability checks to run
|
||||
when performing a click on elements. For Firefoxen prior to version 58.0 some
|
||||
legacy code as imported from an older version of [FirefoxDriver] was in use.
|
||||
when performing a click or sending keys to an elements. For Firefoxen prior to
|
||||
version 58.0 some legacy code as imported from an older version of
|
||||
[FirefoxDriver] was in use.
|
||||
|
||||
With Firefox 58 the interactability checks as required by the [WebDriver]
|
||||
specification are enabled by default. This means geckodriver will additionally
|
||||
check if an element is obscured by another when clicking.
|
||||
check if an element is obscured by another when clicking, and if an element is
|
||||
focusable for sending keys.
|
||||
|
||||
Because of this change in behaviour, we are aware that some extra errors could
|
||||
be returned. In most cases the test in question might have to be updated
|
||||
@ -299,6 +301,8 @@ geckodriver, then please raise an issue in the [issue tracker].
|
||||
To temporarily disable the WebDriver conformant checks use `false` as value
|
||||
for this capability.
|
||||
|
||||
Please note that this capability exists only temporarily, and that it will be
|
||||
removed once the interactability checks have been stabilized.
|
||||
|
||||
`log` object
|
||||
------------
|
||||
|
@ -2,6 +2,8 @@
|
||||
# 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/.
|
||||
|
||||
import urllib
|
||||
|
||||
from marionette_driver.by import By
|
||||
from marionette_driver.keys import Keys
|
||||
from marionette_driver.marionette import Actions
|
||||
@ -9,6 +11,10 @@ from marionette_driver.marionette import Actions
|
||||
from marionette_harness import MarionetteTestCase
|
||||
|
||||
|
||||
def inline(doc):
|
||||
return "data:text/html;charset=utf-8,{}".format(urllib.quote(doc))
|
||||
|
||||
|
||||
class TestMouseAction(MarionetteTestCase):
|
||||
def setUp(self):
|
||||
MarionetteTestCase.setUp(self)
|
||||
@ -34,12 +40,14 @@ class TestMouseAction(MarionetteTestCase):
|
||||
self.action.click(el).perform()
|
||||
|
||||
def test_double_click_action(self):
|
||||
test_html = self.marionette.absolute_url("double_click.html")
|
||||
self.marionette.navigate(test_html)
|
||||
el = self.marionette.find_element(By.ID, "one-word-div")
|
||||
self.marionette.navigate(inline("""
|
||||
<div contenteditable>zyxw</div><input type="text"/>
|
||||
"""))
|
||||
|
||||
el = self.marionette.find_element(By.CSS_SELECTOR, "div")
|
||||
self.action.double_click(el).perform()
|
||||
el.send_keys(self.mod_key + "c")
|
||||
rel = self.marionette.find_element(By.ID, "input-field")
|
||||
rel = self.marionette.find_element(By.CSS_SELECTOR, "input")
|
||||
rel.send_keys(self.mod_key + "v")
|
||||
self.assertEqual("zyxw", rel.get_property("value"))
|
||||
|
||||
|
@ -321,13 +321,6 @@ class TestTypingContent(TypingTestCase):
|
||||
# If we don't get an error below we are good
|
||||
self.marionette.find_element(By.TAG_NAME, "body").send_keys("foo")
|
||||
|
||||
def test_not_interactable_if_hidden(self):
|
||||
test_html = self.marionette.absolute_url("keyboard.html")
|
||||
self.marionette.navigate(test_html)
|
||||
|
||||
not_displayed = self.marionette.find_element(By.ID, "notDisplayed")
|
||||
self.assertRaises(ElementNotInteractableException, not_displayed.send_keys, "foo")
|
||||
|
||||
def test_appends_to_input_text(self):
|
||||
self.marionette.navigate(inline("<input>"))
|
||||
el = self.marionette.find_element(By.TAG_NAME, "input")
|
||||
@ -367,3 +360,12 @@ class TestTypingContent(TypingTestCase):
|
||||
l.send_keys("c")
|
||||
self.assertEqual("abcde",
|
||||
self.marionette.execute_script("return arguments[0].value;", [l]))
|
||||
|
||||
|
||||
class TestTypingContentLegacy(TestTypingContent):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTypingContent, self).setUp()
|
||||
|
||||
self.marionette.delete_session()
|
||||
self.marionette.start_session({"moz:webdriverClick": False})
|
||||
|
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<!-- 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/. -->
|
||||
<head>
|
||||
<title>Testing Double Click</title>
|
||||
</head>
|
||||
<div>
|
||||
<p id="one-word-div">zyxw</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input type="text" id="input-field" size="80"/>
|
||||
</form>
|
||||
</div>
|
||||
</html>
|
@ -60,9 +60,6 @@
|
||||
<option value="bar">Bar</option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label>hidden: <input type="text" id="notDisplayed" style="display: none"></label>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
@ -347,6 +347,33 @@ interaction.focusElement = function(el) {
|
||||
el.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs checks if <var>el</var> is keyboard-interactable.
|
||||
*
|
||||
* To decide if an element is keyboard-interactable various properties,
|
||||
* and computed CSS styles have to be evaluated. Whereby it has to be taken
|
||||
* into account that the element can be part of a container (eg. option),
|
||||
* and as such the container has to be checked instead.
|
||||
*
|
||||
* @param {Element} el
|
||||
* Element to check.
|
||||
*
|
||||
* @return {boolean}
|
||||
* True if element is keyboard-interactable, false otherwise.
|
||||
*/
|
||||
interaction.isKeyboardInteractable = function(el) {
|
||||
const win = getWindow(el);
|
||||
|
||||
// body and document element are always keyboard-interactable
|
||||
if (el.localName === "body" || el === win.document.documentElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
el.focus();
|
||||
|
||||
return el === win.document.activeElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends <var>path</var> to an <tt><input type=file></tt>'s
|
||||
* file list.
|
||||
@ -420,10 +447,47 @@ interaction.setFormControlValue = function(el, value) {
|
||||
* Sequence of keystrokes to send to the element.
|
||||
* @param {boolean=} [strict=false] strict
|
||||
* Enforce strict accessibility tests.
|
||||
* @param {boolean=} [specCompat=false] specCompat
|
||||
* Use WebDriver specification compatible interactability definition.
|
||||
*/
|
||||
interaction.sendKeysToElement = async function(
|
||||
el, value, strict = false) {
|
||||
el, value, strict = false, specCompat = false) {
|
||||
const a11y = accessibility.get(strict);
|
||||
|
||||
if (specCompat) {
|
||||
await webdriverSendKeysToElement(el, value, a11y);
|
||||
} else {
|
||||
await legacySendKeysToElement(el, value, a11y);
|
||||
}
|
||||
};
|
||||
|
||||
async function webdriverSendKeysToElement(el, value, a11y) {
|
||||
const win = getWindow(el);
|
||||
|
||||
let containerEl = element.getContainer(el);
|
||||
|
||||
// TODO: Wait for element to be keyboard-interactible
|
||||
if (!interaction.isKeyboardInteractable(containerEl)) {
|
||||
throw new ElementNotInteractableError(
|
||||
pprint`Element ${el} is not reachable by keyboard`);
|
||||
}
|
||||
|
||||
let acc = await a11y.getAccessible(el, true);
|
||||
a11y.assertActionable(acc, el);
|
||||
|
||||
interaction.focusElement(el);
|
||||
|
||||
if (el.type == "file") {
|
||||
await interaction.uploadFile(el, value);
|
||||
} else if ((el.type == "date" || el.type == "time") &&
|
||||
Preferences.get("dom.forms.datetime")) {
|
||||
interaction.setFormControlValue(el, value);
|
||||
} else {
|
||||
event.sendKeysToElement(value, el, win);
|
||||
}
|
||||
}
|
||||
|
||||
async function legacySendKeysToElement(el, value, a11y) {
|
||||
const win = getWindow(el);
|
||||
|
||||
if (el.type == "file") {
|
||||
@ -447,7 +511,7 @@ interaction.sendKeysToElement = async function(
|
||||
interaction.focusElement(el);
|
||||
event.sendKeysToElement(value, el, win);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the element displayedness of an element.
|
||||
|
@ -1427,6 +1427,7 @@ async function sendKeysToElement(el, val) {
|
||||
await interaction.sendKeysToElement(
|
||||
el, val,
|
||||
capabilities.get("moz:accessibilityChecks"),
|
||||
capabilities.get("moz:webdriverClick"),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -283674,6 +283674,11 @@
|
||||
{}
|
||||
]
|
||||
],
|
||||
"webdriver/tests/element_send_keys/__init__.py": [
|
||||
[
|
||||
{}
|
||||
]
|
||||
],
|
||||
"webdriver/tests/sessions/new_session/conftest.py": [
|
||||
[
|
||||
{}
|
||||
@ -373768,6 +373773,18 @@
|
||||
{}
|
||||
]
|
||||
],
|
||||
"webdriver/tests/element_send_keys/interactability.py": [
|
||||
[
|
||||
"/webdriver/tests/element_send_keys/interactability.py",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"webdriver/tests/element_send_keys/scroll_into_view.py": [
|
||||
[
|
||||
"/webdriver/tests/element_send_keys/scroll_into_view.py",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"webdriver/tests/execute_async_script/user_prompts.py": [
|
||||
[
|
||||
"/webdriver/tests/execute_async_script/user_prompts.py",
|
||||
@ -576410,7 +576427,7 @@
|
||||
"testharness"
|
||||
],
|
||||
"webdriver/OWNERS": [
|
||||
"4986cd1bfa1e4c8e5c836581871745b5b2cc440e",
|
||||
"020bcd036daed8eb8928c2924ea1d04050cf1939",
|
||||
"support"
|
||||
],
|
||||
"webdriver/README.md": [
|
||||
@ -576549,6 +576566,18 @@
|
||||
"918c6e48047f31a088ec44e9b0d070b0ae3d6077",
|
||||
"wdspec"
|
||||
],
|
||||
"webdriver/tests/element_send_keys/__init__.py": [
|
||||
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
"support"
|
||||
],
|
||||
"webdriver/tests/element_send_keys/interactability.py": [
|
||||
"bd7cd6c009c86fe93c0e132c30fb9674413962d4",
|
||||
"wdspec"
|
||||
],
|
||||
"webdriver/tests/element_send_keys/scroll_into_view.py": [
|
||||
"fb192d5d1d93aa729b07cadcadfa630587bd0b39",
|
||||
"wdspec"
|
||||
],
|
||||
"webdriver/tests/execute_async_script/user_prompts.py": [
|
||||
"e31edd4537f9b7479a348465154381f5b18f938c",
|
||||
"wdspec"
|
||||
@ -576674,11 +576703,11 @@
|
||||
"support"
|
||||
],
|
||||
"webdriver/tests/support/asserts.py": [
|
||||
"ae2037918aeb450a86f3615f963fe4a4032324cb",
|
||||
"68bb420a9d85810c9fd8b6eaa569b855dfb83638",
|
||||
"support"
|
||||
],
|
||||
"webdriver/tests/support/fixtures.py": [
|
||||
"2331c38e8de48de41b982dee01b14cfe1092cad0",
|
||||
"b9b62366cd60ae7167ad2d0efdf3790ae2e780a4",
|
||||
"support"
|
||||
],
|
||||
"webdriver/tests/support/http_request.py": [
|
||||
|
@ -4,3 +4,4 @@
|
||||
@lukeis
|
||||
@mjzffr
|
||||
@shs96c
|
||||
@whimboo
|
||||
|
@ -0,0 +1,136 @@
|
||||
from tests.support.asserts import assert_error, assert_same_element, assert_success
|
||||
from tests.support.inline import iframe, inline
|
||||
|
||||
|
||||
def send_keys_to_element(session, element, text):
|
||||
return session.transport.send(
|
||||
"POST",
|
||||
"/session/{session_id}/element/{element_id}/value".format(
|
||||
session_id=session.session_id,
|
||||
element_id=element.id),
|
||||
{"text": text})
|
||||
|
||||
|
||||
def test_body_is_interactable(session):
|
||||
session.url = inline("""
|
||||
<body onkeypress="document.getElementById('result').value += event.key">
|
||||
<input type="text" id="result"/>
|
||||
</body>
|
||||
""")
|
||||
|
||||
element = session.find.css("body", all=False)
|
||||
result = session.find.css("input", all=False)
|
||||
|
||||
# By default body is the active element
|
||||
assert_same_element(session, element, session.active_element)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
assert_same_element(session, element, session.active_element)
|
||||
assert result.property("value") == "foo"
|
||||
|
||||
|
||||
def test_document_element_is_interactable(session):
|
||||
session.url = inline("""
|
||||
<html onkeypress="document.getElementById('result').value += event.key">
|
||||
<input type="text" id="result"/>
|
||||
</html>
|
||||
""")
|
||||
|
||||
body = session.find.css("body", all=False)
|
||||
element = session.find.css(":root", all=False)
|
||||
result = session.find.css("input", all=False)
|
||||
|
||||
# By default body is the active element
|
||||
assert_same_element(session, body, session.active_element)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
assert_same_element(session, element, session.active_element)
|
||||
assert result.property("value") == "foo"
|
||||
|
||||
|
||||
def test_iframe_is_interactable(session):
|
||||
session.url = inline(iframe("""
|
||||
<body onkeypress="document.getElementById('result').value += event.key">
|
||||
<input type="text" id="result"/>
|
||||
</body>
|
||||
"""))
|
||||
|
||||
body = session.find.css("body", all=False)
|
||||
frame = session.find.css("iframe", all=False)
|
||||
|
||||
# By default the body has the focus
|
||||
assert_same_element(session, body, session.active_element)
|
||||
|
||||
response = send_keys_to_element(session, frame, "foo")
|
||||
assert_success(response)
|
||||
assert_same_element(session, frame, session.active_element)
|
||||
|
||||
# Any key events are immediately routed to the nested
|
||||
# browsing context's active document.
|
||||
session.switch_frame(frame)
|
||||
result = session.find.css("input", all=False)
|
||||
assert result.property("value") == "foo"
|
||||
|
||||
|
||||
def test_transparent_element(session):
|
||||
session.url = inline("<input style=\"opacity: 0;\">")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
assert element.property("value") == "foo"
|
||||
|
||||
|
||||
def test_readonly_element(session):
|
||||
session.url = inline("<input readonly>")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
assert element.property("value") == ""
|
||||
|
||||
|
||||
def test_obscured_element(session):
|
||||
session.url = inline("""
|
||||
<input type="text" />
|
||||
<div style="position: relative; top: -3em; height: 5em; background-color: blue"></div>
|
||||
""")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
assert element.property("value") == "foo"
|
||||
|
||||
|
||||
def test_not_a_focusable_element(session):
|
||||
session.url = inline("<div>foo</div>")
|
||||
element = session.find.css("div", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_error(response, "element not interactable")
|
||||
|
||||
|
||||
def test_not_displayed_element(session):
|
||||
session.url = inline("<input style=\"display: none\">")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_error(response, "element not interactable")
|
||||
|
||||
|
||||
def test_hidden_element(session):
|
||||
session.url = inline("<input style=\"visibility: hidden\">")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_error(response, "element not interactable")
|
||||
|
||||
|
||||
def test_disabled_element(session):
|
||||
session.url = inline("<input disabled=\"false\">")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_error(response, "element not interactable")
|
@ -0,0 +1,78 @@
|
||||
from tests.support.asserts import assert_success
|
||||
from tests.support.fixtures import is_element_in_viewport
|
||||
from tests.support.inline import inline
|
||||
|
||||
|
||||
def send_keys_to_element(session, element, text):
|
||||
return session.transport.send(
|
||||
"POST",
|
||||
"/session/{session_id}/element/{element_id}/value".format(
|
||||
session_id=session.session_id,
|
||||
element_id=element.id),
|
||||
{"text": text})
|
||||
|
||||
|
||||
def test_element_outside_of_not_scrollable_viewport(session):
|
||||
session.url = inline("<input style=\"position: relative; left: -9999px;\">")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
|
||||
assert not is_element_in_viewport(session, element)
|
||||
|
||||
|
||||
def test_element_outside_of_scrollable_viewport(session):
|
||||
session.url = inline("<input style=\"margin-top: 102vh;\">")
|
||||
element = session.find.css("input", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
|
||||
assert is_element_in_viewport(session, element)
|
||||
|
||||
|
||||
def test_option_select_container_outside_of_scrollable_viewport(session):
|
||||
session.url = inline("""
|
||||
<select style="margin-top: 102vh;">
|
||||
<option value="foo">foo</option>
|
||||
<option value="bar" id="bar">bar</option>
|
||||
</select>
|
||||
""")
|
||||
element = session.find.css("option#bar", all=False)
|
||||
select = session.find.css("select", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "bar")
|
||||
assert_success(response)
|
||||
|
||||
assert is_element_in_viewport(session, select)
|
||||
assert is_element_in_viewport(session, element)
|
||||
|
||||
|
||||
def test_option_stays_outside_of_scrollable_viewport(session):
|
||||
session.url = inline("""
|
||||
<select multiple style="height: 105vh; margin-top: 100vh;">
|
||||
<option value="foo" id="foo" style="height: 100vh;">foo</option>
|
||||
<option value="bar" id="bar" style="background-color: yellow;">bar</option>
|
||||
</select>
|
||||
""")
|
||||
select = session.find.css("select", all=False)
|
||||
option_foo = session.find.css("option#foo", all=False)
|
||||
option_bar = session.find.css("option#bar", all=False)
|
||||
|
||||
response = send_keys_to_element(session, option_bar, "bar")
|
||||
assert_success(response)
|
||||
|
||||
assert is_element_in_viewport(session, select)
|
||||
assert is_element_in_viewport(session, option_foo)
|
||||
assert not is_element_in_viewport(session, option_bar)
|
||||
|
||||
|
||||
def test_contenteditable_element_outside_of_scrollable_viewport(session):
|
||||
session.url = inline("<div contenteditable style=\"margin-top: 102vh;\"></div>")
|
||||
element = session.find.css("div", all=False)
|
||||
|
||||
response = send_keys_to_element(session, element, "foo")
|
||||
assert_success(response)
|
||||
|
||||
assert is_element_in_viewport(session, element)
|
@ -1,5 +1,6 @@
|
||||
from webdriver import Element, WebDriverException
|
||||
|
||||
|
||||
# WebDriver specification ID: dfn-error-response-data
|
||||
errors = {
|
||||
"element click intercepted": 400,
|
||||
@ -32,6 +33,7 @@ errors = {
|
||||
"unsupported operation": 500,
|
||||
}
|
||||
|
||||
|
||||
# WebDriver specification ID: dfn-send-an-error
|
||||
#
|
||||
# > When required to send an error, with error code, a remote end must run the
|
||||
@ -98,7 +100,7 @@ def assert_dialog_handled(session, expected_text):
|
||||
except:
|
||||
assert (result.status == 200 and
|
||||
result.body["value"] != expected_text), (
|
||||
"Dialog with text '%s' was not handled." % expected_text)
|
||||
"Dialog with text '%s' was not handled." % expected_text)
|
||||
|
||||
|
||||
def assert_same_element(session, a, b):
|
||||
@ -123,7 +125,7 @@ def assert_same_element(session, a, b):
|
||||
return
|
||||
|
||||
message = ("Expected element references to describe the same element, " +
|
||||
"but they did not.")
|
||||
"but they did not.")
|
||||
|
||||
# Attempt to provide more information, accounting for possible errors such
|
||||
# as stale element references or not visible elements.
|
||||
|
@ -267,3 +267,19 @@ def create_dialog(session):
|
||||
def clear_all_cookies(session):
|
||||
"""Removes all cookies associated with the current active document"""
|
||||
session.transport.send("DELETE", "session/%s/cookie" % session.session_id)
|
||||
|
||||
|
||||
def is_element_in_viewport(session, element):
|
||||
"""Check if element is outside of the viewport"""
|
||||
return session.execute_script("""
|
||||
let el = arguments[0];
|
||||
|
||||
let rect = el.getBoundingClientRect();
|
||||
let viewport = {
|
||||
height: window.innerHeight || document.documentElement.clientHeight,
|
||||
width: window.innerWidth || document.documentElement.clientWidth,
|
||||
};
|
||||
|
||||
return !(rect.right < 0 || rect.bottom < 0 ||
|
||||
rect.left > viewport.width || rect.top > viewport.height)
|
||||
""", args=(element,))
|
||||
|
Loading…
Reference in New Issue
Block a user