basic-dom-part-objects.tentative.html (16304B)
1 <!DOCTYPE html> 2 <title>DOM Parts: Basic object structure, imperative API</title> 3 <link rel=author href="mailto:masonf@chromium.org"> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src="./resources/domparts-utils.js"></script> 7 8 <body> 9 <template id=imperative> 10 <div> 11 <div id=target1 style="display:none"> 12 Imperative test element 13 <span id=a>A</span><span id=b>B 14 <span id=sub>B-sub1</span> 15 <span id=sub>B-sub2</span> 16 </span><span id=c>C</span></div> 17 </div> 18 <span id=direct_child_1></span> 19 <span id=direct_child_2></span> 20 </template> 21 22 <script> 23 const template = document.getElementById('imperative'); 24 function addCleanup(t, part) { 25 t.add_cleanup(() => part.disconnect()); 26 return part; 27 } 28 [false,true].forEach(useTemplate => { 29 const doc = useTemplate ? template.content : document; 30 let target,wrapper,directChildren; 31 if (useTemplate) { 32 target = doc.querySelector('#target1'); 33 directChildren = [doc.querySelector('#direct_child_1'),doc.querySelector('#direct_child_2')]; 34 } else { 35 wrapper = document.body.appendChild(document.createElement('div')); 36 wrapper.appendChild(template.content.cloneNode(true)); 37 target = wrapper.querySelector('#target1'); 38 directChildren = [doc.documentElement,doc.documentElement]; 39 } 40 const a = target.querySelector('#a'); 41 const b = target.querySelector('#b'); 42 const c = target.querySelector('#c'); 43 assert_true(!!(doc && target && target.parentElement && a && b && c)); 44 const description = useTemplate ? "DocumentFragment" : "Document"; 45 test((t) => { 46 const root = doc.getPartRoot(); 47 assert_true(root instanceof DocumentPartRoot); 48 const parts = root.getParts(); 49 assert_equals(parts.length,0,'getParts() should start out empty'); 50 assert_true(root.rootContainer instanceof (useTemplate ? DocumentFragment : Document)); 51 52 const nodePart = addCleanup(t,new NodePart(root,target,{metadata: ['foo']})); 53 assertEqualParts([nodePart],[{type:'NodePart',metadata:['foo']}],0,'Basic NodePart'); 54 assert_equals(nodePart.node,target); 55 assert_equals(nodePart.root,root); 56 let runningPartsExpectation = [{type:'NodePart',metadata:['foo']}]; 57 assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart],'getParts() for the root should now have this nodePart'); 58 assert_equals(parts.length,0,'Return value of getParts() is not live'); 59 60 assert_throws_js(TypeError,() => new NodePart(nodePart,target.children[0]),'Constructing a Part with a NodePart as the PartRoot should throw'); 61 62 const attributePart = addCleanup(t,new AttributePart(root,target,'attributename',{metadata: ['attribute']})); 63 assertEqualParts([attributePart],[{type:'AttributePart',metadata:['attribute']}],0,'Basic AttributePart'); 64 assert_equals(attributePart.node,target); 65 assert_equals(attributePart.root,root); 66 assert_equals(attributePart.localName,'attributename'); 67 runningPartsExpectation.push({type:'AttributePart',metadata:['attribute']}); 68 assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart],'getParts() for the root should now have this attributePart'); 69 assert_equals(parts.length,0,'Return value of getParts() is not live'); 70 71 const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2],{metadata:['bar','baz']})); 72 assertEqualParts([childNodePart],[{type:'ChildNodePart',metadata:['bar','baz']}],0,'Basic ChildNodePart'); 73 assert_equals(childNodePart.root,root); 74 assert_equals(childNodePart.previousSibling,target.children[0]); 75 assert_equals(childNodePart.nextSibling,target.children[2]); 76 assert_equals(childNodePart.getParts().length,0,'childNodePart.getParts() should start out empty'); 77 runningPartsExpectation.push({type:'ChildNodePart',metadata:['bar','baz']}); 78 assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart],'getParts() for the root should now have this childNodePart'); 79 80 const nodeBefore = target.previousSibling || target.parentNode; 81 const nodePartBefore = addCleanup(t,new NodePart(root,nodeBefore)); 82 runningPartsExpectation.push({type:'NodePart',metadata:[]}); 83 assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart,nodePartBefore],'getParts() for the root should now have this nodePart, in construction order'); 84 85 const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[2],{metadata:['blah']})); 86 assert_equals(nodePart2.root,childNodePart); 87 assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart,nodePartBefore],'getParts() for the root DocumentPartRoot shouldn\'t change'); 88 assertEqualParts(childNodePart.getParts(),[{type:'NodePart',metadata:['blah']}],[nodePart2],'getParts() for the childNodePart should have it'); 89 90 nodePart2.disconnect(); 91 assert_equals(nodePart2.root,null,'root should be null after disconnect'); 92 assert_equals(nodePart2.node,null,'node should be null after disconnect'); 93 assert_equals(childNodePart.getParts().length,0,'calling disconnect() should remove the part from root.getParts()'); 94 assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart,childNodePart,nodePartBefore],'getParts() for the root DocumentPartRoot still shouldn\'t change'); 95 nodePart2.disconnect(); // Calling twice should be ok. 96 97 childNodePart.disconnect(); 98 assert_equals(childNodePart.root,null,'root should be null after disconnect'); 99 assert_equals(childNodePart.previousSibling,null,'previousSibling should be null after disconnect'); 100 assert_equals(childNodePart.nextSibling,null,'nextSibling should be null after disconnect'); 101 assert_array_equals(root.getParts(),[nodePartBefore,nodePart,attributePart]); 102 }, `Basic imperative DOM Parts object construction (${description})`); 103 104 function cloneRange(parent,previousSibling,nextSibling) { 105 const clone = parent.cloneNode(false); 106 let node = previousSibling; 107 while (node) { 108 clone.appendChild(node.cloneNode(true)); 109 if (node == nextSibling) { 110 break; 111 } 112 node = node.nextSibling; 113 } 114 return clone; 115 } 116 117 test((t) => { 118 const root = doc.getPartRoot(); 119 const nodePart = addCleanup(t,new NodePart(root,target,{metadata:['node1']})); 120 const attributePart = addCleanup(t,new AttributePart(root,target,'attributeName',{metadata: ['attribute']})); 121 const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2],{metadata:['child']})); 122 const nodePart3 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['node 3']})); 123 const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['node 2']})); 124 const childNodePart2 = addCleanup(t,new ChildNodePart(childNodePart,target.children[1].firstElementChild,target.children[1].firstElementChild.nextSibling,{metadata: ['childnodepart2']})); 125 let rootExpectations = [{type:'NodePart',metadata:['node1']},{type:'AttributePart',metadata:['attribute']},{type:'ChildNodePart',metadata:['child']}]; 126 assertEqualParts(root.getParts(),rootExpectations,[nodePart,attributePart,childNodePart],'setup'); 127 let childExpectations = [{type:'NodePart',metadata:['node 3']},{type:'NodePart',metadata:['node 2']},{type:'ChildNodePart',metadata:['childnodepart2']}]; 128 assertEqualParts(childNodePart.getParts(),childExpectations,[nodePart3,nodePart2,childNodePart2],'setup'); 129 assert_array_equals(childNodePart2.getParts(),[]); 130 131 // Test cloning of the entire DocumentPartRoot. 132 const clonedPartRoot = root.clone(); 133 assertEqualParts(root.getParts(),rootExpectations,[nodePart,attributePart,childNodePart],'cloning a part root should not change the original'); 134 const clonedContainer = clonedPartRoot.rootContainer; 135 assert_true(clonedPartRoot instanceof DocumentPartRoot); 136 assert_true(clonedContainer instanceof (useTemplate ? DocumentFragment : Document)); 137 assert_not_equals(clonedPartRoot,root); 138 assert_not_equals(clonedContainer,doc); 139 assert_equals(doc.innerHTML,clonedContainer.innerHTML); 140 assertEqualParts(clonedPartRoot.getParts(),rootExpectations,0,'cloned PartRoot should contain identical parts'); 141 assert_true(!clonedPartRoot.getParts().includes(nodePart),'Original parts should not be retained'); 142 assert_true(!clonedPartRoot.getParts().includes(childNodePart)); 143 const newNodePart = clonedPartRoot.getParts()[0]; 144 const newAttributePart = clonedPartRoot.getParts()[1]; 145 const newChildNodePart = clonedPartRoot.getParts()[2]; 146 assert_not_equals(newNodePart.node,target,'Node references should not point to original nodes'); 147 assert_equals(newNodePart.node.id,target.id,'New parts should point to cloned nodes'); 148 assert_not_equals(newAttributePart.node,target,'Node references should not point to original nodes'); 149 assert_equals(newAttributePart.node.id,target.id,'New parts should point to cloned nodes'); 150 assert_equals(newAttributePart.localName,attributePart.localName,'New attribute parts should carry over localName'); 151 assert_not_equals(newChildNodePart.previousSibling,a,'Node references should not point to original nodes'); 152 assert_equals(newChildNodePart.previousSibling.id,'a'); 153 assert_not_equals(newChildNodePart.nextSibling,c,'Node references should not point to original nodes'); 154 assert_equals(newChildNodePart.nextSibling.id,'c'); 155 assertEqualParts(newChildNodePart.getParts(),childExpectations,0,'cloned PartRoot should contain identical parts'); 156 157 // Test cloning of ChildNodeParts. 158 const clonedChildNodePartRoot = childNodePart.clone(); 159 const clonedChildContainer = clonedChildNodePartRoot.rootContainer; 160 assert_true(clonedChildNodePartRoot instanceof ChildNodePart); 161 assert_true(clonedChildContainer instanceof Element); 162 assert_not_equals(clonedChildContainer,target); 163 assert_equals(clonedChildContainer.outerHTML,cloneRange(target,a,c).outerHTML); 164 assertEqualParts(clonedChildNodePartRoot.getParts(),childExpectations,0,'clone of childNodePart should match'); 165 }, `Cloning (${description})`); 166 167 ['Element','Text','Comment'].forEach(nodeType => { 168 test((t) => { 169 const root = doc.getPartRoot(); 170 assert_equals(root.getParts().length,0); 171 let node; 172 switch (nodeType) { 173 case 'Element' : node = document.createElement('div'); break; 174 case 'Text' : node = document.createTextNode('hello'); break; 175 case 'Comment': node = document.createComment('comment'); break; 176 } 177 t.add_cleanup(() => node.remove()); 178 doc.firstElementChild.append(node); 179 // NodePart 180 const nodePart = addCleanup(t,new NodePart(root,node,{metadata:['foobar']})); 181 assert_true(!!nodePart); 182 const clone = root.clone(); 183 assert_equals(clone.getParts().length,1); 184 assertEqualParts(clone.getParts(),[{type:'NodePart',metadata:['foobar']}],0,'getParts'); 185 assert_true(clone.getParts()[0].node instanceof window[nodeType]); 186 187 // ChildNodePart 188 const node2 = node.cloneNode(false); 189 node.parentElement.appendChild(node2); 190 const childNodePart = addCleanup(t,new ChildNodePart(root,node,node2,{metadata:['baz']})); 191 assert_true(!!childNodePart); 192 const clone2 = root.clone(); 193 assert_equals(clone2.getParts().length,2); 194 assertEqualParts(clone2.getParts(),[{type:'NodePart',metadata:['foobar']},{type:'ChildNodePart',metadata:['baz']}],0,'getParts2'); 195 assert_true(clone2.getParts()[1].previousSibling instanceof window[nodeType]); 196 }, `Cloning ${nodeType} (${description})`); 197 }); 198 199 test((t) => { 200 const root = doc.getPartRoot(); 201 assert_equals(root.getParts().length,0,'Test harness check: tests should clean up parts'); 202 203 const nodePartB = addCleanup(t,new NodePart(root,b)); 204 const nodePartA = addCleanup(t,new NodePart(root,a)); 205 const nodePartC = addCleanup(t,new NodePart(root,c)); 206 assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Parts can be out of order, if added out of order'); 207 b.remove(); 208 assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Removals are not tracked'); 209 target.parentElement.insertBefore(b,target); 210 assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Insertions are not tracked'); 211 target.insertBefore(b,c); 212 assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Nothing is tracked'); 213 nodePartA.disconnect(); 214 nodePartB.disconnect(); 215 nodePartC.disconnect(); 216 assert_array_equals(root.getParts(),[],'disconnections are tracked'); 217 218 const childPartAC = addCleanup(t,new ChildNodePart(root,a,c)); 219 assert_array_equals(root.getParts(),[childPartAC]); 220 a.remove(); 221 assert_array_equals(root.getParts(),[],'Removing endpoints invalidates the part'); 222 target.insertBefore(a,b); // Restore 223 assert_array_equals(root.getParts(),[],'Insertions are not tracked'); 224 225 target.insertBefore(c,a); 226 assert_array_equals(root.getParts(),[],'Endpoints out of order'); 227 target.appendChild(c); // Restore 228 assert_array_equals(root.getParts(),[],'Insertions are not tracked'); 229 230 document.body.appendChild(c); 231 assert_array_equals(root.getParts(),[],'Parts are\'t invalidated when endpoints have different parents'); 232 target.appendChild(c); // Restore 233 assert_array_equals(root.getParts(),[],'Insertions are not tracked'); 234 235 const oldParent = target.parentElement; 236 target.remove(); 237 assert_array_equals(root.getParts(),[],'Parts are\'t invalidated when disconnected'); 238 oldParent.appendChild(target); // Restore 239 assert_array_equals(root.getParts(),[]); 240 }, `DOM mutations are not tracked (${description})`); 241 242 test((t) => { 243 const root = doc.getPartRoot(); 244 assert_equals(root.getParts().length,0,'Test harness check: tests should clean up parts'); 245 const otherNode = document.createElement('div'); 246 247 const childPartAA = addCleanup(t,new ChildNodePart(root,a,a)); 248 const childPartAB = addCleanup(t,new ChildNodePart(root,a,b)); 249 const childPartAC = addCleanup(t,new ChildNodePart(root,a,c)); 250 assert_throws_dom('InvalidStateError',() => childPartAA.replaceChildren(otherNode),'Can\'t replace children if part is invalid'); 251 assert_array_equals(childPartAA.children,[],'Invalid parts should return empty children'); 252 assert_array_equals(childPartAB.children,[],'Children should not include endpoints'); 253 assert_array_equals(childPartAC.children,[b],'Children should not include endpoints'); 254 childPartAB.replaceChildren(otherNode); 255 assert_array_equals(childPartAB.children,[otherNode],'Replacechildren should work'); 256 assert_array_equals(childPartAC.children,[otherNode,b],'replaceChildren should leave endpoints alone'); 257 childPartAC.replaceChildren(otherNode); 258 assert_array_equals(childPartAC.children,[otherNode],'Replacechildren with existing children should work'); 259 assert_array_equals(childPartAB.children,[]); 260 childPartAC.replaceChildren(b); 261 assert_array_equals(target.children,[a,b,c]); 262 }, `ChildNodePart children manipulation (${description})`); 263 264 test((t) => { 265 const root = doc.getPartRoot(); 266 // Make sure no crashes occur for parts with mismatched endpoint nodes. 267 const cornerCasePartsInvalid = [ 268 addCleanup(t,new ChildNodePart(root,target, target.children[2],{metadata: ['different parents']})), 269 addCleanup(t,new ChildNodePart(root,target.children[0], target,{metadata: ['different parents']})), 270 addCleanup(t,new ChildNodePart(root,target.children[2], target.children[0],{metadata: ['reversed endpoints']})), 271 ]; 272 const cornerCasePartsValid = []; 273 if (directChildren[0] !== directChildren[1]) { 274 cornerCasePartsValid.push(addCleanup(t,new ChildNodePart(root,directChildren[0], directChildren[1],{metadata: ['direct parent of the root container']}))); 275 } 276 assert_array_equals(root.getParts(),cornerCasePartsValid); 277 assert_equals(root.clone().getParts().length,cornerCasePartsValid.length); 278 }, `Corner case ChildNodePart construction and cloning (${description})`); 279 280 wrapper?.remove(); // Cleanup 281 }); 282 </script>