[HtmlUnit] SVN: [15517] trunk/htmlunit/src

rbri--- via HtmlUnit-develop <[email protected]> Tue, 14 Aug 2018 19:17:48 +0000
Newsgroups gmane.comp.java.htmlunit.devel
Message-ID <[email protected]>
Revision: 15517
          http://sourceforge.net/p/htmlunit/code/15517
Author:   rbri
Date:     2018-08-14 19:17:44 +0000 (Tue, 14 Aug 2018)
Log Message:
-----------
give this fix a try (wip)

Modified Paths:
--------------
    trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java
    trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventListenersContainer.java
    trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
    trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java

Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java
===================================================================
--- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java	2018-08-12 12:32:23 UTC (rev 15516)
+++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/html/HtmlPage.java	2018-08-14 19:17:44 UTC (rev 15517)
@@ -85,9 +85,9 @@
 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;
@@ -94,6 +94,7 @@
 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;
@@ -1206,18 +1207,23 @@
         // 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;
-            }
             final Event event;
             if (eventType.equals(Event.TYPE_BEFORE_UNLOAD)) {
-                event = new BeforeUnloadEvent(element, eventType);
+                event = new BeforeUnloadEvent(this, eventType);
             }
             else {
-                event = new Event(element, eventType);
+                event = new Event(this, eventType);
             }
-            final ScriptResult result = element.fireEvent(event);
+
+            // 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 EventTarget jsNode = this.getScriptableObject();
+            final ContextFactory cf = ((JavaScriptEngine) getWebClient().getJavaScriptEngine()).getContextFactory();
+            final ScriptResult result = cf.call(cx -> jsNode.fireEvent(event));
+
             if (!isOnbeforeunloadAccepted(this, event, result)) {
                 return false;
             }
@@ -1245,7 +1251,12 @@
                     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;
                     }

Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventListenersContainer.java
===================================================================
--- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventListenersContainer.java	2018-08-12 12:32:23 UTC (rev 15516)
+++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventListenersContainer.java	2018-08-14 19:17:44 UTC (rev 15517)
@@ -19,10 +19,10 @@
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -29,7 +29,6 @@
 
 import com.gargoylesoftware.htmlunit.ScriptResult;
 import com.gargoylesoftware.htmlunit.html.DomNode;
-import com.gargoylesoftware.htmlunit.html.HtmlBody;
 import com.gargoylesoftware.htmlunit.html.HtmlPage;
 import com.gargoylesoftware.htmlunit.javascript.host.Window;
 import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
@@ -54,35 +53,81 @@
 
     private static final Log LOG = LogFactory.getLog(EventListenersContainer.class);
 
