Bug 1417821 - Prevent missing click event from causing deadlock in interaction.flushEventLoop. r?whimboo draft
authorAndreas Tolfsen <ato@sny.no>
Thu, 23 Nov 2017 15:02:17 +0000
changeset 703217 24382949c898a1f3d808c085c329b18867b7de6e
parent 703216 5d2525f930373220dd72cb957ebb50dd96a591af
child 703218 262f107e4ee0af0a536aa2dd2e17d5a3e1e58853
push id90754
push userbmo:ato@sny.no
push dateFri, 24 Nov 2017 16:38:54 +0000
reviewerswhimboo
bugs1417821
milestone59.0a1
Bug 1417821 - Prevent missing click event from causing deadlock in interaction.flushEventLoop. r?whimboo If the DOM click event does not fire, which currently happens in this edge case where Firefox does not bubble the click event up to <body>, interaction.flushEventLoop will hang forever. This is believed to be a bug in Gecko, but it is probably safer to use a TimedPromise here in case anything else bad were to happen. Thanks-to: Alexei Barantsev <barancev@gmail.com> MozReview-Commit-ID: KZBdR8zcfJb
testing/marionette/interaction.js
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/webdriver/tests/element_click/bubbling.py
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -3,25 +3,26 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/atom.js");
+Cu.import("chrome://marionette/content/element.js");
 const {
   ElementClickInterceptedError,
   ElementNotInteractableError,
   InvalidArgumentError,
   InvalidElementStateError,
 } = Cu.import("chrome://marionette/content/error.js", {});
-Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/event.js");
 const {pprint} = Cu.import("chrome://marionette/content/format.js", {});
-Cu.import("chrome://marionette/content/event.js");
+const {TimedPromise} = Cu.import("chrome://marionette/content/sync.js", {});
 
 Cu.importGlobalProperties(["File"]);
 
 this.EXPORTED_SYMBOLS = ["interaction"];
 
 /** XUL elements that support disabled attribute. */
 const DISABLED_ATTRIBUTE_SUPPORTED_XUL = new Set([
   "ARROWSCROLLBOX",
@@ -291,39 +292,44 @@ interaction.selectOption = function(el) 
  * DOM events generated by clicking an element, or until the document
  * is unloaded.
  *
  * @param {Element} el
  *     Element that is expected to receive the click.
  *
  * @return {Promise}
  *     Promise is resolved once <var>el</var> has been clicked
- *     (its <code>click</code> event fires) or the document is unloaded.
+ *     (its <code>click</code> event fires), the document is unloaded,
+ *     or a 500 ms timeout is reached.
  */
 interaction.flushEventLoop = async function(el) {
   const win = el.ownerGlobal;
   let unloadEv, clickEv;
 
-  return new Promise(resolve => {
+  let spinEventLoop = resolve => {
     unloadEv = resolve;
     clickEv = () => {
       if (win.closed) {
         resolve();
       } else {
         win.setTimeout(resolve, 0);
       }
     };
 
     win.addEventListener("unload", unloadEv, {mozSystemGroup: true});
     el.addEventListener("click", clickEv, {mozSystemGroup: true});
-  }).then(() => {
+  };
+  let removeListeners = () => {
     // only one event fires
     win.removeEventListener("unload", unloadEv);
     el.removeEventListener("click", clickEv);
-  });
+  };
+
+  return new TimedPromise(spinEventLoop, {timeout: 500, throws: null})
+      .then(removeListeners);
 };
 
 /**
  * Appends <var>path</var> to an <tt>&lt;input type=file&gt;</tt>'s
  * file list.
  *
  * @param {HTMLInputElement} el
  *     An <tt>&lt;input type=file&gt;</tt> element.
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -373392,16 +373392,22 @@
     ]
    ],
    "webdriver/tests/cookies/get_named_cookie.py": [
     [
      "/webdriver/tests/cookies/get_named_cookie.py",
      {}
     ]
    ],
+   "webdriver/tests/element_click/bubbling.py": [
+    [
+     "/webdriver/tests/element_click/bubbling.py",
+     {}
+    ]
+   ],
    "webdriver/tests/element_click/select.py": [
     [
      "/webdriver/tests/element_click/select.py",
      {}
     ]
    ],
    "webdriver/tests/element_click/stale.py": [
     [
@@ -575969,16 +575975,20 @@
   "webdriver/tests/cookies/get_named_cookie.py": [
    "9455d1504590154ad2a540f102455baff602aefb",
    "wdspec"
   ],
   "webdriver/tests/element_click/__init__.py": [
    "da39a3ee5e6b4b0d3255bfef95601890afd80709",
    "support"
   ],
+  "webdriver/tests/element_click/bubbling.py": [
+   "e73935ac7ba09487a507f796bf0029da51cb2be1",
+   "wdspec"
+  ],
   "webdriver/tests/element_click/select.py": [
    "5ba51b660c7203bba3ada597c2f56fe094358e1f",
    "wdspec"
   ],
   "webdriver/tests/element_click/stale.py": [
    "37af63203540dfe11d36fe05d74694f05c6505f2",
    "wdspec"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/element_click/bubbling.py
@@ -0,0 +1,78 @@
+from tests.support.asserts import assert_success
+from tests.support.inline import inline
+
+
+def click(session, element):
+    return session.transport.send(
+        "POST", "/session/{session_id}/element/{element_id}/click".format(
+            session_id=session.session_id,
+            element_id=element.id))
+
+
+def test_element_disappears_during_click(session):
+    """
+    When an element in the event bubbling order disappears (its CSS
+    display style is set to "none") during a click, Gecko and Blink
+    exhibit different behaviour.  Whilst Chrome fires a "click"
+    DOM event on <body>, Firefox does not.
+
+    A WebDriver implementation may choose to wait for this event to let
+    the event loops spin enough times to let click events propagate,
+    so this is a corner case test that Firefox does not hang indefinitely.
+    """
+    session.url = inline("""
+        <style>
+        #over,
+        #under {
+          position: absolute;
+          top: 8px;
+          left: 8px;
+          width: 100px;
+          height: 100px;
+        }
+
+        #over {
+          background: blue;
+          opacity: .5;
+        }
+        #under {
+          background: yellow;
+        }
+
+        #log {
+          margin-top: 120px;
+        }
+        </style>
+
+        <body id="body">
+          <div id=under></div>
+          <div id=over></div>
+
+           <div id=log></div>
+        </body>
+
+        <script>
+        let under = document.querySelector("#under");
+        let over = document.querySelector("#over");
+        let body = document.querySelector("body");
+        let log = document.querySelector("#log");
+
+        function logEvent({type, target, currentTarget}) {
+          log.innerHTML += "<p></p>";
+          log.lastElementChild.textContent = `${type} in ${target.id} (handled by ${currentTarget.id})`;
+        }
+
+        for (let ev of ["click", "mousedown", "mouseup"]) {
+          under.addEventListener(ev, logEvent);
+          over.addEventListener(ev, logEvent);
+          body.addEventListener(ev, logEvent);
+        }
+
+        over.addEventListener("mousedown", () => over.style.display = "none");
+        </script>
+        """)
+    over = session.find.css("#over", all=False)
+
+    # should not time out
+    response = click(session, over)
+    assert_success(response)