Add declarative DOM Parts syntax

This CL adds the ability to construct DOM Parts during document
parsing via "fake" processing instructions:

<section>
  <h1 id="name">
    <?child-node-part?> <?/child-node-part?>
  </h1>
  Email: <?node-part metadata?><a id="link"></a>
</section>

In this example, <?child-node-part?><?/child-node-part?> will
be left in the document, but a ChildNodePart will also be constructed
with previous/next endpoints of those two comment nodes. And the
<?node-part> will be left in place, but a NodePart will be attached
to the <a> element.

Note that <?foo?> is exactly equivalent to <!--?foo?-->, and both
are tested in the new WPT.

This CL also:
 - adds a virtual test suite that makes sure no functionality breaks
   or leaks when DOMPartAPI is disabled.
 - revamps the WPT, adding a `assertEqualParts()` that checks a given
   parts list via the type of parts and the metadata. This is then
   used everywhere.
 - add testing of declarative DOM Parts for three cases:
   1. main document parsing (passes with this CL)
   2. template parsing (doesn't yet pass)
   3. cloned templates (not even sure if this should ever pass)

Bug: 1453291,1465062
Change-Id: I2eaef72a21fc2deaa251deb0731b5ff55ef19d8c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4686189
Reviewed-by: David Baron <dbaron@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Commit-Queue: Stefan Zager <szager@chromium.org>
Reviewed-by: Stefan Zager <szager@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1170849}
diff --git a/dom/parts/basic-dom-part-objects.tentative.html b/dom/parts/basic-dom-part-objects.tentative.html
index db41c9e..a60ec8b 100644
--- a/dom/parts/basic-dom-part-objects.tentative.html
+++ b/dom/parts/basic-dom-part-objects.tentative.html
@@ -4,34 +4,54 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 
-<template id=template>
-  <div id=target style="display:none">
-    Imperative test element
-    <span id=a>A</span><span id=b>B
-      <span id=sub>B-sub1</span>
-      <span id=sub>B-sub2</span>
-    </span><span id=c>C</span></div>
-</template>
-
-<div style="display:none">
-  Declarative syntax
-  <h1 id="name">First<?child-node-part name?>Middle<?/child-node-part?>Last</h1>
-  Email: <?node-part email-link?><a id="link"></a>
-</div>
+<body>
 
 <script>
-const template = document.getElementById('template');
-document.body.appendChild(template.content.cloneNode(true));
+  function assertEqualParts(parts,partDescriptions,expectedParts,description) {
+    assert_equals(parts.length,partDescriptions.length,`${description}: lengths differ`);
+    for(let i=0;i<parts.length;++i) {
+      assert_true(parts[i] instanceof Part,`${description}: not a Part`);
+      assert_true(parts[i] instanceof window[partDescriptions[i].type],`${description}: index ${i} expected ${partDescriptions[i].type}`);
+      assert_array_equals(parts[i].metadata,partDescriptions[i].metadata,`${description}: index ${i} wrong metadata`);
+      if (expectedParts) {
+        assert_equals(parts[i],expectedParts[i],`${description}: index ${i} object equality`);
+      }
+    }
+  }
+</script>
 
+
+<template id=imperative>
+  <div>
+    <div id=target1 style="display:none">
+      Imperative test element
+      <span id=a>A</span><span id=b>B
+        <span id=sub>B-sub1</span>
+        <span id=sub>B-sub2</span>
+      </span><span id=c>C</span></div>
+  </div>
+</template>
+
+<script>{
+const template = document.getElementById('imperative');
 function addCleanup(t, part) {
   t.add_cleanup(() => part.disconnect());
   return part;
 }
-
 [false,true].forEach(useTemplate => {
   const doc = useTemplate ? template.content : document;
-  const target = doc.querySelector('#target');
-  assert_true(!!(doc && target && target.children.length >= 3));
+  let target,wrapper;
+  if (useTemplate) {
+    target = doc.querySelector('#target1');
+  } else {
+    wrapper = document.body.appendChild(document.createElement('div'));
+    wrapper.appendChild(template.content.cloneNode(true));
+    target = wrapper.querySelector('#target1');
+  }
+  const a = target.querySelector('#a');
+  const b = target.querySelector('#b');
+  const c = target.querySelector('#c');
+  assert_true(!!(doc && target && target.parentElement && a && b && c));
   const description = useTemplate ? "DocumentFragment" : "Document";
   test((t) => {
     const root = doc.getPartRoot();
@@ -41,42 +61,39 @@
     assert_true(root.rootContainer instanceof (useTemplate ? DocumentFragment : Document));
 
     const nodePart = addCleanup(t,new NodePart(root,target,{metadata: ['foo']}));
-    assert_true(nodePart instanceof NodePart);
+    assertEqualParts([nodePart],[{type:'NodePart',metadata:['foo']}],0,'Basic NodePart');
     assert_equals(nodePart.node,target);
     assert_equals(nodePart.root,root);
-    assert_array_equals(nodePart.metadata,['foo']);
-    assert_equals(root.getParts().length,1,'getParts() for the root should now have this nodePart');
-    assert_equals(root.getParts()[0],nodePart);
+    let runningPartsExpectation = [{type:'NodePart',metadata:['foo']}];
+    assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart],'getParts() for the root should now have this nodePart');
     assert_equals(parts.length,0,'Return value of getParts() is not live');
 
     assert_throws_js(TypeError,() => new NodePart(nodePart,target.children[0]),'Constructing a Part with a NodePart as the PartRoot should throw');
 
     const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2],{metadata:['bar','baz']}));
-    assert_true(childNodePart instanceof ChildNodePart);
-    assert_true(childNodePart instanceof Part);
+    assertEqualParts([childNodePart],[{type:'ChildNodePart',metadata:['bar','baz']}],0,'Basic ChildNodePart');
     assert_equals(childNodePart.root,root);
     assert_equals(childNodePart.previousSibling,target.children[0]);
     assert_equals(childNodePart.nextSibling,target.children[2]);
-    assert_array_equals(childNodePart.metadata,['bar','baz']);
     assert_equals(childNodePart.getParts().length,0,'childNodePart.getParts() should start out empty');
-    assert_equals(root.getParts().length,2);
-    assert_equals(root.getParts()[1],childNodePart);
+    runningPartsExpectation.push({type:'ChildNodePart',metadata:['bar','baz']});
+    assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,childNodePart],'getParts() for the root should now have this childNodePart');
 
     const nodeBefore = target.previousSibling || target.parentNode;
     const nodePartBefore = addCleanup(t,new NodePart(root,nodeBefore));
-    assert_equals(root.getParts().length,3,'getParts() for the root should now have this nodePart');
-    assert_array_equals(root.getParts(),[nodePartBefore,nodePart,childNodePart],'getParts() should return nodes in tree order');
+    runningPartsExpectation.unshift({type:'NodePart',metadata:[]});
+    assertEqualParts(root.getParts(),runningPartsExpectation,[nodePartBefore,nodePart,childNodePart],'getParts() for the root should now have this nodePart, in tree order');
 
-    const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[2]));
+    const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[2],{metadata:['blah']}));
     assert_equals(nodePart2.root,childNodePart);
-    assert_equals(root.getParts().length,3,'getParts() for the root DocumentPartRoot shouldn\'t change');
-    assert_array_equals(childNodePart.getParts(),[nodePart2]);
+    assertEqualParts(root.getParts(),runningPartsExpectation,[nodePartBefore,nodePart,childNodePart],'getParts() for the root DocumentPartRoot shouldn\'t change');
+    assertEqualParts(childNodePart.getParts(),[{type:'NodePart',metadata:['blah']}],[nodePart2],'getParts() for the childNodePart should have it');
 
     nodePart2.disconnect();
     assert_equals(nodePart2.root,null,'root should be null after disconnect');
     assert_equals(nodePart2.node,null,'node should be null after disconnect');
     assert_equals(childNodePart.getParts().length,0,'calling disconnect() should remove the part from root.getParts()');
-    assert_equals(root.getParts().length,3,'getParts() for the root DocumentPartRoot still shouldn\'t change');
+    assertEqualParts(root.getParts(),runningPartsExpectation,[nodePartBefore,nodePart,childNodePart],'getParts() for the root DocumentPartRoot still shouldn\'t change');
     nodePart2.disconnect(); // Calling twice should be ok.
 
     childNodePart.disconnect();
@@ -101,13 +118,15 @@
 
   test((t) => {
     const root = doc.getPartRoot();
-    const nodePart = addCleanup(t,new NodePart(root,target));
-    const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2]));
-    const nodePart3 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['this is','part 3']}));
-    const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['this','is part 2']}));
+    const nodePart = addCleanup(t,new NodePart(root,target,{metadata:['node1']}));
+    const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2],{metadata:['child']}));
+    const nodePart3 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['node 3']}));
+    const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['node 2']}));
     const childNodePart2 = addCleanup(t,new ChildNodePart(childNodePart,target.children[1].firstElementChild,target.children[1].firstElementChild.nextSibling,{metadata: ['childnodepart2']}));
-    assert_array_equals(root.getParts(),[nodePart,childNodePart]);
-    assert_array_equals(childNodePart.getParts(),[childNodePart2,nodePart3,nodePart2],'Parts on the same Node are returned in the order they were constructed');
+    let rootExpectations = [{type:'NodePart',metadata:['node1']},{type:'ChildNodePart',metadata:['child']}];
+    assertEqualParts(root.getParts(),rootExpectations,[nodePart,childNodePart],'setup');
+    let childExpectations = [{type:'ChildNodePart',metadata:['childnodepart2']},{type:'NodePart',metadata:['node 3']},{type:'NodePart',metadata:['node 2']}];
+    assertEqualParts(childNodePart.getParts(),childExpectations,[childNodePart2,nodePart3,nodePart2],'setup');
     assert_array_equals(childNodePart2.getParts(),[]);
 
     // Test cloning of the entire DocumentPartRoot.
@@ -118,21 +137,18 @@
     assert_not_equals(clonedPartRoot,root);
     assert_not_equals(clonedContainer,doc);
     assert_equals(doc.innerHTML,clonedContainer.innerHTML);
-    assert_equals(clonedPartRoot.getParts().length,root.getParts().length);
-    assert_array_equals(root.getParts(),[nodePart,childNodePart]);
+    assertEqualParts(clonedPartRoot.getParts(),rootExpectations,0,'cloned PartRoot should contain identical parts');
     assert_true(!clonedPartRoot.getParts().includes(nodePart),'Original parts should not be retained');
     assert_true(!clonedPartRoot.getParts().includes(childNodePart));
     const newNodePart = clonedPartRoot.getParts()[0];
     const newChildNodePart = clonedPartRoot.getParts()[1];
-    assert_true(newChildNodePart instanceof ChildNodePart,'Cloned parts are out of order');
-    assert_true(newNodePart instanceof NodePart);
     assert_not_equals(newNodePart.node,target,'Node references should not point to original nodes');
-    assert_equals(newNodePart.node.id,'target','New parts should point to cloned nodes');
+    assert_equals(newNodePart.node.id,target.id,'New parts should point to cloned nodes');
     assert_not_equals(newChildNodePart.previousSibling,a,'Node references should not point to original nodes');
     assert_equals(newChildNodePart.previousSibling.id,'a');
     assert_not_equals(newChildNodePart.nextSibling,c,'Node references should not point to original nodes');
     assert_equals(newChildNodePart.nextSibling.id,'c');
-    assert_equals(newChildNodePart.getParts().length,childNodePart.getParts().length);
+    assertEqualParts(newChildNodePart.getParts(),childExpectations,0,'cloned PartRoot should contain identical parts');
 
     // Test cloning of ChildNodeParts.
     const clonedChildNodePartRoot = childNodePart.clone();
@@ -141,12 +157,7 @@
     assert_true(clonedChildContainer instanceof Element);
     assert_not_equals(clonedChildContainer,target);
     assert_equals(clonedChildContainer.outerHTML,cloneRange(target,a,c).outerHTML);
-    assert_equals(clonedChildNodePartRoot.getParts().length,3);
-    const clonedSubParts = clonedChildNodePartRoot.getParts();
-    assert_true(clonedSubParts[0] instanceof ChildNodePart);
-    assert_array_equals(clonedSubParts[0].metadata,childNodePart2.metadata,'metadata should be preserved, and...');
-    assert_array_equals(clonedSubParts[1].metadata,nodePart3.metadata,'parts should still be in construction order');
-    assert_array_equals(clonedSubParts[2].metadata,nodePart2.metadata);
+    assertEqualParts(clonedChildNodePartRoot.getParts(),childExpectations,0,'clone of childNodePart should match');
   }, `Cloning (${description})`);
 
   ['Element','Text','Comment','CDATASection','ProcessingInstruction'].forEach(nodeType => {
@@ -166,88 +177,180 @@
       }
       t.add_cleanup(() => node.remove());
       doc.firstElementChild.append(node);
-      const nodePart = addCleanup(t,new NodePart(root,node));
+      const nodePart = addCleanup(t,new NodePart(root,node,{metadata:['foobar']}));
       assert_true(!!nodePart);
       const clone = root.clone();
       assert_equals(clone.getParts().length,1);
-      const clonedPart = clone.getParts()[0];
-      assert_true(clonedPart instanceof NodePart);
-      assert_true(clonedPart.node instanceof window[nodeType]);
+      assertEqualParts(clone.getParts(),[{type:'NodePart',metadata:['foobar']}],0,'getParts');
+      assert_true(clone.getParts()[0].node instanceof window[nodeType]);
     }, `Cloning ${nodeType} (${description})`);
   });
