Bug 1510848 - Do not unattach UA Widget Shadow Root if the element is already re-attached to the tree. r=emilio,smaug, a=RyanVM
authorTimothy Guan-tin Chien <timdream@gmail.com>
Sat, 15 Dec 2018 02:48:46 +0000
changeset 506327 1a45a417a33af7317ff0be891363dacdbdf98b36
parent 506326 fb3197004db503cbbb1cda1c0fbacd21f364dc47
child 506328 dd09f113cb8a6936dd8d92b7b5a425eb43019af0
push id10370
push userryanvm@gmail.com
push dateThu, 20 Dec 2018 03:01:30 +0000
treeherdermozilla-beta@fde04f5ec920 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersemilio, smaug, RyanVM
bugs1510848
milestone65.0
Bug 1510848 - Do not unattach UA Widget Shadow Root if the element is already re-attached to the tree. r=emilio,smaug, a=RyanVM This patch moves all UA Widget calls to helper functions in Element.cpp. The helper function AttachAndSetUAShadowRoot sets the shadow root in a runnable, so that it is in the same order of NotifyUAWidget* runnables. Differential Revision: https://phabricator.services.mozilla.com/D13479
dom/base/Element.cpp
dom/base/Element.h
dom/base/nsObjectLoadingContent.cpp
dom/html/HTMLInputElement.cpp
dom/html/HTMLMarqueeElement.cpp
dom/html/HTMLMediaElement.cpp
dom/html/nsGenericHTMLElement.cpp
dom/html/nsGenericHTMLElement.h
dom/media/tests/crashtests/1510848.html
dom/media/tests/crashtests/crashtests.list
layout/base/nsCSSFrameConstructor.cpp
mobile/android/chrome/geckoview/GeckoViewMediaChild.js
toolkit/actors/UAWidgetsChild.jsm
toolkit/content/widgets/docs/ua_widget.rst
toolkit/modules/ActorManagerParent.jsm
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -1193,16 +1193,83 @@ already_AddRefed<ShadowRoot> Element::At
   }
 
   /**
    * 6. Return shadow.
    */
   return shadowRoot.forget();
 }
 
+void Element::AttachAndSetUAShadowRoot() {
+  MOZ_DIAGNOSTIC_ASSERT(!CanAttachShadowDOM(),
+                        "Cannot be used to attach UI shadow DOM");
+
+  // Attach the UA Widget Shadow Root in a runnable so that the code runs
+  // in the same order of NotifyUAWidget* calls.
+  nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+      "Element::AttachAndSetUAShadowRoot::Runnable",
+      [self = RefPtr<Element>(this)]() {
+        if (self->GetShadowRoot()) {
+          MOZ_ASSERT(self->GetShadowRoot()->IsUAWidget());
+          return;
+        }
+
+        RefPtr<ShadowRoot> shadowRoot =
+            self->AttachShadowWithoutNameChecks(ShadowRootMode::Closed);
+        shadowRoot->SetIsUAWidget();
+      }));
+}
+
+void Element::NotifyUAWidgetSetupOrChange() {
+  MOZ_ASSERT(IsInComposedDoc());
+  // Schedule a runnable, ensure the event dispatches before
+  // returning to content script.
+  // This event cause UA Widget to construct or cause onattributechange callback
+  // of existing UA Widget to run; dispatching this event twice should not cause
+  // UA Widget to re-init.
+  nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+      "Element::NotifyUAWidgetSetupOrChange::UAWidgetSetupOrChange",
+      [self = RefPtr<Element>(this),
+       ownerDoc = RefPtr<nsIDocument>(OwnerDoc())]() {
+        MOZ_ASSERT(self->GetShadowRoot() &&
+                   self->GetShadowRoot()->IsUAWidget());
+
+        nsContentUtils::DispatchChromeEvent(
+            ownerDoc, self, NS_LITERAL_STRING("UAWidgetSetupOrChange"),
+            CanBubble::eYes, Cancelable::eNo);
+      }));
+}
+
+void Element::NotifyUAWidgetTeardown(UnattachShadowRoot aUnattachShadowRoot) {
+  MOZ_ASSERT(IsInComposedDoc());
+  // The runnable will dispatch an event to tear down UA Widget,
+  // and unattach the Shadow Root.
+  nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+      "Element::NotifyUAWidgetTeardownAndUnattachShadow::UAWidgetTeardown",
+      [aUnattachShadowRoot, self = RefPtr<Element>(this),
+       ownerDoc = RefPtr<nsIDocument>(OwnerDoc())]() {
+        if (!self->GetShadowRoot()) {
+          // No UA Widget Shadow Root was ever attached.
+          return;
+        }
+        MOZ_ASSERT(self->GetShadowRoot()->IsUAWidget());
+
+        nsresult rv = nsContentUtils::DispatchChromeEvent(
+            ownerDoc, self, NS_LITERAL_STRING("UAWidgetTeardown"),
+            CanBubble::eYes, Cancelable::eNo);
+        if (NS_WARN_IF(NS_FAILED(rv))) {
+          return;
+        }
+
+        if (aUnattachShadowRoot == UnattachShadowRoot::Yes) {
+          self->UnattachShadow();
+        }
+      }));
+}
+
 void Element::UnattachShadow() {
   ShadowRoot* shadowRoot = GetShadowRoot();
   if (!shadowRoot) {
     return;
   }
 
   nsAutoScriptBlocker scriptBlocker;
 
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -1198,16 +1198,33 @@ class Element : public FragmentOrElement
 
   // Shadow DOM v1
   already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit,
                                             ErrorResult& aError);
   bool CanAttachShadowDOM() const;
 
   already_AddRefed<ShadowRoot> AttachShadowWithoutNameChecks(
       ShadowRootMode aMode);
