Bug 1483882 - Teach IDTracker about Shadow DOM. r=smaug
authorEmilio Cobos Álvarez <emilio@crisal.io>
Fri, 17 Aug 2018 11:35:15 +0000
changeset 487285 3b7671939ef8e47308674f77de1e644008bc9965
parent 487284 2c6eaa99679f4b2fd142cad85e0279216d7c86c7
child 487286 b439cdd35e49d81383827aa03a1b6f0bdfb7f274
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1483882
milestone63.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 1483882 - Teach IDTracker about Shadow DOM. r=smaug Differential Revision: https://phabricator.services.mozilla.com/D3533
dom/base/DocumentOrShadowRoot.cpp
dom/base/DocumentOrShadowRoot.h
dom/base/FragmentOrElement.cpp
dom/base/IDTracker.cpp
dom/base/IDTracker.h
dom/base/nsDocument.cpp
dom/base/nsIDocument.h
dom/smil/nsSMILTimeValueSpec.cpp
layout/reftests/svg/fragid-shadow-1.html
layout/reftests/svg/fragid-shadow-2.html
layout/reftests/svg/fragid-shadow-3.html
layout/reftests/svg/fragid-shadow-4.html
layout/reftests/svg/fragid-shadow-5.html
layout/reftests/svg/fragid-shadow-6.html
layout/reftests/svg/fragid-shadow-7.html
layout/reftests/svg/fragid-shadow-8.html
layout/reftests/svg/fragid-shadow-ref.html
layout/reftests/svg/fragid-shadow-resource.svg
layout/reftests/svg/reftest.list
--- a/dom/base/DocumentOrShadowRoot.cpp
+++ b/dom/base/DocumentOrShadowRoot.cpp
@@ -344,16 +344,28 @@ DocumentOrShadowRoot::RemoveIDTargetObse
   nsIdentifierMapEntry* entry = mIdentifierMap.GetEntry(aID);
   if (!entry) {
     return;
   }
 
   entry->RemoveContentChangeCallback(aObserver, aData, aForImage);
 }
 
+
+Element*
+DocumentOrShadowRoot::LookupImageElement(const nsAString& aId)
+{
+  if (aId.IsEmpty()) {
+    return nullptr;
+  }
+
+  nsIdentifierMapEntry* entry = mIdentifierMap.GetEntry(aId);
+  return entry ? entry->GetImageIdElement() : nullptr;
+}
+
 void
 DocumentOrShadowRoot::ReportEmptyGetElementByIdArg()
 {
   nsContentUtils::ReportEmptyGetElementByIdArg(AsNode().OwnerDoc());
 }
 
 }
 }
--- a/dom/base/DocumentOrShadowRoot.h
+++ b/dom/base/DocumentOrShadowRoot.h
@@ -156,16 +156,25 @@ public:
   /**
    * Remove the (aObserver, aData, aForImage) triple for a specific ID, if
    * registered.
    */
   void RemoveIDTargetObserver(nsAtom* aID, IDTargetObserver aObserver,
                               void* aData, bool aForImage);
 
   /**
+   * Lookup an image element using its associated ID, which is usually provided
+   * by |-moz-element()|. Similar to GetElementById, with the difference that
+   * elements set using mozSetImageElement have higher priority.
+   * @param aId the ID associated the element we want to lookup
+   * @return the element associated with |aId|
+   */
+  Element* LookupImageElement(const nsAString& aElementId);
+
+  /**
    * Check that aId is not empty and log a message to the console
    * service if it is.
    * @returns true if aId looks correct, false otherwise.
    */
   inline bool CheckGetElementByIdArg(const nsAString& aId)
   {
     if (aId.IsEmpty()) {
       ReportEmptyGetElementByIdArg();
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -1459,20 +1459,16 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Fr
     }
   } else if (!tmp->GetParent() && tmp->HasChildren()) {
     ContentUnbinder::Append(tmp);
   } /* else {
     The subtree root will end up to a ContentUnbinder, and that will
     unbind the child nodes.
   } */
 
-  // Clear flag here because unlinking slots will clear the
-  // containing shadow root pointer.
-  tmp->UnsetFlags(NODE_IS_IN_SHADOW_TREE);
-
   if (ShadowRoot* shadowRoot = tmp->GetShadowRoot()) {
     for (nsIContent* child = shadowRoot->GetFirstChild();
          child;
          child = child->GetNextSibling()) {
       child->UnbindFromTree(true, false);
     }
 
     shadowRoot->SetIsComposedDocParticipant(false);
--- a/dom/base/IDTracker.cpp
+++ b/dom/base/IDTracker.cpp
@@ -12,49 +12,66 @@
 #include "nsBindingManager.h"
 #include "nsEscape.h"
 #include "nsXBLPrototypeBinding.h"
 #include "nsCycleCollectionParticipant.h"
 
 namespace mozilla {
 namespace dom {
 
+static DocumentOrShadowRoot*
+DocOrShadowFromContent(nsIContent& aContent)
+{
+  ShadowRoot* shadow = aContent.GetContainingShadow();
+
+  // We never look in <svg:use> shadow trees, for backwards compat.
+  while (shadow && shadow->Host()->IsSVGElement(nsGkAtoms::use)) {
+    shadow = shadow->Host()->GetContainingShadow();
+  }
+
+  if (shadow) {
+    return shadow;
+  }
+
+  return aContent.OwnerDoc();
+}
+
 void
-IDTracker::Reset(nsIContent* aFromContent, nsIURI* aURI,
-                 bool aWatch, bool aReferenceImage)
+IDTracker::Reset(nsIContent* aFromContent,
+                 nsIURI* aURI,
+                 bool aWatch,
+                 bool aReferenceImage)
 {
   MOZ_ASSERT(aFromContent, "Reset() expects non-null content pointer");
 
   Unlink();
 
   if (!aURI)
     return;
 
   nsAutoCString refPart;
   aURI->GetRef(refPart);
   // Unescape %-escapes in the reference. The result will be in the
   // document charset, hopefully...
   NS_UnescapeURL(refPart);
 
-  // Get the current document
-  nsIDocument *doc = aFromContent->OwnerDoc();
-  if (!doc) {
-    return;
-  }
+  // Get the thing to observe changes to.
+  nsIDocument* doc = aFromContent->OwnerDoc();
+  DocumentOrShadowRoot* docOrShadow = DocOrShadowFromContent(*aFromContent);
+  auto encoding = doc->GetDocumentCharacterSet();
 
-  auto encoding = doc->GetDocumentCharacterSet();
   nsAutoString ref;
   nsresult rv = encoding->DecodeWithoutBOMHandling(refPart, ref);
   if (NS_FAILED(rv) || ref.IsEmpty()) {
     return;
   }
   rv = NS_OK;
 
   nsIContent* bindingParent = aFromContent->GetBindingParent();
-  if (bindingParent) {
+  if (bindingParent && !aFromContent->IsInShadowTree()) {
     nsXBLBinding* binding = bindingParent->GetXBLBinding();
     if (!binding) {
       // This happens, for example, if aFromContent is part of the content
       // inserted by a call to nsIDocument::InsertAnonymousContent, which we
       // also want to handle.  (It also happens for <use>'s anonymous
       // content etc.)
       Element* anonRoot =
         doc->GetAnonRootIfInAnonymousContentContainer(aFromContent);
@@ -95,157 +112,152 @@ IDTracker::Reset(nsIContent* aFromConten
   }
 
   bool isEqualExceptRef;
   rv = aURI->EqualsExceptRef(doc->GetDocumentURI(), &isEqualExceptRef);
   if (NS_FAILED(rv) || !isEqualExceptRef) {
     RefPtr<nsIDocument::ExternalResourceLoad> load;
     doc = doc->RequestExternalResource(aURI, aFromContent,
                                        getter_AddRefs(load));
+    docOrShadow = doc;
     if (!doc) {
       if (!load || !aWatch) {
         // Nothing will ever happen here
         return;
       }
 
       DocumentLoadNotification* observer =
         new DocumentLoadNotification(this, ref);
       mPendingNotification = observer;
-      if (observer) {
-        load->AddObserver(observer);
-      }
+      load->AddObserver(observer);
       // Keep going so we set up our watching stuff a bit
     }
   }
 
   if (aWatch) {
     RefPtr<nsAtom> atom = NS_Atomize(ref);
     if (!atom)
       return;
     atom.swap(mWatchID);
   }
 
   mReferencingImage = aReferenceImage;
-
-  HaveNewDocument(doc, aWatch, ref);
+  HaveNewDocumentOrShadowRoot(docOrShadow, aWatch, ref);
 }
 
 void
-IDTracker::ResetWithID(nsIContent* aFromContent, const nsString& aID,
+IDTracker::ResetWithID(nsIContent* aFromContent,
+                       nsAtom* aID,
                        bool aWatch)
 {
-  nsIDocument *doc = aFromContent->OwnerDoc();
-  if (!doc)
-    return;
-
-  // XXX Need to take care of XBL/XBL2
+  MOZ_ASSERT(aFromContent);
+  MOZ_ASSERT(aID);
 
   if (aWatch) {
-    RefPtr<nsAtom> atom = NS_Atomize(aID);
-    if (!atom)
-      return;
+    RefPtr<nsAtom> atom = aID;
     atom.swap(mWatchID);
   }
 
   mReferencingImage = false;
 
-  HaveNewDocument(doc, aWatch, aID);
+  DocumentOrShadowRoot* docOrShadow = DocOrShadowFromContent(*aFromContent);
+  HaveNewDocumentOrShadowRoot(docOrShadow, aWatch, nsDependentAtomString(aID));
 }
 
 void
-IDTracker::HaveNewDocument(nsIDocument* aDocument, bool aWatch,
-                           const nsString& aRef)
+IDTracker::HaveNewDocumentOrShadowRoot(
+  DocumentOrShadowRoot* aDocOrShadow,
+  bool aWatch,
+  const nsString& aRef)
 {
   if (aWatch) {
-    mWatchDocument = aDocument;
-    if (mWatchDocument) {
-      mElement = mWatchDocument->AddIDTargetObserver(mWatchID, Observe, this,
-                                                     mReferencingImage);
+    mWatchDocumentOrShadowRoot = nullptr;
+    if (aDocOrShadow) {
+      mWatchDocumentOrShadowRoot = &aDocOrShadow->AsNode();
+      mElement = aDocOrShadow->AddIDTargetObserver(mWatchID, Observe, this, mReferencingImage);
     }
     return;
   }
 
-  if (!aDocument) {
+  if (!aDocOrShadow) {
     return;
   }
 
-  Element *e = mReferencingImage ? aDocument->LookupImageElement(aRef) :
-                                   aDocument->GetElementById(aRef);
+  Element* e = mReferencingImage ? aDocOrShadow->LookupImageElement(aRef)
+                                 : aDocOrShadow->GetElementById(aRef);
   if (e) {
     mElement = e;
   }
 }
 
 void
 IDTracker::Traverse(nsCycleCollectionTraversalCallback* aCB)
 {
-  NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCB, "mWatchDocument");
-  aCB->NoteXPCOMChild(mWatchDocument);
-  NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCB, "mContent");
+  NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCB, "mWatchDocumentOrShadowRoot");
+  aCB->NoteXPCOMChild(mWatchDocumentOrShadowRoot);
+  NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCB, "mElement");
   aCB->NoteXPCOMChild(mElement);
 }
 
 void
 IDTracker::Unlink()
 {
-  if (mWatchDocument && mWatchID) {
-    mWatchDocument->RemoveIDTargetObserver(mWatchID, Observe, this,
-                                           mReferencingImage);
+  if (mWatchID) {
+    if (DocumentOrShadowRoot* docOrShadow = GetWatchDocOrShadowRoot()) {
+      docOrShadow->RemoveIDTargetObserver(
+        mWatchID, Observe, this, mReferencingImage);
+    }
   }
   if (mPendingNotification) {
     mPendingNotification->Clear();
     mPendingNotification = nullptr;
   }
-  mWatchDocument = nullptr;
+  mWatchDocumentOrShadowRoot = nullptr;
   mWatchID = nullptr;
   mElement = nullptr;
   mReferencingImage = false;
 }
 
 bool
-IDTracker::Observe(Element* aOldElement,
-                   Element* aNewElement, void* aData)
+IDTracker::Observe(Element* aOldElement, Element* aNewElement, void* aData)
 {
   IDTracker* p = static_cast<IDTracker*>(aData);
   if (p->mPendingNotification) {
     p->mPendingNotification->SetTo(aNewElement);
   } else {
     NS_ASSERTION(aOldElement == p->mElement, "Failed to track content!");
     ChangeNotification* watcher =
       new ChangeNotification(p, aOldElement, aNewElement);
     p->mPendingNotification = watcher;
     nsContentUtils::AddScriptRunner(watcher);
   }
   bool keepTracking = p->IsPersistent();
   if (!keepTracking) {
-    p->mWatchDocument = nullptr;
+    p->mWatchDocumentOrShadowRoot = nullptr;
     p->mWatchID = nullptr;
   }
   return keepTracking;
 }
 
-NS_IMPL_ISUPPORTS_INHERITED0(IDTracker::ChangeNotification,
-                             mozilla::Runnable)
-
-NS_IMPL_ISUPPORTS(IDTracker::DocumentLoadNotification,
-                  nsIObserver)
+NS_IMPL_ISUPPORTS_INHERITED0(IDTracker::ChangeNotification, mozilla::Runnable)
+NS_IMPL_ISUPPORTS(IDTracker::DocumentLoadNotification, nsIObserver)
 
 NS_IMETHODIMP
 IDTracker::DocumentLoadNotification::Observe(nsISupports* aSubject,
                                              const char* aTopic,
                                              const char16_t* aData)
 {
   NS_ASSERTION(PL_strcmp(aTopic, "external-resource-document-created") == 0,
                "Unexpected topic");
   if (mTarget) {
     nsCOMPtr<nsIDocument> doc = do_QueryInterface(aSubject);
     mTarget->mPendingNotification = nullptr;
     NS_ASSERTION(!mTarget->mElement, "Why do we have content here?");
     // If we got here, that means we had Reset() called with aWatch ==
     // true.  So keep watching if IsPersistent().
-    mTarget->HaveNewDocument(doc, mTarget->IsPersistent(), mRef);
+    mTarget->HaveNewDocumentOrShadowRoot(doc, mTarget->IsPersistent(), mRef);
     mTarget->ElementChanged(nullptr, mTarget->mElement);
   }
   return NS_OK;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/base/IDTracker.h
+++ b/dom/base/IDTracker.h
@@ -4,16 +4,17 @@
  * 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/. */
 
 #ifndef mozilla_dom_IDTracker_h_
 #define mozilla_dom_IDTracker_h_
 
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/Element.h"
+#include "mozilla/dom/ShadowRoot.h"
 #include "nsAtom.h"
 #include "nsIDocument.h"
 #include "nsThreadUtils.h"
 
 class nsIURI;
 
 namespace mozilla {
 namespace dom {
@@ -34,20 +35,20 @@ namespace dom {
  * the changed-to element.
  * Override IsPersistent to return true if you want to keep tracking after
  * the first change.
  */
 class IDTracker {
 public:
   typedef mozilla::dom::Element Element;
 
-  IDTracker()
-    : mReferencingImage(false)
-  {}
-  ~IDTracker() {
+  IDTracker() = default;
+
+  ~IDTracker()
+  {
     Unlink();
   }
 
   /**
    * Find which element, if any, is referenced.
    */
   Element* get() { return mElement; }
 
@@ -58,61 +59,64 @@ public:
    * @param aFrom the source element for context
    * @param aURI the URI containing a hash-reference to the element
    * @param aWatch if false, then we do not set up the notifications to track
    * changes, so ElementChanged won't fire and get() will always return the same
    * value, the current element for the ID.
    * @param aReferenceImage whether the ID references image elements which are
    * subject to the document's mozSetImageElement overriding mechanism.
    */
-  void Reset(nsIContent* aFrom, nsIURI* aURI, bool aWatch = true,
+  void Reset(nsIContent* aFrom,
+             nsIURI* aURI,
+             bool aWatch = true,
              bool aReferenceImage = false);
 
   /**
    * A variation on Reset() to set up a reference that consists of the ID of
    * an element in the same document as aFrom.
    * @param aFrom the source element for context
    * @param aID the ID of the element
    * @param aWatch if false, then we do not set up the notifications to track
    * changes, so ElementChanged won't fire and get() will always return the same
    * value, the current element for the ID.
    */
-  void ResetWithID(nsIContent* aFrom, const nsString& aID,
-                   bool aWatch = true);
+  void ResetWithID(nsIContent* aFrom, nsAtom* aID, bool aWatch = true);
 
   /**
    * Clears the reference. ElementChanged is not triggered. get() will return
    * null.
    */
   void Unlink();
 
   void Traverse(nsCycleCollectionTraversalCallback* aCB);
 
 protected:
   /**
    * Override this to be notified of element changes. Don't forget
    * to call this superclass method to change mElement. This is called
    * at script-runnable time.
    */
-  virtual void ElementChanged(Element* aFrom, Element* aTo) {
+  virtual void ElementChanged(Element* aFrom, Element* aTo)
+  {
     mElement = aTo;
   }
 
   /**
    * Override this to convert from a single-shot notification to
    * a persistent notification.
    */
   virtual bool IsPersistent() { return false; }
 
   /**
    * Set ourselves up with our new document.  Note that aDocument might be
    * null.  Either aWatch must be false or aRef must be empty.
    */
-  void HaveNewDocument(nsIDocument* aDocument, bool aWatch,
-                       const nsString& aRef);
+  void HaveNewDocumentOrShadowRoot(DocumentOrShadowRoot*,
+                                   bool aWatch,
+                                   const nsString& aRef);
 
 private:
   static bool Observe(Element* aOldElement,
                         Element* aNewElement, void* aData);
 
   class Notification : public nsISupports {
   public:
     virtual void SetTo(Element* aTo) = 0;
@@ -162,19 +166,18 @@ private:
     RefPtr<Element> mTo;
   };
   friend class ChangeNotification;
 
   class DocumentLoadNotification : public Notification,
                                    public nsIObserver
   {
   public:
-    DocumentLoadNotification(IDTracker* aTarget,
-                             const nsString& aRef) :
-      Notification(aTarget)
+    DocumentLoadNotification(IDTracker* aTarget, const nsString& aRef)
+      : Notification(aTarget)
     {
       if (!mTarget->IsPersistent()) {
         mRef = aRef;
       }
     }
 
     NS_DECL_ISUPPORTS
     NS_DECL_NSIOBSERVER
@@ -182,21 +185,34 @@ private:
     virtual ~DocumentLoadNotification() {}
 
     virtual void SetTo(Element* aTo) override { }
 
     nsString mRef;
   };
   friend class DocumentLoadNotification;
 
-  RefPtr<nsAtom>      mWatchID;
-  nsCOMPtr<nsIDocument>  mWatchDocument;
+  DocumentOrShadowRoot* GetWatchDocOrShadowRoot() const
+  {
+    if (!mWatchDocumentOrShadowRoot) {
+      return nullptr;
+    }
+    MOZ_ASSERT(mWatchDocumentOrShadowRoot->IsDocument() ||
+               mWatchDocumentOrShadowRoot->IsShadowRoot());
+    if (ShadowRoot* shadow = ShadowRoot::FromNode(*mWatchDocumentOrShadowRoot)) {
+      return shadow;
+    }
+    return mWatchDocumentOrShadowRoot->AsDocument();
+  }
+
+  RefPtr<nsAtom> mWatchID;
+  nsCOMPtr<nsINode> mWatchDocumentOrShadowRoot; // Always a `DocumentOrShadowRoot`.
   RefPtr<Element> mElement;
   RefPtr<Notification> mPendingNotification;
-  bool                   mReferencingImage;
+  bool mReferencingImage = false;
 };
 
 inline void
 ImplCycleCollectionUnlink(IDTracker& aField)
 {
   aField.Unlink();
 }
 
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -4948,26 +4948,16 @@ nsIDocument::MozSetImageElement(const ns
   if (entry) {
     entry->SetImageElement(aElement);
     if (entry->IsEmpty()) {
       mIdentifierMap.RemoveEntry(entry);
     }
   }
 }
 
-Element*
-nsIDocument::LookupImageElement(const nsAString& aId)
-{
-  if (aId.IsEmpty())
-    return nullptr;
-
-  nsIdentifierMapEntry* entry = mIdentifierMap.GetEntry(aId);
-  return entry ? entry->GetImageIdElement() : nullptr;
-}
-
 void
 nsIDocument::DispatchContentLoadedEvents()
 {
   // If you add early returns from this method, make sure you're
   // calling UnblockOnload properly.
 
   // Unpin references to preloaded images
   mPreloadingImages.Clear();
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -2839,25 +2839,16 @@ public:
     mChangeScrollPosWhenScrollingToRef = aValue;
   }
 
   using mozilla::dom::DocumentOrShadowRoot::GetElementById;
   using mozilla::dom::DocumentOrShadowRoot::GetElementsByTagName;
   using mozilla::dom::DocumentOrShadowRoot::GetElementsByTagNameNS;
   using mozilla::dom::DocumentOrShadowRoot::GetElementsByClassName;
 
-  /**
-   * Lookup an image element using its associated ID, which is usually provided
-   * by |-moz-element()|. Similar to GetElementById, with the difference that
-   * elements set using mozSetImageElement have higher priority.
-   * @param aId the ID associated the element we want to lookup
-   * @return the element associated with |aId|
-   */
-  Element* LookupImageElement(const nsAString& aElementId);
-
   mozilla::dom::DocumentTimeline* Timeline();
   mozilla::LinkedList<mozilla::dom::DocumentTimeline>& Timelines()
   {
     return mTimelines;
   }
 
   void GetAnimations(nsTArray<RefPtr<mozilla::dom::Animation>>& aAnimations);
 
--- a/dom/smil/nsSMILTimeValueSpec.cpp
+++ b/dom/smil/nsSMILTimeValueSpec.cpp
@@ -100,18 +100,17 @@ nsSMILTimeValueSpec::ResolveReferences(n
     return;
 
   // Hold ref to the old element so that it isn't destroyed in between resetting
   // the referenced element and using the pointer to update the referenced
   // element.
   RefPtr<Element> oldReferencedElement = mReferencedElement.get();
 
   if (mParams.mDependentElemID) {
-    mReferencedElement.ResetWithID(aContextNode,
-        nsDependentAtomString(mParams.mDependentElemID));
+    mReferencedElement.ResetWithID(aContextNode, mParams.mDependentElemID);
   } else if (mParams.mType == nsSMILTimeValueSpecParams::EVENT) {
     Element* target = mOwner->GetTargetElement();
     mReferencedElement.ResetWithElement(target);
   } else {
     MOZ_ASSERT(false, "Syncbase or repeat spec without ID");
   }
   UpdateReferencedElement(oldReferencedElement, mReferencedElement.get());
 }
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-1.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<svg style="position: absolute; width: 0; height: 0">
+  <defs>
+    <pattern id="rect" width="100" height="100">
+      <rect fill="red" width="100" height="100" />
+    </pattern>
+  </defs>
+</svg>
+<div id="host"></div>
+<script>
+  // Should peek the pattern from the shadow root (green), not from the document (red).
+  host.attachShadow({ mode: "open" }).innerHTML = `
+    <svg width="100" height="100">
+      <defs>
+        <pattern id="rect" width="100" height="100">
+          <rect fill="lime" width="100" height="100" />
+        </pattern>
+      </defs>
+      <rect fill="url(#rect)" width="100" height="100" />
+    </svg>
+  `;
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-2.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<div id="host"></div>
+<script>
+  // Test dynamic id changes inside the shadow root.
+  host.attachShadow({ mode: "open" }).innerHTML = `
+    <svg width="100" height="100">
+      <defs>
+        <pattern id="rect1" width="100" height="100">
+          <rect fill="lime" width="100" height="100" />
+        </pattern>
+      </defs>
+      <rect fill="url(#rect)" width="100" height="100" />
+    </svg>
+  `;
+  document.body.offsetTop;
+  host.shadowRoot.getElementById("rect1").id = "rect";
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-3.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<div id="host"></div>
+<svg height="0">
+  <!-- use an empty g to force fragid-shadow-resource.svg to load before onload -->
+  <use href="fragid-shadow-resource.svg#empty">
+</svg>
+<script>
+  // Test that external resource URIs resolve properly inside shadow trees.
+  host.attachShadow({ mode: "open" }).innerHTML = `
+    <svg width="100" height="100">
+      <rect fill="url(fragid-shadow-resource.svg#rect)" width="100" height="100" />
+    </svg>
+  `;
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-4.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<div id="host"></div>
+<script>
+  // Test references in <svg:use> work properly.
+  host.attachShadow({ mode: "open" }).innerHTML = `
+    <svg width="100" height="100">
+      <defs>
+        <rect fill="lime" id="rect-4" width="100" height="100">
+      </defs>
+      <use href="#rect-4" />
+    </svg>
+  `;
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-5.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<div id="host"></div>
+<script>
+  // Test absolute URIs inside shadow trees, which behave the same way as just the fragment id.
+  host.attachShadow({ mode: "open" }).innerHTML = `
+    <svg width="100" height="100">
+      <defs>
+        <rect fill="lime" id="rect-5" width="100" height="100">
+      </defs>
+      <use href="${location.href}#rect-5" />
+    </svg>
+  `;
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-6.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<svg style="position: absolute; width: 0; height: 0">
+  <defs>
+    <pattern id="rect" width="100" height="100">
+      <rect fill="red" width="100" height="100" />
+    </pattern>
+  </defs>
+</svg>
+<div id="host"></div>
+<script>
+  // Should peek the pattern from the shadow root (green), not from the document (red),
+  // even though the uri is absolute.
+  host.attachShadow({ mode: "open" }).innerHTML = `
+    <svg width="100" height="100">
+      <defs>
+        <pattern id="rect" width="100" height="100">
+          <rect fill="lime" width="100" height="100" />
+        </pattern>
+      </defs>
+      <rect fill="url(${location.href}#rect)" width="100" height="100" />
+    </svg>
+  `;
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-7.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<svg style="position: absolute; width: 0; height: 0">
+  <defs>
+    <pattern id="rect" width="100" height="100">
+      <rect fill="red" width="100" height="100" />
+    </pattern>
+  </defs>
+</svg>
+<div id="host"></div>
+<script>
+  // Test references from a <svg:use> subtree.
+  host.attachShadow({ mode: "open" }).innerHTML = `
+    <svg width="100" height="100">
+      <defs>
+        <pattern id="rect" width="100" height="100">
+          <rect fill="lime" width="100" height="100" />
+        </pattern>
+        <symbol id="useme">
+          <pattern id="rect" width="100" height="100">
+            <rect fill="red" width="100" height="100" />
+          </pattern>
+          <rect fill="url(#rect)" width="100" height="100" />
+        </symbol>
+      </defs>
+      <use href="#useme" />
+    </svg>
+  `;
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-8.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<svg width="100" height="100">
+  <defs>
+    <pattern id="rect" width="100" height="100">
+      <rect fill="lime" width="100" height="100" />
+    </pattern>
+    <symbol id="useme">
+      <pattern id="rect" width="100" height="100">
+        <rect fill="red" width="100" height="100" />
+      </pattern>
+      <rect fill="url(#rect)" width="100" height="100" />
+    </symbol>
+  </defs>
+  <use href="#useme" />
+</svg>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-ref.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<svg width="100" height="100">
+  <rect fill="lime" width="100" height="100" />
+</svg>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/fragid-shadow-resource.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <g id="empty" />
+    <pattern id="rect" width="100" height="100">
+      <rect fill="lime" width="100" height="100" />
+    </pattern>
+  </defs>
+</svg>
--- a/layout/reftests/svg/reftest.list
+++ b/layout/reftests/svg/reftest.list
@@ -547,8 +547,18 @@ fuzzy-if(skiaContent,0-1,0-100) == tspan
 == winding-01.svg pass.svg
 
 == zero-stroke-01.svg pass.svg
 
 # currentColor override by color attribute
 == currentColor-override-flood.svg pass.svg
 == currentColor-override-lighting.svg currentColor-override-lighting-ref.svg
 == currentColor-override-stop.svg pass.svg
+
+# Shadow DOM id tracking.
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-1.html fragid-shadow-ref.html
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-2.html fragid-shadow-ref.html
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-3.html fragid-shadow-ref.html
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-4.html fragid-shadow-ref.html
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-5.html fragid-shadow-ref.html
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-6.html fragid-shadow-ref.html
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-7.html fragid-shadow-ref.html
+pref(dom.webcomponents.shadowdom.enabled,true) == fragid-shadow-8.html fragid-shadow-ref.html