summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/parts
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/dom/parts
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/dom/parts')
-rw-r--r--testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html101
-rw-r--r--testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html235
-rw-r--r--testing/web-platform/tests/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html129
-rw-r--r--testing/web-platform/tests/dom/parts/basic-dom-part-objects.tentative.html293
-rw-r--r--testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-body.tentative.html21
-rw-r--r--testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-head.tentative.html28
-rw-r--r--testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-root.tentative.html20
-rw-r--r--testing/web-platform/tests/dom/parts/dom-parts-valid-node-types.tentative.html55
-rw-r--r--testing/web-platform/tests/dom/parts/resources/domparts-utils.js12
9 files changed, 894 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html b/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html
new file mode 100644
index 0000000000..58db5cd04f
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<title>DOM Parts: Basic object structure, {{}} declarative API</title>
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<div id=context_elements>
+ <div></div>
+ <div parseparts></div>
+ <template></template>
+ <template parseparts class=expect_success></template>
+</div>
+
+<script>
+function assertIsComment(node,commentText) {
+ assert_true(node instanceof Comment);
+ assert_equals(node.textContent,commentText);
+}
+
+const declarativeOpenerNoParseparts = '<h1>';
+const declarativeOpenerParseparts = '<h1 parseparts>';
+const declarativeContent = '{{#}}First{{#}}<span {{}}>Middle</span>{{/}}Last{{/}}</h1>';
+
+Array.from(document.querySelectorAll('#context_elements>*')).forEach(contextEl => {
+ const expectParts = contextEl.classList.contains('expect_success');
+ [false,true].forEach(addParsePartsInside => {
+ const description = `${contextEl.outerHTML.split('><')[0]}><h1${addParsePartsInside ? " parseparts" : ""}>content...`;
+ const content = (addParsePartsInside ? declarativeOpenerParseparts : declarativeOpenerNoParseparts) + declarativeContent;
+
+ test((t) => {
+ const root = contextEl.content ? contextEl.content.getPartRoot() : document.getPartRoot();
+ assert_equals(root.getParts().length,0,'Should start with no parts');
+ t.add_cleanup(() => {
+ contextEl.replaceChildren();
+ root.getParts().forEach(part => part.disconnect());
+ });
+ contextEl.innerHTML = content;
+ if (expectParts) {
+ let expectedRootParts = [{type:'ChildNodePart',metadata:[]}];
+ assertEqualParts(root.getParts(),expectedRootParts,0,'declarative root missing parts');
+ const childPart1 = root.getParts()[0];
+ assertIsComment(childPart1.previousSibling,'');
+ assertIsComment(childPart1.nextSibling,'');
+ const expectedChild1Parts = [{type:'ChildNodePart',metadata:[]}];
+ assertEqualParts(childPart1.getParts(),expectedChild1Parts,0,'First level childpart should just have one child part');
+ const childPart2 = childPart1.getParts()[0];
+ assertIsComment(childPart2.previousSibling,'');
+ assertIsComment(childPart2.nextSibling,'');
+ const expectedChild2Parts = [{type:'NodePart',metadata:[]}];
+ assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have just the node part');
+ assert_true(childPart2.getParts()[0].node instanceof HTMLSpanElement);
+ assert_equals(childPart2.getParts()[0].node.textContent,'Middle');
+ } else {
+ assert_equals(root.getParts().length,0);
+ }
+ }, `Declarative DOM Parts innerHTML ${description} (expect${expectParts ? "" : " no"} parts)`);
+ });
+});
+
+test((t) => {
+ const tmpl = document.createElement('template');
+ tmpl.parseparts = true;
+ // See crbug.com/1490375.
+ tmpl.innerHTML = '<div {{}}></div>';
+ const root = tmpl.content.getPartRoot();
+ t.add_cleanup(() => {
+ tmpl.remove();
+ root.getParts().forEach(part => part.disconnect());
+ });
+ assert_equals(root.getParts().length,1,'There should be one NodePart');
+}, `Basic NodePart parsing`);
+
+
+test((t) => {
+ const tmpl = document.createElement('template');
+ tmpl.parseparts = true;
+ tmpl.innerHTML = ' <div id={{}} class={{}} foo=baz></div>';
+ const root = tmpl.content.getPartRoot();
+ t.add_cleanup(() => {
+ tmpl.remove();
+ root.getParts().forEach(part => part.disconnect());
+ });
+ assert_equals(root.getParts().length,0,'Declarative AttributeParts should be automatic, and should not show up in getParts()');
+ function checkBasics(checkContent,expectId,expectClass) {
+ const innerDiv = checkContent.firstElementChild;
+ assert_equals(innerDiv.localName,'div');
+ assert_equals(innerDiv.getAttribute('id'),expectId || '','Declarative AttributeParts id attribute');
+ assert_equals(innerDiv.getAttribute('class'),expectClass || '','Declarative AttributeParts class attribute');
+ assert_equals(innerDiv.getAttribute('foo'),'baz','Declarative AttributeParts should not touch other attributes');
+ return innerDiv;
+ }
+ checkBasics(tmpl.content);
+ const clone = root.clone();
+ const clonedDiv = checkBasics(clone.rootContainer);
+ const cloneWithValues = root.clone({attributeValues: ['foo','bar']});
+ const clonedDiv2 = checkBasics(cloneWithValues.rootContainer,'foo','bar');
+}, `Basic AttributePart cloning with values`);
+
+</script>
+
diff --git a/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html b/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html
new file mode 100644
index 0000000000..001ac24a44
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html
@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<title>DOM Parts: Basic object structure, {{}} declarative API</title>
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<div>
+ <!-- Note - the test will remove this chunk of DOM once the test completes -->
+ <div id=target2>
+ Declarative syntax - The *two* templates below should have IDENTICAL STRUCTURE
+ to this one. There are four cases to test:
+ 1. Main document parsing (this chunk)
+ 2. Template parsing (the template below with id=declarative)
+ 3. Template/fragment cloning (a clone of the template with id=declarative)
+ 4. Declarative Shadow DOM parsing (template with id=declarative_shadow_dom and shadowrootmode attribute)
+ <h1 id="name" parseparts>
+ {{#}}
+ First
+ {{#}} <span {{}} id={{}}>Middle</span> {{/}}
+ Last
+ {{/}}
+ <a foo {{}} id=nodepart1>content</a>
+ <a {{}} id=nodepart2>content</a>
+ <a {{}}id=nodepart3>content</a>
+ <a id=nodepart4 {{}}>content</a>
+ <a id=nodepart5 foo {{}}>content</a>
+ <a id=nodepart6 foo {{}} >content</a>
+ </h1>
+ </div>
+</div>
+<template id=declarative>
+ <div>
+ <div id=target3>Declarative syntax
+ <h1 id="name" parseparts>
+ {{#}}
+ First
+ {{#}} <span {{}} id={{}}>Middle</span> {{/}}
+ Last
+ {{/}}
+ <a foo {{}} id=nodepart1>content</a>
+ <a {{}} id=nodepart2>content</a>
+ <a {{}}id=nodepart3>content</a>
+ <a id=nodepart4 {{}}>content</a>
+ <a id=nodepart5 foo {{}}>content</a>
+ <a id=nodepart6 foo {{}} >content</a>
+ </h1>
+ </div>
+ </div>
+</template>
+
+<!-- TODO: This test should look at declarative shadow DOM behavior. -->
+
+<script> {
+function addPartsCleanup(t,partRoot) {
+ t.add_cleanup(() => partRoot.getParts().forEach(part => part.disconnect()));
+}
+
+const template = document.getElementById('declarative');
+['Main Document','Template','Clone','PartClone'].forEach(testCase => {
+ function assertIsComment(node,commentText) {
+ assert_true(node instanceof Comment);
+ assert_equals(node.textContent,commentText);
+ }
+
+ test((t) => {
+ let doc,target,wrapper,cleanup;
+ let expectDOMParts = true;
+ 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');
+ // A "normal" tree clone should not keep DOM Parts:
+ expectDOMParts = false;
+ cleanup = [wrapper];
+ break;
+ case 'PartClone':
+ doc = document;
+ wrapper = document.body.appendChild(document.createElement('div'));
+ wrapper.appendChild(template.content.getPartRoot().clone().rootContainer);
+ target = wrapper.querySelector('#target3');
+ // Even a PartRoot clone should not add parts to the document, when that
+ // clone is appendChilded to the document.
+ expectDOMParts = false;
+ cleanup = [wrapper];
+ break;
+ default:
+ assert_unreached('Invalid test case');
+ }
+ assert_true(!!(doc && target && target.parentElement));
+
+ const root = doc.getPartRoot();
+ t.add_cleanup(() => cleanup.forEach(el => el.remove())); // Cleanup
+ addPartsCleanup(t,root); // Clean up all Parts when this test ends.
+
+ assert_true(root instanceof DocumentPartRoot);
+ if (expectDOMParts) {
+ let expectedRootParts = [{type:'ChildNodePart',metadata:[]}];
+ for(let i=0;i<6;++i) {
+ expectedRootParts.push({type:'NodePart',metadata:[]});
+ }
+ assertEqualParts(root.getParts(),expectedRootParts,0,'declarative root missing parts');
+ for(let i=1;i<=6;++i) {
+ assert_equals(root.getParts()[i].node,target.querySelector(`#nodepart${i}`));
+ }
+ const childPart1 = root.getParts()[0];
+ assertIsComment(childPart1.previousSibling,'');
+ assertIsComment(childPart1.nextSibling,'');
+ const expectedChild1Parts = [{type:'ChildNodePart',metadata:[]}];
+ assertEqualParts(childPart1.getParts(),expectedChild1Parts,0,'First level childpart should just have one child part');
+ const childPart2 = childPart1.getParts()[0];
+ assertIsComment(childPart2.previousSibling,'');
+ assertIsComment(childPart2.nextSibling,'');
+ const expectedChild2Parts = [{type:'NodePart',metadata:[]}];
+ assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have just the node part (AttributePart is automatic)');
+ assert_true(childPart2.getParts()[0].node instanceof HTMLSpanElement);
+ assert_equals(childPart2.getParts()[0].node.textContent,'Middle');
+ } else {
+ assertEqualParts(root.getParts(),[],[]);
+ }
+ }, `Basic declarative DOM Parts (${testCase})`);
+});
+
+}</script>
+
+<div parseparts>Before {{#}}Parts{{/}} After</div>
+<script>
+ test((t) => {
+ const target = document.currentScript.previousElementSibling;
+ t.add_cleanup(() => target.remove());
+ const root = document.getPartRoot();
+ addPartsCleanup(t,root);
+ assert_equals(root.getParts().length,1);
+ assert_equals(target.innerHTML,'Before <!---->Parts<!----> After');
+
+ // Verify that removing the parseparts attribute does nothing.
+ target.removeAttribute('parseparts');
+ assert_equals(root.getParts().length,1);
+ assert_equals(target.innerHTML,'Before <!---->Parts<!----> After');
+ }, 'Post-parsing structure of child parts, and stickiness');
+</script>
+
+<div>Before {{#}}Parts{{/}} After</div>
+<script>{
+ test((t) => {
+ const target = document.currentScript.previousElementSibling;
+ t.add_cleanup(() => target.remove());
+ const root = document.getPartRoot();
+ addPartsCleanup(t,root);
+ assert_equals(root.getParts().length,0);
+ assert_equals(target.innerHTML,'Before {{#}}Parts{{/}} After');
+ target.setAttribute('parseparts','');
+ assert_equals(root.getParts().length,0);
+ assert_equals(target.innerHTML,'Before {{#}}Parts{{/}} After');
+ }, 'Parser only behavior - adding parseparts does nothing');
+}</script>
+
+<div parseparts>{{#}}{{/}}</div>
+<script>{
+ test((t) => {
+ const target = document.currentScript.previousElementSibling;
+ t.add_cleanup(() => target.remove());
+ const root = document.getPartRoot();
+ addPartsCleanup(t,root);
+ assert_equals(root.getParts().length,1);
+ assert_equals(target.innerHTML,'<!----><!---->');
+ }, 'Just parts, no text before');
+}</script>
+
+<div><input parseparts>{{#}}{{/}}</input></div>
+<script>{
+ test((t) => {
+ const target = document.currentScript.previousElementSibling;
+ t.add_cleanup(() => target.remove());
+ const root = document.getPartRoot();
+ addPartsCleanup(t,root);
+ assert_equals(root.getParts().length,0);
+ assert_equals(target.innerHTML,'<input parseparts=\"\">{{#}}{{/}}');
+ }, 'Self closing elements can\'t use parseparts');
+}</script>
+
+<div><head parseparts>{{#}}{{/}}</head></div>
+<script>{
+ test((t) => {
+ const target = document.currentScript.previousElementSibling;
+ t.add_cleanup(() => target.remove());
+ const root = document.getPartRoot();
+ addPartsCleanup(t,root);
+ assert_equals(root.getParts().length,0);
+ assert_equals(target.innerHTML,'{{#}}{{/}}');
+ }, 'Second head element can\'t use parseparts');
+}</script>
+
+<div parseparts><svg>{{#}}<circle/>{{/}}</svg></div>
+
+<script>{
+ test((t) => {
+ const target = document.currentScript.previousElementSibling;
+ t.add_cleanup(() => target.remove());
+ const root = document.getPartRoot();
+ addPartsCleanup(t,root);
+ assert_equals(root.getParts().length,1);
+ assert_equals(target.innerHTML,'<svg><!----><circle/><!----></svg>');
+ }, 'Foreign content should support Parts');
+}</script>
+
+<div>
+ <div parseparts>{{}}{{ }}{{ #}}{{ /}}{{{}}}</div>
+ <div>{{}}{{ }}{{ #}}{{ /}}{{{}}}</div>
+</div>
+<script>{
+ test((t) => {
+ const target = document.currentScript.previousElementSibling;
+ t.add_cleanup(() => target.remove());
+ const root = document.getPartRoot();
+ addPartsCleanup(t,root);
+ assert_equals(root.getParts().length,0);
+ assert_equals(target.childElementCount,2);
+ Array.from(target.children).forEach(el => {
+ assert_equals(el.innerHTML,'{{}}{{ }}{{ #}}{{ /}}{{{}}}');
+ })
+ }, 'Not quite parts syntax - none should become parts, and nothing should crash');
+}</script>
diff --git a/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html b/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html
new file mode 100644
index 0000000000..f646035394
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<title>DOM Parts: Basic object structure, &lt;?child-node-part?> declarative API</title>
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<div>
+ <!-- Note - the test will remove this chunk of DOM once the test completes -->
+ <div id=target2>
+ Declarative syntax - The *two* templates below should have IDENTICAL STRUCTURE
+ to this one. There are four cases to test:
+ 1. Main document parsing (this chunk)
+ 2. Template parsing (the template below with id=declarative)
+ 3. Template/fragment cloning (a clone of the template with id=declarative)
+ 4. Declarative Shadow DOM parsing (template with id=declarative_shadow_dom and shadowrootmode attribute)
+ <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>
+
+<!-- TODO: This test should look at declarative shadow DOM behavior. -->
+
+<script> {
+const template = document.getElementById('declarative');
+['Main Document','Template','Clone','PartClone'].forEach(testCase => {
+ function assertIsComment(node,commentText) {
+ assert_true(node instanceof Comment);
+ assert_equals(node.textContent,commentText);
+ }
+
+ test((t) => {
+ let doc,target,wrapper,cleanup;
+ let expectDOMParts = true;
+ 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');
+ // A "normal" tree clone should not keep DOM Parts:
+ expectDOMParts = false;
+ cleanup = [wrapper];
+ break;
+ case 'PartClone':
+ doc = document;
+ wrapper = document.body.appendChild(document.createElement('div'));
+ assert_true(template.content.getPartRoot().getParts().length != 0);
+ wrapper.appendChild(template.content.getPartRoot().clone().rootContainer);
+ target = wrapper.querySelector('#target3');
+ // Even a PartRoot clone should not add parts to the document, when that
+ // clone is appendChilded to the document.
+ expectDOMParts = false;
+ cleanup = [wrapper];
+ break;
+ default:
+ assert_unreached('Invalid test case');
+ }
+ assert_true(!!(doc && target && target.parentElement));
+ const root = doc.getPartRoot();
+ t.add_cleanup(() => cleanup.forEach(el => el.remove())); // Cleanup
+ t.add_cleanup(() => root.getParts().forEach(part => part.disconnect()));
+
+ assert_true(root instanceof DocumentPartRoot);
+ if (expectDOMParts) {
+ 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 ');
+ } else {
+ assertEqualParts(root.getParts(),[],[]);
+ }
+ }, `Basic declarative DOM Parts (${testCase})`);
+});
+
+}</script>
diff --git a/testing/web-platform/tests/dom/parts/basic-dom-part-objects.tentative.html b/testing/web-platform/tests/dom/parts/basic-dom-part-objects.tentative.html
new file mode 100644
index 0000000000..d7834fe69b
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/basic-dom-part-objects.tentative.html
@@ -0,0 +1,293 @@
+<!DOCTYPE html>
+<title>DOM Parts: Basic object structure, imperative API</title>
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<body>
+<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>
+ <span id=direct_child_1></span>
+ <span id=direct_child_2></span>
+</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;
+ let target,wrapper,directChildren;
+ if (useTemplate) {
+ target = doc.querySelector('#target1');
+ directChildren = [doc.querySelector('#direct_child_1'),doc.querySelector('#direct_child_2')];
+ } else {
+ wrapper = document.body.appendChild(document.createElement('div'));
+ wrapper.appendChild(template.content.cloneNode(true));
+ target = wrapper.querySelector('#target1');
+ directChildren = [doc.documentElement,doc.documentElement];
+ }
+ 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();
+ assert_true(root instanceof DocumentPartRoot);
+ const parts = root.getParts();
+ assert_equals(parts.length,0,'getParts() should start out empty');
+ assert_true(root.rootContainer instanceof (useTemplate ? DocumentFragment : Document));
+
+ const nodePart = addCleanup(t,new NodePart(root,target,{metadata: ['foo']}));
+ assertEqualParts([nodePart],[{type:'NodePart',metadata:['foo']}],0,'Basic NodePart');
+ assert_equals(nodePart.node,target);
+ assert_equals(nodePart.root,root);
+ 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 attributePart = addCleanup(t,new AttributePart(root,target,'attributename',/*automatic*/false,{metadata: ['attribute-non-auto']}));
+ assertEqualParts([attributePart],[{type:'AttributePart',metadata:['attribute-non-auto']}],0,'Basic AttributePart');
+ assert_equals(attributePart.node,target);
+ assert_equals(attributePart.root,root);
+ assert_equals(attributePart.localName,'attributename');
+ assert_equals(attributePart.automatic,false);
+ runningPartsExpectation.push({type:'AttributePart',metadata:['attribute-non-auto']});
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart],'getParts() for the root should now have this attributePart');
+ assert_equals(parts.length,0,'Return value of getParts() is not live');
+
+ const attributePartAuto = addCleanup(t,new AttributePart(root,target,'attributename',/*automatic*/true,{metadata: ['attribute-auto']}));
+ assertEqualParts([attributePartAuto],[{type:'AttributePart',metadata:['attribute-auto']}],0,'Basic automatic AttributePart');
+ assert_equals(attributePartAuto.node,target);
+ assert_equals(attributePartAuto.root,root);
+ assert_equals(attributePartAuto.localName,'attributename');
+ assert_equals(attributePartAuto.automatic,true);
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart],'automatic AttributePart should not get included in getParts()');
+
+ const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2],{metadata:['bar','baz']}));
+ 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_equals(childNodePart.getParts().length,0,'childNodePart.getParts() should start out empty');
+ runningPartsExpectation.push({type:'ChildNodePart',metadata:['bar','baz']});
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart],'getParts() for the root should now have this childNodePart');
+
+ const nodeBefore = target.previousSibling || target.parentNode;
+ const nodePartBefore = addCleanup(t,new NodePart(root,nodeBefore));
+ runningPartsExpectation.push({type:'NodePart',metadata:[]});
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart,nodePartBefore],'getParts() for the root should now have this nodePart, in construction order');
+
+ const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[2],{metadata:['blah']}));
+ assert_equals(nodePart2.root,childNodePart);
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart,nodePartBefore],'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()');
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart,nodePartBefore],'getParts() for the root DocumentPartRoot still shouldn\'t change');
+ nodePart2.disconnect(); // Calling twice should be ok.
+
+ childNodePart.disconnect();
+ assert_equals(childNodePart.root,null,'root should be null after disconnect');
+ 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,attributePart]);
+ }, `Basic imperative DOM Parts object construction (${description})`);
+
+ function cloneRange(parent,previousSibling,nextSibling) {
+ const clone = parent.cloneNode(false);
+ let node = previousSibling;
+ while (node) {
+ clone.appendChild(node.cloneNode(true));
+ if (node == nextSibling) {
+ break;
+ }
+ node = node.nextSibling;
+ }
+ return clone;
+ }
+
+ test((t) => {
+ const root = doc.getPartRoot();
+ const nodePart = addCleanup(t,new NodePart(root,target,{metadata:['node1']}));
+ const attributePart = addCleanup(t,new AttributePart(root,target,'attributeName',/*automatic*/false,{metadata: ['attribute']}));
+ const nonTrackedAttributePart = addCleanup(t,new AttributePart(root,target,'attributeName',/*automatic*/true,{metadata: ['attribute-auto']}));
+ 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']}));
+ let rootExpectations = [{type:'NodePart',metadata:['node1']},{type:'AttributePart',metadata:['attribute']},{type:'ChildNodePart',metadata:['child']}];
+ assertEqualParts(root.getParts(),rootExpectations,[nodePart,attributePart,childNodePart],'setup');
+ let childExpectations = [{type:'NodePart',metadata:['node 3']},{type:'NodePart',metadata:['node 2']},{type:'ChildNodePart',metadata:['childnodepart2']}];
+ assertEqualParts(childNodePart.getParts(),childExpectations,[nodePart3,nodePart2,childNodePart2],'setup');
+ assert_array_equals(childNodePart2.getParts(),[]);
+
+ // Test cloning of the entire DocumentPartRoot.
+ const clonedPartRoot = root.clone();
+ assertEqualParts(root.getParts(),rootExpectations,[nodePart,attributePart,childNodePart],'cloning a part root should not change the original');
+ const clonedContainer = clonedPartRoot.rootContainer;
+ 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);
+ 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 newAttributePart = clonedPartRoot.getParts()[1];
+ const newChildNodePart = clonedPartRoot.getParts()[2];
+ assert_not_equals(newNodePart.node,target,'Node references should not point to original nodes');
+ assert_equals(newNodePart.node.id,target.id,'New parts should point to cloned nodes');
+ assert_not_equals(newAttributePart.node,target,'Node references should not point to original nodes');
+ assert_equals(newAttributePart.node.id,target.id,'New parts should point to cloned nodes');
+ assert_equals(newAttributePart.localName,attributePart.localName,'New attribute parts should carry over localName');
+ assert_equals(newAttributePart.automatic,attributePart.automatic,'New attribute parts should carry over automatic');
+ 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');
+ assertEqualParts(newChildNodePart.getParts(),childExpectations,0,'cloned PartRoot should contain identical parts');
+
+ // Test cloning of ChildNodeParts.
+ const clonedChildNodePartRoot = childNodePart.clone();
+ const clonedChildContainer = clonedChildNodePartRoot.rootContainer;
+ assert_true(clonedChildNodePartRoot instanceof ChildNodePart);
+ assert_true(clonedChildContainer instanceof Element);
+ assert_not_equals(clonedChildContainer,target);
+ assert_equals(clonedChildContainer.outerHTML,cloneRange(target,a,c).outerHTML);
+ assertEqualParts(clonedChildNodePartRoot.getParts(),childExpectations,0,'clone of childNodePart should match');
+ }, `Cloning (${description})`);
+
+ ['Element','Text','Comment'].forEach(nodeType => {
+ test((t) => {
+ const root = doc.getPartRoot();
+ assert_equals(root.getParts().length,0);
+ let node;
+ switch (nodeType) {
+ case 'Element' : node = document.createElement('div'); break;
+ case 'Text' : node = document.createTextNode('hello'); break;
+ case 'Comment': node = document.createComment('comment'); break;
+ }
+ t.add_cleanup(() => node.remove());
+ doc.firstElementChild.append(node);
+ // NodePart
+ const nodePart = addCleanup(t,new NodePart(root,node,{metadata:['foobar']}));
+ assert_true(!!nodePart);
+ const clone = root.clone();
+ assert_equals(clone.getParts().length,1);
+ assertEqualParts(clone.getParts(),[{type:'NodePart',metadata:['foobar']}],0,'getParts');
+ assert_true(clone.getParts()[0].node instanceof window[nodeType]);
+
+ // ChildNodePart
+ const node2 = node.cloneNode(false);
+ node.parentElement.appendChild(node2);
+ const childNodePart = addCleanup(t,new ChildNodePart(root,node,node2,{metadata:['baz']}));
+ assert_true(!!childNodePart);
+ const clone2 = root.clone();
+ assert_equals(clone2.getParts().length,2);
+ assertEqualParts(clone2.getParts(),[{type:'NodePart',metadata:['foobar']},{type:'ChildNodePart',metadata:['baz']}],0,'getParts2');
+ assert_true(clone2.getParts()[1].previousSibling 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(),[nodePartB,nodePartA,nodePartC],'Parts can be out of order, if added out of order');
+ b.remove();
+ assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Removals are not tracked');
+ target.parentElement.insertBefore(b,target);
+ assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Insertions are not tracked');
+ target.insertBefore(b,c);
+ assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Nothing is tracked');
+ nodePartA.disconnect();
+ nodePartB.disconnect();
+ nodePartC.disconnect();
+ assert_array_equals(root.getParts(),[],'disconnections are tracked');
+
+ 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(),[],'Insertions are not tracked');
+
+ target.insertBefore(c,a);
+ assert_array_equals(root.getParts(),[],'Endpoints out of order');
+ target.appendChild(c); // Restore
+ assert_array_equals(root.getParts(),[],'Insertions are not tracked');
+
+ document.body.appendChild(c);
+ assert_array_equals(root.getParts(),[],'Parts are\'t invalidated when endpoints have different parents');
+ target.appendChild(c); // Restore
+ assert_array_equals(root.getParts(),[],'Insertions are not tracked');
+
+ const oldParent = target.parentElement;
+ target.remove();
+ assert_array_equals(root.getParts(),[],'Parts are\'t invalidated when disconnected');
+ oldParent.appendChild(target); // Restore
+ assert_array_equals(root.getParts(),[]);
+ }, `DOM mutations are not tracked (${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})`);
+
+ test((t) => {
+ const root = doc.getPartRoot();
+ // Make sure no crashes occur for parts with mismatched endpoint nodes.
+ const cornerCasePartsInvalid = [
+ addCleanup(t,new ChildNodePart(root,target, target.children[2],{metadata: ['different parents']})),
+ addCleanup(t,new ChildNodePart(root,target.children[0], target,{metadata: ['different parents']})),
+ addCleanup(t,new ChildNodePart(root,target.children[2], target.children[0],{metadata: ['reversed endpoints']})),
+ ];
+ const cornerCasePartsValid = [];
+ if (directChildren[0] !== directChildren[1]) {
+ cornerCasePartsValid.push(addCleanup(t,new ChildNodePart(root,directChildren[0], directChildren[1],{metadata: ['direct parent of the root container']})));
+ }
+ assert_array_equals(root.getParts(),cornerCasePartsValid);
+ assert_equals(root.clone().getParts().length,cornerCasePartsValid.length);
+ }, `Corner case ChildNodePart construction and cloning (${description})`);
+
+ wrapper?.remove(); // Cleanup
+});
+</script>
diff --git a/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-body.tentative.html b/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-body.tentative.html
new file mode 100644
index 0000000000..542c7b2881
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-body.tentative.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<title>DOM Parts: parseparts on body</title>
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<body parseparts>
+<div>{{#}}Parts{{/}}</div>
+
+<script>
+ test(() => {
+ const target = document.currentScript.previousElementSibling;
+ assert_true(document.body.hasAttribute('parseparts'));
+ const root = document.getPartRoot();
+ const expectedRootParts = [{type:'ChildNodePart',metadata:[]}];
+ assertEqualParts(root.getParts(),expectedRootParts,0,'declarative syntax should be recognized');
+ target.remove();
+ }, 'It is possible to put parseparts on the body element');
+</script>
diff --git a/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-head.tentative.html b/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-head.tentative.html
new file mode 100644
index 0000000000..e65c0d728f
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-head.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head parseparts>
+ <title>{{#}}{{/}}DOM Parts: parseparts on head</title>
+
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<!-- no explicit body element-->
+<div>{{#}}Parts{{/}}</div>
+
+<script>
+ test(() => {
+ const target = document.currentScript.previousElementSibling;
+ assert_true(document.head.hasAttribute('parseparts'));
+ assert_false(document.body.hasAttribute('parseparts'));
+ assert_equals(target.previousSibling,null,'The div is the first thing in body, everything else in head');
+ const root = document.getPartRoot();
+ assert_equals(root.getParts().length,0,'No parts should be recognized');
+ assert_equals(target.innerHTML,'{{#}}Parts{{/}}');
+ const titleElement = document.head.querySelector('title');
+ assert_true(titleElement instanceof HTMLTitleElement);
+ assert_equals(titleElement.innerHTML,'{{#}}{{/}}DOM Parts: parseparts on head');
+ target.remove();
+ }, 'It is not possible to put parseparts on the head element');
+</script>
diff --git a/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-root.tentative.html b/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-root.tentative.html
new file mode 100644
index 0000000000..f3ba3eaf6b
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/dom-parts-parseparts-on-root.tentative.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html parseparts>
+<title>DOM Parts: parseparts on root</title>
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<div>{{#}}Parts{{/}}</div>
+
+<script>
+ test(() => {
+ const target = document.currentScript.previousElementSibling;
+ assert_true(document.documentElement.hasAttribute('parseparts'));
+ const root = document.getPartRoot();
+ assert_equals(root.getParts().length,0);
+ assert_equals(target.innerHTML,'{{#}}Parts{{/}}');
+ target.remove();
+ }, 'It is not possible to put parseparts on the root element');
+</script>
diff --git a/testing/web-platform/tests/dom/parts/dom-parts-valid-node-types.tentative.html b/testing/web-platform/tests/dom/parts/dom-parts-valid-node-types.tentative.html
new file mode 100644
index 0000000000..f9ed167e60
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/dom-parts-valid-node-types.tentative.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>DOM Parts: Valid node types for constructors</title>
+<meta name="author" href="mailto:masonf@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/domparts-utils.js"></script>
+
+<body>
+<template foo></template>
+<script>
+ const root = document.getPartRoot();
+ const xml = new DOMParser().parseFromString("<xml></xml>", "application/xml");
+ const cdata = xml.createCDATASection('cdata');
+ document.body.appendChild(cdata);
+ const pi = document.createProcessingInstruction('processing','instruction');
+ document.body.appendChild(pi);
+ const invalidNodes = {
+ document: document.documentElement,
+ documentType: document.doctype,
+ attributeNode: document.querySelector('[foo]').attributes[0],
+ cdataSection: cdata,
+ processingInstruction: pi,
+ documentFragment: document.querySelector('[foo]').content,
+ };
+
+ const types = Object.keys(invalidNodes);
+ for(let i=0;i<types.length;++i) {
+ const type = types[i];
+ const obj = invalidNodes[types[i]];
+ const otherObj = invalidNodes[types[(i+1) % types.length]];
+ test((t) => {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", () => {
+ new NodePart(root, obj, {});
+ });
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", () => {
+ new ChildNodePart(root, obj, otherObj, {});
+ });
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", () => {
+ new ChildNodePart(root, otherObj, obj, {});
+ });
+ },`Invalid node types (${type})`);
+ }
+
+ test((t) => {
+ try {
+ const cnp = new ChildNodePart(root, invalidNodes.documentType, invalidNodes.document, {});
+ cnp.clone();
+ } catch {};
+ try {
+ const np = new NodePart(root, invalidNodes.documentType, {});
+ np.clone();
+ } catch {};
+ // This test passes if it does not crash.
+ },'Crash test');
+</script>
diff --git a/testing/web-platform/tests/dom/parts/resources/domparts-utils.js b/testing/web-platform/tests/dom/parts/resources/domparts-utils.js
new file mode 100644
index 0000000000..5deeec80a3
--- /dev/null
+++ b/testing/web-platform/tests/dom/parts/resources/domparts-utils.js
@@ -0,0 +1,12 @@
+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`);
+ assert_equals(parts[i].root.getPartNode(i),parts[i].node || parts[i].previousSibling,'getPartNode() should return the same node as getParts().node/previousSibling');
+ }
+ }
+}