Bug 887541 - Implement web components event path and event retargeting. r=smaug
authorWilliam Chen <wchen@mozilla.com>
Tue, 26 Aug 2014 20:19:56 -0700
changeset 201772 fff4d503ad889519d0e97359fa2536a747a463a8
parent 201771 c840195920bd2874bda1c7639bce046878bbf4dd
child 201773 8163a245cb74111414be556f4e5f1a770226880e
child 201910 8eaaedaf5d3d0a622a78cb277642b085ed48d0d7
push id48258
push userwchen@mozilla.com
push dateWed, 27 Aug 2014 03:20:36 +0000
treeherdermozilla-inbound@fff4d503ad88 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs887541
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 887541 - Implement web components event path and event retargeting. r=smaug
content/base/public/nsContentUtils.h
content/base/src/FragmentOrElement.cpp
content/base/src/nsContentUtils.cpp
dom/events/Event.cpp
dom/events/Event.h
dom/events/EventDispatcher.cpp
dom/events/EventDispatcher.h
dom/events/MouseEvent.cpp
dom/tests/mochitest/webcomponents/mochitest.ini
dom/tests/mochitest/webcomponents/test_event_dispatch.html
dom/tests/mochitest/webcomponents/test_event_retarget.html
dom/tests/mochitest/webcomponents/test_event_stopping.html
--- a/content/base/public/nsContentUtils.h
+++ b/content/base/public/nsContentUtils.h
@@ -209,17 +209,18 @@ public:
    * or ShadowRoot as an ancestor of things in the corresponding DocumentFragment.
    * See the concept of "host-including inclusive ancestor" in the DOM
    * specification.
    */
   static bool ContentIsHostIncludingDescendantOf(
     const nsINode* aPossibleDescendant, const nsINode* aPossibleAncestor);
 
   /**
-   * Similar to ContentIsDescendantOf except it crosses document boundaries.
+   * Similar to ContentIsDescendantOf except it crosses document boundaries,
+   * also crosses ShadowRoot boundaries from ShadowRoot to its host.
    */
   static bool ContentIsCrossDocDescendantOf(nsINode* aPossibleDescendant,
                                               nsINode* aPossibleAncestor);
 
   /*
    * This method fills the |aArray| with all ancestor nodes of |aNode|
    * including |aNode| at the zero index.
    */
--- a/content/base/src/FragmentOrElement.cpp
+++ b/content/base/src/FragmentOrElement.cpp
@@ -20,16 +20,17 @@
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/EventListenerManager.h"
 #include "mozilla/EventStates.h"
 #include "mozilla/dom/Attr.h"
 #include "nsDOMAttributeMap.h"
 #include "nsIAtom.h"
 #include "mozilla/dom/NodeInfo.h"
+#include "mozilla/dom/Event.h"
 #include "nsIDocumentInlines.h"
 #include "nsIDocumentEncoder.h"
 #include "nsIDOMNodeList.h"
 #include "nsIContentIterator.h"
 #include "nsFocusManager.h"
 #include "nsILinkHandler.h"
 #include "nsIScriptGlobalObject.h"
 #include "nsIURL.h"
@@ -693,24 +694,37 @@ nsIContent::PreHandleEvent(EventChainPre
   // inside chrome access only content.
   bool isAnonForEvents = IsRootOfChromeAccessOnlySubtree();
   if ((aVisitor.mEvent->message == NS_MOUSE_ENTER_SYNTH ||
        aVisitor.mEvent->message == NS_MOUSE_EXIT_SYNTH ||
        aVisitor.mEvent->message == NS_POINTER_OVER ||
        aVisitor.mEvent->message == NS_POINTER_OUT) &&
       // Check if we should stop event propagation when event has just been
       // dispatched or when we're about to propagate from
-      // chrome access only subtree.
+      // chrome access only subtree or if we are about to propagate out of
+      // a shadow root to a shadow root host.
       ((this == aVisitor.mEvent->originalTarget &&
-        !ChromeOnlyAccess()) || isAnonForEvents)) {
+        !ChromeOnlyAccess()) || isAnonForEvents || GetShadowRoot())) {
      nsCOMPtr<nsIContent> relatedTarget =
        do_QueryInterface(aVisitor.mEvent->AsMouseEvent()->relatedTarget);
     if (relatedTarget &&
         relatedTarget->OwnerDoc() == OwnerDoc()) {
 
+      // In the web components case, we may need to stop propagation of events
+      // at shadow root host.
+      if (GetShadowRoot()) {
+        nsIContent* adjustedTarget =
+          Event::GetShadowRelatedTarget(this, relatedTarget);
+        if (this == adjustedTarget) {
+          aVisitor.mParentTarget = nullptr;
+          aVisitor.mCanHandle = false;
+          return NS_OK;
+        }
+      }
+
       // If current target is anonymous for events or we know that related
       // target is descendant of an element which is anonymous for events,
       // we may want to stop event propagation.
       // If this is the original target, aVisitor.mRelatedTargetIsInAnon
       // must be updated.
       if (isAnonForEvents || aVisitor.mRelatedTargetIsInAnon ||
           (aVisitor.mEvent->originalTarget == this &&
            (aVisitor.mRelatedTargetIsInAnon =
@@ -764,16 +778,109 @@ nsIContent::PreHandleEvent(EventChainPre
             }
           }
         }
       }
     }
   }
 
   nsIContent* parent = GetParent();