-    static class TypeContainer implements Serializable {
-        private List<Scriptable> capturingListeners_;
-        private List<Scriptable> bubblingListeners_;
-        private Function handler_;
+    private static class TypeContainer implements Serializable {
+        public static final TypeContainer EMPTY = new TypeContainer();
 
+        // This sentinel value could be some singleton instance but null
+        // isn't used for anything else so why not.
+        private static final Scriptable EVENT_HANDLER_PLACEHOLDER = null;
+
+        private final List<Scriptable> capturingListeners_;
+        private final List<Scriptable> bubblingListeners_;
+        private final List<Scriptable> atTargetListeners_;
+        private final Function handler_;
+
         TypeContainer() {
-            capturingListeners_ = Collections.unmodifiableList(new ArrayList<Scriptable>());
-            bubblingListeners_ = Collections.unmodifiableList(new ArrayList<Scriptable>());
+            capturingListeners_ = Collections.emptyList();
+            bubblingListeners_ = Collections.emptyList();
+            atTargetListeners_ = Collections.emptyList();
+            handler_ = null;
         }
 
         private TypeContainer(final List<Scriptable> capturingListeners,
-                    final List<Scriptable> bubblingListeners, final Function handler) {
-            capturingListeners_ = Collections.unmodifiableList(new ArrayList<>(capturingListeners));
-            bubblingListeners_ = Collections.unmodifiableList(new ArrayList<>(bubblingListeners));
+                    final List<Scriptable> bubblingListeners, final List<Scriptable> atTargetListeners,
+                    final Function handler) {
+            capturingListeners_ = capturingListeners;
+            bubblingListeners_ = bubblingListeners;
+            atTargetListeners_ = atTargetListeners;
             handler_ = handler;
         }
 
-        private List<Scriptable> getListeners(final boolean useCapture) {
-            if (useCapture) {
-                return capturingListeners_;
+        private List<Scriptable> getListeners(final int eventPhase) {
+            switch (eventPhase) {
+                case Event.CAPTURING_PHASE:
+                    return capturingListeners_;
+                case Event.AT_TARGET:
+                    return atTargetListeners_;
+                case Event.BUBBLING_PHASE:
+                    return bubblingListeners_;
+                default:
+                    throw new UnsupportedOperationException("eventPhase: " + eventPhase);
             }
-            return bubblingListeners_;
         }
 
-        private synchronized boolean addListener(final Scriptable listener, final boolean useCapture) {
-            final List<Scriptable> listeners = getListeners(useCapture);
+        public TypeContainer setPropertyHandler(final Function propertyHandler) {
+            if (propertyHandler != null) {
+                // If we already have a handler then the position of the existing
+                // placeholder should not be changed so just change the handler
+                if (handler_ != null) {
+                    if (propertyHandler == handler_) {
+                        return this;
+                    }
+                    return withPropertyHandler(propertyHandler);
+                }
 
+                // Insert the placeholder and set the handler
+                return withPropertyHandler(propertyHandler).addListener(EVENT_HANDLER_PLACEHOLDER, false);
+            }
+            else {
+                if (handler_ == null) {
+                    return this;
+                }
+                return removeListener(EVENT_HANDLER_PLACEHOLDER, false).withPropertyHandler(null);
+            }
+        }
+
+        private TypeContainer withPropertyHandler(final Function propertyHandler) {
+            return new TypeContainer(capturingListeners_, bubblingListeners_, atTargetListeners_, propertyHandler);
+        }
+
+        public TypeContainer addListener(final Scriptable listener, final boolean useCapture) {
+
+            List<Scriptable> capturingListeners = capturingListeners_;
+            List<Scriptable> bubblingListeners = bubblingListeners_;
+            final List<Scriptable> listeners = useCapture ? capturingListeners : bubblingListeners;
+
             if (listeners.contains(listener)) {
-                return false;
+                return this;
             }
 
             List<Scriptable> newListeners = new ArrayList<>(listeners.size() + 1);
@@ -91,21 +136,29 @@
             newListeners = Collections.unmodifiableList(newListeners);
 
             if (useCapture) {
-                capturingListeners_ = newListeners;
+                capturingListeners = newListeners;
             }
             else {
-                bubblingListeners_ = newListeners;
+                bubblingListeners = newListeners;
             }
 
-            return true;
+            List<Scriptable> atTargetListeners = new ArrayList<>(atTargetListeners_.size() + 1);
+            atTargetListeners.addAll(atTargetListeners_);
+            atTargetListeners.add(listener);
+            atTargetListeners = Collections.unmodifiableList(atTargetListeners);
+
+            return new TypeContainer(capturingListeners, bubblingListeners, atTargetListeners, handler_);
         }
 
-        private synchronized void removeListener(final Scriptable listener, final boolean useCapture) {
-            final List<Scriptable> listeners = getListeners(useCapture);
+        public TypeContainer removeListener(final Scriptable listener, final boolean useCapture) {
 
+            List<Scriptable> capturingListeners = capturingListeners_;
+            List<Scriptable> bubblingListeners = bubblingListeners_;
+            final List<Scriptable> listeners = useCapture ? capturingListeners : bubblingListeners;
+
             final int idx = listeners.indexOf(listener);
             if (idx < 0) {
-                return;
+                return this;
             }
 
             List<Scriptable> newListeners = new ArrayList<>(listeners);
@@ -113,20 +166,32 @@
             newListeners = Collections.unmodifiableList(newListeners);
 
             if (useCapture) {
-                capturingListeners_ = newListeners;
+                capturingListeners = newListeners;
             }
             else {
-                bubblingListeners_ = newListeners;
+                bubblingListeners = newListeners;
             }
+
+            List<Scriptable> atTargetListeners = new ArrayList<>(atTargetListeners_);
+            atTargetListeners.remove(listener);
+            atTargetListeners = Collections.unmodifiableList(atTargetListeners);
+
+            return new TypeContainer(capturingListeners, bubblingListeners, atTargetListeners, handler_);
         }
 
+        // Refactoring note: This method doesn't appear to be used
         @Override
         protected TypeContainer clone() {
-            return new TypeContainer(capturingListeners_, bubblingListeners_, handler_);
+            return new TypeContainer(capturingListeners_, bubblingListeners_, atTargetListeners_, handler_);
         }
     }
 
-    private final Map<String, TypeContainer> typeContainers_ = new HashMap<>();
+    // Refactoring note: This seems ad-hoc..  Shouldn't synchronization be orchestrated between
+    // JS thread and main thread at a much higher layer?  Anyways, to preserve behaviour of prior
+    // coding where 'synchronized' was used more explicitly, we're using a ConcurrentHashMap here
+    // and using ConcurrentMap.compute() to mutate below so that mutations are atomic.  This for
+    // example avoids the case where two concurrent addListener()s can result in either being lost.
+    private final ConcurrentMap<String, TypeContainer> typeContainers_ = new ConcurrentHashMap<>();
     private final EventTarget jsNode_;
 
     /**
@@ -151,9 +216,17 @@
             return true;
         }
 
-        final TypeContainer container = getTypeContainer(type);
-        final boolean added = container.addListener(listener, useCapture);
-        if (!added) {
+        final boolean[] added = {false};
+        typeContainers_.compute(type.toLowerCase(Locale.ROOT), (k, container) -> {
+            if (container == null) {
+                container = TypeContainer.EMPTY;
+            }
+            final TypeContainer newContainer = container.addListener(listener, useCapture);
+            added[0] = newContainer != container;
+            return newContainer;
+        });
+
+        if (!added[0]) {
             if (LOG.isDebugEnabled()) {
                 LOG.debug(type + " listener already registered, skipping it (" + listener + ")");
             }
@@ -164,7 +237,7 @@
 
     private TypeContainer getTypeContainer(final String type) {
         final String typeLC = type.toLowerCase(Locale.ROOT);
-        return typeContainers_.computeIfAbsent(typeLC, k -> new TypeContainer());
+        return typeContainers_.getOrDefault(typeLC, TypeContainer.EMPTY);
     }
 
     /**
@@ -172,14 +245,10 @@
      *
      * @param eventType the event type
      * @param useCapture whether to use capture of not
-     * @return the listeners list
+     * @return the listeners list (empty list when empty)
      */
     public List<Scriptable> getListeners(final String eventType, final boolean useCapture) {
-        final TypeContainer container = typeContainers_.get(eventType.toLowerCase(Locale.ROOT));
-        if (container != null) {
-            return container.getListeners(useCapture);
-        }
-        return null;
+        return getTypeContainer(eventType).getListeners(useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE);
     }
 
     /**
@@ -194,10 +263,8 @@
             return;
         }
 
-        final TypeContainer container = typeContainers_.get(eventType.toLowerCase(Locale.ROOT));
-        if (container != null) {
-            container.removeListener(listener, useCapture);
-        }
+        typeContainers_.computeIfPresent(eventType.toLowerCase(Locale.ROOT),
+            (k, container) -> container.removeListener(listener, useCapture));
     }
 
     /**
@@ -216,11 +283,15 @@
             handler = (Function) value;
         }
 
-        final TypeContainer container = getTypeContainer(eventType);
-        container.handler_ = handler;
+        typeContainers_.compute(eventType.toLowerCase(Locale.ROOT), (k, container) -> {
+            if (container == null) {
+                container = TypeContainer.EMPTY;
+            }
+            return container.setPropertyHandler(handler);
+        });
     }
 
-    private ScriptResult executeEventListeners(final boolean useCapture, final Event event, final Object[] args) {
+    private ScriptResult executeEventListeners(final int eventPhase, final Event event, final Object[] args) {
         final DomNode node = jsNode_.getDomNodeOrNull();
         // some event don't apply on all kind of nodes, for instance "blur"
         if (node != null && !node.handles(event)) {
@@ -228,8 +299,9 @@
         }
 
         ScriptResult allResult = null;
-        final List<Scriptable> listeners = getListeners(event.getType(), useCapture);
-        if (listeners != null && !listeners.isEmpty()) {
+        final TypeContainer container = getTypeContainer(event.getType());
+        final List<Scriptable> listeners = container.getListeners(eventPhase);
+        if (!listeners.isEmpty()) {
             event.setCurrentTarget(jsNode_);
 
             final HtmlPage page;
@@ -250,7 +322,12 @@
             }
 
             // no need for a copy, listeners are copy on write
-            for (final Scriptable listener : listeners) {
+            for (Scriptable listener : listeners) {
+                boolean isPropertyHandler = false;
+                if (listener == TypeContainer.EVENT_HANDLER_PLACEHOLDER) {
+                    listener = container.handler_;
+                    isPropertyHandler = true;
+                }
                 Function function = null;
                 Scriptable thisObject = null;
                 if (listener instanceof Function) {
@@ -267,7 +344,8 @@
                 if (function != null) {
                     final ScriptResult result =
                             page.executeJavaScriptFunction(function, thisObject, args, node);
-                    if (event.isPropagationStopped()) {
+                    // Return value is only honoured for property handlers (Chrome/FF)
+                    if (isPropertyHandler) {
                         allResult = result;
                     }
                     if (jsNode_.getBrowserVersion().hasFeature(EVENT_FALSE_RESULT)) {
@@ -290,53 +368,14 @@
         return allResult;
     }
 
-    private ScriptResult executeEventHandler(final Event event, final Object[] propHandlerArgs) {
-        final DomNode node = jsNode_.getDomNodeOrNull();
-        // some event don't apply on all kind of nodes, for instance "blur"
-        if (node != null && !node.handles(event)) {
-            return null;
-        }
-        final Function handler = getEventHandler(event.getType());
-        if (handler != null) {
-            event.setCurrentTarget(jsNode_);
-            final HtmlPage page = (HtmlPage) (node != null
-                    ? node.getPage()
-                    : jsNode_.getWindow().getWebWindow().getEnclosedPage());
-            if (LOG.isDebugEnabled()) {
-                LOG.debug("Executing " + event.getType() + " handler for " + node);
-            }
-            return page.executeJavaScriptFunction(handler, jsNode_,
-                    propHandlerArgs, page);
-        }
-        return null;
-    }
-
     /**
      * Executes bubbling listeners.
      * @param event the event
      * @param args arguments
-     * @param propHandlerArgs handler arguments
      * @return the result
      */
-    public ScriptResult executeBubblingListeners(final Event event, final Object[] args,
-            final Object[] propHandlerArgs) {
-        ScriptResult result = null;
-
-        // the handler declared as property if any (not on body, as handler declared on body goes to the window)
-        final DomNode domNode = jsNode_.getDomNodeOrNull();
-        if (!(domNode instanceof HtmlBody)) {
-            result = executeEventHandler(event, propHandlerArgs);
-            if (event.isPropagationStopped()) {
-                return result;
-            }
-        }
-
-        // the registered listeners (if any)
-        final ScriptResult newResult = executeEventListeners(false, event, args);
-        if (newResult != null) {
-            result = newResult;
-        }
-        return result;
+    public ScriptResult executeBubblingListeners(final Event event, final Object[] args) {
+        return executeEventListeners(Event.BUBBLING_PHASE, event, args);
     }
 
     /**
@@ -346,20 +385,26 @@
      * @return the result
      */
     public ScriptResult executeCapturingListeners(final Event event, final Object[] args) {
-        return executeEventListeners(true, event, args);
+        return executeEventListeners(Event.CAPTURING_PHASE, event, args);
     }
 
     /**
+     * Executes listeners for events targeting the node. (non-propagation phase)
+     * @param event the event
+     * @param args the arguments
+     * @return the result
+     */
+    public ScriptResult executeAtTargetListeners(final Event event, final Object[] args) {
+        return executeEventListeners(Event.AT_TARGET, event, args);
+    }
+
+    /**
      * Returns an event handler.
      * @param eventType the event name (e.g. "click")
      * @return the handler function, {@code null} if the property is null or not a function
      */
     public Function getEventHandler(final String eventType) {
-        final TypeContainer container = typeContainers_.get(eventType.toLowerCase(Locale.ROOT));
-        if (container == null) {
-            return null;
-        }
-        return (Function) container.handler_;
+        return getTypeContainer(eventType).handler_;
     }
 
     /**
@@ -368,50 +413,10 @@
      * @return {@code true} if there are any event listeners for the specified event, {@code false} otherwise
      */
     boolean hasEventListeners(final String eventType) {
-        final TypeContainer container = typeContainers_.get(eventType);
-        return container != null
-            && (container.handler_ instanceof Function
-                    || !container.bubblingListeners_.isEmpty()
-                    || !container.capturingListeners_.isEmpty());
+        return !getTypeContainer(eventType).atTargetListeners_.isEmpty();
     }
 
     /**
-     * Executes listeners.
-     *
-     * @param event the event
-     * @param args the arguments
-     * @param propHandlerArgs handler arguments
-     * @return the result
-     */
-    ScriptResult executeListeners(final Event event, final Object[] args, final Object[] propHandlerArgs) {
-        // the registered capturing listeners (if any)
-        event.setEventPhase(Event.CAPTURING_PHASE);
-        ScriptResult result = executeEventListeners(true, event, args);
-        if (event.isPropagationStopped()) {
-            return result;
-        }
-
-        // the handler declared as property (if any)
-        event.setEventPhase(Event.AT_TARGET);
-        ScriptResult newResult = executeEventHandler(event, propHandlerArgs);
-        if (newResult != null) {
-            result = newResult;
-        }
-        if (event.isPropagationStopped()) {
-            return result;
-        }
-
-        // the registered bubbling listeners (if any)
-        event.setEventPhase(Event.BUBBLING_PHASE);
-        newResult = executeEventListeners(false, event, args);
-        if (newResult != null) {
-            result = newResult;
-        }
-
-        return result;
-    }
-
-    /**
      * {@inheritDoc}
      */
     @Override

Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java
===================================================================
--- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java	2018-08-12 12:32:23 UTC (rev 15516)
+++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/event/EventTarget.java	2018-08-14 19:17:44 UTC (rev 15517)
@@ -40,7 +40,6 @@
 import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
 import com.gargoylesoftware.htmlunit.javascript.host.Window;
 import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
-import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLLabelElement;
 
 import net.sourceforge.htmlunit.corejs.javascript.Context;
 import net.sourceforge.htmlunit.corejs.javascript.Function;
@@ -98,13 +97,11 @@
         final Window window = getWindow();
         final Object[] args = new Object[] {event};
 
-        // handlers declared as property on a node don't receive the event as argument for IE
-        final Object[] propHandlerArgs = args;
-
         final Event previousEvent = window.getCurrentEvent();
         window.setCurrentEvent(event);
         try {
-            return eventListenersContainer.executeListeners(event, args, propHandlerArgs);
+            event.setEventPhase(Event.AT_TARGET);
+            return eventListenersContainer.executeAtTargetListeners(event, args);
         }
         finally {
             window.setCurrentEvent(previousEvent); // reset event
@@ -125,16 +122,16 @@
         final Event previousEvent = window.getCurrentEvent();
         window.setCurrentEvent(event);
 
+        // The load event has some unnatural behaviour that we need to handle specially
+        final boolean isLoadEvent = Event.TYPE_LOAD.equals(event.getType());
+
         try {
-            // window's listeners
-            final EventListenersContainer windowsListeners = window.getEventListenersContainer();
+            // These can be null if we aren't tied to a DOM node
+            final DomNode ourNode = getDomNodeOrNull();
+            final DomNode ourParentNode = (ourNode != null) ? ourNode.getParentNode() : null;
 
-            // capturing phase
-            event.setEventPhase(Event.CAPTURING_PHASE);
-            final boolean windowEventIfDetached = getBrowserVersion().hasFeature(JS_EVENT_WINDOW_EXECUTE_IF_DITACHED);
-
             boolean isAttached = false;
-            for (DomNode node = getDomNodeOrNull(); node != null; node = node.getParentNode()) {
+            for (DomNode node = ourNode; node != null; node = node.getParentNode()) {
                 if (node instanceof Document || node instanceof DomDocumentFragment) {
                     isAttached = true;
                     break;
@@ -141,68 +138,99 @@
                 }
             }
 
-            if (isAttached || windowEventIfDetached) {
-                result = windowsListeners.executeCapturingListeners(event, args);
-                if (event.isPropagationStopped()) {
-                    return result;
+            // Determine the propagation path which is fixed here and not affected by
+            // DOM tree modification from intermediate listeners (tested in Chrome)
+            final List<EventTarget> propagationPath = new ArrayList<>();
+
+            // The window 'load' event targets Document but paths Window only (tested in Chrome/FF)
+            if (!isLoadEvent || !(ourNode instanceof Document)) {
+                // We go on the propagation path first
+                if (isAttached || !(this instanceof HTMLElement)) {
+                    propagationPath.add(this);
                 }
+                // Then add all our parents if we have any (pure JS object such as XMLHttpRequest
+                // and MessagePort, etc. will not have any parents)
+                for (DomNode parent = ourParentNode; parent != null; parent = parent.getParentNode()) {
+                    final EventTarget jsNode = parent.getScriptableObject();
+                    if (isAttached || !(jsNode instanceof HTMLElement)) {
+                        propagationPath.add(jsNode);
+                    }
+                }
             }
-            final List<EventTarget> eventTargetList = new ArrayList<>();
-            EventTarget eventTarget = this;
-            while (eventTarget != null) {
-                if (isAttached) {
-                    eventTargetList.add(eventTarget);
+            // The 'load' event for other elements target that element and but does not path Window
+            // (see Note in https://www.w3.org/TR/DOM-Level-3-Events/#event-type-load)
+            if (!isLoadEvent || ourNode instanceof Document) {
+                if (isAttached || getBrowserVersion().hasFeature(JS_EVENT_WINDOW_EXECUTE_IF_DITACHED)) {
+                    propagationPath.add(window);
                 }
-                final DomNode domNode = eventTarget.getDomNodeOrNull();
-                eventTarget = null;
-                if (domNode != null && domNode.getParentNode() != null) {
-                    eventTarget = domNode.getParentNode().getScriptableObject();
-                }
             }
 
             final boolean ie = getBrowserVersion().hasFeature(JS_CALL_RESULT_IS_LAST_RETURN_VALUE);
-            for (int i = eventTargetList.size() - 1; i >= 0; i--) {
-                final EventTarget jsNode = eventTargetList.get(i);
-                final EventListenersContainer elc = jsNode.eventListenersContainer_;
-                if (elc != null && isAttached) {
-                    final ScriptResult r = elc.executeCapturingListeners(event, args);
-                    result = ScriptResult.combine(r, result, ie);
-                    if (event.isPropagationStopped()) {
-                        return result;
+
+            // Refactoring note: Not sure of the reasoning for this but preserving nonetheless: Nodes
+            // are traversed if they're attached or if they're non-HTMLElement.  However, the capturing
+            // phase only traverses nodes that are attached
+            if (isAttached) {
+                // capturing phase
+                event.setEventPhase(Event.CAPTURING_PHASE);
+
+                for (int i = propagationPath.size() - 1; i >= 1; i--) {
+                    final EventTarget jsNode = propagationPath.get(i);
+                    final EventListenersContainer elc = jsNode.eventListenersContainer_;
+                    if (elc != null) {
+                        final ScriptResult r = elc.executeCapturingListeners(event, args);
+                        result = ScriptResult.combine(r, result, ie);
+                        if (event.isPropagationStopped()) {
+                            return result;
+                        }
                     }
                 }
             }
 
-            // handlers declared as property on a node don't receive the event as argument for IE
-            final Object[] propHandlerArgs = args;
-
-            // bubbling phase
+            // at target phase
             event.setEventPhase(Event.AT_TARGET);
-            eventTarget = this;
-            HtmlLabel label = null;
-            final boolean processLabelAfterBubbling = event.processLabelAfterBubbling();
 
-            while (eventTarget != null) {
-                final EventTarget jsNode = eventTarget;
+            if (!propagationPath.isEmpty()) {
+                // Note: This element is not always the same as event.getTarget():
+                // e.g. the 'load' event targets Document but "at target" is on Window.
+                final EventTarget jsNode = propagationPath.get(0);
                 final EventListenersContainer elc = jsNode.eventListenersContainer_;
-                if (elc != null && !(jsNode instanceof Window) && (isAttached || !(jsNode instanceof HTMLElement))) {
-                    final ScriptResult r = elc.executeBubblingListeners(event, args, propHandlerArgs);
+                if (elc != null) {
+                    final ScriptResult r = elc.executeAtTargetListeners(event, args);
                     result = ScriptResult.combine(r, result, ie);
                     if (event.isPropagationStopped()) {
                         return result;
                     }
                 }
-                final DomNode domNode = eventTarget.getDomNodeOrNull();
-                eventTarget = null;
-                if (domNode != null && domNode.getParentNode() != null) {
-                    eventTarget = domNode.getParentNode().getScriptableObject();
+            }
+
+            // Refactoring note: This should probably be done further down
+            HtmlLabel label = null;
+            if (event.processLabelAfterBubbling()) {
+                for (DomNode parent = ourParentNode; parent != null; parent = parent.getParentNode()) {
+                    if (parent instanceof HtmlLabel) {
+                        label = (HtmlLabel) parent;
+                        break;
+                    }
                 }
+            }
+
+            // bubbling phase
+            if (event.isBubbles()) {
+                // This belongs here inside the block because events that don't bubble never set
+                // eventPhase = 3 (tested in Chrome)
                 event.setEventPhase(Event.BUBBLING_PHASE);
 
-                if (eventTarget != null
-                        && label == null
-                        && processLabelAfterBubbling && eventTarget instanceof HTMLLabelElement) {
-                    label = (HtmlLabel) eventTarget.getDomNodeOrNull();
+                for (int i = 1, size = propagationPath.size(); i < size; i++) {
+                    final EventTarget jsNode = propagationPath.get(i);
+                    final EventListenersContainer elc = jsNode.eventListenersContainer_;
+                    if (elc != null) {
+                        final ScriptResult r = elc.executeBubblingListeners(event, args);
+                        result = ScriptResult.combine(r, result, ie);
+                        if (event.isPropagationStopped()) {
+                            return result;
+                        }
+                    }
                 }
             }
 
@@ -218,10 +246,6 @@
                 }
             }
 
-            if (isAttached || windowEventIfDetached) {
-                final ScriptResult r = windowsListeners.executeBubblingListeners(event, args, propHandlerArgs);
-                result = ScriptResult.combine(r, result, ie);
-            }
         }
         finally {
             event.endFire();

Modified: trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java
===================================================================
--- trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java	2018-08-12 12:32:23 UTC (rev 15516)
+++ trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/Window3Test.java	2018-08-14 19:17:44 UTC (rev 15517)
@@ -16,9 +16,13 @@
 
 import static com.gargoylesoftware.htmlunit.BrowserRunner.TestedBrowser.IE;
 
+import java.io.InputStream;
 import java.net.URL;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.io.IOUtils;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.openqa.selenium.By;
@@ -31,6 +35,7 @@
 import com.gargoylesoftware.htmlunit.BrowserRunner.NotYetImplemented;
 import com.gargoylesoftware.htmlunit.WebDriverTestCase;
 import com.gargoylesoftware.htmlunit.html.HtmlPageTest;
+import com.gargoylesoftware.htmlunit.util.NameValuePair;
 
 /**
  * Tests for {@link Window}.
@@ -1572,4 +1577,574 @@
 
         loadPageWithAlerts2(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.
+     *
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"window DOMContentLoaded 1 capture",
+                        "window DOMContentLoaded 2 capture",
+                        "document DOMContentLoaded 1",
+                        "document DOMContentLoaded 1 capture",
+                        "document DOMContentLoaded 2",
+                        "document DOMContentLoaded 2 capture",
+                        "window DOMContentLoaded 1",
+                        "window DOMContentLoaded 2",
+                        "window at load 1",
+                        "window at load 1 capture",
+                        "window at load 2",
+                        "onload 2",
+                        "window at load 2 capture",
+                        "after"},
+            IE = {"window DOMContentLoaded 1 capture",
+                        "window DOMContentLoaded 2 capture",
+                        "document DOMContentLoaded 1",
+                        "document DOMContentLoaded 1 capture",
+                        "document DOMContentLoaded 2",
+                        "document DOMContentLoaded 2 capture",
+                        "window DOMContentLoaded 1",
+                        "window DOMContentLoaded 2",
+                        "window at load 1",
+                        "window at load 1 capture",
+                        "window at load 2",
+                        "onload 2",
+                        "window at load 2 capture",
+                        "document at load 1 capture",
+                        "document at load 2 capture",
+                        "document at load 1 capture",
+                        "document at load 2 capture",
+                        "after"})
+    public void onload() throws Exception {
+        final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
+            + "<html><head>\n"
+            + "<script>\n"
+            + "  function log(msg) {\n"
+            + "    document.getElementById('log').value += msg + '\\n';\n"
+            + "  }\n"
+
+            // These 'load' events and 'onload' property below target 'document' when fired
+            // but path 'window' only. (Chrome/FF)
+            // This is unlike other events where the path always includes the target and
+            // all ancestors up to 'window'. Ascertaining this is possible by inspecting
+            // the 'event' object which is a property of 'window' in Chrome, or
+            // obtained via the first parameter of the event function in FF: e.g. function (event) { log('xyz', event) }
+            + "  window.addEventListener('load', function () { log('window at load 1') })\n"
+
+            // This 'onload' callback is called when the 'load' event is fired.
+            // Ordering of the call is preserved with respect to other 'load' callbacks and is relative to
+            // the position the property is set.  Subsequent overwriting of 'window.onload' with another
+            // valid function does not move this position.  However, setting 'window.onload' to null or a
+            // non-function value will reset the position and a new position us determined the next time
+            // the property is set. The 'body' tag with an 'onload' property behaves synonymously as
+            // writing 'window.onload = function () { ... }'
+            // at the position the 'body' tag appears.
+            //window.onload = function () { log('onload 1') }
+
+            + "  window.addEventListener('load', function () { log('window at load 1 capture') }, true)\n"
+            // This 'DOMContentLoaded' event targets 'document' and paths [window, document] as expected. (Chrome/FF)
+            + "  window.addEventListener('DOMContentLoaded', function () { log('window DOMContentLoaded 1') })\n"
+            + "  window.addEventListener('DOMContentLoaded', "
+                    + "function () { log('window DOMContentLoaded 1 capture') }, true)\n"
+
+            + "  document.addEventListener('load', function () { log('document at load 1') })\n"
+            + "  document.addEventListener('load', function () { log('document at load 1 capture') }, true)\n"
+            + "  document.addEventListener('DOMContentLoaded', function () { log('document DOMContentLoaded 1') })\n"
+            + "  document.addEventListener('DOMContentLoaded', "
+                    + "function () { log('document DOMContentLoaded 1 capture') }, true)\n"
+            + "</script>\n"
+            + "</head>\n"
+            + "<body>\n"
+            + "<script>\n"
+            + "  window.addEventListener('load', function () { log('window at load 2') })\n"
+            //window.onload = null
+            //window.onload = 123
+            + "  window.onload = function () { log('onload 2') }\n"
+            + "  window.addEventListener('load', function () { log('window at load 2 capture') }, true)\n"
+            + "  window.addEventListener('DOMContentLoaded', function () { log('window DOMContentLoaded 2') })\n"
+            + "  window.addEventListener('DOMContentLoaded', "
+                    + "function () { log('window DOMContentLoaded 2 capture') }, true)\n"
+
+            + "  document.addEventListener('load', function () { log('document at load 2 capture') }, true)\n"
+            + "  document.addEventListener('DOMContentLoaded', function () { log('document DOMContentLoaded 2') })\n"
+            + "  document.addEventListener('DOMContentLoaded', "
+                    + "function () { log('document DOMContentLoaded 2 capture') }, true)\n"
+
+            // This is for testing the state of event.eventPhase afterwards
+            + "  window.addEventListener('load', "
+                    + "function (event) { var x = event; "
+                        + "window.setTimeout(function () { log('after', x.eventPhase) }, 100) }, true)\n"
+            + "</script>\n"
+            + "  <textarea id='log' rows=40 cols=80></textarea>\n"
+            + "</body></html>";
+
+        final WebDriver driver = loadPage2(html);
+        Thread.sleep(200);
+        final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", "");
+        assertEquals(String.join("\n", getExpectedAlerts()), text);
+    }
+
+    /**
+     * Tests load and error events of 'script' tags.
+     * Checks that they should be using EventTarget.fireEvent()
+     * rather than Event.executeEventLocally().
+     *
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"document DOMContentLoaded",
+                        "window DOMContentLoaded",
+                        "window at load",
+                        "window at load capture",
+                        "body onload"},
+            IE = {"document DOMContentLoaded",
+                        "window DOMContentLoaded",
+                        "window at load",
+                        "window at load capture",
+                        "document at load capture",
+                        "body onload"})
+    public void onloadScript() throws Exception {
+        getMockWebConnection().setResponse(URL_SECOND, "");
+
+        final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
+            + "<html><head>\n"
+            + "<script>\n"
+            + "  function log(msg) {\n"
+            + "    document.getElementById('log').value += msg + '\\n';\n"
+            + "  }\n"
+
+            + "  window.addEventListener('load', function () { log('window at load') })\n"
+            + "  window.addEventListener('load', function () { log('window at load capture') }, true)\n"
+            + "  window.addEventListener('error', function () { log('window at error') })\n"
+            + "  window.addEventListener('error', function () { log('window at error capture') }, true)\n"
+            + "  window.addEventListener('DOMContentLoaded', function () { log('window DOMContentLoaded') })\n"
+
+            + "  document.addEventListener('load', function () { log('document at load') })\n"
+            + "  document.addEventListener('load', function () { log('document at load capture') }, true)\n"
+            + "  document.addEventListener('error', function () { log('document at error') })\n"
+            + "  document.addEventListener('error', function () { log('document at error capture') }, true)\n"
+            + "  document.addEventListener('DOMContentLoaded', function () { log('document DOMContentLoaded') })\n"
+
+            + "</script>\n"
+            + "</head>\n"
+            + "<body onload='log(\"body onload\")'>\n"
+            + "  <script src='" + URL_SECOND + "' onload='log(\"element 1 onload\")' "
+                                        + "onerror='log(\"element 1 onerror\")'></script>\n"
+            + "  <script src='missing.txt' onload='log(\"element 2 onload\")' "
+                                        + "onerror='log(\"element 2 onerror\")'></script>\n"
+
+            + "  <textarea id='log' rows=40 cols=80></textarea>\n"
+            + "</body></html>";
+
+        final WebDriver driver = loadPage2(html);
+        Thread.sleep(200);
+        final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", "");
+        assertEquals(String.join("\n", getExpectedAlerts()), text);
+    }
+
+    /**
+     * Tests load and error events of 'img' tags.
+     * Checks that they should be using EventTarget.fireEvent()
+     * rather than Event.executeEventLocally().
+     *
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"window at error capture",
+                        "document at error capture",
+                        "element 2 onerror",
+                        "document DOMContentLoaded",
+                        "window DOMContentLoaded",
+                        "document at load capture",
+                        "element 1 onload",
+                        "window at load",
+                        "window at load capture",
+                        "body onload"},
+            CHROME = {"document DOMContentLoaded",
+                        "window DOMContentLoaded",
+                        "window at error capture",
+                        "document at error capture",
+                        "element 2 onerror",
+                        "document at load capture",
+                        "element 1 onload",
+                        "window at load",
+                        "window at load capture",
+                        "body onload"},
+            IE = {"document at load capture",
+                        "element 1 onload",
+                        "document DOMContentLoaded",
+                        "window DOMContentLoaded",
+                        "window at load",
+                        "window at load capture",
+                        "body onload",
+                        "document at load capture",
+                        "window at error capture",
+                        "document at error capture",
+                        "element 2 onerror"})
+    public void onloadImg() throws Exception {
+        final URL urlImage = new URL(URL_FIRST, "img.jpg");
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream("testfiles/tiny-jpg.img")) {
+            final byte[] directBytes = IOUtils.toByteArray(is);
+            final List<NameValuePair> emptyList = Collections.emptyList();
+            getMockWebConnection().setResponse(urlImage, directBytes, 200, "ok", "image/jpg", emptyList);
+        }
+
+        final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
+            + "<html><head>\n"
+            + "<script>\n"
+            + "  function log(msg) {\n"
+            + "    document.getElementById('log').value += msg + '\\n';\n"
+            + "  }\n"
+
+            + "  window.addEventListener('load', function () { log('window at load') })\n"
+            + "  window.addEventListener('load', function () { log('window at load capture') }, true)\n"
+            + "  window.addEventListener('error', function () { log('window at error') })\n"
+            + "  window.addEventListener('error', function () { log('window at error capture') }, true)\n"
+            + "  window.addEventListener('DOMContentLoaded', function () { log('window DOMContentLoaded') })\n"
+
+            + "  document.addEventListener('load', function () { log('document at load') })\n"
+            + "  document.addEventListener('load', function () { log('document at load capture') }, true)\n"
+            + "  document.addEventListener('error', function () { log('document at error') })\n"
+            + "  document.addEventListener('error', function () { log('document at error capture') }, true)\n"
+            + "  document.addEventListener('DOMContentLoaded', function () { log('document DOMContentLoaded') })\n"
+
+            + "</script>\n"
+            + "</head>\n"
+            + "<body onload='log(\"body onload\")'>\n"
+            + "  <img src='" + urlImage + "' onload='log(\"element 1 onload\")' "
+                                        + "onerror='log(\"element 1 onerror\")'>\n"
+            + "  <img src='' onload='log(\"element 2 onload\")' "
+                                        + "onerror='log(\"element 2 onerror\")'>\n"
+
+            + "  <textarea id='log' rows=40 cols=80></textarea>\n"
+            + "</body></html>";
+
+        final WebDriver driver = loadPage2(html);
+        Thread.sleep(200);
+        final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", "");
+        assertEquals(String.join("\n", getExpectedAlerts()), text);
+    }
+
+    /**
+     * Same as {@link #onload()} but from frame.
+     *
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"framing window DOMContentLoaded 1 capture",
+                        "framing document DOMContentLoaded 1",
+                        "framing document DOMContentLoaded 1 capture",
+                        "framing window DOMContentLoaded 1",
+                        "window DOMContentLoaded 1 capture",
+                        "window DOMContentLoaded 2 capture",
+                        "document DOMContentLoaded 1",
+                        "document DOMContentLoaded 1 capture",
+                        "document DOMContentLoaded 2",
+                        "document DOMContentLoaded 2 capture",
+                        "window DOMContentLoaded 1",
+                        "window DOMContentLoaded 2",
+                        "window at load 1",
+                        "window at load 1 capture",
+                        "window at load 2",
+                        "onload 2",
+                        "window at load 2 capture",
+                        "framing document at load 1 capture",
+                        "frame onload",
+                        "framing window at load 1",
+                        "framing window at load 1 capture",
+                        "frameset onload",
+                        "after"},
+            IE = {"framing window DOMContentLoaded 1 capture",
+                        "framing document DOMContentLoaded 1",
+                        "framing document DOMContentLoaded 1 capture",
+                        "framing window DOMContentLoaded 1",
+                        "framing document at load 1 capture",
+                        "window DOMContentLoaded 1 capture",
+                        "window DOMContentLoaded 2 capture",
+                        "document DOMContentLoaded 1",
+                        "document DOMContentLoaded 1 capture",
+                        "document DOMContentLoaded 2",
+                        "document DOMContentLoaded 2 capture",
+                        "window DOMContentLoaded 1",
+                        "window DOMContentLoaded 2",
+                        "window at load 1",
+                        "window at load 1 capture",
+                        "window at load 2",
+                        "onload 2",
+                        "window at load 2 capture",
+                        "framing document at load 1 capture",
+                        "frame onload",
+                        "framing window at load 1",
+                        "framing window at load 1 capture",
+                        "frameset onload",
+                        "document at load 1 capture",
+                        "document at load 2 capture",
+                        "document at load 1 capture",
+                        "document at load 2 capture",
+                        "after"})
+    public void onloadFrame() throws Exception {
+
+        final String content = HtmlPageTest.STANDARDS_MODE_PREFIX_
+            + "<html><head>\n"
+            + "<script>\n"
+            + "  function log(msg) {\n"
+            + "    window.parent.document.title += msg + ';';\n"
+            + "  }\n"
+
+            + "  window.addEventListener('load', function () { log('window at load 1') })\n"
+
+            + "  window.addEventListener('load', function () { log('window at load 1 capture') }, true)\n"
+            + "  window.addEventListener('DOMContentLoaded', function () { log('window DOMContentLoaded 1') })\n"
+            + "  window.addEventListener('DOMContentLoaded', "
+                    + "function () { log('window DOMContentLoaded 1 capture') }, true)\n"
+
+            + "  document.addEventListener('load', function () { log('document at load 1') })\n"
+            + "  document.addEventListener('load', function () { log('document at load 1 capture') }, true)\n"
+            + "  document.addEventListener('DOMContentLoaded', function () { log('document DOMContentLoaded 1') })\n"
+            + "  document.addEventListener('DOMContentLoaded', "
+                    + "function () { log('document DOMContentLoaded 1 capture') }, true)\n"
+            + "</script>\n"
+            + "</head>\n"
+            + "<body >\n"
+            + "<script>\n"
+            + "  window.addEventListener('load', function () { log('window at load 2') })\n"
+            + "  window.onload = function () { log('onload 2') }\n"
+            + "  window.addEventListener('load', function () { log('window at load 2 capture') }, true)\n"
+            + "  window.addEventListener('DOMContentLoaded', function () { log('window DOMContentLoaded 2') })\n"
+            + "  window.addEventListener('DOMContentLoaded', "
+                    + "function () { log('window DOMContentLoaded 2 capture') }, true)\n"
+
+            + "  document.addEventListener('load', function () { log('document at load 2 capture') }, true)\n"
+            + "  document.addEventListener('DOMContentLoaded', function () { log('document DOMContentLoaded 2') })\n"
+            + "  document.addEventListener('DOMContentLoaded', "
+                    + "function () { log('document DOMContentLoaded 2 capture') }, true)\n"
+
+            + "  window.addEventListener('load', "
+                    + "function (event) { var x = event; "
+                        + "window.setTimeout(function () { log('after', x.eventPhase) }, 100) }, true)\n"
+            + "</script>\n"
+            + "</body></html>";
+
+        getMockWebConnection().setDefaultResponse(content);
+
+        final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
+                + "<html><head>\n"
+                + "<script>\n"
+                + "  function log(msg) {\n"
+                + "    window.document.title += msg + ';';\n"
+                + "  }\n"
+
+                + "  window.addEventListener('load', function () { log('framing window at load 1') })\n"
+                + "  window.addEventListener('load', function () { log('framing window at load 1 capture') }, true)\n"
+                + "  window.addEventListener('DOMContentLoaded', "
+                            + "function () { log('framing window DOMContentLoaded 1') })\n"
+                + "  window.addEventListener('DOMContentLoaded', "
+                            + "function () { log('framing window DOMContentLoaded 1 capture') }, true)\n"
+
+                // should not fire because bubbles = false
+                + "  document.addEventListener('load', "
+                            + "function () { log('framing document at load 1') })\n"
+                + "  document.addEventListener('load', "
+                            + "function () { log('framing document at load 1 capture') }, true)\n"
+                + "  document.addEventListener('DOMContentLoaded', "
+                            + "function () { log('framing document DOMContentLoaded 1') })\n"
+                + "  document.addEventListener('DOMContentLoaded', "
+                            + "function () { log('framing document DOMContentLoaded 1 capture') }, true)\n"
+                + "</script>\n"
+                + "</head>\n"
+                + "<frameset onload='log(\"frameset onload\")'>\n"
+                + "<frame src='test_onload.html' onload='log(\"frame onload\")'>\n"
+                + "</frameset>\n"
+                + "</html>";
+
+        final WebDriver driver = loadPage2(html);
+        Thread.sleep(200);
+        final String text = driver.getTitle().trim().replaceAll(";", "\n").trim();
+        assertEquals(String.join("\n", getExpectedAlerts()), text);
+    }
+
+    /**
+     * 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.
+     *
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"window at click 1 capture",
+                        "window at click 2 capture",
+                        "onclick 2",
+                        "i1 at click 1",
+                        "i1 at click 1 capture",
+                        "i1 at click 2",
+                        "i1 at click 2 capture",
+                        "window at click 1",
+                        "window at click 2"})
+    public void propagation() throws Exception {
+        final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
+            + "<html><head>\n"
+            + "<script>\n"
+            + "  function log(msg) {\n"
+            + "    document.getElementById('log').value += msg + '\\n';\n"
+            + "  }\n"
+            + "</script>\n"
+            + "</head>\n"
+            + "<body>\n"
+            + "  <input id='tester' type='button' value='test' onclick='log(\"onclick\")'>\n"
+            + "  <textarea id='log' rows=40 cols=80></textarea>\n"
+
+            + "<script>\n"
+            + "  window.addEventListener('click', function () { log('window at click 1') })\n"
+            + "  window.addEventListener('click', function () { log('window at click 1 capture') }, true)\n"
+            + "  window.addEventListener('click', function () { log('window at click 2') })\n"
+            + "  window.addEventListener('click', function () { log('window at click 2 capture') }, true)\n"
+
+            + "  tester.addEventListener('click', function () { log('i1 at click 1') })\n"
+            + "  tester.addEventListener('click', function () { log('i1 at click 1 capture') }, true)\n"
+            + "  tester.addEventListener('click', function () { log('i1 at click 2') })\n"
+            + "  tester.onclick = function () { log('onclick 2') }\n"
+            + "  tester.addEventListener('click', function () { log('i1 at click 2 capture') }, true)\n"
+            + "</script>\n"
+            + "</body></html>";
+
+        final WebDriver driver = loadPage2(html);
+        driver.findElement(By.id("tester")).click();
+
+        final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", "");
+        assertEquals(String.join("\n", getExpectedAlerts()), text);
+    }
+
+    /**
+     * Similar as {@link #propagation()} except with a deeper propagation path.
+     * Check bubbling propagation after modification of the DOM tree by an intermediate listener.
+     *
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"d1 at click 1 capture",
+                        "d1 at click 2 capture",
+                        "d2 at click 1 capture",
+                        "d2 at click 2 capture",
+                        "d3 at click 1",
+                        "d3 onclick",
+                        "d3 at click 1 capture",
+                        "d3 at click 2",
+                        "d3 at click 2 capture",
+                        "d2 at click 1",
+                        "d2 onclick",
+                        "d2 at click 2",
+                        "d1 at click 1",
+                        "d1 onclick",
+                        "d1 at click 2"})
+    public void propagationNested() throws Exception {
+        final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
+            + "<html><head>\n"
+            + "<script>\n"
+            + "  function log(msg) {\n"
+            + "    document.getElementById('log').value += msg + '\\n';\n"
+            + "  }\n"
+            + "</script>\n"
+            + "</head>\n"
+            + "<body>\n"
+            + "  <div id='d1' style='width: 150px; height: 150px; background-color: blue'>\n"
+            + "    <div id='d2' style='width: 100px; height: 100px; background-color: green'>\n"
+            + "      <div id='d3' style='width: 50px; height: 50px; background-color: red'>\n"
+            + "      </div>\n"
+            + "    </div>\n"
+            + "  </div>\n"
+
+            + "  <textarea id='log' rows=40 cols=80></textarea>\n"
+
+            + "<script>\n"
+            + "  d1.addEventListener('click', function () { log('d1 at click 1') })\n"
+            + "  d1.onclick = function () { log('d1 onclick') }\n"
+            + "  d1.addEventListener('click', function () { log('d1 at click 1 capture') }, true)\n"
+            + "  d1.addEventListener('click', function () { log('d1 at click 2') })\n"
+            + "  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.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"
+
+            + "  d3.addEventListener('click', function () { log('d3 at click 1') })\n"
+            + "  d3.onclick = function () { log('d3 onclick') }\n"
+            + "  d3.addEventListener('click', function () { log('d3 at click 1 capture') }, true)\n"
+            + "  d3.addEventListener('click', function () { log('d3 at click 2') })\n"
+            + "  d3.addEventListener('click', function () { log('d3 at click 2 capture') }, true)\n"
+            + "</script>\n"
+            + "</body></html>";
+
+        final WebDriver driver = loadPage2(html);
+        driver.findElement(By.id("d3")).click();
+
+        final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", "");
+        assertEquals(String.join("\n", getExpectedAlerts()), text);
+    }
+
+    /**
+     * This test determines that the return value of listeners are apparently
+     * ignored and only that of the property handler is used.
+     *
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"listener: stop propagation & return false",
+                        "FIRED a1",
+                        "listener: return true",
+                        "property: return false",
+                        "listener: return true"})
+    public void stopPropagation() throws Exception {
+        final String html = HtmlPageTest.STANDARDS_MODE_PREFIX_
+            + "<html><head>\n"
+            + "<script>\n"
+            + "  function log(msg) {\n"
+            + "    document.getElementById('log').value += msg + '\\n';\n"
+            + "  }\n"
+            + "</script>\n"
+            + "</head>\n"
+            + "<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"
+
+            + "  <textarea id='log' rows=40 cols=80></textarea>\n"
+
+            + "<script>\n"
+            // The event.stopPropagation() has no bearing on whether 'return false'
+            // below is effective at preventing "href" processing.
+            + "  a1.addEventListener('click', function (event) { "
+                                                + "log('listener: stop propagation & return false');"
+                                                + "event.stopPropagation(); return false })\n"
+
+             // The only return value that matters is the value from the 'onclick' property.  The 'return false' below
+             // prevents "href' being processed.
+             + "  a2.addEventListener('click',"
+             + "        function (event) { log('listener: return true'); event.stopPropagation(); return true })\n"
+             + "  a2.onclick = function () { log('property: return false'); return false }\n"
+             + "  a2.addEventListener('click', function (event) { log('listener: return true'); return true })\n"
+
+             // Uncommenting this causes a2 to fire because propagation is
+             // stopped before 'onclick' property is processed.
+             // Again, the 'return false' here is ineffective.
+             // The return values of non-property handlers are probably ignored. (tested in Chrome/FF)
+             //window.addEventListener("click", function (event) {
+             //                  log('window: stop propagation & return false');
+             //                  event.stopPropagation(); return false }, true)
+            + "</script>\n"
+            + "</body></html>";
+
+        final WebDriver driver = loadPage2(html);
+        driver.findElement(By.id("a1")).click();
+        driver.findElement(By.id("a2")).click();
+
+        final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", "");
+        assertEquals(String.join("\n", getExpectedAlerts()), text);
+    }
 }


------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot