[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