Automated Keyboard Input (#10113)
diff --git a/docs/_writing-tests/testdriver.md b/docs/_writing-tests/testdriver.md
index 3f5787f..5508dba 100644
--- a/docs/_writing-tests/testdriver.md
+++ b/docs/_writing-tests/testdriver.md
@@ -19,6 +19,7 @@
context (and not therefore in any frame or window opened from it).
### `test_driver.click(element)`
+#### `element: a DOM Element object`
This function causes a click to occur on the target element (an
`Element` object), potentially scrolling the document to make it
@@ -30,5 +31,19 @@
document must not have any DOM mutations made between the function
being called and the promise settling.
+### `test_driver.send_keys(element, keys)`
+#### `element: a DOM Element object`
+#### `keys: string to send to the element`
+
+This function causes the string `keys` to be send to the target
+element (an `Element` object), potentially scrolling the document to
+make it possible to send keys. It returns a `Promise` that resolves
+after the keys have been send or rejects if the keys cannot be sent
+to the element.
+
+Note that if the element that's keys need to be send to does not have
+a unique ID, the document must not have any DOM mutations made
+between the function being called and the promise settling.
+
[testharness]: {{ site.baseurl }}{% link _writing-tests/testharness.md %}
diff --git a/html/editing/focus/focus-01-manual.html b/html/editing/focus/focus-01.html
similarity index 86%
rename from html/editing/focus/focus-01-manual.html
rename to html/editing/focus/focus-01.html
index 16e0b0f..331ff53 100644
--- a/html/editing/focus/focus-01-manual.html
+++ b/html/editing/focus/focus-01.html
@@ -7,12 +7,8 @@
<meta assert="assert" content="Check if the key events received by document are targeted at the element when it is focused">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
-<h2>Steps:</h2>
-<ol>
- <li>Input any character into the textbox by keyboard in 10 seconds.</li>
-</ol>
-<h2>Expect results:</h2>
-<p>PASS</p>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
<div id="log"></div>
<input id="test">
<script>
@@ -40,4 +36,10 @@
assert_equals(evt.target, testEle, "The keyup events must be targeted at the input element.");
});
+var input_element = document.getElementById("test");
+
+t1.step(function() {
+ test_driver.send_keys(input_element, "a");
+});
+
</script>
diff --git a/infrastructure/testdriver/send_keys.html b/infrastructure/testdriver/send_keys.html
new file mode 100644
index 0000000..2170347
--- /dev/null
+++ b/infrastructure/testdriver/send_keys.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>TestDriver send keys method</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<input type="text" id="text">Text Input</button>
+
+<script>
+async_test(t => {
+ let input_text = "Hello, wpt!";
+ let text_box = document.getElementById("text");
+ test_driver
+ .send_keys(text_box, input_text)
+ .then(() => {
+ assert_true(text_box.value == input_text);
+ t.done();
+ })
+ .catch(t.unreached_func("send keys failed"));
+});
+</script>
diff --git a/resources/testdriver.js b/resources/testdriver.js
index a6aa298..a09a6e8 100644
--- a/resources/testdriver.js
+++ b/resources/testdriver.js
@@ -83,6 +83,42 @@
return window.test_driver_internal.click(element,
{x: centerPoint[0],
y: centerPoint[1]});
+ },
+
+ /**
+ * Send keys to an element
+ *
+ * This matches the behaviour of the {@link
+ * https://w3c.github.io/webdriver/webdriver-spec.html#element-send-keys|WebDriver
+ * Send Keys command}.
+ *
+ * @param {Element} element - element to send keys to
+ * @param {String} keys - keys to send to the element
+ * @returns {Promise} fulfilled after keys are sent, or rejected in
+ * the cases the WebDriver command errors
+ */
+ send_keys: function(element, keys) {
+ if (window.top !== window) {
+ return Promise.reject(new Error("can only send keys in top-level window"));
+ }
+
+ if (!window.document.contains(element)) {
+ return Promise.reject(new Error("element in different document or shadow tree"));
+ }
+
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element send_keys intercepted error"));
+ }
+
+ return window.test_driver_internal.send_keys(element, keys);
}
};
@@ -96,6 +132,17 @@
*/
click: function(element, coords) {
return Promise.reject(new Error("unimplemented"));
+ },
+
+ /**
+ * Triggers a user-initated click
+ *
+ * @param {Element} element - element to be clicked
+ * @param {String} keys - keys to send to the element
+ * @returns {Promise} fulfilled after keys are sent or rejected if click fails
+ */
+ send_keys: function(element, keys) {
+ return Promise.reject(new Error("unimplemented"));
}
};
})();
diff --git a/tools/wptrunner/wptrunner/executors/base.py b/tools/wptrunner/wptrunner/executors/base.py
index fbb75d6..80549af 100644
--- a/tools/wptrunner/wptrunner/executors/base.py
+++ b/tools/wptrunner/wptrunner/executors/base.py
@@ -501,7 +501,8 @@
}
self.actions = {
- "click": ClickAction(self.logger, self.protocol)
+ "click": ClickAction(self.logger, self.protocol),
+ "send_keys": SendKeysAction(self.logger, self.protocol)
}
def __call__(self, result):
@@ -544,7 +545,6 @@
def _send_message(self, message_type, status, message=None):
self.protocol.testdriver.send_message(message_type, status, message=message)
-
class ClickAction(object):
def __init__(self, logger, protocol):
self.logger = logger
@@ -559,3 +559,19 @@
raise ValueError("Selector matches multiple elements")
self.logger.debug("Clicking element: %s" % selector)
self.protocol.click.element(elements[0])
+
+class SendKeysAction(object):
+ def __init__(self, logger, protocol):
+ self.logger = logger
+ self.protocol = protocol
+
+ def __call__(self, payload):
+ selector = payload["selector"]
+ keys = payload["keys"]
+ elements = self.protocol.select.elements_by_selector(selector)
+ if len(elements) == 0:
+ raise ValueError("Selector matches no elements")
+ elif len(elements) > 1:
+ raise ValueError("Selector matches multiple elements")
+ self.logger.debug("Sending keys to element: %s" % selector)
+ self.protocol.send_keys.send_keys(elements[0], keys)
diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py
index 1c49ab2..60332a6 100644
--- a/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -32,6 +32,7 @@
StorageProtocolPart,
SelectorProtocolPart,
ClickProtocolPart,
+ SendKeysProtocolPart,
TestDriverProtocolPart)
from ..testrunner import Stop
from ..webdriver_server import GeckoDriverServer
@@ -307,6 +308,12 @@
def element(self, element):
return element.click()
+class MarionetteSendKeysProtocolPart(SendKeysProtocolPart):
+ def setup(self):
+ self.marionette = self.parent.marionette
+
+ def send_keys(self, element, keys):
+ return element.send_keys(keys)
class MarionetteTestDriverProtocolPart(TestDriverProtocolPart):
def setup(self):
@@ -329,6 +336,7 @@
MarionetteStorageProtocolPart,
MarionetteSelectorProtocolPart,
MarionetteClickProtocolPart,
+ MarionetteSendKeysProtocolPart,
MarionetteTestDriverProtocolPart]
def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1):
diff --git a/tools/wptrunner/wptrunner/executors/executorselenium.py b/tools/wptrunner/wptrunner/executors/executorselenium.py
index 0b71189..94ef10b 100644
--- a/tools/wptrunner/wptrunner/executors/executorselenium.py
+++ b/tools/wptrunner/wptrunner/executors/executorselenium.py
@@ -19,6 +19,7 @@
Protocol,
SelectorProtocolPart,
ClickProtocolPart,
+ SendKeysProtocolPart,
TestDriverProtocolPart)
from ..testrunner import Stop
@@ -134,6 +135,13 @@
def element(self, element):
return element.click()
+class SeleniumSendKeysProtocolPart(SendKeysProtocolPart):
+ def setup(self):
+ self.webdriver = self.parent.webdriver
+
+ def send_keys(self, element, keys):
+ return element.send_keys(keys)
+
class SeleniumTestDriverProtocolPart(TestDriverProtocolPart):
def setup(self):
@@ -154,6 +162,7 @@
SeleniumTestharnessProtocolPart,
SeleniumSelectorProtocolPart,
SeleniumClickProtocolPart,
+ SeleniumSendKeysProtocolPart,
SeleniumTestDriverProtocolPart]
def __init__(self, executor, browser, capabilities, **kwargs):
diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py
index dc69d24..3c938f0 100644
--- a/tools/wptrunner/wptrunner/executors/protocol.py
+++ b/tools/wptrunner/wptrunner/executors/protocol.py
@@ -259,6 +259,20 @@
:param element: A protocol-specific handle to an element."""
pass
+class SendKeysProtocolPart(ProtocolPart):
+ """Protocol part for performing trusted clicks"""
+ __metaclass__ = ABCMeta
+
+ name = "send_keys"
+
+ @abstractmethod
+ def send_keys(self, element, keys):
+ """Send keys to a specific element.
+
+ :param element: A protocol-specific handle to an element.
+ :param keys: A protocol-specific handle to a string of input keys."""
+ pass
+
class TestDriverProtocolPart(ProtocolPart):
"""Protocol part that implements the basic functionality required for
diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js
index 856a33e..ef962d3 100644
--- a/tools/wptrunner/wptrunner/testdriver-extra.js
+++ b/tools/wptrunner/wptrunner/testdriver-extra.js
@@ -60,4 +60,14 @@
window.opener.postMessage({"type": "action", "action": "click", "selector": selector}, "*");
return pending_promise;
};
+
+ window.test_driver_internal.send_keys = function(element, keys) {
+ const selector = get_selector(element);
+ const pending_promise = new Promise(function(resolve, reject) {
+ pending_resolve = resolve;
+ pending_reject = reject;
+ });
+ window.opener.postMessage({"type": "action", "action": "send_keys", "selector": selector, "keys": keys}, "*");
+ return pending_promise;
+ };
})();