Bug 1448792 - [marionette][wdspec] Make Element Send Keys wdspec conforming for file uploads.
This patch fixes remaining bugs in Marionette for uploading
files via "element_send_keys", and also adds support for
multiple file uploads.
It also adds Wdspec tests to proof the latest changes to
the WebDriver specification.
MozReview-Commit-ID: 2OKGNxMvyMr
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -461,52 +461,55 @@ interaction.isKeyboardInteractable = fun
return true;
}
el.focus();
return el === win.document.activeElement;
};
/**
- * Appends <var>path</var> to an <tt><input type=file></tt>'s
- * file list.
+ * Updates an `<input type=file>`'s file list with given `paths`.
+ *
+ * Hereby will the file list be appended with `paths` if the
+ * element allows multiple files. Otherwise the list will be
+ * replaced.
*
* @param {HTMLInputElement} el
- * An <tt><input type=file></tt> element.
- * @param {string} path
- * Full path to file.
+ * An `input type=file` element.
+ * @param {Array.<string>} paths
+ * List of full paths to any of the files to be uploaded.
*
* @throws {InvalidArgumentError}
- * If <var>path</var> can not be found.
+ * If `path` doesn't exist.
*/
-interaction.uploadFile = async function(el, path) {
- let file;
- try {
- file = await File.createFromFileName(path);
- } catch (e) {
- throw new InvalidArgumentError("File not found: " + path);
+interaction.uploadFiles = async function(el, paths) {
+ let files = [];
+
+ if (el.hasAttribute("multiple")) {
+ // for multiple file uploads new files will be appended
+ files = Array.prototype.slice.call(el.files);
+
+ } else if (paths.length > 1) {
+ throw new InvalidArgumentError(
+ pprint`Element ${el} doesn't accept multiple files`);
}
- let fs = Array.prototype.slice.call(el.files);
- fs.push(file);
+ for (let path of paths) {
+ let file;
- // <input type=file> opens OS widget dialogue
- // which means the mousedown/focus/mouseup/click events
- // occur before the change event
- event.mouseover(el);
- event.mousemove(el);
- event.mousedown(el);
- el.focus();
- event.mouseup(el);
- event.click(el);
+ try {
+ file = await File.createFromFileName(path);
+ } catch (e) {
+ throw new InvalidArgumentError("File not found: " + path);
+ }
- el.mozSetFileArray(fs);
+ files.push(file);
+ }
- event.change(el);
- el.blur();
+ el.mozSetFileArray(files);
};
/**
* Sets a form element's value.
*
* @param {DOMElement} el
* An form element, e.g. input, textarea, etc.
* @param {string} value
@@ -566,30 +569,38 @@ async function webdriverSendKeysToElemen
let acc = await a11y.getAccessible(el, true);
a11y.assertActionable(acc, el);
el.focus();
interaction.moveCaretToEnd(el);
if (el.type == "file") {
- await interaction.uploadFile(el, value);
+ let paths = value.split("\n");
+ await interaction.uploadFiles(el, paths);
+
+ event.input(el);
+ event.change(el);
} 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") {
- await interaction.uploadFile(el, value);
+ el.focus();
+ await interaction.uploadFiles(el, [value]);
+
+ event.input(el);
+ event.change(el);
} else if ((el.type == "date" || el.type == "time") &&
Preferences.get("dom.forms.datetime")) {
interaction.setFormControlValue(el, value);
} else {
let visibilityCheckEl = el;
if (el.localName == "option") {
visibilityCheckEl = element.getContainer(el);
}
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -297551,16 +297551,21 @@
{}
]
],
"webdriver/tests/element_send_keys/__init__.py": [
[
{}
]
],
+ "webdriver/tests/element_send_keys/conftest.py": [
+ [
+ {}
+ ]
+ ],
"webdriver/tests/execute_async_script/__init__.py": [
[
{}
]
],
"webdriver/tests/execute_script/__init__.py": [
[
{}
@@ -404287,16 +404292,22 @@
]
],
"webdriver/tests/element_send_keys/content_editable.py": [
[
"/webdriver/tests/element_send_keys/content_editable.py",
{}
]
],
+ "webdriver/tests/element_send_keys/file_upload.py": [
+ [
+ "/webdriver/tests/element_send_keys/file_upload.py",
+ {}
+ ]
+ ],
"webdriver/tests/element_send_keys/form_controls.py": [
[
"/webdriver/tests/element_send_keys/form_controls.py",
{}
]
],
"webdriver/tests/element_send_keys/interactability.py": [
[
@@ -618642,20 +618653,28 @@
"webdriver/tests/element_click/stale.py": [
"490b6c17365c5eab24fd4a7ac07be6614a86a934",
"wdspec"
],
"webdriver/tests/element_send_keys/__init__.py": [
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
"support"
],
+ "webdriver/tests/element_send_keys/conftest.py": [
+ "79ae8a3775c4f9e2339e202711f31f5a83409b75",
+ "support"
+ ],
"webdriver/tests/element_send_keys/content_editable.py": [
"9c071e60e1203cf31120f20874b5f38ba41dacc3",
"wdspec"
],
+ "webdriver/tests/element_send_keys/file_upload.py": [
+ "156d84b8c195a1ebe83e65ca2ed8a92bb405bf26",
+ "wdspec"
+ ],
"webdriver/tests/element_send_keys/form_controls.py": [
"c628924d912d413c89be38d5e7098bc5aace3d67",
"wdspec"
],
"webdriver/tests/element_send_keys/interactability.py": [
"5374827c90845ded660d540d23bb7e07ac84e445",
"wdspec"
],
@@ -618999,17 +619018,17 @@
"1302349ca7d6a3dcc49e26ca0345023a5c6bbe14",
"wdspec"
],
"webdriver/tests/support/__init__.py": [
"5a31a3917a5157516c10951a3b3d5ffb43b992d9",
"support"
],
"webdriver/tests/support/asserts.py": [
- "fad8bdc53e5dc35e016b467b1be6435b5edeb9cb",
+ "b7424061b41d9bb1b87f147d5b29786695249d10",
"support"
],
"webdriver/tests/support/fixtures.py": [
"f1c704ea7b44e08dd63eaac6b5e9a3370e4c0503",
"support"
],
"webdriver/tests/support/http_request.py": [
"cb40c781fea2280b98135522def5e6a116d7b946",
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/element_send_keys/conftest.py
@@ -0,0 +1,13 @@
+import pytest
+
+
+@pytest.fixture
+def create_file(tmpdir_factory):
+ def inner(filename):
+ fh = tmpdir_factory.mktemp("tmp").join(filename)
+ fh.write(filename)
+
+ return fh
+
+ inner.__name__ = "create_file"
+ return inner
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/element_send_keys/file_upload.py
@@ -0,0 +1,145 @@
+from tests.support.asserts import assert_error, assert_files_uploaded, assert_success
+from tests.support.inline import inline
+
+
+def map_files_to_multiline_text(files):
+ return "\n".join(map(lambda f: str(f), files))
+
+
+def element_send_keys(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_empty_text(session):
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "")
+ assert_error(response, "invalid argument")
+
+
+def test_multiple_files(session, create_file):
+ files = [create_file("foo"), create_file("bar")]
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(files))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, files)
+
+
+def test_multiple_files_last_path_not_found(session, create_file):
+ files = [create_file("foo"), create_file("bar"), "foo bar"]
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(files))
+ assert_error(response, "invalid argument")
+
+ assert_files_uploaded(session, element, [])
+
+
+def test_multiple_files_without_multiple_attribute(session, create_file):
+ files = [create_file("foo"), create_file("bar")]
+
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(files))
+ assert_error(response, "invalid argument")
+
+ assert_files_uploaded(session, element, [])
+
+
+def test_multiple_files_send_twice(session, create_file):
+ first_files = [create_file("foo"), create_file("bar")]
+ second_files = [create_file("john"), create_file("doe")]
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(first_files))
+ assert_success(response)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(second_files))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, first_files + second_files)
+
+
+def test_multiple_files_reset_with_element_clear(session, create_file):
+ first_files = [create_file("foo"), create_file("bar")]
+ second_files = [create_file("john"), create_file("doe")]
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(first_files))
+ assert_success(response)
+
+ # Reset already uploaded files
+ element.clear()
+ assert_files_uploaded(session, element, [])
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(second_files))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, second_files)
+
+
+def test_single_file(session, create_file):
+ single_file = create_file("foo")
+
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(single_file))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, [single_file])
+
+
+def test_single_file_replaces_without_multiple_attribute(session, create_file):
+ first_file = create_file("foo")
+ second_file = create_file("bar")
+
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(first_file))
+ assert_success(response)
+
+ response = element_send_keys(session, element, str(second_file))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, [second_file])
+
+
+def test_single_file_appends_with_multiple_attribute(session, create_file):
+ first_file = create_file("foo")
+ second_file = create_file("bar")
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(first_file))
+ assert_success(response)
+
+ response = element_send_keys(session, element, str(second_file))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, [first_file, second_file])
--- a/testing/web-platform/tests/webdriver/tests/support/asserts.py
+++ b/testing/web-platform/tests/webdriver/tests/support/asserts.py
@@ -1,8 +1,10 @@
+import os
+
from webdriver import Element, WebDriverException
# WebDriver specification ID: dfn-error-response-data
errors = {
"element click intercepted": 400,
"element not selectable": 400,
"element not interactable": 400,
@@ -98,16 +100,50 @@ def assert_dialog_handled(session, expec
try:
assert_error(result, "no such alert")
except:
assert (result.status == 200 and
result.body["value"] != expected_text), (
"Dialog with text '%s' was not handled." % expected_text)
+def assert_files_uploaded(session, element, files):
+
+ def get_file_contents(file_index):
+ return session.execute_async_script("""
+ let files = arguments[0].files;
+ let index = arguments[1];
+ let resolve = arguments[2];
+
+ var reader = new FileReader();
+ reader.onload = function(event) {
+ resolve(reader.result);
+ };
+ reader.readAsText(files[index]);
+ """, (element, file_index))
+
+ def get_uploaded_file_names():
+ return session.execute_script("""
+ let fileList = arguments[0].files;
+ let files = [];
+
+ for (var i = 0; i < fileList.length; i++) {
+ files.push(fileList[i].name);
+ }
+
+ return files;
+ """, args=(element,))
+
+ expected_file_names = [str(f.basename) for f in files]
+ assert get_uploaded_file_names() == expected_file_names
+
+ for index, f in enumerate(files):
+ assert get_file_contents(index) == f.read()
+
+
def assert_same_element(session, a, b):
"""Verify that two element references describe the same element."""
if isinstance(a, dict):
assert Element.identifier in a, "Actual value does not describe an element"
a_id = a[Element.identifier]
elif isinstance(a, Element):
a_id = a.id
else: