| <!DOCTYPE html> |
| <title>DOM Parts: Basic object structure, {{}} declarative API</title> |
| <link rel=author href="mailto:[email protected]"> |
| <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 kChildNodePartStartCommentData = "#"; |
| const kChildNodePartEndCommentData = "/"; |
| function assertIsComment(node, commentText) { |
| assert_true(node instanceof Comment); |
| // TODO(crbug.com/40271855): While developing alternative syntax, the comment might be empty or it might be "#" or "/". |
| assert_true(node.textContent === '' || node.textContent === commentText); |
| } |
| |
| const template = document.getElementById('declarative'); |
| ['Main Document','Template','Clone','PartClone'].forEach(testCase => { |
| 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,kChildNodePartStartCommentData); |
| assertIsComment(childPart1.nextSibling,kChildNodePartEndCommentData); |
| 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,kChildNodePartStartCommentData); |
| assertIsComment(childPart2.nextSibling,kChildNodePartEndCommentData); |
| const expectedChild2Parts = [{type:'NodePart',metadata:[]},{type:'AttributePart',metadata:[]}]; |
| assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have NodePart and AttributePart'); |
| assert_true(childPart2.getParts()[0].node instanceof HTMLSpanElement); |
| assert_equals(childPart2.getParts()[0].node.textContent,'Middle'); |
| assert_true(childPart2.getParts()[1].node instanceof HTMLSpanElement); |
| assert_equals(childPart2.getParts()[1].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> |