Menu

#1984 Reworking the JS Event listeners implementation

Latest SVN
closed
RBRi
None
1
2018-09-07
2018-08-14
No

Problem in brief

Thorough testing has revealed event listeners in HtmlUnit are implemented quite liberally in a few places with regards to how they should work. It's somewhat surprising they're working so well in practice.

This bug report attempts to address the problems and tackle at fixing them. Scope includes xyz.addListenerEvent('foo', function () { ... }) listeners and xyz.onfoo = function () { ... } property handlers.

Problems noticed
  • Ordering of listeners called during the at target phase should honour the order in which the listeners were added, but does not.
  • Ordering of property handlers such as window.onload should honour the timing at which the property was set, with respect to other event listeners, but does not.
  • Events where event.bubbles is false should not have a bubbling phase, but does.
  • The load event for Windowshould only traverse Window but traverses Window, Document instead.
  • The load event for everything else should traverse from Document to all levels down to the element in question, but traverses the element in question only. (Every place using EventTarget.executeEventLocally() potentially applies to this and should probably be using the standard EventTarget.fireEvent() instead.)
  • The load event for the <frame> element should be treated like "everything else" above, but is not. (On the other hand, the onload property for <body> and <frameset> is correctly tied to Window.)
  • The propagation path of events should not be affected by changes to the DOM tree by intermediate listeners, and therefore the bubbling phase should always traverse the same nodes as the capturing phase only in reverse, but is and does not.
  • The return value of event listeners should be ignored (really? it looks that way.. maybe it was different in older IE) and only that of the property handler should be used, but is not.

Test cases

There are five tests which I'm putting in the comments because it's quite long.

  • test_onload.html
  • test_frame.html
  • test_click.html
  • test_nested_click.html
  • test_event_return_value.html

These extra two I'm including but haven't fixed yet. They're limited to the load/error behaviour of <script> and <img> and require fixing up some implementation in HtmlScript and HtmlImage.

  • test_script_onload.html
  • test_img_onload.html

Possible fix

The fix spans three files:

  • html/HtmlPage.java
  • javascript/host/event/EventListenersContainer.java
  • javascript/host/event/EventTarget.java

Changes to HtmlPage is the diff below.

The changes for EventListenersContainer.java and EventTarget.java are rather extensive so I'm attaching the modified files along with their respective base versions suffixed with -2.32.txt.

--- a/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java
+++ b/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java
@@ -86,15 +86,16 @@
 import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
 import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
 import com.gargoylesoftware.htmlunit.javascript.host.Window;
-import com.gargoylesoftware.htmlunit.javascript.host.dom.Node;
 import com.gargoylesoftware.htmlunit.javascript.host.event.BeforeUnloadEvent;
 import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
+import com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget;
 import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
 import com.gargoylesoftware.htmlunit.protocol.javascript.JavaScriptURLConnection;
 import com.gargoylesoftware.htmlunit.util.EncodingSniffer;
 import com.gargoylesoftware.htmlunit.util.UrlUtils;

 import net.sourceforge.htmlunit.corejs.javascript.Context;
+import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
 import net.sourceforge.htmlunit.corejs.javascript.Function;
 import net.sourceforge.htmlunit.corejs.javascript.Script;
 import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
@@ -1214,18 +1215,28 @@ private boolean executeEventHandlersIfNeeded(final String eventType) {
         // Execute the specified event on the document element.
         final WebWindow window = getEnclosingWindow();
         if (window.getScriptableObject() instanceof Window) {

-            final DomElement element = getDocumentElement();
-            if (element == null) { // happens for instance if document.documentElement has been removed from parent
-                return true;
-            }
+            // We need the 'Document' node for these load events but getDocumentElement() returns
+            // <html> (HtmlHtml) which is one below that.  The 'Document' node is incidentally just
+            // us (HtmlPage).  (Some tidbits at https://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
+            final DomNode node = this;
             final Event event;
             if (eventType.equals(Event.TYPE_BEFORE_UNLOAD)) {
-                event = new BeforeUnloadEvent(element, eventType);
+                event = new BeforeUnloadEvent(node, eventType);
             }
             else {
-                event = new Event(element, eventType);
+                event = new Event(node, eventType);
+            }
+
+            // This is the same as DomElement.fireEvent() and was copied
+            // here so it could be used with HtmlPage.
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Firing " + event);
             }
-            final ScriptResult result = element.fireEvent(event);
+
+            final EventTarget jsNode = node.getScriptableObject();
+            final ContextFactory cf = ((JavaScriptEngine) getWebClient().getJavaScriptEngine()).getContextFactory();
+            final ScriptResult result = cf.call(cx -> jsNode.fireEvent(event));
+
             if (!isOnbeforeunloadAccepted(this, event, result)) {
                 return false;
             }
@@ -1253,7 +1264,12 @@ private boolean executeEventHandlersIfNeeded(final String eventType) {
                     else {
                         event = new Event(frame, eventType);
                     }
-                    final ScriptResult result = ((Node) frame.getScriptableObject()).executeEventLocally(event);
+
+                    // This fires the "load" event for the <frame> element which, like all non-window
+                    // load events, propagates up to Document but not Window.  The "load" event for
+                    // <frameset> on the other hand, like that of <body>, is handled above where it is
+                    // fired against Document and directed to Window.
+                    final ScriptResult result = frame.fireEvent(event);
                     if (!isOnbeforeunloadAccepted((HtmlPage) frame.getPage(), event, result)) {
                         return false;
                     }
4 Attachments

Discussion

1 2 > >> (Page 1 of 2)
  • Atsushi Nakagawa

    Five tests are as follows:

    test_onload.html

    • Tests the ordering of DOMContentLoaded for window and document as well as how capturing / bubbling phases are handled.
    • Tests the ordering of load for window and document, and how they relate to the onload property of <body>. Verifies handling of the at target phase.
    • Checks the state of event.eventPhase for a non-bubbling event after the bubbling phase.
    wc.waitForBackgroundJavaScriptStartingBefore(1000);
    
    final String[] expected = {
            "INFO: window DOMContentLoaded 1 capture",
            "INFO: window DOMContentLoaded 2 capture",
            "INFO: document DOMContentLoaded 1",
            "INFO: document DOMContentLoaded 1 capture",
            "INFO: document DOMContentLoaded 2",
            "INFO: document DOMContentLoaded 2 capture",
            "INFO: window DOMContentLoaded 1",
            "INFO: window DOMContentLoaded 2",
            "INFO: window at load 1",
            "INFO: window at load 1 capture",
            "INFO: onload 2",
            "INFO: window at load 2",
            "INFO: window at load 2 capture",
            "INFO: after 2",
    };
    

    test_frame.html

    • Similar to above except in a frame.
    • Tests the load event of a <frame>.
    • Checks that the onload property of a <frameset> catches the load event for the window.
    wc.waitForBackgroundJavaScriptStartingBefore(1000);
    
    final String[] expected = {
            "INFO: framing window DOMContentLoaded 1 capture",
            "INFO: framing document DOMContentLoaded 1",
            "INFO: framing document DOMContentLoaded 1 capture",
            "INFO: framing window DOMContentLoaded 1",
            "INFO: window DOMContentLoaded 1 capture",
            "INFO: window DOMContentLoaded 2 capture",
            "INFO: document DOMContentLoaded 1",
            "INFO: document DOMContentLoaded 1 capture",
            "INFO: document DOMContentLoaded 2",
            "INFO: document DOMContentLoaded 2 capture",
            "INFO: window DOMContentLoaded 1",
            "INFO: window DOMContentLoaded 2",
            "INFO: window at load 1",
            "INFO: window at load 1 capture",
            "INFO: onload 2",
            "INFO: window at load 2",
            "INFO: window at load 2 capture",
            "INFO: framing document at load 1 capture",
            "INFO: frame onload",
            "INFO: framing window at load 1",
            "INFO: framing window at load 1 capture",
            "INFO: frameset onload",
            "INFO: after 2",
    };
    

    test_click.html

    • Tests propagation of a more or less basic event (click event) with regards to handling of the capturing / bubbling / at target phases.
    • Tests listener and property handler ordering.
    p.<HtmlButtonInput>getFirstByXPath("//input[@type='button' and @value='test']").click();
    
    final String[] expected = {
            "INFO: window at click 1 capture",
            "INFO: window at click 2 capture",
            "INFO: onclick 2",
            "INFO: i1 at click 1",
            "INFO: i1 at click 1 capture",
            "INFO: i1 at click 2",
            "INFO: i1 at click 2 capture",
            "INFO: window at click 1",
            "INFO: window at click 2",
    };
    

    test_nested_click.html

    • Similar as above except with a deeper propagation path.
    • Check bubbling propagation after modification of the DOM tree by an intermediate listener.
    p.<HtmlElement>getFirstByXPath("//div[@id='d3']").click();
    
    final String[] expected = {
            "INFO: d1 at click 1 capture",
            "INFO: d1 at click 2 capture",
            "INFO: d2 at click 1 capture",
            "INFO: d2 at click 2 capture",
            "INFO: d3 at click 1",
            "INFO: d3 onclick",
            "INFO: d3 at click 1 capture",
            "INFO: d3 at click 2",
            "INFO: d3 at click 2 capture",
            "INFO: d2 at click 1",
            "INFO: d2 onclick",
            "INFO: d2 at click 2",
            "INFO: d1 at click 1",
            "INFO: d1 onclick",
            "INFO: d1 at click 2",
    };
    

    test_event_return_value.html

    • This test determines that the return value of listeners are apparently ignored and only that of the property handler is used.
    p.<HtmlElement>getFirstByXPath("//a[@id='a1']").click();
    cc.append("--");
    p.<HtmlElement>getFirstByXPath("//a[@id='a2']").click();
    
    final String[] expected = {
            "INFO: listener: stop propagation & return false",
            "INFO: FIRED",
            "--",
            "INFO: listener: return true",
            "INFO: property: return false",
            "INFO: listener: return true",
    };
    
     
  • Atsushi Nakagawa

    Two extra tests are as follows:

    test_script_onload.html test_img_onload.html

    • Tests load and error events of <script> and <img>
    • Checks that they should be using EventTarget.fireEvent() rather than Event.executeEventLocally().
    wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
    wc.getPage(...);
    
    final String[] expected = {
            // FIXME: missing
            //"INFO: document at load capture",
            "INFO: element 1 onload",
            // FIXME: missing
            //"INFO: window at error capture",
            //"INFO: document at error capture",
            "INFO: element 2 onerror",
            "INFO: document DOMContentLoaded",
            "INFO: window DOMContentLoaded",
            "INFO: window at load",
            "INFO: window at load capture",
            "INFO: body onload",
    };
    
     
  • RBRi

    RBRi - 2018-08-14
    • status: open --> accepted
    • assigned_to: RBRi
     
  • RBRi

    RBRi - 2018-08-14

    Will work on this - looks like a great improvement

     
  • Atsushi Nakagawa

    I've made further fixes to EventTarget.java which I'm attaching here.

    Fixes:

    • Events should fire on elements even if they aren't attached. (Verified in Chrome/FF/IE11)
    • Refactor away the no longer necessary isAttached variable.

    I've changed test_nested_click.html (attached) to include a test for this. This obsoletes test_click.html because the tests now overlap.

    Notes on subtle changes

    • Import for Document has changed from org.w3c.dom.Document to com.gargoylesoftware.htmlunit.javascript.host.dom.Document.
    • BrowserVersionFeatures.JS_EVENT_WINDOW_EXECUTE_IF_DITACHED is no longer used because Chrome doesn't seem to behave that way.

    Test 1: Standard propagation

    p.<HtmlElement>getFirstByXPath("//div[@id='d3']").click();
    
    final String[] expected = {
            "INFO: window at click 1 capture",
            "INFO: window at click 2 capture",
            "INFO: d1 at click 1 capture",
            "INFO: d1 at click 2 capture",
            "INFO: d2 at click 1 capture",
            "INFO: d2 at click 2 capture",
            "INFO: d3 at click 1",
            "INFO: d3 onclick",
            "INFO: d3 at click 1 capture",
            "INFO: d3 at click 2",
            "INFO: d3 at click 2 capture",
            "INFO: d2 at click 1",
            "INFO: d2 onclick",
            "INFO: d2 at click 2",
            "INFO: d1 at click 1",
            "INFO: d1 onclick",
            "INFO: d1 at click 2",
            "INFO: window at click 1",
            "INFO: window at click 2",
    };
    

    Test 2: Detached propagation

    p.<HtmlElement>getFirstByXPath("//input[@id='detach_click']").click();
    
    final String[] expected = {
            "INFO: window at click 1 capture",
            "INFO: window at click 2 capture",
            "INFO: begin detach click",
            "INFO: d2 at click 1 capture",
            "INFO: d2 at click 2 capture",
            "INFO: d3 at click 1",
            "INFO: d3 onclick",
            "INFO: d3 at click 1 capture",
            "INFO: d3 at click 2",
            "INFO: d3 at click 2 capture",
            "INFO: d2 at click 1",
            "INFO: d2 onclick",
            "INFO: d2 at click 2",
            "INFO: end detach click",
            "INFO: window at click 1",
            "INFO: window at click 2",
    };
    
     
  • RBRi

    RBRi - 2018-08-15

    Have added all your cases as test cases to Window3Test; the expectations are the ones i got when runnung unsing the rela browsers (switching test.properties to real browsers).

    Have added all your patches -> please verigy the changes and the tests.

    Now the build is not happy, some other test cases are failing - any idea

     
    • Atsushi Nakagawa

      Woah, that was quick! I'm in the process of downloading trunk snapshot @ r15520 so I can try run the tests.

       
    • Atsushi Nakagawa

      (switching test.properties to real browsers)

      Btw, how did you do this? I'm doing it by placing a breakpoint after startWebServer() in loadPage2() and directing Chrome to http://localhost:12345. Is there a better way?

       
  • RBRi

    RBRi - 2018-08-15

    Or simply you can have a look at he build server https://ci.canoo.com/teamcity/project.html?projectId=HtmlUnit&branch_HtmlUnit=all_branches (Login as Guest)

     
    • Atsushi Nakagawa

      Oh that's handy..

      Okay, I figured out one problem. We had fix from a while back that I hadn't reported here.

      It's problem between <iframe>s and DOMContentLoaded but our test for it isn't very minimal (it imports jQuery). However, I think Window3Test.onloadFrame() now covers this so I'm pasting the fix only:

      --- a/HtmlPage.java-2.32.txt
      +++ b/HtmlPage.java
      @@ -244,6 +248,9 @@ public void initialize() throws IOException, FailingHttpStatusCodeException {
                       }
                   }
               }
      +
      
      +        executeEventHandlersIfNeeded(Event.TYPE_DOM_DOCUMENT_LOADED);
      +
               loadFrames();
      
               // don't set the ready state if we really load the blank page into the window
      @@ -256,7 +263,6 @@ public void initialize() throws IOException, FailingHttpStatusCodeException {
                   getDocumentElement().setReadyState(READY_STATE_COMPLETE);
               }
      
      
      -        executeEventHandlersIfNeeded(Event.TYPE_DOM_DOCUMENT_LOADED);
               executeDeferredScriptsIfNeeded();
               setReadyStateOnDeferredScriptsIfNeeded();
      
       
  • RBRi

    RBRi - 2018-08-16

    Have commited you last change also and made some imporvements on the tests. Looks a bit better now.
    But our test suite still reports many problems.

     
  • RBRi

    RBRi - 2018-08-16

    Will have a look at some of the test cases also, maybe i can migrate some to WebDriverTestCase and see if the expectations are still true.

    But there is this EventTest - At the moment i have no idea why real chrome is different is some tests from the other browsers and in some not. I can't see the difference in the test cases - any idea?

     
  • Atsushi Nakagawa

    I'm going through them one by one and here's what I've done so far:

    This fixes:

    • EventTest.iframeOnload
    • EventTest.thisInEventHandler
    • TypingTest.canSafelyTypeOnElementThatIsRemovedFromTheDomOnKeyPress

    But there is this EventTest - At the moment i have no idea why real chrome is different is some tests

    I thought the test was wrong because testing with Chrome against http://localhost:12345 didn't produce that result in CHROME.

    diff --git a/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTest.java b/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTest.java
    --- a/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTest.java
    +++ b/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTest.java
    @@ -392,8 +392,7 @@ public class EventTest extends WebDriverTestCase {
    
          * @throws Exception if the test fails
          */
         @Test
    -    @Alerts(DEFAULT = "frame1",
    -            CHROME = {})
    +    @Alerts(DEFAULT = "frame1")
         public void thisInEventHandler() throws Exception {
             final String html
                 = "<html><head></head>\n"
    @@ -413,8 +412,7 @@ public class EventTest extends WebDriverTestCase {
          * @throws Exception if the test fails
          */
         @Test
    -    @Alerts(DEFAULT = "called",
    -            CHROME = {})
    +    @Alerts(DEFAULT = "called")
         public void iframeOnload() throws Exception {
             final String html
                 = "<html><head>\n"
    diff --git a/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/selenium/TypingTest.java b/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/selenium/TypingTest.java
    --- a/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/selenium/TypingTest.java
    +++ b/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/selenium/TypingTest.java
    @@ -342,26 +342,36 @@ public class TypingTest extends SeleniumTest {
          * A test.
          */
         @Test
    -    @Alerts(DEFAULT = {"keydown (target) keyup (target) keyup (body)",
    -            "keydown (target) keyup (target) keyup (body) keydown (target) a pressed; removing"},
    -            CHROME = {"keydown (target) keyup (target) keyup (body)",
    -            "keydown (target) keyup (target) keyup (body) keydown (target) a pressed; removing keyup (body)"})
    +    @Alerts({"keydown (target) keydown (body) keyup (target) keyup (body)",
    +            "keydown (target) keydown (body) a pressed; removing keyup (target)"})
         public void canSafelyTypeOnElementThatIsRemovedFromTheDomOnKeyPress() {
             final WebDriver driver = getWebDriver("/key_tests/remove_on_keypress.html");
    
             final WebElement input = driver.findElement(By.id("target"));
             final WebElement log = driver.findElement(By.id("log"));
    
    +        final WebElement clear = driver.findElement(By.id("clear"));
    
             assertEquals("", log.getAttribute("value"));
    
             input.sendKeys("b");
             assertEquals(getExpectedAlerts()[0], getValueText(log).replace('\n', ' '));
    -
    
    +        clear.click();
    +
    +        // Note: Reproducing this in real browsers is not as simple as focusing the checkbox and
    +        // pressing 'a' on the keyboard.  Real keystrokes target the "focused element" so removing
    +        // "target" in the test's event handler causes a change in focus between "keypress" and
    +        // "keyup", with the latter being sent to the new holder.  In some browsers (which?), the
    +        // new holder is nowhere and "keyup (body)" is never seen, while in most, another child or
    +        // <body> itself is focused and "keyup (body)" is seen (Chrome, FF, IE11).
    +        //
    +        // Our code below is not concerned with the notion of "focus" and instead targets "target"
    +        // explicitly.  To test this in a real browser, the following JS may be used:
    +        //
    +        // var x = window.target
    +        // x.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 97, bubbles: true}))
    +        // x.dispatchEvent(new KeyboardEvent('keypress', {keyCode: 97, bubbles: true}))
    +        // x.dispatchEvent(new KeyboardEvent('keyup', {keyCode: 97, bubbles: true}))
             input.sendKeys("a");
    -
    -        // Some drivers (IE, Firefox) do not always generate the final keyup event since the element
    -        // is removed from the DOM in response to the keypress (note, this is a product of how events
    -        // are generated and does not match actual user behavior).
             assertEquals(getExpectedAlerts()[1], getValueText(log).replace('\n', ' '));
         }
    
    diff --git a/htmlunit/src/test/resources/selenium/key_tests/remove_on_keypress.html b/htmlunit/src/test/resources/selenium/key_tests/remove_on_keypress.html
    --- a/htmlunit/src/test/resources/selenium/key_tests/remove_on_keypress.html
    +++ b/htmlunit/src/test/resources/selenium/key_tests/remove_on_keypress.html
    @@ -14,9 +14,8 @@
         document.getElementById('log').value += msg + '\n';
       }
    
    
    -  document.body.onkeyup = function() {
    -    log('keyup (body)');
    -  };
    +  document.body.onkeydown = function() { log('keydown (body)'); };
    +  document.body.onkeyup = function() { log('keyup (body)'); };
    
       document.getElementById('clear').onclick = function() {
         document.getElementById('log').value = '';
    
     
  • RBRi

    RBRi - 2018-08-16

    Why you think we should change this?

    • @Alerts(DEFAULT = "frame1",
    • CHROME = {})
    • @Alerts(DEFAULT = "frame1")
      public void thisInEventHandler() throws Exception {

      When running these test with real browsers i got excactly the original results. Do you get really "frame1" with Chrome?

     
    • Atsushi Nakagawa

      Strange, I'm using Chrome 68.0.3440.106 and that's how it works here. (Image attached)

      However, I noticed if I save the HTML to a file and open that, I don't get "frame1".

      I think the difference is timing, and how long it takes Chrome to "load" about:blank. This is demonstated by putting an onload on the <iframe> like so:

      <html><head></head>
      <body>
      <button name='button1' id='button1' onclick='alert(this.name)'>1</button>
      <iframe src='about:blank' name='frame1' id='frame1' onload="alert('iframe loaded first')"></iframe>
      <script>
        var e = document.getElementById('frame1');
        e.onload = document.getElementById('button1').onclick;
      </script>
      </body></html>
      

      If you change about:blank to something like http://www.example.com to make it slower, I think you'll also get "frame1".

      Above all, think the purpose of EventTest.thisInEventHandler is to test this in an event, and in this regard, timing problem aside, I think HtmlUnit's behaviour can be considered "correct" for Chrome.

       
  • Atsushi Nakagawa

    @RBRi
    Here's another fix, this should fix bulk of the onbeforeunload problems:

    --- a/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
    +++ b/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
    @@ -140,7 +140,9 @@ public class EventTarget extends SimpleScriptable {
                 }
    
                 // The load event has some unnatural behaviour that we need to handle specially
    
    -            if (Event.TYPE_LOAD.equals(event.getType())) {
    +            if (Event.TYPE_LOAD.equals(event.getType())
    +                    || Event.TYPE_UNLOAD.equals(event.getType())
    +                    || Event.TYPE_BEFORE_UNLOAD.equals(event.getType())) {
    
                     // The Window load event targets Document but paths Window only (tested in Chrome/FF)
                     if (this instanceof Document) {
    
     
    • Atsushi Nakagawa

      Here's another fix, this should fix bulk of the onbeforeunload problems:

      I've changed my mind with regards to above, I think this is better:

      --- a/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java
      +++ b/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java
      @@ -1223,7 +1223,14 @@ public class HtmlPage extends SgmlPage {
                   if (LOG.isDebugEnabled()) {
                       LOG.debug("Firing " + event);
                   }
      
      -            final EventTarget jsNode = this.getScriptableObject();
      +            final EventTarget jsNode;
      +            if (Event.TYPE_DOM_DOCUMENT_LOADED.equals(eventType)) {
      +                jsNode = this.getScriptableObject();
      +            }
      +            else {
      +                // The load/beforeunload/unload events target Document but paths Window only (tested in Chrome/FF)
      +                jsNode = window.getScriptableObject();
      +            }
                   final ContextFactory cf = ((JavaScriptEngine) getWebClient().getJavaScriptEngine()).getContextFactory();
                   final ScriptResult result = cf.call(cx -> jsNode.fireEvent(event));
      
      --- a/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
      +++ b/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
      @@ -139,21 +139,12 @@ public class EventTarget extends SimpleScriptable {
                       propagationPath.add(parent.getScriptableObject());
                   }
      
      
      -            // The load event has some unnatural behaviour that we need to handle specially
      -            if (Event.TYPE_LOAD.equals(event.getType())
      -                    || Event.TYPE_UNLOAD.equals(event.getType())
      -                    || Event.TYPE_BEFORE_UNLOAD.equals(event.getType())) {
      -
      -                // The Window load event targets Document but paths Window only (tested in Chrome/FF)
      -                if (this instanceof Document) {
      -                    propagationPath.clear();
      -                    propagationPath.add(window);
      -                }
      -                else {
      -                    // The load event for other elements target that element and but path only
      -                    // up to Document and not Window, so do nothing here
      -                    // (see Note in https://www.w3.org/TR/DOM-Level-3-Events/#event-type-load)
      -                }
      +            // The load event for elements has some unnatural behaviour that we handle specially
      +            if (Event.TYPE_LOAD.equals(event.getType())) {
      +
      +                // The load event for elements target that element and but path only up to Document
      +                // and not Window, so do nothing here.  (This does not apply to beforeunload and unload)
      +                // (see Note in https://www.w3.org/TR/DOM-Level-3-Events/#event-type-load)
                   }
                   else {
                       // Add Window if the the propagation path reached Document
      
       
  • Atsushi Nakagawa

    @RBRi Thanks for apply the changes.

    Some insight:

    • onloadImg and onloadScript are NotYetImplemented as they require changes to HtmlImage and HtmlScript.

    • These lines in onload [IE] and onloadFrame[IE]are the result of IE11 firing load for <script>, even though they have no src. We can handle this with special branching when we get around to fixing HtmlScript.

    "document at load 1 capture",
    "document at load 2 capture",
    "document at load 1 capture",
    "document at load 2 capture",
    


    • stopPropagation [IE]: I'm working on a new test case and a fix for this currently.

    • propagationNestedDetached: This is working in my fork so I think there's another fix I have locally that I haven't reported here . I'll try pinpoint what it is.

     
  • Atsushi Nakagawa

    I'm attaching another patch (fixes.patch.txt) with the following changes:

    • Fixes stopPropagation [IE]
    • Add support for Chrome/Edge's Event.returnValue which is backed by Event.defaultPrevented.
    • Add better compatibility for BeforeUnloadEvent.returnValue in Chrome/FF and IE11/Edge.

    Subtle changes:

    • This stops using JS_CALL_RESULT_IS_LAST_RETURN_VALUE but I've left the code because deleting it creates too much diff in this patch.
    • This obsoletes EventTarget.fireEvent()'s return value.

    Test expects:

    test_event_return_priority1.html:

    p.<HtmlElement>getFirstByXPath("//a[@id='anchor']").click();
    
    final String[] expected = {
            "INFO: anchor onclick prevented=false",
            "INFO: document onclick prevented=false",
            "INFO: window onclick prevented=true",
    };
    

    test_event_return_priority2.html

    p.<HtmlElement>getFirstByXPath("//a[@id='anchor']").click();
    
    final String[] expected = {
            "INFO: window at beforeunload rv=", // Chrome/FF
            //"INFO: window at beforeunload rv=undefined", // IE/Edge
            "INFO: onbeforeunload rv=1",
            "INFO: window at beforeunload rv=1", // Chrome/FF
            //"INFO: window at beforeunload rv=2", // IE/Edge
    };
    
     
  • RBRi

    RBRi - 2018-08-17

    Tests added and your code is hopefully merged in. But again more tests failing - have i done it wrong?

     
    • Atsushi Nakagawa

      But again more tests failing

      My bad, this serves me right for not refactoring.

      Fixes

      1. This one's a straight out bug

      Fixes: BeforeUnloadEvent2Test.returnString

      --- a/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/BeforeUnloadEvent.java
      +++ b/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/BeforeUnloadEvent.java
      @@ -123,7 +123,7 @@ public class BeforeUnloadEvent extends Event {
      
               if (!Undefined.isUndefined(returnValue) && (returnValue != null || browserVersion.isIE())) {
                   if (!browserVersion.hasFeature(EVENT_BEFORE_UNLOAD_USES_HANDLER_RETURN_ONLY_IF_FIRST)
      
      -                    || !getReturnValueDefault(browserVersion).equals(getReturnValue())) {
      +                    || getReturnValueDefault(browserVersion).equals(getReturnValue())) {
                       setReturnValue(returnValue);
                   }
               }
      
      2. We really should get rid of EventTarget.fireEvent()'s return value because it's ambiguous, but it appears to be public API so I wasn't sure if we can... Anyhow, here's a stop-gap fix that prevents us from doing wholesale refactoring.

      Fixes: HtmlRadioButtonInputTest.setChecked

      --- a/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
      +++ b/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
      @@ -113,6 +113,16 @@ public class EventTarget extends SimpleScriptable {
      
            * @return the result
            */
           public ScriptResult fireEvent(final Event event) {
      +        fireEventImpl(event);
      +        // This is deprecated but there're still a few places using ScriptResult.getNewPage()
      +        return new ScriptResult(null, getWindow().getWebWindow().getWebClient().getCurrentWindow().getEnclosedPage());
      +    }
      +
      +    /**
      +     * Fires the event on the node with capturing and bubbling phase.
      +     * @param event the event
      +     */
      +    private void fireEventImpl(final Event event) {
               final Window window = getWindow();
               final Object[] args = new Object[] {event};
      
      @@ -165,7 +175,7 @@ public class EventTarget extends SimpleScriptable {
                           final ScriptResult r = elc.executeCapturingListeners(event, args);
                           result = ScriptResult.combine(r, result, ie);
                           if (event.isPropagationStopped()) {
      
      -                        return result;
      +                        return;
                           }
                       }
                   }
      @@ -182,7 +192,7 @@ public class EventTarget extends SimpleScriptable {
                           final ScriptResult r = elc.executeAtTargetListeners(event, args);
                           result = ScriptResult.combine(r, result, ie);
                           if (event.isPropagationStopped()) {
      -                        return result;
      +                        return;
                           }
                       }
                   }
      @@ -211,7 +221,7 @@ public class EventTarget extends SimpleScriptable {
                               final ScriptResult r = elc.executeBubblingListeners(event, args);
                               result = ScriptResult.combine(r, result, ie);
                               if (event.isPropagationStopped()) {
      -                            return result;
      +                            return;
                               }
                           }
                       }
      @@ -234,8 +244,6 @@ public class EventTarget extends SimpleScriptable {
                   event.endFire();
                   window.setCurrentEvent(previousEvent); // reset event
               }
      -
      -        return result;
           }
      
           /**
      
      3. There's a slight problem with r15529

      You can't get rid of Event.getReturnValue() / Event.setReturnValue() because it's required by Event.returnValue. (You probably saw that there was no @JsxGetter / @JsxSetter but it's done by reflection in Event.initEvent())

      I like the change to HtmlPage.java but I think the change to Event.java should be reverted. I need to give you my test for Event.returnValue.

      Fixes: Event2Test.returnPriority

      4. Node2Test.eventListener_return_false [IE] is slightly stale (probably pre-IE11) so here's a fix
      --- a/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/dom/Node2Test.java
      +++ b/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/dom/Node2Test.java
      @@ -119,17 +119,9 @@ public class Node2Test extends SimpleWebTestCase {
               final List<String> collectedAlerts = new ArrayList<>();
               final HtmlPage page = loadPage(html, collectedAlerts);
               final HtmlPage page2 = page.getHtmlElementById("myAnchor").click();
      
      -        //IE doesn't have specific order
      -        Collections.sort(collectedAlerts);
               assertEquals(getExpectedAlerts(), collectedAlerts);
      
      
      -        final URL expectedURL;
      -        if (getBrowserVersion().isIE()) {
      -            expectedURL = URL_FIRST;
      -        }
      -        else {
      -            expectedURL = URL_SECOND;
      -        }
      +        final URL expectedURL = URL_SECOND;
               assertEquals(expectedURL.toExternalForm(), page2.getUrl().toExternalForm());
           }
      
       
  • Atsushi Nakagawa

    1. There's a slight problem with r15529

    Oh, I see now why you got rid of Event.getReturnValue() / Event.setReturnValue().

    I'd intended it to be used but I hadn't tested it.

    Here's now a test for this (because it's used for Chrome/Edge) and some fixes required to make it work. I'm also attaching the same test as an html file incase you want to try it in a browser.

    --- a/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/Event.java
    +++ b/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/Event.java
    @@ -14,7 +14,6 @@
      */
     package com.gargoylesoftware.htmlunit.javascript.host.event;
    
    -import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_FOCUS_FOCUS_IN_BLUR_OUT;
     import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONLOAD_CANCELABLE_FALSE;
     import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_RETURN_VALUE_IS_PREVENT_DEFAULT;
     import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
    @@ -22,7 +21,6 @@ import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBr
     import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
     import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
    
    -import java.lang.reflect.Method;
     import java.util.LinkedList;
    
     import com.gargoylesoftware.htmlunit.ScriptResult;
    @@ -170,8 +168,6 @@ public class Event extends SimpleScriptable {
         @JsxConstant(FF)
         public static final int META_MASK = 0x8;
    
    
    -    private Boolean returnValueIsPreventDefault_;
    -
         private Object srcElement_;        // IE-only writable equivalent of target.
         private EventTarget target_;       // W3C standard read-only equivalent of srcElement.
         private Scriptable currentTarget_; // Changes during event capturing and bubbling.
    @@ -183,9 +179,15 @@ public class Event extends SimpleScriptable {
         private String propertyName_;
         private boolean stopPropagation_;
         private boolean stopImmediatePropagation_;
    -    private Object returnValue_;
         private boolean preventDefault_;
    
    
    +    /**
    +     * In some browsers, event.returnValue is mapped to !preventDefault_
    +     * while in others it's a discreet value.
    +     */
    +    private Boolean returnValueIsPreventDefault_;
    +    private Object returnValue_ = Undefined.instance;
    +
         /**
          * The current event phase. This is a W3C standard attribute. One of {@link #NONE},
          * {@link #CAPTURING_PHASE}, {@link #AT_TARGET} or {@link #BUBBLING_PHASE}.
    @@ -530,7 +532,7 @@ public class Event extends SimpleScriptable {
          * called for this event. Otherwise this attribute must return {@code false}.
          * @return {@code true} if this event has been cancelled or not
          */
    -    @JsxGetter({FF, IE, EDGE})
    +    @JsxGetter
         public boolean isDefaultPrevented() {
             return cancelable_ && preventDefault_;
         }
    @@ -598,6 +600,7 @@ public class Event extends SimpleScriptable {
          * Returns the return value associated with the event.
          * @return the return value associated with the event
          */
    +    @JsxGetter
         public Object getReturnValue() {
             if (isReturnValueBackedByPreventDefault()) {
                 return !preventDefault_;
    @@ -609,6 +612,7 @@ public class Event extends SimpleScriptable {
          * Sets the return value associated with the event.
          * @param returnValue the return value associated with the event
          */
    +    @JsxSetter
         public void setReturnValue(final Object returnValue) {
             if (isReturnValueBackedByPreventDefault()) {
                 preventDefault_ = !ScriptRuntime.toBoolean(returnValue);
    @@ -647,17 +651,6 @@ public class Event extends SimpleScriptable {
             type_ = type;
             bubbles_ = bubbles;
             cancelable_ = cancelable;
    -        if (TYPE_BEFORE_UNLOAD.equals(type) && getBrowserVersion().hasFeature(EVENT_FOCUS_FOCUS_IN_BLUR_OUT)) {
    -            try {
    -                final Class<?> klass = getClass();
    -                final Method readMethod = klass.getMethod("getReturnValue");
    -                final Method writeMethod = klass.getMethod("setReturnValue", Object.class);
    -                defineProperty("returnValue", null, readMethod, writeMethod, ScriptableObject.EMPTY);
    -            }
    -            catch (final Exception e) {
    -                throw Context.throwAsScriptRuntimeEx(e);
    -            }
    -        }
         }
    
         /**
    --- a/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java
    +++ b/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java
    @@ -2190,11 +2190,28 @@ public class Window3Test extends WebDriverTestCase {
    
          * @throws Exception if the test fails
          */
         @Test
    -    @Alerts({"listener: stop propagation & return false",
    +    @Alerts(DEFAULT = {"listener: stop propagation & return false",
                     "FIRED a1",
                     "listener: return true",
                     "property: return false",
    -                "listener: return true"})
    +                "listener: return true",
    +                "listener: prevented=false returnValue: undefined -> false (false)",
    +                "listener: prevented=false returnValue: false -> true (true)",
    +                "listener: prevented=false returnValue: true -> preventDefault() (true)",
    +                "property: prevented=true returnValue: true -> return true",
    +                "listener: prevented=true returnValue: true -> x (x)",
    +                "listener: prevented=true returnValue: x -> null (null)"},
    +            CHROME = {"listener: stop propagation & return false",
    +                "FIRED a1",
    +                "listener: return true",
    +                "property: return false",
    +                "listener: return true",
    +                "listener: prevented=false returnValue: true -> false (false)",
    +                "listener: prevented=true returnValue: false -> true (true)",
    +                "listener: prevented=false returnValue: true -> preventDefault() (false)",
    +                "property: prevented=true returnValue: false -> return true",
    +                "listener: prevented=true returnValue: false -> x (true)",
    +                "listener: prevented=false returnValue: true -> null (false)"})
         public void stopPropagation() throws Exception {
             final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
                 + "<html><head>\n"
    @@ -2207,6 +2224,7 @@ public class Window3Test extends WebDriverTestCase {
                 + "<body>\n"
                 + "  <div><a id='a1' href='javascript:log(\"FIRED a1\")'>test: listener return false</a></div>\n"
                 + "  <div><a id='a2' href='javascript:log(\"FIRED a2\")'>test: property return false</a></div>\n"
    +            + "  <div><a id='a3' href='javascript:log(\"FIRED a3\")'>test: listener returnValue = false</a></div>\n"
    
    
                 + "  <textarea id='log' rows=40 cols=80></textarea>\n"
    
    @@ -2231,12 +2249,42 @@ public class Window3Test extends WebDriverTestCase {
                  //window.addEventListener("click", function (event) {
                  //                  log('window: stop propagation & return false');
                  //                  event.stopPropagation(); return false }, true)
    +
    
    +            // In Chrome/Edge, this sets event.returnValue to 'false' which is synonymous with setting 'event.defaultPrevented'
    +            // In FF/IE11, event.returnValue is settable but does not appear to be used for anything
    +            + "  a3.addEventListener('click', function (event) {"
    +            + "      var a = event.returnValue, p = event.defaultPrevented, b = false; event.returnValue = b;"
    +            + "      log('listener: prevented=' + p + ' returnValue: ' + a + ' -> ' + b + ' (' + event.returnValue + ')') })\n"
    +            // This shows it's possible to set event.returnValue back to 'true' from 'false'
    +            + "  a3.addEventListener('click', function (event) {"
    +            + "      var a = event.returnValue, p = event.defaultPrevented, b = true; event.returnValue = b;"
    +            + "      log('listener: prevented=' + p + ' returnValue: ' + a + ' -> ' + b + ' (' + event.returnValue + ')') })\n"
    +            // The value of event.returnValue is consitent across multiple listener calls of the same event
    +            + "  a3.addEventListener('click', function (event) {"
    +            + "      var a = event.returnValue, p = event.defaultPrevented, b = 'preventDefault()'; event.preventDefault();"
    +            + "      log('listener: prevented=' + p + ' returnValue: ' + a + ' -> ' + b + ' (' + event.returnValue + ')') })\n"
    +            // This shows a property handler returning 'true' will not change event.returnValue if it's already 'false'
    +            + "  a3.onclick = function (event) {"
    +            + "      var a = event.returnValue, p = event.defaultPrevented; b = true;"
    +            + "      log('property: prevented=' + p + ' returnValue: ' + a + ' -> return ' + b); return b }\n"
    +            // Instead of returning 'true', the property handler can directly set event.returnValue to set it to 'true' from 'false'
    +            //+ "  a3.onclick = function (event) {"
    +            //+ "      var a = event.returnValue, p = event.defaultPrevented; b = true;"
    +            //+ "      log('property: prevented=' + p + ' returnValue: ' + a + ' -> true'); event.returnValue = b }\n"
    +            // These shows setting event.returnValue cannot be set to a non-boolean value in Chrome/Edge but can in (FF/IE11)
    +            + "  a3.addEventListener('click', function (event) {"
    +            + "        var a = event.returnValue, p = event.defaultPrevented, b = 'x'; event.returnValue = b;"
    +            + "        log('listener: prevented=' + p + ' returnValue: ' + a + ' -> ' + b + ' (' + event.returnValue + ')') })\n"
    +            + "  a3.addEventListener('click', function (event) {"
    +            + "        var a = event.returnValue, p = event.defaultPrevented, b = null; event.returnValue = b;"
    +            + "        log('listener: prevented=' + p + ' returnValue: ' + a + ' -> ' + b + ' (' + event.returnValue + ')') })\n"
                 + "</script>\n"
                 + "</body></html>";
    
             final WebDriver driver = loadPage2(html);
             driver.findElement(By.id("a1")).click();
             driver.findElement(By.id("a2")).click();
    
    +        driver.findElement(By.id("a3")).click();
    
             final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", "");
             assertEquals(String.join("\n", getExpectedAlerts()), text);
    
     
  • RBRi

    RBRi - 2018-08-20

    Have to work on this later...
    Btw. working on the migration of HtmlUnit code to github, hope this will make it simpler for you and all the other to provide patches. But this requires time.

     
  • Atsushi Nakagawa

    working on the migration of HtmlUnit code to github

    Yes!!!

    That should definitely lower the hurdle of contributors.

    Here's some more fixes:

    There was a very subtle diff missing: (Fixes Window3Test.propagationNestedDetached)

    --- a/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java
    +++ b/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java
    @@ -2163,7 +2163,7 @@ public class Window3Test extends WebDriverTestCase {
    
                 + "  d1.addEventListener('click', function () { log('d1 at click 2 capture') }, true)\n"
    
    
                 + "  d2.addEventListener('click', function () { log('d2 at click 1') })\n"
    -            + "  d2.onclick = function () { log('d2 onclick'); d2.parentNode.removeChild(d2) }\n"
    +            + "  d2.onclick = function () { log('d2 onclick'); if (d2.parentNode) d2.parentNode.removeChild(d2) }\n"
                 + "  d2.addEventListener('click', function () { log('d2 at click 1 capture') }, true)\n"
                 + "  d2.addEventListener('click', function () { log('d2 at click 2') })\n"
                 + "  d2.addEventListener('click', function () { log('d2 at click 2 capture') }, true)\n"
    

    Regarding Event.returnValue

    I'm on the fence about my previous comment regarding Event.getReturnValue() / Event.setReturnValue() (sourceforge.net).

    On one hand, applying that diff gives us a more or less faithful implementation of what Chrome/Edge does with Event.returnValue.

    On the other, EVENT_RETURN_VALUE_IS_PREVENT_DEFAULT is hacky and has a poor feature advantage, especially since Event.returnValue is non-standard and developers are recommeneded to use Event.preventDefault() anyhow (developer.mozilla.org).

    I'm fine with just going with what you did in r15529 and removing EVENT_RETURN_VALUE_IS_PREVENT_DEFAULT altogether. In that case, these changes are still required:

    --- a/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/Event.java
    +++ b/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/Event.java
    @@ -14,7 +14,6 @@
      */
     package com.gargoylesoftware.htmlunit.javascript.host.event;
    
    -import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_FOCUS_FOCUS_IN_BLUR_OUT;
     import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONLOAD_CANCELABLE_FALSE;
     import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_RETURN_VALUE_IS_PREVENT_DEFAULT;
     import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
    @@ -22,7 +21,6 @@ import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBr
     import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
     import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
    
    -import java.lang.reflect.Method;
     import java.util.LinkedList;
    
     import com.gargoylesoftware.htmlunit.ScriptResult;
    @@ -530,7 +532,7 @@ public class Event extends SimpleScriptable {
    
          * called for this event. Otherwise this attribute must return {@code false}.
          * @return {@code true} if this event has been cancelled or not
          */
    -    @JsxGetter({FF, IE, EDGE})
    +    @JsxGetter
         public boolean isDefaultPrevented() {
             return cancelable_ && preventDefault_;
         }
    @@ -647,17 +651,6 @@ public class Event extends SimpleScriptable {
             type_ = type;
             bubbles_ = bubbles;
             cancelable_ = cancelable;
    -        if (TYPE_BEFORE_UNLOAD.equals(type) && getBrowserVersion().hasFeature(EVENT_FOCUS_FOCUS_IN_BLUR_OUT)) {
    -            try {
    -                final Class<?> klass = getClass();
    -                final Method readMethod = klass.getMethod("getReturnValue");
    -                final Method writeMethod = klass.getMethod("setReturnValue", Object.class);
    -                defineProperty("returnValue", null, readMethod, writeMethod, ScriptableObject.EMPTY);
    -            }
    -            catch (final Exception e) {
    -                throw Context.throwAsScriptRuntimeEx(e);
    -            }
    -        }
         }
    
         /**
    
     

    Last edit: Atsushi Nakagawa 2018-08-20
  • Atsushi Nakagawa

    I like what you did in r15531-r15535. It simplifies my hack alot.

    Just once thing I noticied, I thought it should be like this:

    Because calling event.preventDefault() changes the value of event.returnValue. (The failing test in Window3Test.stopPropagation [Chrome] tests for this.)

        /**
    
         * @return the return value property
         */
        @JsxGetter(CHROME})
        public Object getReturnValue() {
            return !preventDefault_;
        }
    
        /**
    
         * @param newValue the new return value
         */
        @JsxSetter(CHROME)
        public void setReturnValue(final Object newValue) {
            preventDefault_ = !ScriptRuntime.toBoolean(newValue);
        }
    

    FWIW, this also applies to Edge.

     
1 2 > >> (Page 1 of 2)

Log in to post a comment.