diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/dom/parts | |
parent | Initial commit. (diff) | |
download | firefox-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')
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, <?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'); + } + } +} |