|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const C_i = Components.interfaces; |
|
6 |
|
7 const UNORDERED_TYPE = C_i.nsIDOMXPathResult.ANY_UNORDERED_NODE_TYPE; |
|
8 |
|
9 /** |
|
10 * Determine if the data node has only ignorable white-space. |
|
11 * |
|
12 * @return nsIDOMNodeFilter.FILTER_SKIP if it does. |
|
13 * @return nsIDOMNodeFilter.FILTER_ACCEPT otherwise. |
|
14 */ |
|
15 function isWhitespace(aNode) { |
|
16 return ((/\S/).test(aNode.nodeValue)) ? |
|
17 C_i.nsIDOMNodeFilter.FILTER_SKIP : |
|
18 C_i.nsIDOMNodeFilter.FILTER_ACCEPT; |
|
19 } |
|
20 |
|
21 /** |
|
22 * Create a DocumentFragment with cloned children equaling a node's children. |
|
23 * |
|
24 * @param aNode The node to copy from. |
|
25 * |
|
26 * @return DocumentFragment node. |
|
27 */ |
|
28 function getFragment(aNode) { |
|
29 var frag = aNode.ownerDocument.createDocumentFragment(); |
|
30 for (var i = 0; i < aNode.childNodes.length; i++) { |
|
31 frag.appendChild(aNode.childNodes.item(i).cloneNode(true)); |
|
32 } |
|
33 return frag; |
|
34 } |
|
35 |
|
36 // Goodies from head_content.js |
|
37 const serializer = new DOMSerializer(); |
|
38 const parser = new DOMParser(); |
|
39 |
|
40 /** |
|
41 * Dump the contents of a document fragment to the console. |
|
42 * |
|
43 * @param aFragment The fragment to serialize. |
|
44 */ |
|
45 function dumpFragment(aFragment) { |
|
46 dump(serializer.serializeToString(aFragment) + "\n\n"); |
|
47 } |
|
48 |
|
49 /** |
|
50 * Translate an XPath to a DOM node. This method uses a document |
|
51 * fragment as context node. |
|
52 * |
|
53 * @param aContextNode The context node to apply the XPath to. |
|
54 * @param aPath The XPath to use. |
|
55 * |
|
56 * @return nsIDOMNode The target node retrieved from the XPath. |
|
57 */ |
|
58 function evalXPathInDocumentFragment(aContextNode, aPath) { |
|
59 do_check_true(aContextNode instanceof C_i.nsIDOMDocumentFragment); |
|
60 do_check_true(aContextNode.childNodes.length > 0); |
|
61 if (aPath == ".") { |
|
62 return aContextNode; |
|
63 } |
|
64 |
|
65 // Separate the fragment's xpath lookup from the rest. |
|
66 var firstSlash = aPath.indexOf("/"); |
|
67 if (firstSlash == -1) { |
|
68 firstSlash = aPath.length; |
|
69 } |
|
70 var prefix = aPath.substr(0, firstSlash); |
|
71 var realPath = aPath.substr(firstSlash + 1); |
|
72 if (!realPath) { |
|
73 realPath = "."; |
|
74 } |
|
75 |
|
76 // Set up a special node filter to look among the fragment's child nodes. |
|
77 var childIndex = 1; |
|
78 var bracketIndex = prefix.indexOf("["); |
|
79 if (bracketIndex != -1) { |
|
80 childIndex = Number(prefix.substring(bracketIndex + 1, prefix.indexOf("]"))); |
|
81 do_check_true(childIndex > 0); |
|
82 prefix = prefix.substr(0, bracketIndex); |
|
83 } |
|
84 |
|
85 var targetType = C_i.nsIDOMNodeFilter.SHOW_ELEMENT; |
|
86 var targetNodeName = prefix; |
|
87 if (prefix.indexOf("processing-instruction(") == 0) { |
|
88 targetType = C_i.nsIDOMNodeFilter.SHOW_PROCESSING_INSTRUCTION; |
|
89 targetNodeName = prefix.substring(prefix.indexOf("(") + 2, prefix.indexOf(")") - 1); |
|
90 } |
|
91 switch (prefix) { |
|
92 case "text()": |
|
93 targetType = C_i.nsIDOMNodeFilter.SHOW_TEXT | |
|
94 C_i.nsIDOMNodeFilter.SHOW_CDATA_SECTION; |
|
95 targetNodeName = null; |
|
96 break; |
|
97 case "comment()": |
|
98 targetType = C_i.nsIDOMNodeFilter.SHOW_COMMENT; |
|
99 targetNodeName = null; |
|
100 break; |
|
101 case "node()": |
|
102 targetType = C_i.nsIDOMNodeFilter.SHOW_ALL; |
|
103 targetNodeName = null; |
|
104 } |
|
105 |
|
106 var filter = { |
|
107 count: 0, |
|
108 |
|
109 // nsIDOMNodeFilter |
|
110 acceptNode: function acceptNode(aNode) { |
|
111 if (aNode.parentNode != aContextNode) { |
|
112 // Don't bother looking at kids either. |
|
113 return C_i.nsIDOMNodeFilter.FILTER_REJECT; |
|
114 } |
|
115 |
|
116 if (targetNodeName && targetNodeName != aNode.nodeName) { |
|
117 return C_i.nsIDOMNodeFilter.FILTER_SKIP; |
|
118 } |
|
119 |
|
120 this.count++; |
|
121 if (this.count != childIndex) { |
|
122 return C_i.nsIDOMNodeFilter.FILTER_SKIP; |
|
123 } |
|
124 |
|
125 return C_i.nsIDOMNodeFilter.FILTER_ACCEPT; |
|
126 } |
|
127 }; |
|
128 |
|
129 // Look for the node matching the step from the document fragment. |
|
130 var walker = aContextNode.ownerDocument.createTreeWalker( |
|
131 aContextNode, |
|
132 targetType, |
|
133 filter); |
|
134 var targetNode = walker.nextNode(); |
|
135 do_check_neq(targetNode, null); |
|
136 |
|
137 // Apply our remaining xpath to the found node. |
|
138 var expr = aContextNode.ownerDocument.createExpression(realPath, null); |
|
139 var result = expr.evaluate(targetNode, UNORDERED_TYPE, null); |
|
140 do_check_true(result instanceof C_i.nsIDOMXPathResult); |
|
141 return result.singleNodeValue; |
|
142 } |
|
143 |
|
144 /** |
|
145 * Get a DOM range corresponding to the test's source node. |
|
146 * |
|
147 * @param aSourceNode <source/> element with range information. |
|
148 * @param aFragment DocumentFragment generated with getFragment(). |
|
149 * |
|
150 * @return Range object. |
|
151 */ |
|
152 function getRange(aSourceNode, aFragment) { |
|
153 do_check_true(aSourceNode instanceof C_i.nsIDOMElement); |
|
154 do_check_true(aFragment instanceof C_i.nsIDOMDocumentFragment); |
|
155 var doc = aSourceNode.ownerDocument; |
|
156 |
|
157 var containerPath = aSourceNode.getAttribute("startContainer"); |
|
158 var startContainer = evalXPathInDocumentFragment(aFragment, containerPath); |
|
159 var startOffset = Number(aSourceNode.getAttribute("startOffset")); |
|
160 |
|
161 containerPath = aSourceNode.getAttribute("endContainer"); |
|
162 var endContainer = evalXPathInDocumentFragment(aFragment, containerPath); |
|
163 var endOffset = Number(aSourceNode.getAttribute("endOffset")); |
|
164 |
|
165 var range = doc.createRange(); |
|
166 range.setStart(startContainer, startOffset); |
|
167 range.setEnd(endContainer, endOffset); |
|
168 return range; |
|
169 } |
|
170 |
|
171 /** |
|
172 * Get the document for a given path, and clean it up for our tests. |
|
173 * |
|
174 * @param aPath The path to the local document. |
|
175 */ |
|
176 function getParsedDocument(aPath) { |
|
177 var doc = do_parse_document(aPath, "application/xml"); |
|
178 do_check_true(doc.documentElement.localName != "parsererror"); |
|
179 do_check_true(doc instanceof C_i.nsIDOMXPathEvaluator); |
|
180 do_check_true(doc instanceof C_i.nsIDOMDocument); |
|
181 |
|
182 // Clean out whitespace. |
|
183 var walker = doc.createTreeWalker(doc, |
|
184 C_i.nsIDOMNodeFilter.SHOW_TEXT | |
|
185 C_i.nsIDOMNodeFilter.SHOW_CDATA_SECTION, |
|
186 isWhitespace); |
|
187 while (walker.nextNode()) { |
|
188 var parent = walker.currentNode.parentNode; |
|
189 parent.removeChild(walker.currentNode); |
|
190 walker.currentNode = parent; |
|
191 } |
|
192 |
|
193 // Clean out mandatory splits between nodes. |
|
194 var splits = doc.getElementsByTagName("split"); |
|
195 var i; |
|
196 for (i = splits.length - 1; i >= 0; i--) { |
|
197 var node = splits.item(i); |
|
198 node.parentNode.removeChild(node); |
|
199 } |
|
200 splits = null; |
|
201 |
|
202 // Replace empty CDATA sections. |
|
203 var emptyData = doc.getElementsByTagName("empty-cdata"); |
|
204 for (i = emptyData.length - 1; i >= 0; i--) { |
|
205 var node = emptyData.item(i); |
|
206 var cdata = doc.createCDATASection(""); |
|
207 node.parentNode.replaceChild(cdata, node); |
|
208 } |
|
209 |
|
210 return doc; |
|
211 } |
|
212 |
|
213 /** |
|
214 * Run the extraction tests. |
|
215 */ |
|
216 function run_extract_test() { |
|
217 var filePath = "test_delete_range.xml"; |
|
218 var doc = getParsedDocument(filePath); |
|
219 var tests = doc.getElementsByTagName("test"); |
|
220 |
|
221 // Run our deletion, extraction tests. |
|
222 for (var i = 0; i < tests.length; i++) { |
|
223 dump("Configuring for test " + i + "\n"); |
|
224 var currentTest = tests.item(i); |
|
225 |
|
226 // Validate the test is properly formatted for what this harness expects. |
|
227 var baseSource = currentTest.firstChild; |
|
228 do_check_eq(baseSource.nodeName, "source"); |
|
229 var baseResult = baseSource.nextSibling; |
|
230 do_check_eq(baseResult.nodeName, "result"); |
|
231 var baseExtract = baseResult.nextSibling; |
|
232 do_check_eq(baseExtract.nodeName, "extract"); |
|
233 do_check_eq(baseExtract.nextSibling, null); |
|
234 |
|
235 /* We do all our tests on DOM document fragments, derived from the test |
|
236 element's children. This lets us rip the various fragments to shreds, |
|
237 while preserving the original elements so we can make more copies of |
|
238 them. |
|
239 |
|
240 After the range's extraction or deletion is done, we use |
|
241 nsIDOMNode.isEqualNode() between the altered source fragment and the |
|
242 result fragment. We also run isEqualNode() between the extracted |
|
243 fragment and the fragment from the baseExtract node. If they are not |
|
244 equal, we have failed a test. |
|
245 |
|
246 We also have to ensure the original nodes on the end points of the |
|
247 range are still in the source fragment. This is bug 332148. The nodes |
|
248 may not be replaced with equal but separate nodes. The range extraction |
|
249 may alter these nodes - in the case of text containers, they will - but |
|
250 the nodes must stay there, to preserve references such as user data, |
|
251 event listeners, etc. |
|
252 |
|
253 First, an extraction test. |
|
254 */ |
|
255 |
|
256 var resultFrag = getFragment(baseResult); |
|
257 var extractFrag = getFragment(baseExtract); |
|
258 |
|
259 dump("Extract contents test " + i + "\n\n"); |
|
260 var baseFrag = getFragment(baseSource); |
|
261 var baseRange = getRange(baseSource, baseFrag); |
|
262 var startContainer = baseRange.startContainer; |
|
263 var endContainer = baseRange.endContainer; |
|
264 |
|
265 var cutFragment = baseRange.extractContents(); |
|
266 dump("cutFragment: " + cutFragment + "\n"); |
|
267 if (cutFragment) { |
|
268 do_check_true(extractFrag.isEqualNode(cutFragment)); |
|
269 } else { |
|
270 do_check_eq(extractFrag.firstChild, null); |
|
271 } |
|
272 do_check_true(baseFrag.isEqualNode(resultFrag)); |
|
273 |
|
274 dump("Ensure the original nodes weren't extracted - test " + i + "\n\n"); |
|
275 var walker = doc.createTreeWalker(baseFrag, |
|
276 C_i.nsIDOMNodeFilter.SHOW_ALL, |
|
277 null); |
|
278 var foundStart = false; |
|
279 var foundEnd = false; |
|
280 do { |
|
281 if (walker.currentNode == startContainer) { |
|
282 foundStart = true; |
|
283 } |
|
284 |
|
285 if (walker.currentNode == endContainer) { |
|
286 // An end container node should not come before the start container node. |
|
287 do_check_true(foundStart); |
|
288 foundEnd = true; |
|
289 break; |
|
290 } |
|
291 } while (walker.nextNode()) |
|
292 do_check_true(foundEnd); |
|
293 |
|
294 /* Now, we reset our test for the deleteContents case. This one differs |
|
295 from the extractContents case only in that there is no extracted document |
|
296 fragment to compare against. So we merely compare the starting fragment, |
|
297 minus the extracted content, against the result fragment. |
|
298 */ |
|
299 dump("Delete contents test " + i + "\n\n"); |
|
300 baseFrag = getFragment(baseSource); |
|
301 baseRange = getRange(baseSource, baseFrag); |
|
302 var startContainer = baseRange.startContainer; |
|
303 var endContainer = baseRange.endContainer; |
|
304 baseRange.deleteContents(); |
|
305 do_check_true(baseFrag.isEqualNode(resultFrag)); |
|
306 |
|
307 dump("Ensure the original nodes weren't deleted - test " + i + "\n\n"); |
|
308 walker = doc.createTreeWalker(baseFrag, |
|
309 C_i.nsIDOMNodeFilter.SHOW_ALL, |
|
310 null); |
|
311 foundStart = false; |
|
312 foundEnd = false; |
|
313 do { |
|
314 if (walker.currentNode == startContainer) { |
|
315 foundStart = true; |
|
316 } |
|
317 |
|
318 if (walker.currentNode == endContainer) { |
|
319 // An end container node should not come before the start container node. |
|
320 do_check_true(foundStart); |
|
321 foundEnd = true; |
|
322 break; |
|
323 } |
|
324 } while (walker.nextNode()) |
|
325 do_check_true(foundEnd); |
|
326 |
|
327 // Clean up after ourselves. |
|
328 walker = null; |
|
329 } |
|
330 } |
|
331 |
|
332 /** |
|
333 * Miscellaneous tests not covered above. |
|
334 */ |
|
335 function run_miscellaneous_tests() { |
|
336 var filePath = "test_delete_range.xml"; |
|
337 var doc = getParsedDocument(filePath); |
|
338 var tests = doc.getElementsByTagName("test"); |
|
339 |
|
340 // Let's try some invalid inputs to our DOM range and see what happens. |
|
341 var currentTest = tests.item(0); |
|
342 var baseSource = currentTest.firstChild; |
|
343 var baseResult = baseSource.nextSibling; |
|
344 var baseExtract = baseResult.nextSibling; |
|
345 |
|
346 var baseFrag = getFragment(baseSource); |
|
347 |
|
348 var baseRange = getRange(baseSource, baseFrag); |
|
349 var startContainer = baseRange.startContainer; |
|
350 var endContainer = baseRange.endContainer; |
|
351 var startOffset = baseRange.startOffset; |
|
352 var endOffset = baseRange.endOffset; |
|
353 |
|
354 // Text range manipulation. |
|
355 if ((endOffset > startOffset) && |
|
356 (startContainer == endContainer) && |
|
357 (startContainer instanceof C_i.nsIDOMText)) { |
|
358 // Invalid start node |
|
359 try { |
|
360 baseRange.setStart(null, 0); |
|
361 do_throw("Should have thrown NOT_OBJECT_ERR!"); |
|
362 } catch (e) { |
|
363 do_check_eq(e.constructor.name, "TypeError"); |
|
364 } |
|
365 |
|
366 // Invalid start node |
|
367 try { |
|
368 baseRange.setStart({}, 0); |
|
369 do_throw("Should have thrown SecurityError!"); |
|
370 } catch (e) { |
|
371 do_check_eq(e.constructor.name, "TypeError"); |
|
372 } |
|
373 |
|
374 // Invalid index |
|
375 try { |
|
376 baseRange.setStart(startContainer, -1); |
|
377 do_throw("Should have thrown IndexSizeError!"); |
|
378 } catch (e) { |
|
379 do_check_eq(e.name, "IndexSizeError"); |
|
380 } |
|
381 |
|
382 // Invalid index |
|
383 var newOffset = startContainer instanceof C_i.nsIDOMText ? |
|
384 startContainer.nodeValue.length + 1 : |
|
385 startContainer.childNodes.length + 1; |
|
386 try { |
|
387 baseRange.setStart(startContainer, newOffset); |
|
388 do_throw("Should have thrown IndexSizeError!"); |
|
389 } catch (e) { |
|
390 do_check_eq(e.name, "IndexSizeError"); |
|
391 } |
|
392 |
|
393 newOffset--; |
|
394 // Valid index |
|
395 baseRange.setStart(startContainer, newOffset); |
|
396 do_check_eq(baseRange.startContainer, baseRange.endContainer); |
|
397 do_check_eq(baseRange.startOffset, newOffset); |
|
398 do_check_true(baseRange.collapsed); |
|
399 |
|
400 // Valid index |
|
401 baseRange.setEnd(startContainer, 0); |
|
402 do_check_eq(baseRange.startContainer, baseRange.endContainer); |
|
403 do_check_eq(baseRange.startOffset, 0); |
|
404 do_check_true(baseRange.collapsed); |
|
405 } else { |
|
406 do_throw("The first test should be a text-only range test. Test is invalid.") |
|
407 } |
|
408 |
|
409 /* See what happens when a range has a startContainer in one fragment, and an |
|
410 endContainer in another. According to the DOM spec, section 2.4, the range |
|
411 should collapse to the new container and offset. */ |
|
412 baseRange = getRange(baseSource, baseFrag); |
|
413 startContainer = baseRange.startContainer; |
|
414 var startOffset = baseRange.startOffset; |
|
415 endContainer = baseRange.endContainer; |
|
416 var endOffset = baseRange.endOffset; |
|
417 |
|
418 dump("External fragment test\n\n"); |
|
419 |
|
420 var externalTest = tests.item(1); |
|
421 var externalSource = externalTest.firstChild; |
|
422 var externalFrag = getFragment(externalSource); |
|
423 var externalRange = getRange(externalSource, externalFrag); |
|
424 |
|
425 baseRange.setEnd(externalRange.endContainer, 0); |
|
426 do_check_eq(baseRange.startContainer, externalRange.endContainer); |
|
427 do_check_eq(baseRange.startOffset, 0); |
|
428 do_check_true(baseRange.collapsed); |
|
429 |
|
430 /* |
|
431 // XXX ajvincent if rv == WRONG_DOCUMENT_ERR, return false? |
|
432 do_check_false(baseRange.isPointInRange(startContainer, startOffset)); |
|
433 do_check_false(baseRange.isPointInRange(startContainer, startOffset + 1)); |
|
434 do_check_false(baseRange.isPointInRange(endContainer, endOffset)); |
|
435 */ |
|
436 |
|
437 // Requested by smaug: A range involving a comment as a document child. |
|
438 doc = parser.parseFromString("<!-- foo --><foo/>", "application/xml"); |
|
439 do_check_true(doc instanceof C_i.nsIDOMDocument); |
|
440 do_check_eq(doc.childNodes.length, 2); |
|
441 baseRange = doc.createRange(); |
|
442 baseRange.setStart(doc.firstChild, 1); |
|
443 baseRange.setEnd(doc.firstChild, 2); |
|
444 var frag = baseRange.extractContents(); |
|
445 do_check_eq(frag.childNodes.length, 1); |
|
446 do_check_true(frag.firstChild instanceof C_i.nsIDOMComment); |
|
447 do_check_eq(frag.firstChild.nodeValue, "f"); |
|
448 |
|
449 /* smaug also requested attribute tests. Sadly, those are not yet supported |
|
450 in ranges - see https://bugzilla.mozilla.org/show_bug.cgi?id=302775. |
|
451 */ |
|
452 } |
|
453 |
|
454 function run_test() { |
|
455 run_extract_test(); |
|
456 run_miscellaneous_tests(); |
|
457 } |