+
+  test((t) => {
+    const root = doc.getPartRoot();
+    assert_equals(root.getParts().length,0,'Test harness check: tests should clean up parts');
+
+    const nodePartB = addCleanup(t,new NodePart(root,b));
+    const nodePartA = addCleanup(t,new NodePart(root,a));
+    const nodePartC = addCleanup(t,new NodePart(root,c));
+    assert_array_equals(root.getParts(),[nodePartA,nodePartB,nodePartC]);
+    b.remove();
+    assert_array_equals(root.getParts(),[nodePartA,nodePartC]);
+    target.parentElement.insertBefore(b,target);
+    assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC]);
+    target.insertBefore(b,c);
+    assert_array_equals(root.getParts(),[nodePartA,nodePartB,nodePartC]);
+    nodePartA.disconnect();
+    nodePartB.disconnect();
+    nodePartC.disconnect();
+    assert_array_equals(root.getParts(),[]);
+
+    const childPartAC = addCleanup(t,new ChildNodePart(root,a,c));
+    assert_array_equals(root.getParts(),[childPartAC]);
+    a.remove();
+    assert_array_equals(root.getParts(),[],'Removing endpoints invalidates the part');
+    target.insertBefore(a,b); // Restore
+    assert_array_equals(root.getParts(),[childPartAC]);
+
+    target.insertBefore(c,a);
+    assert_array_equals(root.getParts(),[],'Endpoints out of order');
+    target.appendChild(c); // Restore
+    assert_array_equals(root.getParts(),[childPartAC]);
+
+    document.body.appendChild(c);
+    assert_array_equals(root.getParts(),[],'Children need to have same parent');
+    target.appendChild(c); // Restore
+    assert_array_equals(root.getParts(),[childPartAC]);
+
+    const oldParent = target.parentElement;
+    target.remove();
+    assert_array_equals(root.getParts(),[],'Parent needs to be connected');
+    oldParent.appendChild(target); // Restore
+    assert_array_equals(root.getParts(),[childPartAC]);
+  }, `DOM mutation support (${description})`);
+
+  test((t) => {
+    const root = doc.getPartRoot();
+    assert_equals(root.getParts().length,0,'Test harness check: tests should clean up parts');
+    const otherNode = document.createElement('div');
+
+    const childPartAA = addCleanup(t,new ChildNodePart(root,a,a));
+    const childPartAB = addCleanup(t,new ChildNodePart(root,a,b));
+    const childPartAC = addCleanup(t,new ChildNodePart(root,a,c));
+    assert_throws_dom('InvalidStateError',() => childPartAA.replaceChildren(otherNode),'Can\'t replace children if part is invalid');
+    assert_array_equals(childPartAA.children,[],'Invalid parts should return empty children');
+    assert_array_equals(childPartAB.children,[],'Children should not include endpoints');
+    assert_array_equals(childPartAC.children,[b],'Children should not include endpoints');
+    childPartAB.replaceChildren(otherNode);
+    assert_array_equals(childPartAB.children,[otherNode],'Replacechildren should work');
+    assert_array_equals(childPartAC.children,[otherNode,b],'replaceChildren should leave endpoints alone');
+    childPartAC.replaceChildren(otherNode);
+    assert_array_equals(childPartAC.children,[otherNode],'Replacechildren with existing children should work');
+    assert_array_equals(childPartAB.children,[]);
+    childPartAC.replaceChildren(b);
+    assert_array_equals(target.children,[a,b,c]);
+  }, `ChildNodePart children manipulation (${description})`);
+
+  wrapper?.remove(); // Cleanup
+});
+}</script>
+
+<div>
+  <!-- Note - the test will remove this chunk of DOM once the test completes -->
+  <div id=target2>
+    Declarative syntax - The template below (with id=declarative) should have
+    IDENTICAL STRUCTURE. There are three cases to test:
+      1. Main document parsing (this chunk)
+      2. Template parsing (the template below)
+      3. Template/fragment cloning (the clone of the template below)
+    <h1 id="name">
+      <?child-node-part fullname?>
+        First
+        <!--?child-node-part middle?--> <?node-part middle-node?>Middle <?/child-node-part middle?>
+        Last
+    <!-- ?/child-node-part foobar? -->
+    </h1>
+    Email: <?node-part email-link?><a id="link"></a>
+
+    Here are some invalid parts that should not get parsed:
+    <!--child-node-part test comment without leading ?-->
+    <child-node-part test PI without leading ?>
+    <!--?child-node-partfoobar?-->
+    <?child-node-partfoobar?>
+</div>
+</div>
+<template id=declarative>
+  <div>
+    <div id=target3>Declarative syntax
+      <h1 id="name">
+        <?child-node-part fullname?>
+          First
+          <!--?child-node-part middle?--> <?node-part middle-node?>Middle <?/child-node-part middle?>
+          Last
+        <!-- ?/child-node-part foobar? -->
+      </h1>
+      Email: <?node-part email-link?><a id="link"></a>
+
+      Here are some invalid parts that should not get parsed:
+      <!--child-node-part test comment without leading ?-->
+      <child-node-part test PI without leading ?>
+      <!--?child-node-partfoobar?-->
+      <?child-node-partfoobar?>
+</div>
+  </div>
+</template>
+
+<script> {
+const template = document.getElementById('declarative');
+['Main Document','Template','Clone'].forEach(testCase => {
+  let doc,target,cleanup;
+  switch (testCase) {
+    case 'Main Document':
+      doc = document;
+      target = doc.querySelector('#target2');
+      cleanup = [target.parentElement];
+      break;
+    case 'Template':
+      doc = template.content;
+      target = doc.querySelector('#target3');
+      cleanup = [];
+      break;
+    case 'Clone':
+      doc = document;
+      wrapper = document.body.appendChild(document.createElement('div'));
+      wrapper.appendChild(template.content.cloneNode(true));
+      target = wrapper.querySelector('#target3');
+      cleanup = [wrapper];
+      break;
+  }
+  assert_true(!!(doc && target && target.parentElement));
+
+  function assertIsComment(node,commentText) {
+    assert_true(node instanceof Comment);
+    assert_equals(node.textContent,commentText);
+  }
+
+  test((t) => {
+    const root = doc.getPartRoot();
+    assert_true(root instanceof DocumentPartRoot);
+    const expectedRootParts = [{type:'ChildNodePart',metadata:['fullname']},{type:'NodePart',metadata:['email-link']}];
+    assertEqualParts(root.getParts(),expectedRootParts,0,'declarative root should have two parts');
+    assert_equals(root.getParts()[1].node,target.querySelector('#link'));
+    const childPart1 = root.getParts()[0];
+    assertIsComment(childPart1.previousSibling,'?child-node-part fullname?');
+    assertIsComment(childPart1.nextSibling,' ?/child-node-part foobar? ');
+    const expectedChild1Parts = [{type:'ChildNodePart',metadata:['middle']}];
+    assertEqualParts(childPart1.getParts(),expectedChild1Parts,0,'First level childpart should just have one child part');
+    const childPart2 = childPart1.getParts()[0];
+    assertIsComment(childPart2.previousSibling,'?child-node-part middle?');
+    assertIsComment(childPart2.nextSibling,'?/child-node-part middle?');
+    const expectedChild2Parts = [{type:'NodePart',metadata:['middle-node']}];
+    assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have just the node part');
+    assert_true(childPart2.getParts()[0].node instanceof Text);
+    assert_equals(childPart2.getParts()[0].node.textContent,'Middle ');
+  }, `Basic declarative DOM Parts (${testCase})`);
+
+  cleanup.forEach(el => el.remove()); // Cleanup
 });
 
-test((t) => {
-  const root = document.getPartRoot();
-  assert_equals(root.getParts().length,0,'Test harness check: tests should clean up parts');
-  const target = document.querySelector('#target');
-  const a = document.querySelector('#a');
-  const b = document.querySelector('#b');
-  const c = document.querySelector('#c');
-  assert_true(!!(target && a && b && c));
-
-  const nodePartB = addCleanup(t,new NodePart(root,b));
-  const nodePartA = addCleanup(t,new NodePart(root,a));
-  const nodePartC = addCleanup(t,new NodePart(root,c));
-  assert_array_equals(root.getParts(),[nodePartA,nodePartB,nodePartC]);
-  b.remove();
-  assert_array_equals(root.getParts(),[nodePartA,nodePartC]);
-  document.body.appendChild(b);
-  assert_array_equals(root.getParts(),[nodePartA,nodePartC,nodePartB]);
-  target.insertBefore(b,a);
-  assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC]);
-  nodePartA.disconnect();
-  nodePartB.disconnect();
-  nodePartC.disconnect();
-  assert_array_equals(root.getParts(),[]);
-
-  const childPartAC = addCleanup(t,new ChildNodePart(root,a,c));
-  assert_array_equals(root.getParts(),[childPartAC]);
-  a.remove();
-  assert_array_equals(root.getParts(),[],'Removing endpoints invalidates the part');
-  target.insertBefore(a,b); // Restore
-  assert_array_equals(root.getParts(),[childPartAC]);
-
-  target.insertBefore(c,a);
-  assert_array_equals(root.getParts(),[],'Endpoints out of order');
-  target.appendChild(c); // Restore
-  assert_array_equals(root.getParts(),[childPartAC]);
-
-  document.body.appendChild(c);
-  assert_array_equals(root.getParts(),[],'Children need to have same parent');
-  target.appendChild(c); // Restore
-  assert_array_equals(root.getParts(),[childPartAC]);
-
-  target.remove();
-  assert_array_equals(root.getParts(),[],'Parent needs to be connected');
-  document.body.appendChild(target); // Restore
-  assert_array_equals(root.getParts(),[childPartAC]);
-}, 'DOM mutation support');
-
-
-test((t) => {
-  const root = document.getPartRoot();
-  assert_equals(root.getParts().length,0,'Test harness check: tests should clean up parts');
-  const target = document.querySelector('#target');
-  const a = document.querySelector('#a');
-  const b = document.querySelector('#b');
-  const c = document.querySelector('#c');
-  const otherNode = document.createElement('div');
-
-  const childPartAA = addCleanup(t,new ChildNodePart(root,a,a));
-  const childPartAB = addCleanup(t,new ChildNodePart(root,a,b));
-  const childPartAC = addCleanup(t,new ChildNodePart(root,a,c));
-  assert_throws_dom('InvalidStateError',() => childPartAA.replaceChildren(otherNode),'Can\'t replace children if part is invalid');
-  assert_array_equals(childPartAA.children,[],'Invalid parts should return empty children');
-  assert_array_equals(childPartAB.children,[],'Children should not include endpoints');
-  assert_array_equals(childPartAC.children,[b],'Children should not include endpoints');
-  childPartAB.replaceChildren(otherNode);
-  assert_array_equals(childPartAB.children,[otherNode],'Replacechildren should work');
-  assert_array_equals(childPartAC.children,[otherNode,b],'replaceChildren should leave endpoints alone');
-  childPartAC.replaceChildren(otherNode);
-  assert_array_equals(childPartAC.children,[otherNode],'Replacechildren with existing children should work');
-  assert_array_equals(childPartAB.children,[]);
-  childPartAC.replaceChildren(b);
-  assert_array_equals(target.children,[a,b,c]);
-}, 'ChildNodePart children manipulation');
-</script>
+}</script>