+
+  // Web components have a special event chain that need to account
+  // for destination insertion points where nodes have been distributed.
+  nsTArray<nsIContent*>* destPoints = GetExistingDestInsertionPoints();
+  if (destPoints && !destPoints->IsEmpty()) {
+    // Push destination insertion points to aVisitor.mDestInsertionPoints
+    // excluding shadow insertion points.
+    bool didPushNonShadowInsertionPoint = false;
+    for (uint32_t i = 0; i < destPoints->Length(); i++) {
+      nsIContent* point = destPoints->ElementAt(i);
+      if (!ShadowRoot::IsShadowInsertionPoint(point)) {
+        aVisitor.mDestInsertionPoints.AppendElement(point);
+        didPushNonShadowInsertionPoint = true;
+      }
+    }
+
+    // Next node in the event path is the final destination
+    // (non-shadow) insertion point that was pushed.
+    if (didPushNonShadowInsertionPoint) {
+      parent = aVisitor.mDestInsertionPoints.LastElement();
+      aVisitor.mDestInsertionPoints.SetLength(
+        aVisitor.mDestInsertionPoints.Length() - 1);
+    }
+  }
+
+  ShadowRoot* thisShadowRoot = ShadowRoot::FromNode(this);
+  if (thisShadowRoot) {
+    // The following events must always be stopped at the root node of the node tree:
+    //   abort
+    //   error
+    //   select
+    //   change
+    //   load
+    //   reset
+    //   resize
+    //   scroll
+    //   selectstart
+    bool stopEvent = false;
+    switch (aVisitor.mEvent->message) {
+      case NS_IMAGE_ABORT:
+      case NS_LOAD_ERROR:
+      case NS_FORM_SELECTED:
+      case NS_FORM_CHANGE:
+      case NS_LOAD:
+      case NS_FORM_RESET:
+      case NS_RESIZE_EVENT:
+      case NS_SCROLL_EVENT:
+        stopEvent = true;
+        break;
+      case NS_USER_DEFINED_EVENT:
+        if (aVisitor.mDOMEvent) {
+          nsAutoString eventType;
+          aVisitor.mDOMEvent->GetType(eventType);
+          if (eventType.EqualsLiteral("abort") ||
+              eventType.EqualsLiteral("error") ||
+              eventType.EqualsLiteral("select") ||
+              eventType.EqualsLiteral("change") ||
+              eventType.EqualsLiteral("load") ||
+              eventType.EqualsLiteral("reset") ||
+              eventType.EqualsLiteral("resize") ||
+              eventType.EqualsLiteral("scroll") ||
+              eventType.EqualsLiteral("selectstart")) {
+            stopEvent = true;
+          }
+        }
+        break;
+    }
+
+    if (stopEvent) {
+      // If we do stop propagation, we still want to propagate
+      // the event to chrome (nsPIDOMWindow::GetParentTarget()).
+      // The load event is special in that we don't ever propagate it
+      // to chrome.
+      nsCOMPtr<nsPIDOMWindow> win = OwnerDoc()->GetWindow();
+      EventTarget* parentTarget = win && aVisitor.mEvent->message != NS_LOAD
+        ? win->GetParentTarget() : nullptr;
+
+      aVisitor.mParentTarget = parentTarget;
+      return NS_OK;
+    }
+
+    if (!aVisitor.mDestInsertionPoints.IsEmpty()) {
+      parent = aVisitor.mDestInsertionPoints.LastElement();
+      aVisitor.mDestInsertionPoints.SetLength(
+        aVisitor.mDestInsertionPoints.Length() - 1);
+    } else {
+      // The pool host for the youngest shadow root is shadow DOM host,
+      // for older shadow roots, it is the shadow insertion point
+      // where the shadow root is projected, nullptr if none exists.
+      parent = thisShadowRoot->GetPoolHost();
+    }
+  }
+
   // Event may need to be retargeted if this is the root of a native
   // anonymous content subtree or event is dispatched somewhere inside XBL.
   if (isAnonForEvents) {
 #ifdef DEBUG
     // If a DOM event is explicitly dispatched using node.dispatchEvent(), then
     // all the events are allowed even in the native anonymous content..
     nsCOMPtr<nsIContent> t = do_QueryInterface(aVisitor.mEvent->originalTarget);
     NS_ASSERTION(!t || !t->ChromeOnlyAccess() ||
@@ -799,17 +906,17 @@ nsIContent::PreHandleEvent(EventChainPre
     if (insertionParent) {
       parent = insertionParent;
     }
   }
 
   if (parent) {
     aVisitor.mParentTarget = parent;
   } else {
-    aVisitor.mParentTarget = GetCurrentDoc();
+    aVisitor.mParentTarget = GetComposedDoc();
   }
   return NS_OK;
 }
 
 bool
 nsIContent::GetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                     nsAString& aResult) const
 {
--- a/content/base/src/nsContentUtils.cpp
+++ b/content/base/src/nsContentUtils.cpp
@@ -2077,16 +2077,23 @@ nsContentUtils::ContentIsCrossDocDescend
                                               nsINode* aPossibleAncestor)
 {
   NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
   NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
 
   do {
     if (aPossibleDescendant == aPossibleAncestor)
       return true;
+
+    // Step over shadow root to the host node.
+    ShadowRoot* shadowRoot = ShadowRoot::FromNode(aPossibleDescendant);
+    if (shadowRoot) {
+      aPossibleDescendant = shadowRoot->GetHost();
+    }
+
     aPossibleDescendant = GetCrossDocParentNode(aPossibleDescendant);
   } while (aPossibleDescendant);
 
   return false;
 }
 
 
 // static
