Implement checking for "is declarative shadow root" in attachShadow()

With this CL, if a declarative shadow root is already present when
attachShadow() is called, the contents of this shadow root are
removed, and the existing shadow root is returned. This is per Step 5
of the new spec [1], to allow coexistence of declarative content and
custom element hydration code.

This CL also adds an exhaustive test of all element types, both those
supporting and disallowing shadow root attachment.

[1] https://whatpr.org/dom/858.html#concept-attach-a-shadow-root

Bug: 1042130
Change-Id: Ie9fc39eeb324934f3cb13ab2eb6bc8a82d99d667
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2159987
Commit-Queue: Mason Freed <masonfreed@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Auto-Submit: Mason Freed <masonfreed@chromium.org>
Cr-Commit-Position: refs/heads/master@{#761964}
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index 0422124..4703f4fa1 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -3700,10 +3700,7 @@
 
   // 4. If shadow host has a non-null shadow root whose is declarative shadow
   // root property is false, then throw an "NotSupportedError" DOMException.
-  // 5. TODO(masonfreed): If shadow host has a non-null shadow root whose is
-  // declarative shadow root property is true, then remove all of shadow root’s
-  // children, in tree order. Return shadow host’s shadow root.
-  if (GetShadowRoot()) {
+  if (GetShadowRoot() && !GetShadowRoot()->IsDeclarativeShadowRoot()) {
     return "Shadow root cannot be created on a host "
            "which already hosts a shadow tree.";
   }
@@ -3784,6 +3781,15 @@
 
   GetDocument().SetShadowCascadeOrder(ShadowCascadeOrder::kShadowCascadeV1);
 
+  if (auto* shadow_root = GetShadowRoot()) {
+    // 5. If shadow host has a non-null shadow root whose "is declarative shadow
+    // root property is true, then remove all of shadow root’s children, in tree
+    // order. Return shadow host’s shadow root.
+    DCHECK(shadow_root->IsDeclarativeShadowRoot());
+    shadow_root->RemoveChildren();
+    return *shadow_root;
+  }
+
   // 6. Let shadow be a new shadow root whose node document is shadow host’s
   // node document, host is shadow host, and mode is mode.
   // 9. Set shadow host’s shadow root to shadow.
diff --git a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
index f07a1db..c6551c6 100644
--- a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
+++ b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
@@ -942,13 +942,19 @@
   tree_.ActiveFormattingElements()->ClearToLastMarker();
   template_insertion_modes_.pop_back();
   ResetInsertionModeAppropriately();
-  // Check for a declarative shadow root.
   if (RuntimeEnabledFeatures::DeclarativeShadowDOMEnabled() &&
       template_stack_item) {
     DCHECK(template_stack_item->IsElementNode());
     HTMLTemplateElement* template_element =
         DynamicTo<HTMLTemplateElement>(template_stack_item->GetElement());
-    if (template_element->IsDeclarativeShadowRoot()) {
+    // 9. If the start tag for the declarative template element did not have an
+    // attribute with the name "shadowroot" whose value was an ASCII
+    // case-insensitive match for the strings "open" or "closed", then stop this
+    // algorithm.
+    // 10. If the adjusted current node is the topmost element in the stack of
+    // open elements, then stop this algorithm.
+    if (template_element->IsDeclarativeShadowRoot() &&
+        shadow_host_stack_item->GetNode() != tree_.OpenElements()->RootNode()) {
       DCHECK(shadow_host_stack_item);
       DCHECK(shadow_host_stack_item->IsElementNode());
       UseCounter::Count(shadow_host_stack_item->GetElement()->GetDocument(),
diff --git a/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-attachment.tentative.html b/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-attachment.tentative.html
new file mode 100644
index 0000000..0acdeb8
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-attachment.tentative.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<title>Declarative Shadow DOM Element Attachment</title>
+<link rel='author' title='Mason Freed' href='mailto:masonfreed@chromium.org'>
+<link rel='help' href='https://github.com/whatwg/dom/issues/831'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/shadow-dom-utils.js'></script>
+
+<script>
+const shadowContent = '<span>Shadow tree</span><slot></slot>';
+function getDeclarativeContent(mode, delegatesFocus) {
+  const delegatesFocusText = delegatesFocus ? ' shadowrootdelegatesfocus' : '';
+  return `<template shadowroot=${mode}${delegatesFocusText}>${shadowContent}</template>`;
+}
+
+const lightDomTextContent = 'Light DOM';
+function addDeclarativeShadowRoot(elementType, mode, delegatesFocus) {
+  const declarativeString = `<${elementType} id=theelement>${getDeclarativeContent(mode, delegatesFocus)}
+     <span class='lightdom'>${lightDomTextContent}</span></${elementType}>`;
+  const wrapper = document.createElement('div');
+  wrapper.innerHTML = declarativeString;
+  const element = wrapper.querySelector('#theelement');
+  return {wrapper: wrapper, element: element};
+}
+
+function testElementType(allowed, nochildren, elementType, mode, delegatesFocus) {
+  var t = test(function() {
+    const nodes = addDeclarativeShadowRoot(elementType, mode, delegatesFocus);
+    if (allowed) {
+      const element = nodes.element;
+      assert_true(!!element, 'Unable to locate the element');
+      // Just one light DOM child, and no leftover template.
+      assert_true(!nodes.wrapper.querySelector('template'));
+      assert_equals(element.children.length, 1);
+      assert_equals(element.children[0].textContent, lightDomTextContent);
+      let originalShadowRoot = null;
+      if (mode === 'open') {
+        // TODO(masonfreed): Add a check for ElementInternals.shadowRoot once that exists.
+        assert_true(!!element.shadowRoot, 'Shadow root should be present');
+        assert_equals(element.shadowRoot.innerHTML, shadowContent, 'Correct shadow content');
+        originalShadowRoot = element.shadowRoot;
+      }
+
+      // Now, call attachShadow() and make sure we get back the same (original) shadowRoot, but empty.
+      const oppositeMode = (mode === 'open') ? 'closed' : 'open';
+      const newShadow = element.attachShadow({mode: oppositeMode}); // Should be no exception here
+      if (mode === 'open') {
+        // TODO(masonfreed): Add a check for ElementInternals.shadowRoot once that exists.
+        assert_equals(element.shadowRoot, originalShadowRoot, 'The same shadow root should be returned');
+        assert_equals(element.shadowRoot.innerHTML, '', 'Empty shadow content');
+        assert_equals(element.shadowRoot.mode, mode, 'Original shadow mode');
+      }
+    } else {
+      const wrapper = nodes.wrapper;
+      if (!nochildren) {
+        // Invalid elements should retain a <template> element child with a shadowroot attribute.
+        const template = nodes.wrapper.querySelector('template[shadowroot]');
+        assert_true(!!template);
+        assert_equals(template.getAttribute('shadowroot'), mode, `Template with shadowroot=${mode} should be left over`);
+        const span = nodes.wrapper.querySelector('span.lightdom');
+        assert_true(!!span);
+        assert_equals(span.textContent, lightDomTextContent);
+        if (nodes.element) {
+          // For some tags (e.g. <html>) there won't be an element inside wrapper.
+          assert_true(!nodes.element.shadowRoot, 'Shadow root should not be present');
+        }
+      }
+    }
+  }, `Declarative Shadow DOM as a child of <${elementType}>, with mode=${mode}, delegatesFocus=${delegatesFocus}. Should be ${allowed ? 'safelisted' : 'disallowed'}.`);
+}
+
+function runAllTests() {
+  const noChildElements = ['iframe','noscript','script','select','style','textarea','title'];
+  const noCheck = ['body'];
+  const safelisted = ATTACHSHADOW_SAFELISTED_ELEMENTS.filter(el => !noCheck.includes(el));
+  const disallowed = ATTACHSHADOW_DISALLOWED_ELEMENTS.filter(el => !noCheck.includes(el));
+  const minimumKnownElements = 113; // We should have at least this many elements in the lists from shadow-dom-utils.js.
+  assert_true(safelisted.length + disallowed.length + noChildElements.length >= minimumKnownElements,'All element types should be tested');
+  for (let delegatesFocus of [false, true]) {
+    for (let mode of ['open', 'closed', 'invalid']) {
+      for (let elementName of safelisted) {
+        testElementType(mode !== 'invalid', false, elementName, mode, delegatesFocus);
+      }
+      for (let elementName of disallowed) {
+        testElementType(false, noChildElements.includes(elementName), elementName, mode, delegatesFocus);
+      }
+    }
+  }
+}
+
+runAllTests();
+
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom.tentative.html b/third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-basic.tentative.html
similarity index 100%
rename from third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom.tentative.html
rename to third_party/blink/web_tests/external/wpt/shadow-dom/declarative/declarative-shadow-dom-basic.tentative.html