summaryrefslogtreecommitdiffstats
path: root/dom/base/test/unit/test_range.js
blob: 8b9f5c0b8b327e7eba8a5f46df9b6aeca9c3165b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const UNORDERED_TYPE = 8; // XPathResult.ANY_UNORDERED_NODE_TYPE

/**
 * Determine if the data node has only ignorable white-space.
 *
 * @return NodeFilter.FILTER_SKIP if it does.
 * @return NodeFilter.FILTER_ACCEPT otherwise.
 */
function isWhitespace(aNode) {
  return /\S/.test(aNode.nodeValue)
    ? NodeFilter.FILTER_SKIP
    : NodeFilter.FILTER_ACCEPT;
}

/**
 * Create a DocumentFragment with cloned children equaling a node's children.
 *
 * @param aNode The node to copy from.
 *
 * @return DocumentFragment node.
 */
function getFragment(aNode) {
  var frag = aNode.ownerDocument.createDocumentFragment();
  for (var i = 0; i < aNode.childNodes.length; i++) {
    frag.appendChild(aNode.childNodes.item(i).cloneNode(true));
  }
  return frag;
}

// Goodies from head_content.js
const parser = getParser();

/**
 * Translate an XPath to a DOM node. This method uses a document
 * fragment as context node.
 *
 * @param aContextNode The context node to apply the XPath to.
 * @param aPath        The XPath to use.
 *
 * @return Node  The target node retrieved from the XPath.
 */
function evalXPathInDocumentFragment(aContextNode, aPath) {
  Assert.equal(ChromeUtils.getClassName(aContextNode), "DocumentFragment");
  Assert.ok(aContextNode.childNodes.length);
  if (aPath == ".") {
    return aContextNode;
  }

  // Separate the fragment's xpath lookup from the rest.
  var firstSlash = aPath.indexOf("/");
  if (firstSlash == -1) {
    firstSlash = aPath.length;
  }
  var prefix = aPath.substr(0, firstSlash);
  var realPath = aPath.substr(firstSlash + 1);
  if (!realPath) {
    realPath = ".";
  }

  // Set up a special node filter to look among the fragment's child nodes.
  var childIndex = 1;
  var bracketIndex = prefix.indexOf("[");
  if (bracketIndex != -1) {
    childIndex = Number(
      prefix.substring(bracketIndex + 1, prefix.indexOf("]"))
    );
    Assert.ok(childIndex > 0);
    prefix = prefix.substr(0, bracketIndex);
  }

  var targetType = NodeFilter.SHOW_ELEMENT;
  var targetNodeName = prefix;
  if (prefix.indexOf("processing-instruction(") == 0) {
    targetType = NodeFilter.SHOW_PROCESSING_INSTRUCTION;
    targetNodeName = prefix.substring(
      prefix.indexOf("(") + 2,
      prefix.indexOf(")") - 1
    );
  }
  switch (prefix) {
    case "text()":
      targetType = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION;
      targetNodeName = null;
      break;
    case "comment()":
      targetType = NodeFilter.SHOW_COMMENT;
      targetNodeName = null;
      break;
    case "node()":
      targetType = NodeFilter.SHOW_ALL;
      targetNodeName = null;
  }

  var filter = {
    count: 0,

    // NodeFilter
    acceptNode: function acceptNode(aNode) {
      if (aNode.parentNode != aContextNode) {
        // Don't bother looking at kids either.
        return NodeFilter.FILTER_REJECT;
      }

      if (targetNodeName && targetNodeName != aNode.nodeName) {
        return NodeFilter.FILTER_SKIP;
      }

      this.count++;
      if (this.count != childIndex) {
        return NodeFilter.FILTER_SKIP;
      }

      return NodeFilter.FILTER_ACCEPT;
    },
  };

  // Look for the node matching the step from the document fragment.
  var walker = aContextNode.ownerDocument.createTreeWalker(
    aContextNode,
    targetType,
    filter
  );
  var targetNode = walker.nextNode();
  Assert.notEqual(targetNode, null);

  // Apply our remaining xpath to the found node.
  var expr = aContextNode.ownerDocument.createExpression(realPath, null);
  var result = expr.evaluate(targetNode, UNORDERED_TYPE, null);
  return result.singleNodeValue;
}

/**
 * Get a DOM range corresponding to the test's source node.
 *
 * @param aSourceNode <source/> element with range information.
 * @param aFragment   DocumentFragment generated with getFragment().
 *
 * @return Range object.
 */
function getRange(aSourceNode, aFragment) {
  Assert.ok(Element.isInstance(aSourceNode));
  Assert.equal(ChromeUtils.getClassName(aFragment), "DocumentFragment");
  var doc = aSourceNode.ownerDocument;

  var containerPath = aSourceNode.getAttribute("startContainer");
  var startContainer = evalXPathInDocumentFragment(aFragment, containerPath);
  var startOffset = Number(aSourceNode.getAttribute("startOffset"));

  containerPath = aSourceNode.getAttribute("endContainer");
  var endContainer = evalXPathInDocumentFragment(aFragment, containerPath);
  var endOffset = Number(aSourceNode.getAttribute("endOffset"));

  var range = doc.createRange();
  range.setStart(startContainer, startOffset);
  range.setEnd(endContainer, endOffset);
  return range;
}

/**
 * Get the document for a given path, and clean it up for our tests.
 *
 * @param aPath The path to the local document.
 */
function getParsedDocument(aPath) {
  return do_parse_document(aPath, "application/xml").then(
    processParsedDocument
  );
}

function processParsedDocument(doc) {
  Assert.ok(doc.documentElement.localName != "parsererror");
  Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument");

  // Clean out whitespace.
  var walker = doc.createTreeWalker(
    doc,
    NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION,
    isWhitespace
  );
  while (walker.nextNode()) {
    var parent = walker.currentNode.parentNode;
    parent.removeChild(walker.currentNode);
    walker.currentNode = parent;
  }

  // Clean out mandatory splits between nodes.
  var splits = doc.getElementsByTagName("split");
  var i;
  for (i = splits.length - 1; i >= 0; i--) {
    let node = splits.item(i);
    node.remove();
  }
  splits = null;

  // Replace empty CDATA sections.
  var emptyData = doc.getElementsByTagName("empty-cdata");
  for (i = emptyData.length - 1; i >= 0; i--) {
    let node = emptyData.item(i);
    var cdata = doc.createCDATASection("");
    node.parentNode.replaceChild(cdata, node);
  }

  return doc;
}

/**
 * Run the extraction tests.
 */
function run_extract_test() {
  var filePath = "test_delete_range.xml";
  getParsedDocument(filePath).then(do_extract_test);
}

function do_extract_test(doc) {
  var tests = doc.getElementsByTagName("test");

  // Run our deletion, extraction tests.
  for (var i = 0; i < tests.length; i++) {
    dump("Configuring for test " + i + "\n");
    var currentTest = tests.item(i);

    // Validate the test is properly formatted for what this harness expects.
    var baseSource = currentTest.firstChild;
    Assert.equal(baseSource.nodeName, "source");
    var baseResult = baseSource.nextSibling;
    Assert.equal(baseResult.nodeName, "result");
    var baseExtract = baseResult.nextSibling;
    Assert.equal(baseExtract.nodeName, "extract");
    Assert.equal(baseExtract.nextSibling, null);

    /* We do all our tests on DOM document fragments, derived from the test
       element's children.  This lets us rip the various fragments to shreds,
       while preserving the original elements so we can make more copies of
       them.

       After the range's extraction or deletion is done, we use
       Node.isEqualNode() between the altered source fragment and the
       result fragment.  We also run isEqualNode() between the extracted
       fragment and the fragment from the baseExtract node.  If they are not
       equal, we have failed a test.

       We also have to ensure the original nodes on the end points of the
       range are still in the source fragment.  This is bug 332148.  The nodes
       may not be replaced with equal but separate nodes.  The range extraction
       may alter these nodes - in the case of text containers, they will - but
       the nodes must stay there, to preserve references such as user data,
       event listeners, etc.

       First, an extraction test.
     */

    var resultFrag = getFragment(baseResult);
    var extractFrag = getFragment(baseExtract);

    dump("Extract contents test " + i + "\n\n");
    var baseFrag = getFragment(baseSource);
    var baseRange = getRange(baseSource, baseFrag);
    var startContainer = baseRange.startContainer;
    var endContainer = baseRange.endContainer;

    var cutFragment = baseRange.extractContents();
    dump("cutFragment: " + cutFragment + "\n");
    if (cutFragment) {
      Assert.ok(extractFrag.isEqualNode(cutFragment));
    } else {
      Assert.equal(extractFrag.firstChild, null);
    }
    Assert.ok(baseFrag.isEqualNode(resultFrag));

    dump("Ensure the original nodes weren't extracted - test " + i + "\n\n");
    var walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null);
    var foundStart = false;
    var foundEnd = false;
    do {
      if (walker.currentNode == startContainer) {
        foundStart = true;
      }

      if (walker.currentNode == endContainer) {
        // An end container node should not come before the start container node.
        Assert.ok(foundStart);
        foundEnd = true;
        break;
      }
    } while (walker.nextNode());
    Assert.ok(foundEnd);

    /* Now, we reset our test for the deleteContents case.  This one differs
       from the extractContents case only in that there is no extracted document
       fragment to compare against.  So we merely compare the starting fragment,
       minus the extracted content, against the result fragment.
     */
    dump("Delete contents test " + i + "\n\n");
    baseFrag = getFragment(baseSource);
    baseRange = getRange(baseSource, baseFrag);
    startContainer = baseRange.startContainer;
    endContainer = baseRange.endContainer;
    baseRange.deleteContents();
    Assert.ok(baseFrag.isEqualNode(resultFrag));

    dump("Ensure the original nodes weren't deleted - test " + i + "\n\n");
    walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null);
    foundStart = false;
    foundEnd = false;
    do {
      if (walker.currentNode == startContainer) {
        foundStart = true;
      }

      if (walker.currentNode == endContainer) {
        // An end container node should not come before the start container node.
        Assert.ok(foundStart);
        foundEnd = true;
        break;
      }
    } while (walker.nextNode());
    Assert.ok(foundEnd);

    // Clean up after ourselves.
    walker = null;
  }
}

