Add `clone` to DocumentPartRoot

You can now clone a DocumentPartRoot, and it will a) clone the
underlying document or document_fragment, b) clone all contained `Part`
references from the original to the clone, and c) return you the new
DocumentPartRoot. To allow access to the underlying cloned Node tree, I
also added a DocumentPartRoot.root, which returns the owning Document
or DocumentFragment.

So:
  const root = document.getPartRoot();
  { ... add NodePart's and ChildNodePart's to root ... }
  const clonedPartRoot = root.clone();
  const clonedDocument = clonedPartRoot.root;

To make this work, during cloning (via partRoot.clone), references
are kept in NodeCloningData that point from cloned Nodes to their
clones, and from cloned PartRoots to their clones. Also, a "queue"
of to-be-cloned Parts, keyed by the PartRoot they're waiting for,
is used to handle the common case that the PartRoot for one part
might not be cloned before a Part that refers to it. An example is:

<div id=parent>
  <div id=previous_sibling></div>
  <div id=middle></div>
  <div id=next_sibling></div>
</div>

Assume there is a ChildNodePart with previous_sibling/next_sibling,
and a NodePart pointing to middle which has ChildNodePart as its
PartRoot. During the clone of this tree, the ChildNodePart can only
be cloned once parent, previous_sibling, and next_sibling have all
been cloned. However, middle (and its NodePart) will get reached by
the clone operation before next_sibling. So NodePart here needs to
be queued and retried once ChildNodePart is cloned.

Bug: 1453291
Change-Id: I4a85c3ee02547627bcf73ed610310c742238f12c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4671112
Auto-Submit: Mason Freed <masonf@chromium.org>
Reviewed-by: David Baron <dbaron@chromium.org>
Commit-Queue: David Baron <dbaron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1168260}
diff --git a/dom/parts/basic-dom-part-objects.tentative.html b/dom/parts/basic-dom-part-objects.tentative.html
index b226c40..7b6ac3a 100644
--- a/dom/parts/basic-dom-part-objects.tentative.html
+++ b/dom/parts/basic-dom-part-objects.tentative.html
@@ -8,7 +8,9 @@
   <div id=target style="display:none">
     Imperative test element
     <span id=a>A</span>
-    <span id=b>B</span>
+    <span id=b>B
+      <span id=sub>B-sub</span>
+    </span>
     <span id=c>C</span>
   </div>
 </template>
@@ -29,20 +31,17 @@
 }
 
 [false,true].forEach(useTemplate => {
+  const doc = useTemplate ? template.content : document;
+  const target = doc.querySelector('#target');
+  assert_true(!!(doc && target && target.children.length >= 3));
+  const description = useTemplate ? "DocumentFragment" : "Document";
   test((t) => {
-    let doc;
-    if (useTemplate) {
-      doc = template.content;
-    } else {
-      doc = document;
-    }
-    const target = doc.querySelector('#target');
-    assert_true(!!(doc && target && target.children.length >= 3));
     const root = doc.getPartRoot();
     assert_true(root instanceof DocumentPartRoot);
     assert_true(root instanceof PartRoot);
     const parts = root.getParts();
     assert_equals(parts.length,0,'getParts() should start out empty');
+    assert_true(root.root instanceof (useTemplate ? DocumentFragment : Document));
 
     const nodePart = addCleanup(t,new NodePart(root,target));
     assert_true(nodePart instanceof NodePart);
@@ -86,7 +85,42 @@
     assert_equals(childNodePart.previousSibling,null,'previousSibling should be null after disconnect');
     assert_equals(childNodePart.nextSibling,null,'nextSibling should be null after disconnect');
     assert_array_equals(root.getParts(),[nodePartBefore,nodePart]);
-  }, `Basic imperative DOM Parts object construction (${useTemplate ? "DocumentFragment" : "Document"})`);
+  }, `Basic imperative DOM Parts object construction (${description})`);
+
+  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));
+    const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild));
+    const childNodePart2 = addCleanup(t,new ChildNodePart(childNodePart,target.children[1].firstElementChild,target.children[1].firstElementChild));
+    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');
+    assert_array_equals(childNodePart2.getParts(),[]);
+
+    const clonedPartRoot = root.clone();
+    const clonedContainer = clonedPartRoot.root;
+    assert_true(clonedPartRoot instanceof DocumentPartRoot);
+    assert_true(clonedContainer instanceof (useTemplate ? DocumentFragment : Document));
+    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]);
+    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_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);
+  }, `Cloning (${description})`);
 });
 
 test((t) => {