--- a/dom/events/Event.cpp
+++ b/dom/events/Event.cpp
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AccessCheck.h"
 #include "base/basictypes.h"
 #include "ipc/IPCMessageUtils.h"
 #include "mozilla/dom/Event.h"
+#include "mozilla/dom/ShadowRoot.h"
 #include "mozilla/ContentEvents.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/InternalMutationEvent.h"
 #include "mozilla/MiscEvents.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/TextEvents.h"
@@ -1114,16 +1115,54 @@ Event::SetOwner(mozilla::dom::EventTarge
   }
 
 #ifdef DEBUG
   nsCOMPtr<nsPIWindowRoot> root = do_QueryInterface(aOwner);
   MOZ_ASSERT(root, "Unexpected EventTarget!");
 #endif
 }
 
+// static
+nsIContent*
+Event::GetShadowRelatedTarget(nsIContent* aCurrentTarget,
+                              nsIContent* aRelatedTarget)
+{
+  if (!aCurrentTarget || !aRelatedTarget) {
+    return nullptr;
+  }
+
+  // Walk up the ancestor node trees of the related target until
+  // we encounter the node tree of the current target in order
+  // to find the adjusted related target. Walking up the tree may
+  // not find a common ancestor node tree if the related target is in
+  // an ancestor tree, but in that case it does not need to be adjusted.
+  ShadowRoot* currentTargetShadow = aCurrentTarget->GetContainingShadow();
+  if (!currentTargetShadow) {
+    return nullptr;
+  }
+
+  nsIContent* relatedTarget = aCurrentTarget;
+  while (relatedTarget) {
+    ShadowRoot* ancestorShadow = relatedTarget->GetContainingShadow();
+    if (currentTargetShadow == ancestorShadow) {
+      return relatedTarget;
+    }
+
+    // Didn't find the ancestor tree, thus related target does not have to
+    // adjusted.
+    if (!ancestorShadow) {
+      return nullptr;
+    }
+
+    relatedTarget = ancestorShadow->GetHost();
+  }
+
+  return nullptr;
+}
+
 } // namespace dom
 } // namespace mozilla
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 nsresult
 NS_NewDOMEvent(nsIDOMEvent** aInstancePtrResult,
--- a/dom/events/Event.h
+++ b/dom/events/Event.h
@@ -216,16 +216,24 @@ public:
    */
   void PreventDefaultInternal(bool aCalledByDefaultHandler);
 
   bool IsMainThreadEvent()
   {
     return mIsMainThreadEvent;
   }
 
+  /**
+   * For a given current target, returns the related target adjusted with
+   * shadow DOM retargeting rules. Returns nullptr if related target
+   * is not adjusted.
+   */
+  static nsIContent* GetShadowRelatedTarget(nsIContent* aCurrentTarget,
+                                            nsIContent* aRelatedTarget);
+
 protected:
 
   // Internal helper functions
   void SetEventType(const nsAString& aEventTypeArg);
   already_AddRefed<nsIContent> GetTargetFromFrame();
 
   /**
    * IsChrome() returns true if aCx is chrome context or the event is created
--- a/dom/events/EventDispatcher.cpp
+++ b/dom/events/EventDispatcher.cpp
@@ -516,17 +516,18 @@ EventDispatcher::Dispatch(nsISupports* a
       aEvent->originalTarget->GetTargetForEventTargetChain();
     NS_ENSURE_STATE(aEvent->originalTarget);
   }
   else {
     aEvent->originalTarget = aEvent->target;
   }
 
   nsCOMPtr<nsIContent> content = do_QueryInterface(aEvent->originalTarget);
-  bool isInAnon = (content && content->IsInAnonymousSubtree());
+  bool isInAnon = (content && (content->IsInAnonymousSubtree() ||
+                               content->IsInShadowTree()));
 
   aEvent->mFlags.mIsBeingDispatched = true;
 
   // Create visitor object and start event dispatching.
   // PreHandleEvent for the original target.
   nsEventStatus status = aEventStatus ? *aEventStatus : nsEventStatus_eIgnore;
   EventChainPreVisitor preVisitor(aPresContext, aEvent, aDOMEvent, status,
                                   isInAnon);
--- a/dom/events/EventDispatcher.h
+++ b/dom/events/EventDispatcher.h
@@ -4,20 +4,22 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifdef MOZILLA_INTERNAL_API
 #ifndef mozilla_EventDispatcher_h_
 #define mozilla_EventDispatcher_h_
 
 #include "mozilla/EventForwards.h"
 #include "nsCOMPtr.h"
+#include "nsTArray.h"
 
 // Microsoft's API Name hackery sucks
 #undef CreateEvent
 
+class nsIContent;
 class nsIDOMEvent;
 class nsIScriptGlobalObject;
 class nsPresContext;
 
 template<class E> class nsCOMArray;
 
 namespace mozilla {
 namespace dom {
@@ -188,16 +190,23 @@ public:
    */
   dom::EventTarget* mParentTarget;
 
   /**
    * If the event needs to be retargeted, this is the event target,
    * which should be used when the event is handled at mParentTarget.
    */
   dom::EventTarget* mEventTargetAtParent;
+
+  /**
+   * An array of destination insertion points that need to be inserted
+   * into the event path of nodes that are distributed by the
+   * web components distribution algorithm.
+   */
+  nsTArray<nsIContent*> mDestInsertionPoints;
 };
 
 class EventChainPostVisitor : public mozilla::EventChainVisitor
 {
 public:
   explicit EventChainPostVisitor(EventChainVisitor& aOther)
     : EventChainVisitor(aOther.mPresContext, aOther.mEvent,
                         aOther.mDOMEvent, aOther.mEventStatus)
--- a/dom/events/MouseEvent.cpp
+++ b/dom/events/MouseEvent.cpp
@@ -274,16 +274,23 @@ MouseEvent::GetRelatedTarget()
         do_QueryInterface(mEvent->AsMouseEventBase()->relatedTarget);
       break;
     default:
       break;
   }
 
   if (relatedTarget) {
     nsCOMPtr<nsIContent> content = do_QueryInterface(relatedTarget);
+    nsCOMPtr<nsIContent> currentTarget = do_QueryInterface(mEvent->currentTarget);
+
+    nsIContent* shadowRelatedTarget = GetShadowRelatedTarget(currentTarget, content);
+    if (shadowRelatedTarget) {
+      relatedTarget = shadowRelatedTarget;
+    }
+
     if (content && content->ChromeOnlyAccess() &&
         !nsContentUtils::CanAccessNativeAnon()) {
       relatedTarget = do_QueryInterface(content->FindFirstNonChromeOnlyAccessContent());
     }
 
     if (relatedTarget) {
       relatedTarget = relatedTarget->GetTargetForDOMEvent();
     }
--- a/dom/tests/mochitest/webcomponents/mochitest.ini
+++ b/dom/tests/mochitest/webcomponents/mochitest.ini
@@ -11,16 +11,19 @@ support-files =
 [test_fallback_dest_insertion_points.html]
 [test_dynamic_content_element_matching.html]
 [test_document_register.html]
 [test_document_register_base_queue.html]
 [test_document_register_lifecycle.html]
 [test_document_register_parser.html]
 [test_document_register_stack.html]
 [test_document_shared_registry.html]
+[test_event_dispatch.html]
+[test_event_retarget.html]
+[test_event_stopping.html]
 [test_template.html]
 [test_template_xhtml.html]
 [test_shadowroot.html]
 [test_shadowroot_inert_element.html]
 [test_shadowroot_style.html]
 [test_shadowroot_style_multiple_shadow.html]
 [test_shadowroot_style_order.html]
 [test_style_fallback_content.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/webcomponents/test_event_dispatch.html
@@ -0,0 +1,458 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=887541
+-->
+<head>
+  <title>Test for event model in web components</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=887541">Bug 887541</a>
+<script>
+
+var els = SpecialPowers.Cc["@mozilla.org/eventlistenerservice;1"]
+            .getService(SpecialPowers.Ci.nsIEventListenerService);
+
+function eventListener(e) {
+  eventChain.push(this);
+}
+
+function isEventChain(actual, expected, msg) {
+  is(actual.length, expected.length, msg);
+  for (var i = 0; i < expected.length; i++) {
+    is(actual[i], expected[i], msg + " at " + i);
+  }
+
+  // Check to make sure the event chain matches what we get back from nsIEventListenerService.getEventTargetChainFor
+  if (0 < actual.length) {
+    var chain = els.getEventTargetChainFor(actual[0]); // Events should be dispatched on actual[0].
+    for (var i = 0; i < expected.length; i++) {
+      ok(SpecialPowers.compare(chain[i], expected[i]), msg + " at " + i + " for nsIEventListenerService");
+    }
+  }
+}
+
+/*
+ * Test 1: Test of event dispatch through a basic ShadowRoot with content a insertion point.
+ *
+ * <div elemOne> ------ <shadow-root shadowOne>
+ *        |                        |
+ * <div elemTwo>            <span elemThree>
+ *                                 |
+ *                         <content elemFour>
+ */
+
+var elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+var elemTwo = document.createElement("div");
+elemTwo.addEventListener("custom", eventListener);
+
+var elemThree = document.createElement("span");
+elemThree.addEventListener("custom", eventListener);
+
+var elemFour = document.createElement("content");
+elemFour.addEventListener("custom", eventListener);
+
+var shadowOne = elemOne.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+elemThree.appendChild(elemFour);
+shadowOne.appendChild(elemThree);
+elemOne.appendChild(elemTwo);
+
+var eventChain = [];
+var customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemTwo.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemTwo, elemFour, elemThree, shadowOne, elemOne], "Event path for test 1 for event dispatched on elemTwo.");
+
+/*
+ * Test 2: Test of event dispatch through a nested ShadowRoots with content insertion points.
+ *
+ * <div elemFive> --- <shadow-root shadowTwo>
+ *       |                       |
+ * <div elemOne>          <div elemFour> ----- <shadow-root shadowOne>
+ *                               |                        |
+ *                       <content elemTwo>           <p elemSix>
+ *                                                        |
+ *                                               <content elemThree>
+ */
+
+elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+elemTwo = document.createElement("content");
+elemTwo.addEventListener("custom", eventListener);
+
+elemThree = document.createElement("content");
+elemThree.addEventListener("custom", eventListener);
+
+var elemFour = document.createElement("div");
+elemFour.addEventListener("custom", eventListener);
+
+var elemFive = document.createElement("div");
+elemFive.addEventListener("custom", eventListener);
+
+var elemSix = document.createElement("p");
+elemSix.addEventListener("custom", eventListener);
+
+var shadowOne = elemFour.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+var shadowTwo = elemFive.createShadowRoot();
+shadowTwo.addEventListener("custom", eventListener);
+
+elemFive.appendChild(elemOne);
+shadowTwo.appendChild(elemFour);
+elemFour.appendChild(elemTwo);
+shadowOne.appendChild(elemSix);
+elemSix.appendChild(elemThree);
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemOne.dispatchEvent(customEvent);
+is(elemOne.getDestinationInsertionPoints().length, 2, "yes");
+isEventChain(eventChain, [elemOne, elemThree, elemSix, shadowOne, elemTwo, elemFour, shadowTwo, elemFive], "Event path for test 2 for event dispatched on elemOne.");
+
+/*
+ * Test 3: Test of event dispatch through nested ShadowRoot with content insertion points.
+ *
+ * <div elemOne> ------- <shadow-root shadowOne>
+ *        |                        |
+ * <span elemTwo>          <span elemThree> ------------ <shadow-root shadowTwo>
+ *                                 |                               |
+ *                          <span elemFour>                <content elemSix>
+ *                                 |
+ *                         <content elemFive>
+ */
+
+elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+elemTwo = document.createElement("span");
+elemTwo.addEventListener("custom", eventListener);
+
+elemThree = document.createElement("span");
+elemThree.addEventListener("custom", eventListener);
+
+elemFour = document.createElement("span");
+elemFour.addEventListener("custom", eventListener);
+
+elemFive = document.createElement("content");
+elemFive.addEventListener("custom", eventListener);
+
+elemSix = document.createElement("content");
+elemSix.addEventListener("custom", eventListener);
+
+shadowOne = elemOne.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+shadowTwo = elemThree.createShadowRoot();
+shadowTwo.addEventListener("custom", eventListener);
+
+elemOne.appendChild(elemTwo);
+shadowOne.appendChild(elemThree);
+elemThree.appendChild(elemFour);
+elemFour.appendChild(elemFive);
+shadowTwo.appendChild(elemSix);
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemTwo.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemTwo, elemFive, elemFour, elemSix, shadowTwo, elemThree, shadowOne, elemOne], "Event path for test 3 for event dispatched on elemTwo.");
+
+/*
+ * Test 4: Test of event dispatch through host with multiple ShadowRoots with shadow insertion point.
+ *
+ * <div elemSeven> --- <shadow-root shadowTwo> (younger ShadowRoot)
+ *       |         |             |
+ * <div elemOne>   |      <div elemSix> -------- <shadow-root shadowOne>
+ *                 |             |                         |
+ *                 |     <shadow elemFour>         <content elemFive>
+ *                 |             |
+ *                 |     <content elemTwo>
+ *                 |
+ *                 --- <shadow-root shadowThree> (older ShadowRoot)
+ *                         |                |
+ *                         |       <content elemThree>
+ *                         |
+ *                  <div elemEight>
+ */
+
+elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+elemTwo = document.createElement("content");
+elemTwo.addEventListener("custom", eventListener);
+
+elemThree = document.createElement("content");
+elemThree.addEventListener("custom", eventListener);
+
+elemFour = document.createElement("shadow");
+elemFour.addEventListener("custom", eventListener);
+
+elemFive = document.createElement("content");
+elemFive.addEventListener("custom", eventListener);
+
+elemSix = document.createElement("div");
+elemSix.addEventListener("custom", eventListener);
+
+var elemSeven = document.createElement("div");
+elemSeven.addEventListener("custom", eventListener);
+
+var elemEight = document.createElement("div");
+elemEight.addEventListener("custom", eventListener);
+
+var shadowThree = elemSeven.createShadowRoot();
+shadowThree.addEventListener("custom", eventListener);
+
+shadowTwo = elemSeven.createShadowRoot();
+shadowTwo.addEventListener("custom", eventListener);
+
+shadowOne = elemSix.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+elemSeven.appendChild(elemOne);
+shadowTwo.appendChild(elemSix);
+elemSix.appendChild(elemFour);
+elemFour.appendChild(elemTwo);
+shadowThree.appendChild(elemEight);
+shadowThree.appendChild(elemThree);
+shadowOne.appendChild(elemFive);
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemOne.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemOne, elemFive, shadowOne, elemThree, shadowThree, elemTwo, elemFour, elemSix, shadowTwo, elemSeven], "Event path for test 4 for event dispatched on elemOne.");
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemEight.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemEight, elemFive, shadowOne, elemSix, shadowTwo, elemSeven], "Event path for test 4 for event dispatched on elemEight.");
+
+/*
+ * Test 5: Test of event dispatch through nested shadowroot with insertion points that match specific tags.
+ *
+ * <div elemOne> --------- <shadow-root shadowOne>
+ *    |      |                        |
+ *    |  <p elemThree>        <span elemFour> ------------------------ <shadow-root shadowTwo>
+ *    |                          |       |                                        |
+ * <span elemTwo>                |   <content select="p" elemFive>       <content elemSeven>
+ *                               |
+ *                       <content select="span" elemSix>
+ */
+
+elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+elemTwo = document.createElement("span");
+elemTwo.addEventListener("custom", eventListener);
+
+elemThree = document.createElement("p");
+elemThree.addEventListener("custom", eventListener);
+
+elemFour = document.createElement("span");
+elemFour.addEventListener("custom", eventListener);
+
+elemFive = document.createElement("content");
+elemFive.select = "p";
+elemFive.addEventListener("custom", eventListener);
+
+elemSix = document.createElement("content");
+elemSix.select = "span";
+elemSix.addEventListener("custom", eventListener);
+
+elemSeven = document.createElement("content");
+elemSeven.addEventListener("custom", eventListener);
+
+shadowTwo = elemFour.createShadowRoot();
+shadowTwo.addEventListener("custom", eventListener);
+
+shadowOne = elemOne.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+elemOne.appendChild(elemTwo);
+elemOne.appendChild(elemThree);
+shadowOne.appendChild(elemFour);
+elemFour.appendChild(elemSix);
+elemFour.appendChild(elemFive);
+shadowTwo.appendChild(elemSeven);
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemTwo.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemTwo, elemSeven, shadowTwo, elemSix, elemFour, shadowOne, elemOne], "Event path for test 5 for event dispatched on elemTwo.");
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemSeven, shadowTwo, elemFive, elemFour, shadowOne, elemOne], "Event path for test 5 for event dispatched on elemThree.");
+
+/*
+ * Test 6: Test of event dispatch through nested shadowroot with insertion points that match specific tags.
+ *
+ * <div elemOne> --------- <shadow-root shadowOne>;
+ *    |      |                        |
+ *    |  <p elemThree>         <span elemFour> ------ <shadow-root shadowTwo>
+ *    |                               |                   |            |
+ * <span elemTwo>            <content elemFive>           |  <content select="p" elemSeven>
+ *                                                        |
+ *                                                 <content select="span" elemSix>
+ */
+
+elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+elemTwo = document.createElement("span");
+elemTwo.addEventListener("custom", eventListener);
+
+elemThree = document.createElement("p");
+elemThree.addEventListener("custom", eventListener);
+
+elemFour = document.createElement("span");
+elemFour.addEventListener("custom", eventListener);
+
+elemFive = document.createElement("content");
+elemFive.addEventListener("custom", eventListener);
+
+elemSix = document.createElement("content");
+elemSix.select = "span";
+elemSix.addEventListener("custom", eventListener);
+
+elemSeven = document.createElement("content");
+elemSeven.select = "p";
+elemSeven.addEventListener("custom", eventListener);
+
+shadowTwo = elemFour.createShadowRoot();
+shadowTwo.addEventListener("custom", eventListener);
+
+shadowOne = elemOne.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+elemOne.appendChild(elemTwo);
+elemOne.appendChild(elemThree);
+shadowOne.appendChild(elemFour);
+elemFour.appendChild(elemFive);
+shadowTwo.appendChild(elemSix);
+shadowTwo.appendChild(elemSeven);
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemTwo.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemTwo, elemSix, shadowTwo, elemFive, elemFour, shadowOne, elemOne], "Event path for test 6 for event dispatched on elemTwo.");
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemSeven, shadowTwo, elemFive, elemFour, shadowOne, elemOne], "Event path for test 6 for event dispatched on elemThree.");
+
+/*
+ * Test 7: Test of event dispatch through nested shadowroot with insertion points that match specific tags.
+ *
+ * <div elemOne> --------- <shadow-root shadowOne>
+ *    |      |                        |
+ *    |  <p elemThree>         <span elemFour> ------ <shadow-root shadowTwo>
+ *    |                               |                         |
+ * <span elemTwo>            <content elemFive>           <span elemEight>
+ *                                                           |        |
+ *                                                           |   <content select="p" elemSeven>
+ *                                                           |
+ *                                              <content select="span" elemSix>
+ */
+
+elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+elemTwo = document.createElement("span");
+elemTwo.addEventListener("custom", eventListener);
+
+elemThree = document.createElement("p");
+elemThree.addEventListener("custom", eventListener);
+
+elemFour = document.createElement("span");
+elemFour.addEventListener("custom", eventListener);
+
+elemFive = document.createElement("content");
+elemFive.addEventListener("custom", eventListener);
+
+elemSix = document.createElement("content");
+elemSix.select = "span";
+elemSix.addEventListener("custom", eventListener);
+
+elemSeven = document.createElement("content");
+elemSeven.select = "p";
+elemSeven.addEventListener("custom", eventListener);
+
+elemEight = document.createElement("span");
+elemEight.addEventListener("custom", eventListener);
+
+shadowTwo = elemFour.createShadowRoot();
+shadowTwo.addEventListener("custom", eventListener);
+
+shadowOne = elemOne.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+elemOne.appendChild(elemTwo);
+elemOne.appendChild(elemThree);
+shadowOne.appendChild(elemFour);
+elemFour.appendChild(elemFive);
+shadowTwo.appendChild(elemEight);
+elemEight.appendChild(elemSix);
+elemEight.appendChild(elemSeven);
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemTwo.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemTwo, elemSix, elemEight, shadowTwo, elemFive, elemFour, shadowOne, elemOne], "Event path for test 7 for event dispatched on elemTwo.");
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemSeven, elemEight, shadowTwo, elemFive, elemFour, shadowOne, elemOne], "Event path for test 7 for event dispatched on elemThree.");
+
+/*
+ * Test 8: Test of event dispatch through host with multiple ShadowRoots with shadow insertion point.
+ *
+ * <div elemOne> --- <shadow-root shadowOne> (younger ShadowRoot)
+ *               |             |
+ *               |      <div elemFour>
+ *               |             |
+ *               |     <shadow elemTwo>
+ *               |
+ *               --- <shadow-root shadowTwo> (older ShadowRoot)
+ *                             |
+ *                      <div elemThree>
+ */
+
+elemOne = document.createElement("div");
+elemOne.addEventListener("custom", eventListener);
+
+elemTwo = document.createElement("shadow");
+elemTwo.addEventListener("custom", eventListener);
+
+elemThree = document.createElement("div");
+elemThree.addEventListener("custom", eventListener);
+
+elemFour = document.createElement("div");
+elemFour.addEventListener("custom", eventListener);
+
+shadowTwo = elemOne.createShadowRoot();
+shadowTwo.addEventListener("custom", eventListener);
+
+shadowOne = elemOne.createShadowRoot();
+shadowOne.addEventListener("custom", eventListener);
+
+shadowOne.appendChild(elemFour);
+elemFour.appendChild(elemTwo);
+shadowTwo.appendChild(elemThree);
+
+eventChain = [];
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, shadowTwo, elemTwo, elemFour, shadowOne, elemOne], "Event path for test 8 for event dispatched on elemThree.");
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/webcomponents/test_event_retarget.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=887541
+-->
+<head>
+  <title>Test for event retargeting in web components</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=887541">Bug 887541</a>
+<script>
+
+/*
+ * Creates an event listener with an expected event target.
+ */
+function createEventListener(expectedTarget, msg) {
+  return function(e) {
+    is(e.target, expectedTarget, msg);
+  };
+}
+
+/*
+ * Test of event retargeting through a basic ShadowRoot with a content insertion point.
+ *
+ * <div elemThree> ---- <shadow-root shadowOne>
+ *        |                        |
+ * <div elemOne>            <content elemTwo>
+ *
+ * Dispatch event on elemOne
+ */
+
+var elemOne = document.createElement("div");
+var elemTwo = document.createElement("content");
+var elemThree = document.createElement("div");
+var shadowOne = elemThree.createShadowRoot();
+
+elemThree.appendChild(elemOne);
+shadowOne.appendChild(elemTwo);
+
+elemOne.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemOne."));
+elemTwo.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemTwo."));
+elemThree.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemThree."));
+shadowOne.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of shadowOne."));
+
+var customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemOne.dispatchEvent(customEvent);
+
+/*
+ * Test of event retargeting through a basic ShadowRoot with a content insertion point.
+ *
+ * <div elemThree> ---- <shadow-root shadowOne>
+ *        |                        |
+ * <div elemOne>            <content elemTwo>
+ *
+ * Dispatch event on elemTwo
+ */
+
+elemOne = document.createElement("div");
+elemTwo = document.createElement("content");
+elemThree = document.createElement("div");
+shadowOne = elemThree.createShadowRoot();
+
+elemThree.appendChild(elemOne);
+shadowOne.appendChild(elemTwo);
+
+elemTwo.addEventListener("custom", createEventListener(elemTwo, "elemTwo is in common ancestor tree of elemTwo."));
+elemThree.addEventListener("custom", createEventListener(elemThree, "elemThree is in common ancestor tree of elemThree."));
+shadowOne.addEventListener("custom", createEventListener(elemTwo, "elemTwo is in common ancestor tree of shadowOne."));
+
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemTwo.dispatchEvent(customEvent);
+
+/*
+ * Test of event retargeting through a nested ShadowRoots with content insertion points.
+ *
+ * <div elemFive> --- <shadow-root shadowTwo>
+ *       |                       |
+ * <div elemOne>          <div elemFour> ----- <shadow-root shadowOne>
+ *                               |                        |
+ *                       <content elemTwo>       <content elemThree>
+ *
+ * Dispatch custom event on elemOne.
+ */
+
+elemOne = document.createElement("div");
+elemTwo = document.createElement("content");
+elemThree = document.createElement("content");
+var elemFour = document.createElement("div");
+var elemFive = document.createElement("div");
+var shadowTwo = elemFive.createShadowRoot();
+shadowOne = elemFour.createShadowRoot();
+
+elemFive.appendChild(elemOne);
+shadowTwo.appendChild(elemFour);
+elemFour.appendChild(elemTwo);
+shadowOne.appendChild(elemThree);
+
+elemOne.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemOne."));
+elemTwo.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemTwo."));
+elemThree.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemThree."));
+elemFour.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemFour."));
+elemFive.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of elemFive."));
+shadowOne.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of shadowOne."));
+shadowTwo.addEventListener("custom", createEventListener(elemOne, "elemOne is in common ancestor tree of shadowTwo."));
+
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemOne.dispatchEvent(customEvent);
+
+/*
+ * Test of event retargeting through a nested ShadowRoots with content insertion points.
+ *
+ * <div elemFive> --- <shadow-root shadowTwo>
+ *       |                       |
+ * <div elemOne>          <div elemFour> ----- <shadow-root shadowOne>
+ *                               |                        |
+ *                       <content elemTwo>       <content elemThree>
+ *
+ * Dispatch custom event on elemThree.
+ */
+
+elemOne = document.createElement("div");
+elemTwo = document.createElement("content");
+elemThree = document.createElement("content");
+elemFour = document.createElement("div");
+elemFive = document.createElement("div");
+shadowTwo = elemFive.createShadowRoot();
+shadowOne = elemFour.createShadowRoot();
+
+elemFive.appendChild(elemOne);
+shadowTwo.appendChild(elemFour);
+elemFour.appendChild(elemTwo);
+shadowOne.appendChild(elemThree);
+
+elemThree.addEventListener("custom", createEventListener(elemThree, "elemThree is in common ancestor tree of elemThree."));
+elemFour.addEventListener("custom", createEventListener(elemFour, "elemFour is in common ancestor tree of elemFour."));
+elemFive.addEventListener("custom", createEventListener(elemFive, "elemFive is in common ancestor tree of elemFive."));
+shadowOne.addEventListener("custom", createEventListener(elemThree, "elemThree is in common ancestor tree of shadowOne."));
+shadowTwo.addEventListener("custom", createEventListener(elemFour, "elemFour is in common ancestor tree of shadowTwo."));
+
+customEvent = new CustomEvent("custom", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/webcomponents/test_event_stopping.html
@@ -0,0 +1,168 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=887541
+-->
+<head>
+  <title>Test for event model in web components</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=887541">Bug 887541</a>
+<script>
+
+var els = SpecialPowers.Cc["@mozilla.org/eventlistenerservice;1"]
+            .getService(SpecialPowers.Ci.nsIEventListenerService);
+
+function eventListener(e) {
+  eventChain.push(this);
+}
+
+function isEventChain(actual, expected, msg) {
+  is(actual.length, expected.length, msg);
+  for (var i = 0; i < expected.length; i++) {
+    is(actual[i], expected[i], msg + " at " + i);
+  }
+
+  if (0 < actual.length) {
+    var chain = els.getEventTargetChainFor(actual[0]); // Events should be dispatched on actual[0].
+    ok(expected.length < chain.length, "There should be additional chrome event targets.");
+  }
+}
+
+/*
+ * <div elemOne> ------ <shadow-root shadowOne>
+ *                                 |
+ *                          <span elemTwo>
+ *                                 |
+ *                         <span elemThree>
+ */
+
+var elemOne = document.createElement("div");
+var elemTwo = document.createElement("span");
+var elemThree = document.createElement("span");
+var shadowOne = elemOne.createShadowRoot();
+
+shadowOne.appendChild(elemTwo);
+elemTwo.appendChild(elemThree);
+
+// Test stopping "abort" event.
+
+elemOne.addEventListener("abort", eventListener);
+elemTwo.addEventListener("abort", eventListener);
+elemThree.addEventListener("abort", eventListener);
+shadowOne.addEventListener("abort", eventListener);
+
+var eventChain = [];
+
+var customEvent = new CustomEvent("abort", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that abort event is stopped at shadow root.");
+
+// Test stopping "error" event.
+
+elemOne.addEventListener("error", eventListener);
+elemTwo.addEventListener("error", eventListener);
+elemThree.addEventListener("error", eventListener);
+shadowOne.addEventListener("error", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("error", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that error event is stopped at shadow root.");
+
+// Test stopping "select" event.
+
+elemOne.addEventListener("select", eventListener);
+elemTwo.addEventListener("select", eventListener);
+elemThree.addEventListener("select", eventListener);
+shadowOne.addEventListener("select", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("select", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that select event is stopped at shadow root.");
+
+// Test stopping "change" event.
+
+elemOne.addEventListener("change", eventListener);
+elemTwo.addEventListener("change", eventListener);
+elemThree.addEventListener("change", eventListener);
+shadowOne.addEventListener("change", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("change", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+
+// Test stopping "reset" event.
+
+elemOne.addEventListener("reset", eventListener);
+elemTwo.addEventListener("reset", eventListener);
+elemThree.addEventListener("reset", eventListener);
+shadowOne.addEventListener("reset", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("reset", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that reset event is stopped at shadow root.");
+
+// Test stopping "load" event.
+
+elemOne.addEventListener("load", eventListener);
+elemTwo.addEventListener("load", eventListener);
+elemThree.addEventListener("load", eventListener);
+shadowOne.addEventListener("load", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("load", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that load event is stopped at shadow root.");
+
+// Test stopping "resize" event.
+
+elemOne.addEventListener("resize", eventListener);
+elemTwo.addEventListener("resize", eventListener);
+elemThree.addEventListener("resize", eventListener);
+shadowOne.addEventListener("resize", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("resize", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that resize event is stopped at shadow root.");
+
+// Test stopping "scroll" event.
+
+elemOne.addEventListener("scroll", eventListener);
+elemTwo.addEventListener("scroll", eventListener);
+elemThree.addEventListener("scroll", eventListener);
+shadowOne.addEventListener("scroll", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("scroll", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that scroll event is stopped at shadow root.");
+
+// Test stopping "selectstart" event.
+
+elemOne.addEventListener("selectstart", eventListener);
+elemTwo.addEventListener("selectstart", eventListener);
+elemThree.addEventListener("selectstart", eventListener);
+shadowOne.addEventListener("selectstart", eventListener);
+
+eventChain = [];
+
+customEvent = new CustomEvent("selectstart", { "bubbles" : true });
+elemThree.dispatchEvent(customEvent);
+isEventChain(eventChain, [elemThree, elemTwo, shadowOne], "Test that selectstart event is stopped at shadow root.");
+
+</script>
+</body>
+</html>