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
--- 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><input type=file></tt>'s
* file list.
*
* @param {HTMLInputElement} el
* An <tt><input type=file></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)