+
+  // Attach UA Shadow Root if it is not attached.
+  void AttachAndSetUAShadowRoot();
+
+  // Dispatch an event to UAWidgetsChild, triggering construction
+  // or onattributechange callback on the existing widget.
+  void NotifyUAWidgetSetupOrChange();
+
+  enum class UnattachShadowRoot {
+    No,
+    Yes,
+  };
+
+  // Dispatch an event to UAWidgetsChild, triggering UA Widget destruction.
+  // and optionally remove the shadow root.
+  void NotifyUAWidgetTeardown(UnattachShadowRoot = UnattachShadowRoot::Yes);
+
   void UnattachShadow();
 
   ShadowRoot* GetShadowRootByMode() const;
   void SetSlot(const nsAString& aName, ErrorResult& aError);
   void GetSlot(nsAString& aName);
 
   ShadowRoot* GetShadowRoot() const {
     const nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots();
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -593,26 +593,18 @@ void nsObjectLoadingContent::UnbindFromT
     // nsImageLoadingContent handles the image case.
     // Reset state and clear pending events
     /// XXX(johns): The implementation for GenericFrame notes that ideally we
     ///             would keep the docshell around, but trash the frameloader
     UnloadObject();
   }
 
   // Unattach plugin problem UIWidget if any.
-  if (thisElement->IsInComposedDoc() && thisElement->GetShadowRoot()) {
-    nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
-        "nsObjectLoadingContent::UnbindFromTree::UAWidgetUnbindFromTree",
-        [thisElement]() {
-          nsContentUtils::DispatchChromeEvent(
-              thisElement->OwnerDoc(), thisElement,
-              NS_LITERAL_STRING("UAWidgetUnbindFromTree"), CanBubble::eYes,
-              Cancelable::eNo);
-          thisElement->UnattachShadow();
-        }));
+  if (thisElement->IsInComposedDoc() && nsContentUtils::IsUAWidgetEnabled()) {
+    thisElement->NotifyUAWidgetTeardown();
   }
 
   if (mType == eType_Plugin) {
     nsIDocument* doc = thisElement->GetComposedDoc();
     if (doc && doc->IsActive()) {
       nsCOMPtr<nsIRunnable> ev =
           new nsSimplePluginEvent(doc, NS_LITERAL_STRING("PluginRemoved"));
       NS_DispatchToCurrentThread(ev);
@@ -2586,32 +2578,20 @@ void nsObjectLoadingContent::NotifyState
           NS_EVENT_STATE_TYPE_CLICK_TO_PLAY |
           NS_EVENT_STATE_VULNERABLE_UPDATABLE |
           NS_EVENT_STATE_VULNERABLE_NO_UPDATE;
 
       bool hadProblemState = !(aOldState & pluginProblemState).IsEmpty();
       bool hasProblemState = !(newState & pluginProblemState).IsEmpty();
 
       if (hadProblemState && !hasProblemState) {
-        nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
-            "nsObjectLoadingContent::UnbindFromTree::UAWidgetUnbindFromTree",
-            [thisEl]() {
-              nsContentUtils::DispatchChromeEvent(
-                  thisEl->OwnerDoc(), thisEl,
-                  NS_LITERAL_STRING("UAWidgetUnbindFromTree"), CanBubble::eYes,
-                  Cancelable::eNo);
-              thisEl->UnattachShadow();
-            }));
+        thisEl->NotifyUAWidgetTeardown();
       } else if (!hadProblemState && hasProblemState) {
-        nsGenericHTMLElement::FromNode(thisEl)->AttachAndSetUAShadowRoot();
-
-        AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
-            thisEl, NS_LITERAL_STRING("UAWidgetBindToTree"), CanBubble::eYes,
-            ChromeOnlyDispatch::eYes);
-        dispatcher->RunDOMEventWhenSafe();
+        thisEl->AttachAndSetUAShadowRoot();
+        thisEl->NotifyUAWidgetSetupOrChange();
       }
     }
   } else if (aOldType != mType) {
     // If our state changed, then we already recreated frames
     // Otherwise, need to do that here
     nsCOMPtr<nsIPresShell> shell = doc->GetShell();
     if (shell) {
       shell->PostRecreateFramesFor(thisEl);
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -4326,20 +4326,17 @@ nsresult HTMLInputElement::BindToTree(ns
 
   // And now make sure our state is up to date
   UpdateState(false);
 
   if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) &&
       nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
     // Construct Shadow Root so web content can be hidden in the DOM.
     AttachAndSetUAShadowRoot();
-    AsyncEventDispatcher* dispatcher =
-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("UAWidgetBindToTree"),
-                                 CanBubble::eYes, ChromeOnlyDispatch::eYes);
-    dispatcher->RunDOMEventWhenSafe();
+    NotifyUAWidgetSetupOrChange();
   }
 
   if (mType == NS_FORM_INPUT_PASSWORD) {
     if (IsInComposedDoc()) {
       AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
           this, NS_LITERAL_STRING("DOMInputPasswordAdded"), CanBubble::eYes,
           ChromeOnlyDispatch::eYes);
       dispatcher->PostDOMEvent();
@@ -4358,26 +4355,19 @@ void HTMLInputElement::UnbindFromTree(bo
   // nsGenericHTMLFormElementWithState::UnbindFromTree() will unset the form and
   // that takes care of form's WillRemove so we just have to take care
   // of the case where we're removing from the document and we don't
   // have a form
   if (!mForm && mType == NS_FORM_INPUT_RADIO) {
     WillRemoveFromRadioGroup();
   }
 
-  if (GetShadowRoot() && IsInComposedDoc()) {
-    nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
-        "HTMLInputElement::UnbindFromTree::UAWidgetUnbindFromTree",
-        [self = RefPtr<Element>(this)]() {
-          nsContentUtils::DispatchChromeEvent(
-              self->OwnerDoc(), self,
-              NS_LITERAL_STRING("UAWidgetUnbindFromTree"), CanBubble::eYes,
-              Cancelable::eNo);
-          self->UnattachShadow();
-        }));
+  if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) &&
+      nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
+    NotifyUAWidgetTeardown();
   }
 
   nsImageLoadingContent::UnbindFromTree(aDeep, aNullParent);
   nsGenericHTMLFormElementWithState::UnbindFromTree(aDeep, aNullParent);
 
   // GetCurrentDoc is returning nullptr so we can update the value
   // missing validity state to reflect we are no longer into a doc.
   UpdateValueMissingValidityState();
@@ -4546,40 +4536,25 @@ void HTMLInputElement::HandleTypeChange(
         ChromeOnlyDispatch::eYes);
     dispatcher->PostDOMEvent();
   }
 
   if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
     if (oldType == NS_FORM_INPUT_TIME || oldType == NS_FORM_INPUT_DATE) {
       if (mType != NS_FORM_INPUT_TIME && mType != NS_FORM_INPUT_DATE) {
         // Switch away from date/time type.
-        RefPtr<Element> self = this;
-        nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
-            "HTMLInputElement::UnbindFromTree::UAWidgetUnbindFromTree",
-            [self]() {
-              nsContentUtils::DispatchChromeEvent(
-                  self->OwnerDoc(), self,
-                  NS_LITERAL_STRING("UAWidgetUnbindFromTree"), CanBubble::eYes,
-                  Cancelable::eNo);
-              self->UnattachShadow();
-            }));
+        NotifyUAWidgetTeardown();
       } else {
         // Switch between date and time.
-        AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
-            this, NS_LITERAL_STRING("UAWidgetAttributeChanged"),
-            CanBubble::eYes, ChromeOnlyDispatch::eYes);
-        dispatcher->RunDOMEventWhenSafe();
+        NotifyUAWidgetSetupOrChange();
       }
     } else if (mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) {
       // Switch to date/time type.
       AttachAndSetUAShadowRoot();
-      AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
-          this, NS_LITERAL_STRING("UAWidgetBindToTree"), CanBubble::eYes,
-          ChromeOnlyDispatch::eYes);
-      dispatcher->RunDOMEventWhenSafe();
+      NotifyUAWidgetSetupOrChange();
     }
   }
 }
 
 void HTMLInputElement::SanitizeValue(nsAString& aValue) {
   NS_ASSERTION(mDoneCreating, "The element creation should be finished!");
 
   switch (mType) {
--- a/dom/html/HTMLMarqueeElement.cpp
+++ b/dom/html/HTMLMarqueeElement.cpp
@@ -56,31 +56,27 @@ nsresult HTMLMarqueeElement::BindToTree(
                                         nsIContent* aParent,
                                         nsIContent* aBindingParent) {
   nsresult rv =
       nsGenericHTMLElement::BindToTree(aDocument, aParent, aBindingParent);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
     AttachAndSetUAShadowRoot();
-    AsyncEventDispatcher* dispatcher =
-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("UAWidgetBindToTree"),
-                                 CanBubble::eYes, ChromeOnlyDispatch::eYes);
-    dispatcher->RunDOMEventWhenSafe();
+    NotifyUAWidgetSetupOrChange();
   }
 
   return rv;
 }
 
 void HTMLMarqueeElement::UnbindFromTree(bool aDeep, bool aNullParent) {
-  if (GetShadowRoot() && IsInComposedDoc()) {
-    AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
-        this, NS_LITERAL_STRING("UAWidgetUnbindFromTree"), CanBubble::eYes,
-        ChromeOnlyDispatch::eYes);
-    dispatcher->RunDOMEventWhenSafe();
+  if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
+    // We don't want to unattach the shadow root because it used to
+    // contain a <slot>.
+    NotifyUAWidgetTeardown(UnattachShadowRoot::No);
   }
 
   nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
 }
 
 void HTMLMarqueeElement::SetStartStopCallback(
     FunctionStringCallback* aCallback) {
   mStartStopCallback = aCallback;
@@ -135,20 +131,17 @@ bool HTMLMarqueeElement::ParseAttribute(
 
 nsresult HTMLMarqueeElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
                                           const nsAttrValue* aValue,
                                           const nsAttrValue* aOldValue,
                                           nsIPrincipal* aMaybeScriptedPrincipal,
                                           bool aNotify) {
   if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc() &&
       aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::direction) {
-    AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
-        this, NS_LITERAL_STRING("UAWidgetAttributeChanged"), CanBubble::eYes,
-        ChromeOnlyDispatch::eYes);
-    dispatcher->RunDOMEventWhenSafe();
+    NotifyUAWidgetSetupOrChange();
   }
   return nsGenericHTMLElement::AfterSetAttr(
       aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
 }
 
 void HTMLMarqueeElement::MapAttributesIntoRule(
     const nsMappedAttributes* aAttributes, MappedDeclarations& aDecls) {
   nsGenericHTMLElement::MapImageMarginAttributeInto(aAttributes, aDecls);
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -3988,22 +3988,17 @@ nsresult HTMLMediaElement::AfterSetAttr(
     } else if (aName == nsGkAtoms::preload) {
       UpdatePreloadAction();
     } else if (aName == nsGkAtoms::loop) {
       if (mDecoder) {
         mDecoder->SetLooping(!!aValue);
       }
     } else if (nsContentUtils::IsUAWidgetEnabled() &&
                aName == nsGkAtoms::controls && IsInComposedDoc()) {
-      AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
-          this, NS_LITERAL_STRING("UAWidgetAttributeChanged"), CanBubble::eYes,
-          ChromeOnlyDispatch::eYes);
-      // This has to happen at this tick so that UA Widget could respond
-      // before returning to content script.
-      dispatcher->RunDOMEventWhenSafe();
+      NotifyUAWidgetSetupOrChange();
     }
   }
 
   // Since AfterMaybeChangeAttr may call DoLoad, make sure that it is called
   // *after* any possible changes to mSrcMediaSource.
   if (aValue) {
     AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify);
   }
@@ -4035,32 +4030,26 @@ nsresult HTMLMediaElement::BindToTree(ns
                                       nsIContent* aBindingParent) {
   nsresult rv =
       nsGenericHTMLElement::BindToTree(aDocument, aParent, aBindingParent);
 
   if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
     // Construct Shadow Root so web content can be hidden in the DOM.
     AttachAndSetUAShadowRoot();
 #ifdef ANDROID
-    AsyncEventDispatcher* dispatcher =
-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("UAWidgetBindToTree"),
-                                 CanBubble::eYes, ChromeOnlyDispatch::eYes);
-    dispatcher->RunDOMEventWhenSafe();
+    NotifyUAWidgetSetupOrChange();
 #else
     // We don't want to call into JS if the website never asks for native
     // video controls.
     // If controls attribute is set later, controls is constructed lazily
     // with the UAWidgetAttributeChanged event.
     // This only applies to Desktop because on Fennec we would need to show
     // an UI if the video is blocked.
     if (Controls()) {
-      AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(
-          this, NS_LITERAL_STRING("UAWidgetBindToTree"), CanBubble::eYes,
-          ChromeOnlyDispatch::eYes);
-      dispatcher->RunDOMEventWhenSafe();
+      NotifyUAWidgetSetupOrChange();
     }
 #endif
   }
 
   mUnboundFromTree = false;
 
   if (aDocument) {
     // The preload action depends on the value of the autoplay attribute.
@@ -4271,26 +4260,18 @@ void HTMLMediaElement::ReportTelemetry()
     }
   }
 }
 
 void HTMLMediaElement::UnbindFromTree(bool aDeep, bool aNullParent) {
   mUnboundFromTree = true;
   mVisibilityState = Visibility::UNTRACKED;
 
-  if (GetShadowRoot() && IsInComposedDoc()) {
-    nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
-        "HTMLMediaElement::UnbindFromTree::UAWidgetUnbindFromTree",
-        [self = RefPtr<Element>(this)]() {
-          nsContentUtils::DispatchChromeEvent(
-              self->OwnerDoc(), self,
-              NS_LITERAL_STRING("UAWidgetUnbindFromTree"), CanBubble::eYes,
-              Cancelable::eNo);
-          self->UnattachShadow();
-        }));
+  if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
+    NotifyUAWidgetTeardown();
   }
 
   nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
 
   MOZ_ASSERT(IsHidden());
   NotifyDecoderActivityChanges();
 
   RefPtr<HTMLMediaElement> self(this);
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -2564,29 +2564,16 @@ nsSize nsGenericHTMLElement::GetWidthHei
   NS_ASSERTION(size.height >= 0, "negative height");
   return size;
 }
 
 bool nsGenericHTMLElement::IsEventAttributeNameInternal(nsAtom* aName) {
   return nsContentUtils::IsEventAttributeName(aName, EventNameType_HTML);
 }
 
-void nsGenericHTMLElement::AttachAndSetUAShadowRoot() {
-  MOZ_DIAGNOSTIC_ASSERT(!CanAttachShadowDOM(),
-                        "Cannot be used to attach UI shadow DOM");
-  if (GetShadowRoot()) {
-    MOZ_ASSERT(GetShadowRoot()->IsUAWidget());
-    return;
-  }
-
-  RefPtr<ShadowRoot> shadowRoot =
-      AttachShadowWithoutNameChecks(ShadowRootMode::Closed);
-  shadowRoot->SetIsUAWidget();
-}
-
 /**
  * Construct a URI from a string, as an element.src attribute
  * would be set to. Helper for the media elements.
  */
 nsresult nsGenericHTMLElement::NewURIFromString(const nsAString& aURISpec,
                                                 nsIURI** aURI) {
   NS_ENSURE_ARG_POINTER(aURI);
 
--- a/dom/html/nsGenericHTMLElement.h
+++ b/dom/html/nsGenericHTMLElement.h
@@ -221,19 +221,16 @@ class nsGenericHTMLElement : public nsGe
     return mNodeInfo->Equals(aTag);
   }
 
   template <typename First, typename... Args>
   inline bool IsAnyOfHTMLElements(First aFirst, Args... aArgs) const {
     return IsNodeInternal(aFirst, aArgs...);
   }
 
-  // Attach UA Shadow Root if it is not attached.
-  void AttachAndSetUAShadowRoot();
-
  protected:
   virtual ~nsGenericHTMLElement() {}
 
  public:
   /**
    * Get width and height, using given image request if attributes are unset.
    * Pass a reference to the image request, since the method may change the
    * value and we want to use the updated value.
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/crashtests/1510848.html
@@ -0,0 +1,4 @@
+<a>
+<dd>
+<video>
+<a>
--- a/dom/media/tests/crashtests/crashtests.list
+++ b/dom/media/tests/crashtests/crashtests.list
@@ -21,8 +21,9 @@ load 1348381.html
 load 1367930_1.html
 load 1367930_2.html
 pref(browser.link.open_newwindow,2) load 1429507_1.html # window.open() in tab doesn't work for crashtest in e10s, this opens a new window instead
 pref(browser.link.open_newwindow,2) load 1429507_2.html # window.open() in tab doesn't work for crashtest in e10s, this opens a new window instead
 load 1453030.html
 skip-if(Android) load 1490700.html # No screenshare on Android
 load 1505957.html
 load 1511130.html
+load 1510848.html
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -1684,17 +1684,16 @@ void nsCSSFrameConstructor::CreateGenera
   MOZ_ASSERT(aPseudoElement == CSSPseudoElementType::before ||
                  aPseudoElement == CSSPseudoElementType::after,
              "unexpected aPseudoElement");
 
   if (aParentFrame && (aParentFrame->IsHTMLVideoFrame() ||
                        aParentFrame->IsDateTimeControlFrame())) {
     // Video frames and date time control frames may not be leafs when backed by
     // an UA widget, but we still don't want to expose generated content.
-    MOZ_ASSERT(aOriginatingElement.GetShadowRoot()->IsUAWidget());
     return;
   }
 
   ServoStyleSet* styleSet = mPresShell->StyleSet();
 
   // Probe for the existence of the pseudo-element
   RefPtr<ComputedStyle> pseudoStyle = styleSet->ProbePseudoElementStyle(
       aOriginatingElement, aPseudoElement, &aStyle);
--- a/mobile/android/chrome/geckoview/GeckoViewMediaChild.js
+++ b/mobile/android/chrome/geckoview/GeckoViewMediaChild.js
@@ -23,17 +23,17 @@ class GeckoViewMediaChild extends GeckoV
       this.handleMediaEvent(event);
     };
     this._fullscreenMedia = null;
     this._stateSymbol = Symbol();
   }
 
   onEnable() {
     debug `onEnable`;
-    addEventListener("UAWidgetBindToTree", this, false);
+    addEventListener("UAWidgetSetupOrChange", this, false);
     addEventListener("MozDOMFullscreen:Entered", this, false);
     addEventListener("MozDOMFullscreen:Exited", this, false);
     addEventListener("pagehide", this, false);
 
     this.messageManager.addMessageListener("GeckoView:MediaObserve", this);
     this.messageManager.addMessageListener("GeckoView:MediaUnobserve", this);
     this.messageManager.addMessageListener("GeckoView:MediaPlay", this);
     this.messageManager.addMessageListener("GeckoView:MediaPause", this);
@@ -41,17 +41,17 @@ class GeckoViewMediaChild extends GeckoV
     this.messageManager.addMessageListener("GeckoView:MediaSetVolume", this);
     this.messageManager.addMessageListener("GeckoView:MediaSetMuted", this);
     this.messageManager.addMessageListener("GeckoView:MediaSetPlaybackRate", this);
   }
 
   onDisable() {
     debug `onDisable`;
 
-    removeEventListener("UAWidgetBindToTree", this);
+    removeEventListener("UAWidgetSetupOrChange", this);
     removeEventListener("MozDOMFullscreen:Entered", this);
     removeEventListener("MozDOMFullscreen:Exited", this);
     removeEventListener("pagehide", this);
 
     this.messageManager.removeMessageListener("GeckoView:MediaObserve", this);
     this.messageManager.removeMessageListener("GeckoView:MediaUnobserve", this);
     this.messageManager.removeMessageListener("GeckoView:MediaPlay", this);
     this.messageManager.removeMessageListener("GeckoView:MediaPause", this);
@@ -98,17 +98,17 @@ class GeckoViewMediaChild extends GeckoV
         break;
     }
   }
 
   handleEvent(aEvent) {
     debug `handleEvent: ${aEvent.type}`;
 
     switch (aEvent.type) {
-      case "UAWidgetBindToTree":
+      case "UAWidgetSetupOrChange":
         this.handleNewMedia(aEvent.composedTarget);
         break;
       case "MozDOMFullscreen:Entered":
         const element = content && content.document.fullscreenElement;
         if (this.isMedia(element)) {
           this.handleFullscreenChange(element);
         } else {
           // document.fullscreenElement can be a div container instead of the HTMLMediaElement
--- a/toolkit/actors/UAWidgetsChild.jsm
+++ b/toolkit/actors/UAWidgetsChild.jsm
@@ -13,21 +13,20 @@ class UAWidgetsChild extends ActorChild 
   constructor(dispatcher) {
     super(dispatcher);
 
     this.widgets = new WeakMap();
   }
 
   handleEvent(aEvent) {
     switch (aEvent.type) {
-      case "UAWidgetBindToTree":
-      case "UAWidgetAttributeChanged":
+      case "UAWidgetSetupOrChange":
         this.setupOrNotifyWidget(aEvent.target);
         break;
-      case "UAWidgetUnbindFromTree":
+      case "UAWidgetTeardown":
         this.teardownWidget(aEvent.target);
         break;
     }
 
     // In case we are a nested frame, prevent the message manager of the
     // parent frame from receving the event.
     aEvent.stopPropagation();
   }
@@ -67,22 +66,23 @@ class UAWidgetsChild extends ActorChild 
         break;
       case "marquee":
         uri = "chrome://global/content/elements/marquee.js";
         widgetName = "MarqueeWidget";
         break;
     }
 
     if (!uri || !widgetName) {
+      Cu.reportError("Getting a UAWidgetSetupOrChange event on undefined element.");
       return;
     }
 
     let shadowRoot = aElement.openOrClosedShadowRoot;
     if (!shadowRoot) {
-      Cu.reportError("Getting a UAWidgetBindToTree/UAWidgetAttributeChanged event without the Shadow Root.");
+      Cu.reportError("Getting a UAWidgetSetupOrChange event without the Shadow Root.");
       return;
     }
 
     let isSystemPrincipal = aElement.nodePrincipal.isSystemPrincipal;
     let sandbox = isSystemPrincipal ?
       Object.create(null) : Cu.getUAWidgetScope(aElement.nodePrincipal);
 
     if (!sandbox[widgetName]) {
--- a/toolkit/content/widgets/docs/ua_widget.rst
+++ b/toolkit/content/widgets/docs/ua_widget.rst
@@ -6,27 +6,27 @@ Introduction
 
 UA Widgets are intended to be a replacement of our usage of XBL bindings in web content. These widgets run JavaScript inside extended principal per-origin sandboxes. They insert their own DOM inside of a special, closed Shadow Root inaccessible to the page, called a UA Widget Shadow Root.
 
 UA Widget lifecycle
 -------------------
 
 UA Widgets are generally constructed when the element is appended to the document and destroyed when the element is removed from the tree. Yet, in order to be fast, specialization was made to each of the widgets.
 
-When the element is appended to the tree, a chrome-only ``UAWidgetBindToTree`` event is dispatched and is caught by a frame script, namely UAWidgetsChild.
+When the element is appended to the tree, a chrome-only ``UAWidgetSetupOrChange`` event is dispatched and is caught by a frame script, namely UAWidgetsChild.
 
 UAWidgetsChild then grabs the sandbox for that origin (lazily creating it as needed), loads the script as needed, and initializes an instance by calling the JS constructor with a reference to the UA Widget Shadow Root created by the DOM. We will discuss the sandbox in the latter section.
 
 The ``onsetup`` method is called right after the instance is constructed. The call to constructor must not throw, or UAWidgetsChild will be confused since an instance of the widget will not be returned, but the widget is already half-initalized. If the ``onsetup`` method call throws, UAWidgetsChild will still be able to hold the reference of the widget and call the destructor later on.
 
-When the element is removed from the tree, ``UAWidgetUnbindFromTree`` is dispatched so UAWidgetsChild can destroy the widget, if it exists. If so, the UAWidgetsChild calls the ``destructor()`` method on the widget, causing the widget to destruct itself.
+When the element is removed from the tree, ``UAWidgetTeardown`` is dispatched so UAWidgetsChild can destroy the widget, if it exists. If so, the UAWidgetsChild calls the ``destructor()`` method on the widget, causing the widget to destruct itself.
 
 When a UA Widget initializes, it should create its own DOM inside the passed UA Widget Shadow Root, including the ``<link>`` element necessary to load the stylesheet, add event listeners, etc. When destroyed (i.e. the destructor method is called), it should do the opposite.
 
-**Specialization**: for video controls, we do not want to do the work if the control is not needed (i.e. when the ``<video>`` or ``<audio>`` element has no "controls" attribute set), so we forgo dispatching the event from HTMLMediaElement in the BindToTree method. Instead, another ``UAWidgetAttributeChanged`` event will cause the sandbox and the widget instance to construct when the attribute is set to true. The same event is also responsible for triggering the ``onchange()`` method on UA Widgets if the widget is already initialized.
+**Specialization**: for video controls, we do not want to do the work if the control is not needed (i.e. when the ``<video>`` or ``<audio>`` element has no "controls" attribute set), so we forgo dispatching the event from HTMLMediaElement in the BindToTree method. Instead, another ``UAWidgetSetupOrChange`` event will cause the sandbox and the widget instance to construct when the attribute is set to true. The same event is also responsible for triggering the ``onchange()`` method on UA Widgets if the widget is already initialized.
 
 Likewise, the datetime box widget is only loaded when the ``type`` attribute of an ``<input>`` is either `date` or `time`.
 
 The specialization does not apply to the lifecycle of the UA Widget Shadow Root. It is always constructed in order to suppress children of the DOM element from the web content from receiving a layout frame.
 
 UA Widget Shadow Root
 ---------------------
 
--- a/toolkit/modules/ActorManagerParent.jsm
+++ b/toolkit/modules/ActorManagerParent.jsm
@@ -262,19 +262,18 @@ let ACTORS = {
       ],
     },
   },
 
   UAWidgets: {
     child: {
       module: "resource://gre/actors/UAWidgetsChild.jsm",
       events: {
-        "UAWidgetBindToTree": {},
-        "UAWidgetAttributeChanged": {},
-        "UAWidgetUnbindFromTree": {},
+        "UAWidgetSetupOrChange": {},
+        "UAWidgetTeardown": {},
       },
     },
   },
 
   UnselectedTabHover: {
     child: {
       module: "resource://gre/actors/UnselectedTabHoverChild.jsm",
       events: {