Bug 1438210: Make slot assignment sound with layout after bug 1409975. r=smaug
authorEmilio Cobos Álvarez <emilio@crisal.io>
Tue, 13 Mar 2018 15:27:50 +0100
changeset 408056 6b06d04460074d2d4f81ada49ff36a57ccbf5ff7
parent 408055 b9370ab70cbf1ae719bc271830be8ce995e6938c
child 408057 7e5eb5740cac2dbac9d803d5151ef4958bc51862
push id100842
push userecoal95@gmail.com
push dateWed, 14 Mar 2018 09:54:41 +0000
treeherdermozilla-inbound@6b06d0446007 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1438210, 1409975
milestone61.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 1438210: Make slot assignment sound with layout after bug 1409975. r=smaug Summary: Before that bug, we carefully told layout about all distribution changes before they happened, so layout could cleanup frames and styles synchronously properly (since otherwise there's no way afterwards to figure out what the tree shape was). That bug made it not do it correctly, causing this. I obviously need to write a bunch of tests for this... Reviewers: smaug Bug #: 1438210 Differential Revision: https://phabricator.services.mozilla.com/D724 MozReview-Commit-ID: 8uupNhoFwme
dom/base/ShadowRoot.cpp
dom/base/ShadowRoot.h
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-001.html
testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-002.html
testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-003.html
testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-004.html
testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-005.html
--- a/dom/base/ShadowRoot.cpp
+++ b/dom/base/ShadowRoot.cpp
@@ -108,16 +108,35 @@ ShadowRoot::CloneInternalDataFrom(Shadow
         Servo_AuthorStyles_AppendStyleSheet(mServoStyles.get(),
                                             clonedSheet->AsServo());
       }
     }
   }
 }
 
 void
+ShadowRoot::InvalidateStyleAndLayoutOnSubtree(Element* aElement)
+{
+  MOZ_ASSERT(aElement);
+
+  if (!IsComposedDocParticipant()) {
+    return;
+  }
+
+  MOZ_ASSERT(GetComposedDoc() == OwnerDoc());
+
+  nsIPresShell* shell = OwnerDoc()->GetShell();
+  if (!shell) {
+    return;
+  }
+
+  shell->DestroyFramesForAndRestyle(aElement);
+}
+
+void
 ShadowRoot::AddSlot(HTMLSlotElement* aSlot)
 {
   MOZ_ASSERT(aSlot);
 
   // Note that if name attribute missing, the slot is a default slot.
   nsAutoString name;
   aSlot->GetName(name);
 
@@ -129,45 +148,48 @@ ShadowRoot::AddSlot(HTMLSlotElement* aSl
   TreeOrderComparator comparator;
   currentSlots->InsertElementSorted(aSlot, comparator);
 
   HTMLSlotElement* currentSlot = currentSlots->ElementAt(0);
   if (currentSlot != aSlot) {
     return;
   }
 
-  bool doEnqueueSlotChange = false;
   if (oldSlot && oldSlot != currentSlot) {
     // Move assigned nodes from old slot to new slot.
+    InvalidateStyleAndLayoutOnSubtree(oldSlot);
     const nsTArray<RefPtr<nsINode>>& assignedNodes = oldSlot->AssignedNodes();
+    bool doEnqueueSlotChange = false;
     while (assignedNodes.Length() > 0) {
       nsINode* assignedNode = assignedNodes[0];
 
       oldSlot->RemoveAssignedNode(assignedNode);
       currentSlot->AppendAssignedNode(assignedNode);
       doEnqueueSlotChange = true;
     }
 
     if (doEnqueueSlotChange) {
       oldSlot->EnqueueSlotChangeEvent();
       currentSlot->EnqueueSlotChangeEvent();
     }
   } else {
+    bool doEnqueueSlotChange = false;
     // Otherwise add appropriate nodes to this slot from the host.
     for (nsIContent* child = GetHost()->GetFirstChild();
          child;
          child = child->GetNextSibling()) {
       nsAutoString slotName;
       if (child->IsElement()) {
         child->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::slot, slotName);
       }
-      if (child->IsSlotable() && slotName.Equals(name)) {
-        currentSlot->AppendAssignedNode(child);
-        doEnqueueSlotChange = true;
+      if (!child->IsSlotable() || !slotName.Equals(name)) {
+        continue;
       }
+      doEnqueueSlotChange = true;
+      currentSlot->AppendAssignedNode(child);
     }
 
     if (doEnqueueSlotChange) {
       currentSlot->EnqueueSlotChangeEvent();
     }
   }
 }
 
@@ -338,29 +360,29 @@ ShadowRoot::GetEventTargetParent(EventCh
   nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mEvent->mTarget));
   if (content && content->GetBindingParent() == shadowHost) {
     aVisitor.mEventTargetAtParent = shadowHost;
   }
 
   return NS_OK;
 }
 
-const HTMLSlotElement*
-ShadowRoot::AssignSlotFor(nsIContent* aContent)
+ShadowRoot::SlotAssignment
+ShadowRoot::SlotAssignmentFor(nsIContent* aContent)
 {
   nsAutoString slotName;
   // Note that if slot attribute is missing, assign it to the first default
   // slot, if exists.
   if (aContent->IsElement()) {
     aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::slot, slotName);
   }
 
   nsTArray<HTMLSlotElement*>* slots = mSlotMap.Get(slotName);
   if (!slots) {
-    return nullptr;
+    return { };
   }
 
   HTMLSlotElement* slot = slots->ElementAt(0);
   MOZ_ASSERT(slot);
 
   // Find the appropriate position in the assigned node list for the
   // newly assigned content.
   const nsTArray<RefPtr<nsINode>>& assignedNodes = slot->AssignedNodes();
@@ -378,68 +400,58 @@ ShadowRoot::AssignSlotFor(nsIContent* aC
       currentContent = currentContent->GetNextSibling();
     }
 
     if (insertionIndex) {
       break;
     }
   }
 
-  if (insertionIndex) {
-    slot->InsertAssignedNode(*insertionIndex, aContent);
-  } else {
-    slot->AppendAssignedNode(aContent);
-  }
-
-  return slot;
+  return { slot, insertionIndex };
 }
 
-const HTMLSlotElement*
-ShadowRoot::UnassignSlotFor(nsIContent* aNode, const nsAString& aSlotName)
+void
+ShadowRoot::MaybeReassignElement(Element* aElement)
 {
-  // Find the insertion point to which the content belongs. Note that if slot
-  // attribute is missing, unassign it from the first default slot, if exists.
-  nsTArray<HTMLSlotElement*>* slots = mSlotMap.Get(aSlotName);
-  if (!slots) {
-    return nullptr;
-  }
+  MOZ_ASSERT(aElement->GetParent() == GetHost());
+  HTMLSlotElement* oldSlot = aElement->GetAssignedSlot();
+  SlotAssignment assignment = SlotAssignmentFor(aElement);
 
-  HTMLSlotElement* slot = slots->ElementAt(0);
-  MOZ_ASSERT(slot);
-
-  if (!slot->AssignedNodes().Contains(aNode)) {
-    return nullptr;
+  if (assignment.mSlot == oldSlot) {
+    // Nothing to do here.
+    return;
   }
 
-  slot->RemoveAssignedNode(aNode);
-  return slot;
-}
-
-bool
-ShadowRoot::MaybeReassignElement(Element* aElement,
-                                 const nsAttrValue* aOldValue)
-{
-  nsIContent* parent = aElement->GetParent();
-  if (parent && parent == GetHost()) {
-    const HTMLSlotElement* oldSlot = UnassignSlotFor(aElement,
-      aOldValue ? aOldValue->GetStringValue() : EmptyString());
-    const HTMLSlotElement* newSlot = AssignSlotFor(aElement);
-
-    if (oldSlot != newSlot) {
-      if (oldSlot) {
-        oldSlot->EnqueueSlotChangeEvent();
-      }
-      if (newSlot) {
-        newSlot->EnqueueSlotChangeEvent();
-      }
-      return true;
-    }
+  // If the old slot is about to become empty, let layout know that it needs to
+  // do work.
+  if (oldSlot && oldSlot->AssignedNodes().Length() == 1) {
+    InvalidateStyleAndLayoutOnSubtree(oldSlot);
+  }
+  // Ditto if the new slot will stop showing fallback content.
+  if (assignment.mSlot && assignment.mSlot->AssignedNodes().IsEmpty()) {
+    InvalidateStyleAndLayoutOnSubtree(assignment.mSlot);
   }
 
-  return false;
+  // Otherwise we only need to care about the reassigned element. Note that this
+  // is a no-op if we hit the `oldSlot` path above.
+  InvalidateStyleAndLayoutOnSubtree(aElement);
+
+  if (oldSlot) {
+    oldSlot->RemoveAssignedNode(aElement);
+    oldSlot->EnqueueSlotChangeEvent();
+  }
+
+  if (assignment.mSlot) {
+    if (assignment.mIndex) {
+      assignment.mSlot->InsertAssignedNode(*assignment.mIndex, aElement);
+    } else {
+      assignment.mSlot->AppendAssignedNode(aElement);
+    }
+    assignment.mSlot->EnqueueSlotChangeEvent();
+  }
 }
 
 Element*
 ShadowRoot::GetActiveElement()
 {
   return GetRetargetedFocusedElement();
 }
 
@@ -461,32 +473,21 @@ ShadowRoot::AttributeChanged(Element* aE
                              nsAtom* aAttribute,
                              int32_t aModType,
                              const nsAttrValue* aOldValue)
 {
   if (aNameSpaceID != kNameSpaceID_None || aAttribute != nsGkAtoms::slot) {
     return;
   }
 
-  // Attributes may change insertion point matching, find its new distribution.
-  if (!MaybeReassignElement(aElement, aOldValue)) {
+  if (aElement->GetParent() != GetHost()) {
     return;
   }
 
-  if (!aElement->IsInComposedDoc()) {
-    return;
-  }
-
-  auto* shell = OwnerDoc()->GetShell();
-  if (!shell) {
-    return;
-  }
-
-  // FIXME(emilio): We could be more granular in a bunch of cases.
-  shell->DestroyFramesForAndRestyle(aElement);
+  MaybeReassignElement(aElement);
 }
 
 void
 ShadowRoot::ContentAppended(nsIContent* aFirstNewContent)
 {
   for (nsIContent* content = aFirstNewContent;
        content;
        content = content->GetNextSibling()) {
@@ -504,19 +505,32 @@ ShadowRoot::ContentInserted(nsIContent* 
     return;
   }
 
   if (!aChild->IsSlotable()) {
     return;
   }
 
   if (aChild->GetParent() == GetHost()) {
-    if (const HTMLSlotElement* slot = AssignSlotFor(aChild)) {
-      slot->EnqueueSlotChangeEvent();
+    SlotAssignment assignment = SlotAssignmentFor(aChild);
+    if (!assignment.mSlot) {
+      return;
     }
+
+    // Fallback content will go away, let layout know.
+    if (assignment.mSlot->AssignedNodes().IsEmpty()) {
+      InvalidateStyleAndLayoutOnSubtree(assignment.mSlot);
+    }
+
+    if (assignment.mIndex) {
+      assignment.mSlot->InsertAssignedNode(*assignment.mIndex, aChild);
+    } else {
+      assignment.mSlot->AppendAssignedNode(aChild);
+    }
+    assignment.mSlot->EnqueueSlotChangeEvent();
     return;
   }
 
   // If parent's root is a shadow root, and parent is a slot whose assigned
   // nodes is the empty list, then run signal a slot change for parent.
   HTMLSlotElement* slot = HTMLSlotElement::FromContentOrNull(aChild->GetParent());
   if (slot && slot->GetContainingShadow() == this &&
       slot->AssignedNodes().IsEmpty()) {
@@ -534,21 +548,23 @@ ShadowRoot::ContentRemoved(nsIContent* a
     return;
   }
 
   if (!aChild->IsSlotable()) {
     return;
   }
 
   if (aChild->GetParent() == GetHost()) {
-    nsAutoString slotName;
-    if (aChild->IsElement()) {
-      aChild->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::slot, slotName);
-    }
-    if (const HTMLSlotElement* slot = UnassignSlotFor(aChild, slotName)) {
+    if (HTMLSlotElement* slot = aChild->GetAssignedSlot()) {
+      // If the slot is going to start showing fallback content, we need to tell
+      // layout about it.
+      if (slot->AssignedNodes().Length() == 1) {
+        InvalidateStyleAndLayoutOnSubtree(slot);
+      }
+      slot->RemoveAssignedNode(aChild);
       slot->EnqueueSlotChangeEvent();
     }
     return;
   }
 
   // If parent's root is a shadow root, and parent is a slot whose assigned
   // nodes is the empty list, then run signal a slot change for parent.
   HTMLSlotElement* slot = HTMLSlotElement::FromContentOrNull(aChild->GetParent());
--- a/dom/base/ShadowRoot.h
+++ b/dom/base/ShadowRoot.h
@@ -94,33 +94,51 @@ public:
    */
   void CloneInternalDataFrom(ShadowRoot* aOther);
 private:
 
   /**
    * Try to reassign an element to a slot and returns whether the assignment
    * changed.
    */
-  bool MaybeReassignElement(Element* aElement, const nsAttrValue* aOldValue);
+  void MaybeReassignElement(Element* aElement);
 
   /**
-   * Try to assign aContent to a slot in the shadow tree, returns the assigned
-   * slot if found.
+   * Represents the insertion point in a slot for a given node.
    */
-  const HTMLSlotElement* AssignSlotFor(nsIContent* aContent);
+  struct SlotAssignment
+  {
+    HTMLSlotElement* mSlot = nullptr;
+    Maybe<uint32_t> mIndex;
+  };
 
   /**
-   * Unassign aContent from the assigned slot in the shadow tree, returns the
-   * assigned slot if found.
+   * Return the assignment corresponding to the content node at this particular
+   * point in time.
+   *
+   * It's the caller's responsibility to actually call InsertAssignedNode /
+   * AppendAssignedNode in the slot as needed.
+   */
+  SlotAssignment SlotAssignmentFor(nsIContent* aContent);
+
+  /**
+   * Explicitly invalidates the style and layout of the flattened-tree subtree
+   * rooted at the element.
    *
-   * Note: slot attribute of aContent may have changed already, so pass slot
-   *       name explicity here.
+   * You need to use this whenever the flat tree is going to be shuffled in a
+   * way that layout doesn't understand via the usual ContentInserted /
+   * ContentAppended / ContentRemoved notifications. For example, if removing an
+   * element will cause a change in the flat tree such that other element will
+   * start showing up (like fallback content), this method needs to be called on
+   * an ancestor of that element.
+   *
+   * It is important that this runs _before_ actually shuffling the flat tree
+   * around, so that layout knows the actual tree that it needs to invalidate.
    */
-  const HTMLSlotElement* UnassignSlotFor(nsIContent* aContent,
-                                         const nsAString& aSlotName);
+  void InvalidateStyleAndLayoutOnSubtree(Element*);
 
 public:
   void AddSlot(HTMLSlotElement* aSlot);
   void RemoveSlot(HTMLSlotElement* aSlot);
 
   const RawServoAuthorStyles* ServoStyles() const
   {
     return mServoStyles.get();
@@ -158,18 +176,16 @@ public:
     mIsComposedDocParticipant = aIsComposedDocParticipant;
   }
 
   nsresult GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
 
 protected:
   virtual ~ShadowRoot();
 
-  void SyncServoStyles();
-
   const ShadowRootMode mMode;
 
   // The computed data from the style sheets.
   UniquePtr<RawServoAuthorStyles> mServoStyles;
   UniquePtr<mozilla::ServoStyleRuleMap> mStyleRuleMap;
 
   using SlotArray = AutoTArray<HTMLSlotElement*, 1>;
   // Map from name of slot to an array of all slots in the shadow DOM with with
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -124012,16 +124012,76 @@
       [
        "/css/css-scoping/reference/green-box.html",
        "=="
       ]
      ],
      {}
     ]
    ],
+   "css/css-scoping/shadow-fallback-dynamic-001.html": [
+    [
+     "/css/css-scoping/shadow-fallback-dynamic-001.html",
+     [
+      [
+       "/css/css-scoping/reference/green-box.html",
+       "=="
+      ]
+     ],
+     {}
+    ]
+   ],
+   "css/css-scoping/shadow-fallback-dynamic-002.html": [
+    [
+     "/css/css-scoping/shadow-fallback-dynamic-002.html",
+     [
+      [
+       "/css/css-scoping/reference/green-box.html",
+       "=="
+      ]
+     ],
+     {}
+    ]
+   ],
+   "css/css-scoping/shadow-fallback-dynamic-003.html": [
+    [
+     "/css/css-scoping/shadow-fallback-dynamic-003.html",
+     [
+      [
+       "/css/css-scoping/reference/green-box.html",
+       "=="
+      ]
+     ],
+     {}
+    ]
+   ],
+   "css/css-scoping/shadow-fallback-dynamic-004.html": [
+    [
+     "/css/css-scoping/shadow-fallback-dynamic-004.html",
+     [
+      [
+       "/css/css-scoping/reference/green-box.html",
+       "=="
+      ]
+     ],
+     {}
+    ]
+   ],
+   "css/css-scoping/shadow-fallback-dynamic-005.html": [
+    [
+     "/css/css-scoping/shadow-fallback-dynamic-005.html",
+     [
+      [
+       "/css/css-scoping/reference/green-box.html",
+       "=="
+      ]
+     ],
+     {}
+    ]
+   ],
    "css/css-scoping/slotted-with-pseudo-element.html": [
     [
      "/css/css-scoping/slotted-with-pseudo-element.html",
      [
       [
        "/css/css-scoping/slotted-with-pseudo-element-ref.html",
        "=="
       ]
@@ -500792,16 +500852,36 @@
   "css/css-scoping/reference/green-box.html": [
    "a736f68dc602c0fccab56ec5cc6234cb3298c88d",
    "support"
   ],
   "css/css-scoping/shadow-cascade-order-001.html": [
    "46913ea7e47811b11be898de5c3bd0a330ea6637",
    "testharness"
   ],
+  "css/css-scoping/shadow-fallback-dynamic-001.html": [
+   "062c99df18077a0205d0170d641b1d1e61199657",
+   "reftest"
+  ],
+  "css/css-scoping/shadow-fallback-dynamic-002.html": [
+   "2f66c8bca48c2ce5c9e82c5d67b152e2d143f4c6",
+   "reftest"
+  ],
+  "css/css-scoping/shadow-fallback-dynamic-003.html": [
+   "f054b0974277fbee38a96a26559c9a15400266db",
+   "reftest"
+  ],
+  "css/css-scoping/shadow-fallback-dynamic-004.html": [
+   "50c12659c9625801c6b4bb25ab76acddd50c8e90",
+   "reftest"
+  ],
+  "css/css-scoping/shadow-fallback-dynamic-005.html": [
+   "46d78b6d6931505bbc4bfc2f83e2bd0bac0d3472",
+   "reftest"
+  ],
   "css/css-scoping/slotted-invalidation.html": [
    "c500e1ceba1b293d45df5f66fd89d4a5d9ceb952",
    "testharness"
   ],
   "css/css-scoping/slotted-parsing.html": [
    "6bac5b15011d7177a40f7ca3e3c5f7e410643920",
    "testharness"
   ],
@@ -591057,17 +591137,17 @@
    "bb08ac2ee06bc12e75cc44b76f06d3f0262f44ba",
    "testharness"
   ],
   "user-timing/idlharness.html": [
    "b24d4d5faf5df5d67135560d5fe362cc6d72c28f",
    "testharness"
   ],
   "user-timing/invoke_with_timing_attributes.html": [
-   "c946c734573a3e8598389ae854c1d9792e96440c",
+   "05283be9a7230ba0c4af09fb5dac98d828bfaf2e",
    "testharness"
   ],
   "user-timing/invoke_with_timing_attributes.worker.js": [
    "f86e2132ced139cfe712a20ee4f64ba9e44459d2",
    "testharness"
   ],
   "user-timing/invoke_without_parameter.html": [
    "2246a41be7f7cca7574e57d76753c9347f738781",
@@ -591081,17 +591161,17 @@
    "c86845475d74791d9907716ae34743606b2465de",
    "testharness"
   ],
   "user-timing/measure.html": [
    "b021b6706afbf40f59ba1bbc743f4f4e57ea4f66",
    "testharness"
   ],
   "user-timing/measure_exceptions_navigation_timing.html": [
-   "e557969014be8b0ed1870e288e7f06f4b2a149a4",
+   "ed9d9be01e740d282ec94379bfd78aca07b56325",
    "testharness"
   ],
   "user-timing/measure_navigation_timing.html": [
    "ff8fb60c227078ab96eddbaee26c6c3b1fa3a9d4",
    "testharness"
   ],
   "user-timing/measure_syntax_err.any.js": [
    "e04fed804b4d89be63b8fdcbf12774c9a613f6d3",
@@ -601013,17 +601093,17 @@
    "b7ac10fed9c8a07afcd13f1d4906e10996ae56c6",
    "testharness"
   ],
   "xhr/responsexml-basic.htm": [
    "962765bd28850b740b0945d08f31fd94c8883191",
    "testharness"
   ],
   "xhr/responsexml-document-properties.htm": [
-   "3171001d9f35e2524a575d02c581c2cbe813f973",
+   "432b8327dbb09533bb64e8dabf72754d85c25e4f",
    "testharness"
   ],
   "xhr/responsexml-get-twice.htm": [
    "6291caac16b148f2265968820a8bd460a1a77092",
    "testharness"
   ],
   "xhr/responsexml-media-type.htm": [
    "82f735476786b2cbe5c62de17642ab42125e08ee",
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-001.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>CSS Scoping Module Level 1 - Dynamic fallback content</title>
+<link rel="author" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-scoping/#selectors-data-model">
+<link rel="match" href="reference/green-box.html"/>
+<p>Test passes if you see a single 100px by 100px green box below.</p>
+<div id="host">
+  <span slot="myslot">FAIL</span>
+</div>
+<script>
+  let root = host.attachShadow({ mode: "open" });
+  root.innerHTML = `
+    <slot name="myslot">
+      <div style="width: 100px; height: 100px; background: green"></div>
+    </slot>
+  `;
+  document.body.offsetTop;
+  host.firstElementChild.remove();
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-002.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>CSS Scoping Module Level 1 - Dynamic fallback content</title>
+<link rel="author" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-scoping/#selectors-data-model">
+<link rel="match" href="reference/green-box.html"/>
+<p>Test passes if you see a single 100px by 100px green box below.</p>
+<div id="host">
+  <span slot="myslot">FAIL</span>
+</div>
+<script>
+  let root = host.attachShadow({ mode: "open" });
+  root.innerHTML = `
+    <slot name="myslot">
+      <div style="width: 100px; height: 100px; background: green"></div>
+    </slot>
+  `;
+  document.body.offsetTop;
+  host.firstElementChild.removeAttribute("slot");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-003.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>CSS Scoping Module Level 1 - Dynamic fallback content</title>
+<link rel="author" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-scoping/#selectors-data-model">
+<link rel="match" href="reference/green-box.html"/>
+<p>Test passes if you see a single 100px by 100px green box below.</p>
+<div id="host">
+</div>
+<script>
+  let root = host.attachShadow({ mode: "open" });
+  root.innerHTML = `
+    <slot name="myslot">FAIL</slot>
+  `;
+  document.body.offsetTop;
+  host.innerHTML = `
+    <div slot="myslot" style="width: 100px; height: 100px; background: green"></div>
+  `;
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-004.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>CSS Scoping Module Level 1 - Dynamic fallback content</title>
+<link rel="author" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-scoping/#selectors-data-model">
+<link rel="match" href="reference/green-box.html"/>
+<p>Test passes if you see a single 100px by 100px green box below.</p>
+<div id="host">
+  <div slot="myslot" style="width: 100px; height: 100px; background: green"></div>
+</div>
+<script>
+  let root = host.attachShadow({ mode: "open" });
+  root.innerHTML = `
+    <slot name="myslot"></slot>
+  `;
+  document.body.offsetTop;
+  let newSlot = document.createElement('slot');
+  newSlot.appendChild(document.createTextNode("FAIL"));
+  newSlot.setAttribute("name", "myslot");
+  root.insertBefore(newSlot, root.firstChild);
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-005.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>CSS Scoping Module Level 1 - Dynamic fallback content</title>
+<link rel="author" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-scoping/#selectors-data-model">
+<link rel="match" href="reference/green-box.html"/>
+<p>Test passes if you see a single 100px by 100px green box below.</p>
+<div id="host">
+  <div slot="myslot" style="width: 100px; height: 100px; background: green"></div>
+</div>
+<script>
+  let root = host.attachShadow({ mode: "open" });
+  root.innerHTML = `
+    <slot name="myslot"></slot>
+    <slot name="myotherslot">
+      FAIL
+    </slot>
+  `;
+  document.body.offsetTop;
+  host.firstElementChild.setAttribute("slot", "myotherslot");
+</script>