2 <title>DOM Parts: Basic object structure, imperative API
</title>
3 <meta name=
"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>
9 <template id=imperative
>
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>
18 <span id=direct_child_1
></span>
19 <span id=direct_child_2
></span>
23 const template
= document
.getElementById('imperative');
24 function addCleanup(t
, part
) {
25 t
.add_cleanup(() => part
.disconnect());
28 [false,true].forEach(useTemplate
=> {
29 const doc
= useTemplate
? template
.content
: document
;
30 let target
,wrapper
,directChildren
;
32 target
= doc
.querySelector('#target1');
33 directChildren
= [doc
.querySelector('#direct_child_1'),doc
.querySelector('#direct_child_2')];
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
];
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";
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
));
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');
60 assert_throws_js(TypeError
,() => new NodePart(nodePart
,target
.children
[0]),'Constructing a Part with a NodePart as the PartRoot should throw');
62 const attributePart
= addCleanup(t
,new AttributePart(root
,target
,'attributename',/*automatic*/false,{metadata
: ['attribute-non-auto']}));
63 assertEqualParts([attributePart
],[{type
:'AttributePart',metadata
:['attribute-non-auto']}],0,'Basic AttributePart');
64 assert_equals(attributePart
.node
,target
);
65 assert_equals(attributePart
.root
,root
);
66 assert_equals(attributePart
.localName
,'attributename');
67 assert_equals(attributePart
.automatic
,false);
68 runningPartsExpectation
.push({type
:'AttributePart',metadata
:['attribute-non-auto']});
69 assertEqualParts(root
.getParts(),runningPartsExpectation
,[nodePart
,attributePart
],'getParts() for the root should now have this attributePart');
70 assert_equals(parts
.length
,0,'Return value of getParts() is not live');
72 const attributePartAuto
= addCleanup(t
,new AttributePart(root
,target
,'attributename',/*automatic*/true,{metadata
: ['attribute-auto']}));
73 assertEqualParts([attributePartAuto
],[{type
:'AttributePart',metadata
:['attribute-auto']}],0,'Basic automatic AttributePart');
74 assert_equals(attributePartAuto
.node
,target
);
75 assert_equals(attributePartAuto
.root
,root
);
76 assert_equals(attributePartAuto
.localName
,'attributename');
77 assert_equals(attributePartAuto
.automatic
,true);
78 assertEqualParts(root
.getParts(),runningPartsExpectation
,[nodePart
,attributePart
],'automatic AttributePart should not get included in getParts()');
80 const childNodePart
= addCleanup(t
,new ChildNodePart(root
,target
.children
[0], target
.children
[2],{metadata
:['bar','baz']}));
81 assertEqualParts([childNodePart
],[{type
:'ChildNodePart',metadata
:['bar','baz']}],0,'Basic ChildNodePart');
82 assert_equals(childNodePart
.root
,root
);
83 assert_equals(childNodePart
.previousSibling
,target
.children
[0]);
84 assert_equals(childNodePart
.nextSibling
,target
.children
[2]);
85 assert_equals(childNodePart
.getParts().length
,0,'childNodePart.getParts() should start out empty');
86 runningPartsExpectation
.push({type
:'ChildNodePart',metadata
:['bar','baz']});
87 assertEqualParts(root
.getParts(),runningPartsExpectation
,[nodePart
,attributePart
,childNodePart
],'getParts() for the root should now have this childNodePart');
89 const nodeBefore
= target
.previousSibling
|| target
.parentNode
;
90 const nodePartBefore
= addCleanup(t
,new NodePart(root
,nodeBefore
));
91 runningPartsExpectation
.push({type
:'NodePart',metadata
:[]});
92 assertEqualParts(root
.getParts(),runningPartsExpectation
,[nodePart
,attributePart
,childNodePart
,nodePartBefore
],'getParts() for the root should now have this nodePart, in construction order');
94 const nodePart2
= addCleanup(t
,new NodePart(childNodePart
,target
.children
[2],{metadata
:['blah']}));
95 assert_equals(nodePart2
.root
,childNodePart
);
96 assertEqualParts(root
.getParts(),runningPartsExpectation
,[nodePart
,attributePart
,childNodePart
,nodePartBefore
],'getParts() for the root DocumentPartRoot shouldn\'t change');
97 assertEqualParts(childNodePart
.getParts(),[{type
:'NodePart',metadata
:['blah']}],[nodePart2
],'getParts() for the childNodePart should have it');
99 nodePart2
.disconnect();
100 assert_equals(nodePart2
.root
,null,'root should be null after disconnect');
101 assert_equals(nodePart2
.node
,null,'node should be null after disconnect');
102 assert_equals(childNodePart
.getParts().length
,0,'calling disconnect() should remove the part from root.getParts()');
103 assertEqualParts(root
.getParts(),runningPartsExpectation
,[nodePart
,attributePart
,childNodePart
,nodePartBefore
],'getParts() for the root DocumentPartRoot still shouldn\'t change');
104 nodePart2
.disconnect(); // Calling twice should be ok.
106 childNodePart
.disconnect();
107 assert_equals(childNodePart
.root
,null,'root should be null after disconnect');
108 assert_equals(childNodePart
.previousSibling
,null,'previousSibling should be null after disconnect');
109 assert_equals(childNodePart
.nextSibling
,null,'nextSibling should be null after disconnect');
110 assert_array_equals(root
.getParts(),[nodePartBefore
,nodePart
,attributePart
]);
111 }, `Basic imperative DOM Parts object construction (${description})`);
113 function cloneRange(parent
,previousSibling
,nextSibling
) {
114 const clone
= parent
.cloneNode(false);
115 let node
= previousSibling
;
117 clone
.appendChild(node
.cloneNode(true));
118 if (node
== nextSibling
) {
121 node
= node
.nextSibling
;
127 const root
= doc
.getPartRoot();
128 const nodePart
= addCleanup(t
,new NodePart(root
,target
,{metadata
:['node1']}));
129 const attributePart
= addCleanup(t
,new AttributePart(root
,target
,'attributeName',/*automatic*/false,{metadata
: ['attribute']}));
130 const nonTrackedAttributePart
= addCleanup(t
,new AttributePart(root
,target
,'attributeName',/*automatic*/true,{metadata
: ['attribute-auto']}));
131 const childNodePart
= addCleanup(t
,new ChildNodePart(root
,target
.children
[0], target
.children
[2],{metadata
:['child']}));
132 const nodePart3
= addCleanup(t
,new NodePart(childNodePart
,target
.children
[1].firstChild
,{metadata
: ['node 3']}));
133 const nodePart2
= addCleanup(t
,new NodePart(childNodePart
,target
.children
[1].firstChild
,{metadata
: ['node 2']}));
134 const childNodePart2
= addCleanup(t
,new ChildNodePart(childNodePart
,target
.children
[1].firstElementChild
,target
.children
[1].firstElementChild
.nextSibling
,{metadata
: ['childnodepart2']}));
135 let rootExpectations
= [{type
:'NodePart',metadata
:['node1']},{type
:'AttributePart',metadata
:['attribute']},{type
:'ChildNodePart',metadata
:['child']}];
136 assertEqualParts(root
.getParts(),rootExpectations
,[nodePart
,attributePart
,childNodePart
],'setup');
137 let childExpectations
= [{type
:'NodePart',metadata
:['node 3']},{type
:'NodePart',metadata
:['node 2']},{type
:'ChildNodePart',metadata
:['childnodepart2']}];
138 assertEqualParts(childNodePart
.getParts(),childExpectations
,[nodePart3
,nodePart2
,childNodePart2
],'setup');
139 assert_array_equals(childNodePart2
.getParts(),[]);
141 // Test cloning of the entire DocumentPartRoot.
142 const clonedPartRoot
= root
.clone();
143 assertEqualParts(root
.getParts(),rootExpectations
,[nodePart
,attributePart
,childNodePart
],'cloning a part root should not change the original');
144 const clonedContainer
= clonedPartRoot
.rootContainer
;
145 assert_true(clonedPartRoot
instanceof DocumentPartRoot
);
146 assert_true(clonedContainer
instanceof (useTemplate
? DocumentFragment
: Document
));
147 assert_not_equals(clonedPartRoot
,root
);
148 assert_not_equals(clonedContainer
,doc
);
149 assert_equals(doc
.innerHTML
,clonedContainer
.innerHTML
);
150 assertEqualParts(clonedPartRoot
.getParts(),rootExpectations
,0,'cloned PartRoot should contain identical parts');
151 assert_true(!clonedPartRoot
.getParts().includes(nodePart
),'Original parts should not be retained');
152 assert_true(!clonedPartRoot
.getParts().includes(childNodePart
));
153 const newNodePart
= clonedPartRoot
.getParts()[0];
154 const newAttributePart
= clonedPartRoot
.getParts()[1];
155 const newChildNodePart
= clonedPartRoot
.getParts()[2];
156 assert_not_equals(newNodePart
.node
,target
,'Node references should not point to original nodes');
157 assert_equals(newNodePart
.node
.id
,target
.id
,'New parts should point to cloned nodes');
158 assert_not_equals(newAttributePart
.node
,target
,'Node references should not point to original nodes');
159 assert_equals(newAttributePart
.node
.id
,target
.id
,'New parts should point to cloned nodes');
160 assert_equals(newAttributePart
.localName
,attributePart
.localName
,'New attribute parts should carry over localName');
161 assert_equals(newAttributePart
.automatic
,attributePart
.automatic
,'New attribute parts should carry over automatic');
162 assert_not_equals(newChildNodePart
.previousSibling
,a
,'Node references should not point to original nodes');
163 assert_equals(newChildNodePart
.previousSibling
.id
,'a');
164 assert_not_equals(newChildNodePart
.nextSibling
,c
,'Node references should not point to original nodes');
165 assert_equals(newChildNodePart
.nextSibling
.id
,'c');
166 assertEqualParts(newChildNodePart
.getParts(),childExpectations
,0,'cloned PartRoot should contain identical parts');
168 // Test cloning of ChildNodeParts.
169 const clonedChildNodePartRoot
= childNodePart
.clone();
170 const clonedChildContainer
= clonedChildNodePartRoot
.rootContainer
;
171 assert_true(clonedChildNodePartRoot
instanceof ChildNodePart
);
172 assert_true(clonedChildContainer
instanceof Element
);
173 assert_not_equals(clonedChildContainer
,target
);
174 assert_equals(clonedChildContainer
.outerHTML
,cloneRange(target
,a
,c
).outerHTML
);
175 assertEqualParts(clonedChildNodePartRoot
.getParts(),childExpectations
,0,'clone of childNodePart should match');
176 }, `Cloning (${description})`);
178 ['Element','Text','Comment'].forEach(nodeType
=> {
180 const root
= doc
.getPartRoot();
181 assert_equals(root
.getParts().length
,0);
184 case 'Element' : node
= document
.createElement('div'); break;
185 case 'Text' : node
= document
.createTextNode('hello'); break;
186 case 'Comment': node
= document
.createComment('comment'); break;
188 t
.add_cleanup(() => node
.remove());
189 doc
.firstElementChild
.append(node
);
191 const nodePart
= addCleanup(t
,new NodePart(root
,node
,{metadata
:['foobar']}));
192 assert_true(!!nodePart
);
193 const clone
= root
.clone();
194 assert_equals(clone
.getParts().length
,1);
195 assertEqualParts(clone
.getParts(),[{type
:'NodePart',metadata
:['foobar']}],0,'getParts');
196 assert_true(clone
.getParts()[0].node
instanceof window
[nodeType
]);
199 const node2
= node
.cloneNode(false);
200 node
.parentElement
.appendChild(node2
);
201 const childNodePart
= addCleanup(t
,new ChildNodePart(root
,node
,node2
,{metadata
:['baz']}));
202 assert_true(!!childNodePart
);
203 const clone2
= root
.clone();
204 assert_equals(clone2
.getParts().length
,2);
205 assertEqualParts(clone2
.getParts(),[{type
:'NodePart',metadata
:['foobar']},{type
:'ChildNodePart',metadata
:['baz']}],0,'getParts2');
206 assert_true(clone2
.getParts()[1].previousSibling
instanceof window
[nodeType
]);
207 }, `Cloning ${nodeType} (${description})`);
211 const root
= doc
.getPartRoot();
212 assert_equals(root
.getParts().length
,0,'Test harness check: tests should clean up parts');
214 const nodePartB
= addCleanup(t
,new NodePart(root
,b
));
215 const nodePartA
= addCleanup(t
,new NodePart(root
,a
));
216 const nodePartC
= addCleanup(t
,new NodePart(root
,c
));
217 assert_array_equals(root
.getParts(),[nodePartB
,nodePartA
,nodePartC
],'Parts can be out of order, if added out of order');
219 assert_array_equals(root
.getParts(),[nodePartB
,nodePartA
,nodePartC
],'Removals are not tracked');
220 target
.parentElement
.insertBefore(b
,target
);
221 assert_array_equals(root
.getParts(),[nodePartB
,nodePartA
,nodePartC
],'Insertions are not tracked');
222 target
.insertBefore(b
,c
);
223 assert_array_equals(root
.getParts(),[nodePartB
,nodePartA
,nodePartC
],'Nothing is tracked');
224 nodePartA
.disconnect();
225 nodePartB
.disconnect();
226 nodePartC
.disconnect();
227 assert_array_equals(root
.getParts(),[],'disconnections are tracked');
229 const childPartAC
= addCleanup(t
,new ChildNodePart(root
,a
,c
));
230 assert_array_equals(root
.getParts(),[childPartAC
]);
232 assert_array_equals(root
.getParts(),[],'Removing endpoints invalidates the part');
233 target
.insertBefore(a
,b
); // Restore
234 assert_array_equals(root
.getParts(),[],'Insertions are not tracked');
236 target
.insertBefore(c
,a
);
237 assert_array_equals(root
.getParts(),[],'Endpoints out of order');
238 target
.appendChild(c
); // Restore
239 assert_array_equals(root
.getParts(),[],'Insertions are not tracked');
241 document
.body
.appendChild(c
);
242 assert_array_equals(root
.getParts(),[],'Parts are\'t invalidated when endpoints have different parents');
243 target
.appendChild(c
); // Restore
244 assert_array_equals(root
.getParts(),[],'Insertions are not tracked');
246 const oldParent
= target
.parentElement
;
248 assert_array_equals(root
.getParts(),[],'Parts are\'t invalidated when disconnected');
249 oldParent
.appendChild(target
); // Restore
250 assert_array_equals(root
.getParts(),[]);
251 }, `DOM mutations are not tracked (${description})`);
254 const root
= doc
.getPartRoot();
255 assert_equals(root
.getParts().length
,0,'Test harness check: tests should clean up parts');
256 const otherNode
= document
.createElement('div');
258 const childPartAA
= addCleanup(t
,new ChildNodePart(root
,a
,a
));
259 const childPartAB
= addCleanup(t
,new ChildNodePart(root
,a
,b
));
260 const childPartAC
= addCleanup(t
,new ChildNodePart(root
,a
,c
));
261 assert_throws_dom('InvalidStateError',() => childPartAA
.replaceChildren(otherNode
),'Can\'t replace children if part is invalid');
262 assert_array_equals(childPartAA
.children
,[],'Invalid parts should return empty children');
263 assert_array_equals(childPartAB
.children
,[],'Children should not include endpoints');
264 assert_array_equals(childPartAC
.children
,[b
],'Children should not include endpoints');
265 childPartAB
.replaceChildren(otherNode
);
266 assert_array_equals(childPartAB
.children
,[otherNode
],'Replacechildren should work');
267 assert_array_equals(childPartAC
.children
,[otherNode
,b
],'replaceChildren should leave endpoints alone');
268 childPartAC
.replaceChildren(otherNode
);
269 assert_array_equals(childPartAC
.children
,[otherNode
],'Replacechildren with existing children should work');
270 assert_array_equals(childPartAB
.children
,[]);
271 childPartAC
.replaceChildren(b
);
272 assert_array_equals(target
.children
,[a
,b
,c
]);
273 }, `ChildNodePart children manipulation (${description})`);
276 const root
= doc
.getPartRoot();
277 // Make sure no crashes occur for parts with mismatched endpoint nodes.
278 const cornerCasePartsInvalid
= [
279 addCleanup(t
,new ChildNodePart(root
,target
, target
.children
[2],{metadata
: ['different parents']})),
280 addCleanup(t
,new ChildNodePart(root
,target
.children
[0], target
,{metadata
: ['different parents']})),
281 addCleanup(t
,new ChildNodePart(root
,target
.children
[2], target
.children
[0],{metadata
: ['reversed endpoints']})),
283 const cornerCasePartsValid
= [];
284 if (directChildren
[0] !== directChildren
[1]) {
285 cornerCasePartsValid
.push(addCleanup(t
,new ChildNodePart(root
,directChildren
[0], directChildren
[1],{metadata
: ['direct parent of the root container']})));
287 assert_array_equals(root
.getParts(),cornerCasePartsValid
);
288 assert_equals(root
.clone().getParts().length
,cornerCasePartsValid
.length
);
289 }, `Corner case ChildNodePart construction and cloning (${description})`);
291 wrapper
?.remove(); // Cleanup