Bug 1354211 - Make WebDriver:ElementClear conforming to standard. r=automatedtester
authorAndreas Tolfsen <ato@sny.no>
Sun, 31 Dec 2017 14:53:42 +0000
changeset 450793 f9f835bce31b7a0f40c2a8e69b83cf8aaf4b4024
parent 450792 66f58d8f33559bf9b1f8206a857312641ee1b169
child 450794 9ab0d72e5e0bdb73f6d0c88a563b2720d839bc5b
push id8543
push userryanvm@gmail.com
push dateTue, 16 Jan 2018 14:33:22 +0000
treeherdermozilla-beta@a6525ed16a32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester
bugs1354211
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1354211 - Make WebDriver:ElementClear conforming to standard. r=automatedtester This implements the remote end steps for the Element Clear command from WebDriver in Marionette. The WPT test webdriver/tests/interaction/element_clear.py was deleted because it tested a previous definition of the Element Clear command and many of its tests were either incorrect or replaced by the new tests. MozReview-Commit-ID: C2xmIlhSAdW
testing/marionette/interaction.js
testing/marionette/listener.js
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/webdriver/tests/interaction/element_clear.py
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -284,16 +284,62 @@ interaction.selectOption = function(el) 
     event.input(containerEl);
     event.change(containerEl);
   }
 
   event.mouseup(containerEl);
   event.click(containerEl);
 };
 
+interaction.clearElement = function(el) {
+  if (element.isDisabled(el)) {
+    throw new InvalidElementStateError(pprint`Element is disabled: ${el}`);
+  }
+  if (element.isReadOnly(el)) {
+    throw new InvalidElementStateError(pprint`Element is read-only: ${el}`);
+  }
+  if (!element.isEditable(el)) {
+    throw new InvalidElementStateError(
+        pprint`Unable to clear element that cannot be edited: ${el}`);
+  }
+
+  if (!element.isInView(el)) {
+    element.scrollIntoView(el);
+  }
+  if (!element.isInView(el)) {
+    throw new ElementNotInteractableError(
+        pprint`Element ${el} could not be scrolled into view`);
+  }
+
+  let attr;
+  if (element.isEditingHost(el)) {
+    attr = "innerHTML";
+  } else {
+    attr = "value";
+  }
+
+  switch (el.type) {
+    case "file":
+      if (el.files.length == 0) {
+        return;
+      }
+      break;
+
+    default:
+      if (el[attr] === "") {
+        return;
+      }
+      break;
+  }
+
+  event.focus(el);
+  el[attr] = "";
+  event.blur(el);
+};
+
 /**
  * Waits until the event loop has spun enough times to process the
  * DOM events generated by clicking an element, or until the document
  * is unloaded.
  *
  * @param {Element} el
  *     Element that is expected to receive the click.
  *
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -23,17 +23,16 @@ Cu.import("chrome://marionette/content/c
 const {
   element,
   WebElement,
 } = Cu.import("chrome://marionette/content/element.js", {});
 const {
   ElementNotInteractableError,
   InsecureCertificateError,
   InvalidArgumentError,
-  InvalidElementStateError,
   InvalidSelectorError,
   NoSuchElementError,
   NoSuchFrameError,
   pprint,
   TimeoutError,
   UnknownError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 Cu.import("chrome://marionette/content/evaluate.js");
@@ -1319,31 +1318,17 @@ async function sendKeysToElement(el, val
       el, val,
       capabilities.get("moz:accessibilityChecks"),
       capabilities.get("moz:webdriverClick"),
   );
 }
 
 /** Clear the text of an element. */
 function clearElement(el) {
-  try {
-    if (el.type == "file") {
-      el.value = null;
-    } else {
-      atom.clearElement(el, curContainer.frame);
-    }
-  } catch (e) {
-    // Bug 964738: Newer atoms contain status codes which makes wrapping
-    // this in an error prototype that has a status property unnecessary
-    if (e.name == "InvalidElementStateError") {
-      throw new InvalidElementStateError(e.message);
-    } else {
-      throw e;
-    }
-  }
+  interaction.clearElement(el);
 }
 
 /** Switch the current context to the specified host's Shadow DOM. */
 function switchToShadowRoot(el) {
   if (!el) {
     // If no host element is passed, attempt to find a parent shadow
     // root or, if none found, unset the current shadow root
     if (curContainer.shadowRoot) {
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -574243,17 +574243,17 @@
    "9fe4c10b921a84dc086cea47d48bb34fdbb28eee",
    "testharness"
   ],
   "service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html": [
    "f574c7a96a1ca766445cd0b427b9963b18c62795",
    "testharness"
   ],
   "service-workers/service-worker/about-blank-replacement.https.html": [
-   "0eee8e63450cff5a2f67b6d71d565b99baddbb69",
+   "d2cc0fc99820308096d549d892962fe10b19f0ae",
    "testharness"
   ],
   "service-workers/service-worker/activate-event-after-install-state-change.https.html": [
    "9d1971d9b5dcb52a14a0d2313065e27766c0489a",
    "testharness"
   ],
   "service-workers/service-worker/activation-after-registration.https.html": [
    "913c58ba58de077b82d0ec9cc21258610b26fe97",
@@ -583331,17 +583331,17 @@
    "817011a8cdff7cfd7e445fb8ecb84e5d91f03993",
    "wdspec"
   ],
   "webdriver/tests/get_window_rect.py": [
    "c9139c16aa950c734c776887d6a762b867790812",
    "wdspec"
   ],
   "webdriver/tests/interaction/element_clear.py": [
-   "9b598e993e404275f1fe4bdb1967d8e1950e25cb",
+   "109a1b9fed21b257503321b42bd670f9c36a0bcc",
    "wdspec"
   ],
   "webdriver/tests/interaction/send_keys_content_editable.py": [
    "9c071e60e1203cf31120f20874b5f38ba41dacc3",
    "wdspec"
   ],
   "webdriver/tests/interface.html": [
    "6625887cfa7f461dc428c11861fce71c47bef57d",
--- a/testing/web-platform/tests/webdriver/tests/interaction/element_clear.py
+++ b/testing/web-platform/tests/webdriver/tests/interaction/element_clear.py
@@ -1,185 +1,363 @@
 import pytest
+
 from tests.support.asserts import assert_error, assert_success
 from tests.support.inline import inline
 
 
-def clear(session, element):
-    return session.transport.send("POST", "session/{session_id}/element/{element_id}/clear"
-                                  .format(session_id=session.session_id,
-                                          element_id=element.id))
+@pytest.fixture(scope="session")
+def text_file(tmpdir_factory):
+    fh = tmpdir_factory.mktemp("tmp").join("hello.txt")
+    fh.write("hello")
+    return fh
 
 
-# 14.2 Element Clear
+def element_clear(session, element):
+    return session.transport.send("POST", "/session/%s/element/%s/clear" %
+                                  (session.session_id, element.id))
+
 
-def test_no_browsing_context(session, create_window):
-    # 14.2 step 1
-    session.url = inline("<p>This is not an editable paragraph.")
-    element = session.find.css("p", all=False)
-
-    session.window_handle = create_window()
+def test_closed_context(session, create_window):
+    new_window = create_window()
+    session.window_handle = new_window
+    session.url = inline("<input>")
+    element = session.find.css("input", all=False)
     session.close()
 
-    response = clear(session, element)
+    response = element_clear(session, element)
     assert_error(response, "no such window")
 
 
-def test_element_not_found(session):
-    # 14.2 Step 2
-    response = session.transport.send("POST", "session/{session_id}/element/{element_id}/clear"
-                                      .format(session_id=session.session_id,
-                                              element_id="box1"))
+def test_connected_element(session):
+    session.url = inline("<input>")
+    element = session.find.css("input", all=False)
+
+    session.url = inline("<input>")
+    response = element_clear(session, element)
+    assert_error(response, "stale element reference")
+
+
+def test_pointer_interactable(session):
+    session.url = inline("<input style='margin-left: -1000px' value=foobar>")
+    element = session.find.css("input", all=False)
+
+    response = element_clear(session, element)
+    assert_error(response, "element not interactable")
+
 
-    assert_error(response, "no such element")
+def test_keyboard_interactable(session):
+    session.url = inline("""
+        <input value=foobar>
+        <div></div>
+
+        <style>
+        div {
+          position: absolute;
+          background: blue;
+          top: 0;
+        }
+        </style>
+        """)
+    element = session.find.css("input", all=False)
+    assert element.property("value") == "foobar"
+
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == ""
 
 
-def test_element_not_editable(session):
-    # 14.2 Step 3
-    session.url = inline("<p>This is not an editable paragraph.")
+@pytest.mark.parametrize("type,value,default",
+                         [("number", "42", ""),
+                          ("range", "42", "50"),
+                          ("email", "foo@example.com", ""),
+                          ("password", "password", ""),
+                          ("search", "search", ""),
+                          ("tel", "999", ""),
+                          ("text", "text", ""),
+                          ("url", "https://example.com/", ""),
+                          ("color", "#ff0000", "#000000"),
+                          ("date", "2017-12-26", ""),
+                          ("datetime", "2017-12-26T19:48", ""),
+                          ("datetime-local", "2017-12-26T19:48", ""),
+                          ("time", "19:48", ""),
+                          ("month", "2017-11", ""),
+                          ("week", "2017-W52", "")])
+def test_input(session, type, value, default):
+    session.url = inline("<input type=%s value='%s'>" % (type, value))
+    element = session.find.css("input", all=False)
+    assert element.property("value") == value
 
-    element = session.find.css("p", all=False)
-    response = clear(session, element)
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == default
+
+
+@pytest.mark.parametrize("type",
+                         ["number",
+                          "range",
+                          "email",
+                          "password",
+                          "search",
+                          "tel",
+                          "text",
+                          "url",
+                          "color",
+                          "date",
+                          "datetime",
+                          "datetime-local",
+                          "time",
+                          "month",
+                          "week",
+                          "file"])
+def test_input_disabled(session, type):
+    session.url = inline("<input type=%s disabled>" % type)
+    element = session.find.css("input", all=False)
+
+    response = element_clear(session, element)
     assert_error(response, "invalid element state")
 
 
-def test_button_element_not_resettable(session):
-    # 14.2 Step 3
-    session.url = inline("<input type=button value=Federer>")
+@pytest.mark.parametrize("type",
+                         ["number",
+                          "range",
+                          "email",
+                          "password",
+                          "search",
+                          "tel",
+                          "text",
+                          "url",
+                          "color",
+                          "date",
+                          "datetime",
+                          "datetime-local",
+                          "time",
+                          "month",
+                          "week",
+                          "file"])
+def test_input_readonly(session, type):
+    session.url = inline("<input type=%s readonly>" % type)
+    element = session.find.css("input", all=False)
 
-    element = session.find.css("input", all=False)
-    response = clear(session, element)
+    response = element_clear(session, element)
     assert_error(response, "invalid element state")
 
 
-def test_disabled_element_not_resettable(session):
-    # 14.2 Step 3
-    session.url = inline("<input type=text value=Federer disabled>")
+def test_textarea(session):
+    session.url = inline("<textarea>foobar</textarea>")
+    element = session.find.css("textarea", all=False)
+    assert element.property("value") == "foobar"
+
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == ""
+
 
-    element = session.find.css("input", all=False)
-    response = clear(session, element)
+def test_textarea_disabled(session):
+    session.url = inline("<textarea disabled></textarea>")
+    element = session.find.css("textarea", all=False)
+
+    response = element_clear(session, element)
+    assert_error(response, "invalid element state")
+
+
+def test_textarea_readonly(session):
+    session.url = inline("<textarea readonly></textarea>")
+    element = session.find.css("textarea", all=False)
+
+    response = element_clear(session, element)
     assert_error(response, "invalid element state")
 
 
-def test_scroll_into_element_view(session):
-    # 14.2 Step 4
-    session.url = inline("<input type=text value=Federer><div style= \"height: 200vh; width: 5000vh\">")
-
-    # Scroll to the bottom right of the page
-    session.execute_script("window.scrollTo(document.body.scrollWidth, document.body.scrollHeight);")
+def test_input_file(session, text_file):
+    session.url = inline("<input type=file>")
     element = session.find.css("input", all=False)
-    # Clear and scroll back to the top of the page
-    response = clear(session, element)
-    assert_success(response)
+    element.send_keys(str(text_file))
 
-    # Check if element cleared is scrolled into view
-    rect = session.execute_script("return document.getElementsByTagName(\"input\")[0].getBoundingClientRect()")
-
-    pageDict = {}
-
-    pageDict["innerHeight"] = session.execute_script("return window.innerHeight")
-    pageDict["innerWidth"] = session.execute_script("return window.innerWidth")
-    pageDict["pageXOffset"] = session.execute_script("return window.pageXOffset")
-    pageDict["pageYOffset"] = session.execute_script("return window.pageYOffset")
-
-    assert rect["top"] < (pageDict["innerHeight"] + pageDict["pageYOffset"]) and \
-           rect["left"] < (pageDict["innerWidth"] + pageDict["pageXOffset"]) and \
-           (rect["top"] + element.rect["height"]) > pageDict["pageYOffset"] and \
-           (rect["left"] + element.rect["width"]) > pageDict["pageXOffset"]
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == ""
 
 
-# TODO
-# Any suggestions on implementation?
-# def test_session_implicit_wait_timeout(session):
-    # 14.2 Step 5
+def test_input_file_multiple(session, text_file):
+    session.url = inline("<input type=file multiple>")
+    element = session.find.css("input", all=False)
+    element.send_keys(str(text_file))
+    element.send_keys(str(text_file))
 
-# TODO
-# Any suggestions on implementation?
-# def test_element_not_interactable(session):
-#     # 14.2 Step 6
-#     assert_error(response, "element not interactable")
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == ""
 
 
-def test_element_readonly(session):
-    # 14.2 Step 7
-    session.url = inline("<input type=text readonly value=Federer>")
+def test_select(session):
+    session.url = inline("""
+        <select disabled>
+          <option>foo
+        </select>
+        """)
+    select = session.find.css("select", all=False)
+    option = session.find.css("option", all=False)
 
-    element = session.find.css("input", all=False)
-    response = clear(session, element)
+    response = element_clear(session, select)
+    assert_error(response, "invalid element state")
+    response = element_clear(session, option)
     assert_error(response, "invalid element state")
 
 
-def test_element_disabled(session):
-    # 14.2 Step 7
-    session.url = inline("<input type=text disabled value=Federer>")
+def test_button(session):
+    session.url = inline("<button></button>")
+    button = session.find.css("button", all=False)
 
-    element = session.find.css("input", all=False)
-    response = clear(session, element)
+    response = element_clear(session, button)
     assert_error(response, "invalid element state")
 
 
-def test_element_pointer_events_disabled(session):
-    # 14.2 Step 7
-    session.url = inline("<input type=text value=Federer style=\"pointer-events: none\">")
+def test_button_with_subtree(session):
+    """
+    Whilst an <input> is normally editable, the focusable area
+    where it is placed will default to the <button>.  I.e. if you
+    try to click <input> to focus it, you will hit the <button>.
+    """
+    session.url = inline("""
+        <button>
+          <input value=foobar>
+        </button>
+        """)
+    text_field = session.find.css("input", all=False)
+
+    response = element_clear(session, text_field)
+    assert_error(response, "element not interactable")
+
+
+def test_contenteditable(session):
+    session.url = inline("<p contenteditable>foobar</p>")
+    element = session.find.css("p", all=False)
+    assert element.property("innerHTML") == "foobar"
+
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("innerHTML") == ""
+
+
+def test_contenteditable_focus(session):
+    session.url = inline("""
+        <p contenteditable>foobar</p>
+
+        <script>
+        window.events = [];
+        let p = document.querySelector("p");
+        for (let ev of ["focus", "blur"]) {
+          p.addEventListener(ev, ({type}) => window.events.push(type));
+        }
+        </script>
+        """)
+    element = session.find.css("p", all=False)
+    assert element.property("innerHTML") == "foobar"
+
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("innerHTML") == ""
+    assert session.execute_script("return window.events") == ["focus", "blur"]
+
+
+def test_designmode(session):
+    session.url = inline("foobar")
+    element = session.find.css("body", all=False)
+    assert element.property("innerHTML") == "foobar"
+    session.execute_script("document.designMode = 'on'")
 
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("innerHTML") == "<br>"
+
+
+def test_resettable_element_focus(session):
+    session.url = inline("""
+        <input value="foobar">
+
+        <script>
+        window.events = [];
+        let input = document.querySelector("input");
+        for (let ev of ["focus", "blur"]) {
+          input.addEventListener(ev, ({type}) => window.events.push(type));
+        }
+        </script>
+        """)
     element = session.find.css("input", all=False)
-    response = clear(session, element)
+    assert element.property("value") == "foobar"
+
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == ""
+    assert session.execute_script("return window.events") == ["focus", "blur"]
+
+
+def test_resettable_element_focus_when_empty(session):
+    session.url = inline("""
+        <input>
+
+        <script>
+        window.events = [];
+        let p = document.querySelector("input");
+        for (let ev of ["focus", "blur"]) {
+          p.addEventListener(ev, ({type}) => window.events.push(type));
+        }
+        </script>
+        """)
+    element = session.find.css("input", all=False)
+    assert element.property("value") == ""
+
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == ""
+    assert session.execute_script("return window.events") == []
+
+
+@pytest.mark.parametrize("type",
+                         ["checkbox",
+                          "radio",
+                          "hidden",
+                          "submit",
+                          "button",
+                          "image"])
+def test_non_editable_inputs(session, type):
+    session.url = inline("<input type=%s>" % type)
+    element = session.find.css("input", all=False)
+
+    response = element_clear(session, element)
     assert_error(response, "invalid element state")
 
 
-@pytest.mark.parametrize("element", [["text", "<input id=text type=text value=\"Federer\"><input id=empty type=text value=\"\">"],
-                                    ["search", "<input id=search type=search value=\"Federer\"><input id=empty type=search value=\"\">"],
-                                    ["url", "<input id=url type=url value=\"www.hello.com\"><input id=empty type=url value=\"\">"],
-                                    ["tele", "<input id=tele type=telephone value=\"2061234567\"><input id=empty type=telephone value=\"\">"],
-                                    ["email", "<input id=email type=email value=\"hello@world.com\"><input id=empty type=email value=\"\">"],
-                                    ["password", "<input id=password type=password value=\"pass123\"><input id=empty type=password value=\"\">"],
-                                    ["date", "<input id=date type=date value=\"2017-12-25\"><input id=empty type=date value=\"\">"],
-                                    ["time", "<input id=time type=time value=\"11:11\"><input id=empty type=time value=\"\">"],
-                                    ["number", "<input id=number type=number value=\"19\"><input id=empty type=number value=\"\">"],
-                                    ["range", "<input id=range type=range min=\"0\" max=\"10\"><input id=empty type=range value=\"\">"],
-                                    ["color", "<input id=color type=color value=\"#ff0000\"><input id=empty type=color value=\"\">"],
-                                    ["file", "<input id=file type=file value=\"C:\\helloworld.txt\"><input id=empty type=file value=\"\">"],
-                                    ["textarea", "<textarea id=textarea>Hello World</textarea><textarea id=empty></textarea>"],
-                                    ["sel", "<select id=sel><option></option><option>a</option><option>b</option></select><select id=empty><option></option></select>"],
-                                    ["out", "<output id=out value=100></output><output id=empty></output>"],
-                                    ["para", "<p id=para contenteditable=true>This is an editable paragraph.</p><p id=empty contenteditable=true></p>"]])
+def test_scroll_into_view(session):
+    session.url = inline("""
+        <input value=foobar>
+        <div style='height: 200vh; width: 5000vh'>
+        """)
+    element = session.find.css("input", all=False)
+    assert element.property("value") == "foobar"
+    assert session.execute_script("return window.scrollY") == 0
 
-def test_clear_content_editable_resettable_element(session, element):
-    # 14.2 Step 8
-    url = element[1] + """<input id=focusCheck type=checkbox>
-                    <input id=blurCheck type=checkbox>
-                    <script>
-                    var id = "%s";
-                    document.getElementById(id).addEventListener("focus", checkFocus);
-                    document.getElementById(id).addEventListener("blur", checkBlur);
-                    document.getElementById("empty").addEventListener("focus", checkFocus);
-                    document.getElementById("empty").addEventListener("blur", checkBlur);
+    # scroll to the bottom right of the page
+    session.execute_script("""
+        let {scrollWidth, scrollHeight} = document.body;
+        window.scrollTo(scrollWidth, scrollHeight);
+        """)
 
-                    function checkFocus() {
-                        document.getElementById("focusCheck").checked = true;
-                    }
-                    function checkBlur() {
-                        document.getElementById("blurCheck").checked = true;
-                    }
-                    </script>""" % element[0]
-    session.url = inline(url)
-    # Step 1
-    empty_element = session.find.css("#empty", all=False)
-    clear_element_test_helper(session, empty_element, False)
-    session.execute_script("document.getElementById(\"focusCheck\").checked = false;")
-    session.execute_script("document.getElementById(\"blurCheck\").checked = false;")
-    # Step 2 - 4
-    test_element = session.find.css("#" + element[0], all=False)
-    clear_element_test_helper(session, test_element, True)
+    # clear and scroll back to the top of the page
+    response = element_clear(session, element)
+    assert_success(response)
+    assert element.property("value") == ""
 
+    # check if element cleared is scrolled into view
+    rect = session.execute_script("""
+        let [input] = arguments;
+        return input.getBoundingClientRect();
+        """, args=(element,))
+    window = session.execute_script("""
+        let {innerHeight, innerWidth, pageXOffset, pageYOffset} = window;
+        return {innerHeight, innerWidth, pageXOffset, pageYOffset};
+        """)
 
-def clear_element_test_helper(session, element, value):
-    response = clear(session, element)
-    assert_success(response)
-    response = session.execute_script("return document.getElementById(\"focusCheck\").checked;")
-    assert response is value
-    response = session.execute_script("return document.getElementById(\"blurCheck\").checked;")
-    assert response is value
-    if element.name == "p":
-        response = session.execute_script("return document.getElementById(\"para\").innerHTML;")
-        assert response == ""
-    else:
-        assert element.property("value") == ""
+    assert rect["top"] < (window["innerHeight"] + window["pageYOffset"]) and \
+           rect["left"] < (window["innerWidth"] + window["pageXOffset"]) and \
+           (rect["top"] + element.rect["height"]) > window["pageYOffset"] and \
+           (rect["left"] + element.rect["width"]) > window["pageXOffset"]