/**
 * Miscellaneous tests not covered above.
 */
function run_miscellaneous_tests() {
  var filePath = "test_delete_range.xml";
  getParsedDocument(filePath).then(do_miscellaneous_tests);
}

function isText(node) {
  return (
    node.nodeType == node.TEXT_NODE || node.nodeType == node.CDATA_SECTION_NODE
  );
}

function do_miscellaneous_tests(doc) {
  var tests = doc.getElementsByTagName("test");

  // Let's try some invalid inputs to our DOM range and see what happens.
  var currentTest = tests.item(0);
  var baseSource = currentTest.firstChild;

  var baseFrag = getFragment(baseSource);

  var baseRange = getRange(baseSource, baseFrag);
  var startContainer = baseRange.startContainer;
  var endContainer = baseRange.endContainer;
  var startOffset = baseRange.startOffset;
  var endOffset = baseRange.endOffset;

  // Text range manipulation.
  if (
    endOffset > startOffset &&
    startContainer == endContainer &&
    isText(startContainer)
  ) {
    // Invalid start node
    try {
      baseRange.setStart(null, 0);
      do_throw("Should have thrown NOT_OBJECT_ERR!");
    } catch (e) {
      Assert.equal(e.constructor.name, "TypeError");
    }

    // Invalid start node
    try {
      baseRange.setStart({}, 0);
      do_throw("Should have thrown SecurityError!");
    } catch (e) {
      Assert.equal(e.constructor.name, "TypeError");
    }

    // Invalid index
    try {
      baseRange.setStart(startContainer, -1);
      do_throw("Should have thrown IndexSizeError!");
    } catch (e) {
      Assert.equal(e.name, "IndexSizeError");
    }

    // Invalid index
    var newOffset = isText(startContainer)
      ? startContainer.nodeValue.length + 1
      : startContainer.childNodes.length + 1;
    try {
      baseRange.setStart(startContainer, newOffset);
      do_throw("Should have thrown IndexSizeError!");
    } catch (e) {
      Assert.equal(e.name, "IndexSizeError");
    }

    newOffset--;
    // Valid index
    baseRange.setStart(startContainer, newOffset);
    Assert.equal(baseRange.startContainer, baseRange.endContainer);
    Assert.equal(baseRange.startOffset, newOffset);
    Assert.ok(baseRange.collapsed);

    // Valid index
    baseRange.setEnd(startContainer, 0);
    Assert.equal(baseRange.startContainer, baseRange.endContainer);
    Assert.equal(baseRange.startOffset, 0);
    Assert.ok(baseRange.collapsed);
  } else {
    do_throw(
      "The first test should be a text-only range test.  Test is invalid."
    );
  }

  /* See what happens when a range has a startContainer in one fragment, and an
     endContainer in another.  According to the DOM spec, section 2.4, the range
     should collapse to the new container and offset. */
  baseRange = getRange(baseSource, baseFrag);
  startContainer = baseRange.startContainer;
  startOffset = baseRange.startOffset;
  endContainer = baseRange.endContainer;
  endOffset = baseRange.endOffset;

  dump("External fragment test\n\n");

  var externalTest = tests.item(1);
  var externalSource = externalTest.firstChild;
  var externalFrag = getFragment(externalSource);
  var externalRange = getRange(externalSource, externalFrag);

  baseRange.setEnd(externalRange.endContainer, 0);
  Assert.equal(baseRange.startContainer, externalRange.endContainer);
  Assert.equal(baseRange.startOffset, 0);
  Assert.ok(baseRange.collapsed);

  /*
  // XXX ajvincent if rv == WRONG_DOCUMENT_ERR, return false?
  do_check_false(baseRange.isPointInRange(startContainer, startOffset));
  do_check_false(baseRange.isPointInRange(startContainer, startOffset + 1));
  do_check_false(baseRange.isPointInRange(endContainer, endOffset));
  */

  // Requested by smaug:  A range involving a comment as a document child.
  doc = parser.parseFromString("<!-- foo --><foo/>", "application/xml");
  Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument");
  Assert.equal(doc.childNodes.length, 2);
  baseRange = doc.createRange();
  baseRange.setStart(doc.firstChild, 1);
  baseRange.setEnd(doc.firstChild, 2);
  var frag = baseRange.extractContents();
  Assert.equal(frag.childNodes.length, 1);
  Assert.ok(ChromeUtils.getClassName(frag.firstChild) == "Comment");
  Assert.equal(frag.firstChild.nodeType, frag.COMMENT_NODE);
  Assert.equal(frag.firstChild.nodeValue, "f");

  /* smaug also requested attribute tests.  Sadly, those are not yet supported
     in ranges - see https://bugzilla.mozilla.org/show_bug.cgi?id=302775.
   */
}

function run_test() {
  run_extract_test();
  run_miscellaneous_tests();
}