Bug 1448792 - [marionette][wdspec] Make Element Send Keys wdspec conforming for file uploads. draft
authorHenrik Skupin <mail@hskupin.info>
Mon, 28 May 2018 17:35:34 +0200
changeset 808623 ad540da5a097bf4ecfad0d291650e4432f9693ad
parent 808622 9af3303adcdf46d9e0e881c7ec6381be2e56a56f
child 808624 d82396ba911f22379d5352a3aeac62451f145ca8
push id113443
push userbmo:hskupin@gmail.com
push dateWed, 20 Jun 2018 07:16:03 +0000
bugs1448792
milestone62.0a1
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
testing/marionette/interaction.js
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/webdriver/tests/element_send_keys/conftest.py
testing/web-platform/tests/webdriver/tests/element_send_keys/file_upload.py
testing/web-platform/tests/webdriver/tests/support/asserts.py
--- 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>&lt;input type=file&gt;</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>&lt;input type=file&gt;</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: