Bug 1354211 - Make WebDriver:ElementClear conforming to standard. r?automatedtester draft
authorAndreas Tolfsen <ato@sny.no>
Sun, 31 Dec 2017 14:53:42 +0000
changeset 719576 5cba9c9af3c7412d4dd5d62586741d33a4405174
parent 719575 c32906294b1b1bc5f089957ea06cee91c63619f8
child 719577 1ade9ef271e00c038787e543690576d0b6bf45a3
push id95293
push userbmo:ato@sny.no
push dateFri, 12 Jan 2018 10:32:36 +0000
reviewersautomatedtester
bugs1354211
milestone59.0a1
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");
@@ -1321,31 +1320,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"]