tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Range-mutations.js (41999B)


      1 "use strict";
      2 
      3 // These tests probably use too much abstraction and too little copy-paste.
      4 // Reader beware.
      5 //
      6 // TODO:
      7 //
      8 // * Lots and lots and lots more different types of ranges
      9 // * insertBefore() with DocumentFragments
     10 // * Fill out other insert/remove tests
     11 // * normalize() (https://www.w3.org/Bugs/Public/show_bug.cgi?id=13843)
     12 
     13 // Give a textual description of the range we're testing, for the test names.
     14 function describeRange(startContainer, startOffset, endContainer, endOffset) {
     15  if (startContainer == endContainer && startOffset == endOffset) {
     16    return "range collapsed at (" + startContainer + ", " + startOffset + ")";
     17  } else if (startContainer == endContainer) {
     18    return "range on " + startContainer + " from " + startOffset + " to " + endOffset;
     19  } else {
     20    return "range from (" + startContainer + ", " + startOffset + ") to (" + endContainer + ", " + endOffset + ")";
     21  }
     22 }
     23 
     24 // Lists of the various types of nodes we'll want to use.  We use strings that
     25 // we can later eval(), so that we can produce legible test names.
     26 var textNodes = [
     27  "paras[0].firstChild",
     28  "paras[1].firstChild",
     29  "foreignTextNode",
     30  "xmlTextNode",
     31  "detachedTextNode",
     32  "detachedForeignTextNode",
     33  "detachedXmlTextNode",
     34 ];
     35 var commentNodes = [
     36  "comment",
     37  "foreignComment",
     38  "xmlComment",
     39  "detachedComment",
     40  "detachedForeignComment",
     41  "detachedXmlComment",
     42 ];
     43 var characterDataNodes = textNodes.concat(commentNodes);
     44 
     45 // This function is slightly scary, but it works well enough, so . . .
     46 // sourceTests is an array of test data that will be altered in mysterious ways
     47 // before being passed off to doTest, descFn is something that takes an element
     48 // of sourceTests and produces the first part of a human-readable description
     49 // of the test, testFn is the function that doTest will call to do the actual
     50 // work and tell it what results to expect.
     51 function doTests(sourceTests, descFn, testFn) {
     52  var tests = [];
     53  for (var i = 0; i < sourceTests.length; i++) {
     54    var params = sourceTests[i];
     55    var len = params.length;
     56    tests.push([
     57      descFn(params) + ", with unselected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]),
     58      // The closure here ensures that the params that testFn get are the
     59      // current version of params, not the version from the last
     60      // iteration of this loop.  We test that none of the parameters
     61      // evaluate to undefined to catch bugs in our eval'ing, like
     62      // mistyping a property name.
     63      function(params) { return function() {
     64        var evaledParams = params.map(eval);
     65        for (var i = 0; i < evaledParams.length; i++) {
     66          assert_not_equals(typeof evaledParams[i], "undefined",
     67            "Test bug: " + params[i] + " is undefined");
     68        }
     69        return testFn.apply(null, evaledParams);
     70      } }(params),
     71      false,
     72      params[len - 4],
     73      params[len - 3],
     74      params[len - 2],
     75      params[len - 1]
     76    ]);
     77    tests.push([
     78      descFn(params) + ", with selected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]),
     79      function(params) { return function(selectedRange) {
     80        var evaledParams = params.slice(0, len - 4).map(eval);
     81        for (var i = 0; i < evaledParams.length; i++) {
     82          assert_not_equals(typeof evaledParams[i], "undefined",
     83            "Test bug: " + params[i] + " is undefined");
     84        }
     85        // Override input range with the one that was actually selected when computing the expected result.
     86        evaledParams = evaledParams.concat([selectedRange.startContainer, selectedRange.startOffset, selectedRange.endContainer, selectedRange.endOffset]);
     87        return testFn.apply(null, evaledParams);
     88      } }(params),
     89      true,
     90      params[len - 4],
     91      params[len - 3],
     92      params[len - 2],
     93      params[len - 1]
     94    ]);
     95  }
     96  generate_tests(doTest, tests);
     97 }
     98 
     99 // Set up the range, call the callback function to do the DOM modification and
    100 // tell us what to expect.  The callback function needs to return a
    101 // four-element array with the expected start/end containers/offsets, and
    102 // receives no arguments.  useSelection tells us whether the Range should be
    103 // added to a Selection and the Selection tested to ensure that the mutation
    104 // affects user selections as well as other ranges; every test is run with this
    105 // both false and true, because when it's set to true WebKit and Opera fail all
    106 // tests' sanity checks, which is unhelpful.  The last four parameters just
    107 // tell us what range to build.
    108 function doTest(callback, useSelection, startContainer, startOffset, endContainer, endOffset) {
    109  // Recreate all the test nodes in case they were altered by the last test
    110  // run.
    111  setupRangeTests();
    112  startContainer = eval(startContainer);
    113  startOffset = eval(startOffset);
    114  endContainer = eval(endContainer);
    115  endOffset = eval(endOffset);
    116 
    117  var ownerDoc = startContainer.nodeType == Node.DOCUMENT_NODE
    118    ? startContainer
    119    : startContainer.ownerDocument;
    120  var range = ownerDoc.createRange();
    121  range.setStart(startContainer, startOffset);
    122  range.setEnd(endContainer, endOffset);
    123 
    124  if (useSelection) {
    125    getSelection().removeAllRanges();
    126    getSelection().addRange(range);
    127 
    128    // Some browsers refuse to add a range unless it results in an actual visible selection.
    129    if (!getSelection().rangeCount)
    130        return;
    131 
    132    // Override range with the one that was actually selected as it differs in some browsers.
    133    range = getSelection().getRangeAt(0);
    134  }
    135 
    136  var expected = callback(range);
    137 
    138  assert_equals(range.startContainer, expected[0],
    139    "Wrong start container");
    140  assert_equals(range.startOffset, expected[1],
    141    "Wrong start offset");
    142  assert_equals(range.endContainer, expected[2],
    143    "Wrong end container");
    144  assert_equals(range.endOffset, expected[3],
    145    "Wrong end offset");
    146 }
    147 
    148 
    149 // Now we get to the specific tests.
    150 
    151 function testSplitText(oldNode, offset, startContainer, startOffset, endContainer, endOffset) {
    152  // Save these for later
    153  var originalStartOffset = startOffset;
    154  var originalEndOffset = endOffset;
    155  var originalLength = oldNode.length;
    156 
    157  var newNode;
    158  try {
    159    newNode = oldNode.splitText(offset);
    160  } catch (e) {
    161    // Should only happen if offset is negative
    162    return [startContainer, startOffset, endContainer, endOffset];
    163  }
    164 
    165  // First we adjust for replacing data:
    166  //
    167  // "Replace data with offset offset, count count, and data the empty
    168  // string."
    169  //
    170  // That translates to offset = offset, count = originalLength - offset,
    171  // data = "".  node is oldNode.
    172  //
    173  // "For every boundary point whose node is node, and whose offset is
    174  // greater than offset but less than or equal to offset plus count, set its
    175  // offset to offset."
    176  if (startContainer == oldNode
    177  && startOffset > offset
    178  && startOffset <= originalLength) {
    179    startOffset = offset;
    180  }
    181 
    182  if (endContainer == oldNode
    183  && endOffset > offset
    184  && endOffset <= originalLength) {
    185    endOffset = offset;
    186  }
    187 
    188  // "For every boundary point whose node is node, and whose offset is
    189  // greater than offset plus count, add the length of data to its offset,
    190  // then subtract count from it."
    191  //
    192  // Can't happen: offset plus count is originalLength.
    193 
    194  // Now we insert a node, if oldNode's parent isn't null: "For each boundary
    195  // point whose node is the new parent of the affected node and whose offset
    196  // is greater than the new index of the affected node, add one to the
    197  // boundary point's offset."
    198  if (startContainer == oldNode.parentNode
    199  && startOffset > 1 + indexOf(oldNode)) {
    200    startOffset++;
    201  }
    202 
    203  if (endContainer == oldNode.parentNode
    204  && endOffset > 1 + indexOf(oldNode)) {
    205    endOffset++;
    206  }
    207 
    208  // Finally, the splitText stuff itself:
    209  //
    210  // "If parent is not null, run these substeps:
    211  //
    212  //   * "For each range whose start node is node and start offset is greater
    213  //   than offset, set its start node to new node and decrease its start
    214  //   offset by offset.
    215  //
    216  //   * "For each range whose end node is node and end offset is greater
    217  //   than offset, set its end node to new node and decrease its end offset
    218  //   by offset.
    219  //
    220  //   * "For each range whose start node is parent and start offset is equal
    221  //   to the index of node + 1, increase its start offset by one.
    222  //
    223  //   * "For each range whose end node is parent and end offset is equal to
    224  //   the index of node + 1, increase its end offset by one."
    225  if (oldNode.parentNode) {
    226    if (startContainer == oldNode && originalStartOffset > offset) {
    227      startContainer = newNode;
    228      startOffset = originalStartOffset - offset;
    229    }
    230 
    231    if (endContainer == oldNode && originalEndOffset > offset) {
    232      endContainer = newNode;
    233      endOffset = originalEndOffset - offset;
    234    }
    235 
    236    if (startContainer == oldNode.parentNode
    237    && startOffset == 1 + indexOf(oldNode)) {
    238      startOffset++;
    239    }
    240 
    241    if (endContainer == oldNode.parentNode
    242    && endOffset == 1 + indexOf(oldNode)) {
    243      endOffset++;
    244    }
    245  }
    246 
    247  return [startContainer, startOffset, endContainer, endOffset];
    248 }
    249 
    250 // The offset argument is unsigned, so per WebIDL -1 should wrap to 4294967295,
    251 // which is probably longer than the length, so it should throw an exception.
    252 // This is no different from the other cases where the offset is longer than
    253 // the length, and the wrapping complicates my testing slightly, so I won't
    254 // bother testing negative values here or in other cases.
    255 var splitTextTests = [];
    256 for (var i = 0; i < textNodes.length; i++) {
    257  var node = textNodes[i];
    258  splitTextTests.push([node, 376, node, 0, node, 1]);
    259  splitTextTests.push([node, 0, node, 0, node, 0]);
    260  splitTextTests.push([node, 1, node, 1, node, 1]);
    261  splitTextTests.push([node, node + ".length", node, node + ".length", node, node + ".length"]);
    262  splitTextTests.push([node, 1, node, 1, node, 3]);
    263  splitTextTests.push([node, 2, node, 1, node, 3]);
    264  splitTextTests.push([node, 3, node, 1, node, 3]);
    265 }
    266 
    267 splitTextTests.push(
    268  ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 0],
    269  ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 1],
    270  ["paras[0].firstChild", 1, "paras[0]", 1, "paras[0]", 1],
    271  ["paras[0].firstChild", 1, "paras[0].firstChild", 1, "paras[0]", 1],
    272  ["paras[0].firstChild", 2, "paras[0].firstChild", 1, "paras[0]", 1],
    273  ["paras[0].firstChild", 3, "paras[0].firstChild", 1, "paras[0]", 1],
    274  ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0].firstChild", 3],
    275  ["paras[0].firstChild", 2, "paras[0]", 0, "paras[0].firstChild", 3],
    276  ["paras[0].firstChild", 3, "paras[0]", 0, "paras[0].firstChild", 3]
    277 );
    278 
    279 
    280 function testReplaceDataAlgorithm(node, offset, count, data, callback, startContainer, startOffset, endContainer, endOffset) {
    281  // Mutation works the same any time DOM Core's "replace data" algorithm is
    282  // invoked.  node, offset, count, data are as in that algorithm.  The
    283  // callback is what does the actual setting.  Not to be confused with
    284  // testReplaceData, which tests the replaceData() method.
    285 
    286  // Barring any provision to the contrary, the containers and offsets must
    287  // not change.
    288  var expectedStartContainer = startContainer;
    289  var expectedStartOffset = startOffset;
    290  var expectedEndContainer = endContainer;
    291  var expectedEndOffset = endOffset;
    292 
    293  var originalParent = node.parentNode;
    294  var originalData = node.data;
    295 
    296  var exceptionThrown = false;
    297  try {
    298    callback();
    299  } catch (e) {
    300    // Should only happen if offset is greater than length
    301    exceptionThrown = true;
    302  }
    303 
    304  assert_equals(node.parentNode, originalParent,
    305    "Sanity check failed: changing data changed the parent");
    306 
    307  // "User agents must run the following steps whenever they replace data of
    308  // a CharacterData node, as though they were written in the specification
    309  // for that algorithm after all other steps. In particular, the steps must
    310  // not be executed if the algorithm threw an exception."
    311  if (exceptionThrown) {
    312    assert_equals(node.data, originalData,
    313      "Sanity check failed: exception thrown but data changed");
    314  } else {
    315    assert_equals(node.data,
    316      originalData.substr(0, offset) + data + originalData.substr(offset + count),
    317      "Sanity check failed: data not changed as expected");
    318  }
    319 
    320  // "For every boundary point whose node is node, and whose offset is
    321  // greater than offset but less than or equal to offset plus count, set
    322  // its offset to offset."
    323  if (!exceptionThrown
    324  && startContainer == node
    325  && startOffset > offset
    326  && startOffset <= offset + count) {
    327    expectedStartOffset = offset;
    328  }
    329 
    330  if (!exceptionThrown
    331  && endContainer == node
    332  && endOffset > offset
    333  && endOffset <= offset + count) {
    334    expectedEndOffset = offset;
    335  }
    336 
    337  // "For every boundary point whose node is node, and whose offset is
    338  // greater than offset plus count, add the length of data to its offset,
    339  // then subtract count from it."
    340  if (!exceptionThrown
    341  && startContainer == node
    342  && startOffset > offset + count) {
    343    expectedStartOffset += data.length - count;
    344  }
    345 
    346  if (!exceptionThrown
    347  && endContainer == node
    348  && endOffset > offset + count) {
    349    expectedEndOffset += data.length - count;
    350  }
    351 
    352  return [expectedStartContainer, expectedStartOffset, expectedEndContainer, expectedEndOffset];
    353 }
    354 
    355 function testInsertData(node, offset, data, startContainer, startOffset, endContainer, endOffset) {
    356  return testReplaceDataAlgorithm(node, offset, 0, data,
    357    function() { node.insertData(offset, data) },
    358    startContainer, startOffset, endContainer, endOffset);
    359 }
    360 
    361 var insertDataTests = [];
    362 for (var i = 0; i < characterDataNodes.length; i++) {
    363  var node = characterDataNodes[i];
    364  insertDataTests.push([node, 376, '"foo"', node, 0, node, 1]);
    365  insertDataTests.push([node, 0, '"foo"', node, 0, node, 0]);
    366  insertDataTests.push([node, 1, '"foo"', node, 1, node, 1]);
    367  insertDataTests.push([node, node + ".length", '"foo"', node, node + ".length", node, node + ".length"]);
    368  insertDataTests.push([node, 1, '"foo"', node, 1, node, 3]);
    369  insertDataTests.push([node, 2, '"foo"', node, 1, node, 3]);
    370  insertDataTests.push([node, 3, '"foo"', node, 1, node, 3]);
    371 
    372  insertDataTests.push([node, 376, '""', node, 0, node, 1]);
    373  insertDataTests.push([node, 0, '""', node, 0, node, 0]);
    374  insertDataTests.push([node, 1, '""', node, 1, node, 1]);
    375  insertDataTests.push([node, node + ".length", '""', node, node + ".length", node, node + ".length"]);
    376  insertDataTests.push([node, 1, '""', node, 1, node, 3]);
    377  insertDataTests.push([node, 2, '""', node, 1, node, 3]);
    378  insertDataTests.push([node, 3, '""', node, 1, node, 3]);
    379 }
    380 
    381 insertDataTests.push(
    382  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 0],
    383  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 1],
    384  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 1, "paras[0]", 1],
    385  ["paras[0].firstChild", 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    386  ["paras[0].firstChild", 2, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    387  ["paras[0].firstChild", 3, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    388  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    389  ["paras[0].firstChild", 2, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    390  ["paras[0].firstChild", 3, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
    391 );
    392 
    393 
    394 function testAppendData(node, data, startContainer, startOffset, endContainer, endOffset) {
    395  return testReplaceDataAlgorithm(node, node.length, 0, data,
    396    function() { node.appendData(data) },
    397    startContainer, startOffset, endContainer, endOffset);
    398 }
    399 
    400 var appendDataTests = [];
    401 for (var i = 0; i < characterDataNodes.length; i++) {
    402  var node = characterDataNodes[i];
    403  appendDataTests.push([node, '"foo"', node, 0, node, 1]);
    404  appendDataTests.push([node, '"foo"', node, 0, node, 0]);
    405  appendDataTests.push([node, '"foo"', node, 1, node, 1]);
    406  appendDataTests.push([node, '"foo"', node, 0, node, node + ".length"]);
    407  appendDataTests.push([node, '"foo"', node, 1, node, node + ".length"]);
    408  appendDataTests.push([node, '"foo"', node, node + ".length", node, node + ".length"]);
    409  appendDataTests.push([node, '"foo"', node, 1, node, 3]);
    410 
    411  appendDataTests.push([node, '""', node, 0, node, 1]);
    412  appendDataTests.push([node, '""', node, 0, node, 0]);
    413  appendDataTests.push([node, '""', node, 1, node, 1]);
    414  appendDataTests.push([node, '""', node, 0, node, node + ".length"]);
    415  appendDataTests.push([node, '""', node, 1, node, node + ".length"]);
    416  appendDataTests.push([node, '""', node, node + ".length", node, node + ".length"]);
    417  appendDataTests.push([node, '""', node, 1, node, 3]);
    418 }
    419 
    420 appendDataTests.push(
    421  ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 0],
    422  ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 1],
    423  ["paras[0].firstChild", '""', "paras[0]", 1, "paras[0]", 1],
    424  ["paras[0].firstChild", '""', "paras[0].firstChild", 1, "paras[0]", 1],
    425  ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0].firstChild", 3],
    426 
    427  ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 0],
    428  ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 1],
    429  ["paras[0].firstChild", '"foo"', "paras[0]", 1, "paras[0]", 1],
    430  ["paras[0].firstChild", '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    431  ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
    432 );
    433 
    434 
    435 function testDeleteData(node, offset, count, startContainer, startOffset, endContainer, endOffset) {
    436  return testReplaceDataAlgorithm(node, offset, count, "",
    437    function() { node.deleteData(offset, count) },
    438    startContainer, startOffset, endContainer, endOffset);
    439 }
    440 
    441 var deleteDataTests = [];
    442 for (var i = 0; i < characterDataNodes.length; i++) {
    443  var node = characterDataNodes[i];
    444  deleteDataTests.push([node, 376, 2, node, 0, node, 1]);
    445  deleteDataTests.push([node, 0, 2, node, 0, node, 0]);
    446  deleteDataTests.push([node, 1, 2, node, 1, node, 1]);
    447  deleteDataTests.push([node, node + ".length", 2, node, node + ".length", node, node + ".length"]);
    448  deleteDataTests.push([node, 1, 2, node, 1, node, 3]);
    449  deleteDataTests.push([node, 2, 2, node, 1, node, 3]);
    450  deleteDataTests.push([node, 3, 2, node, 1, node, 3]);
    451 
    452  deleteDataTests.push([node, 376, 0, node, 0, node, 1]);
    453  deleteDataTests.push([node, 0, 0, node, 0, node, 0]);
    454  deleteDataTests.push([node, 1, 0, node, 1, node, 1]);
    455  deleteDataTests.push([node, node + ".length", 0, node, node + ".length", node, node + ".length"]);
    456  deleteDataTests.push([node, 1, 0, node, 1, node, 3]);
    457  deleteDataTests.push([node, 2, 0, node, 1, node, 3]);
    458  deleteDataTests.push([node, 3, 0, node, 1, node, 3]);
    459 
    460  deleteDataTests.push([node, 376, 631, node, 0, node, 1]);
    461  deleteDataTests.push([node, 0, 631, node, 0, node, 0]);
    462  deleteDataTests.push([node, 1, 631, node, 1, node, 1]);
    463  deleteDataTests.push([node, node + ".length", 631, node, node + ".length", node, node + ".length"]);
    464  deleteDataTests.push([node, 1, 631, node, 1, node, 3]);
    465  deleteDataTests.push([node, 2, 631, node, 1, node, 3]);
    466  deleteDataTests.push([node, 3, 631, node, 1, node, 3]);
    467 }
    468 
    469 deleteDataTests.push(
    470  ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 0],
    471  ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 1],
    472  ["paras[0].firstChild", 1, 2, "paras[0]", 1, "paras[0]", 1],
    473  ["paras[0].firstChild", 1, 2, "paras[0].firstChild", 1, "paras[0]", 1],
    474  ["paras[0].firstChild", 2, 2, "paras[0].firstChild", 1, "paras[0]", 1],
    475  ["paras[0].firstChild", 3, 2, "paras[0].firstChild", 1, "paras[0]", 1],
    476  ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0].firstChild", 3],
    477  ["paras[0].firstChild", 2, 2, "paras[0]", 0, "paras[0].firstChild", 3],
    478  ["paras[0].firstChild", 3, 2, "paras[0]", 0, "paras[0].firstChild", 3]
    479 );
    480 
    481 
    482 function testReplaceData(node, offset, count, data, startContainer, startOffset, endContainer, endOffset) {
    483  return testReplaceDataAlgorithm(node, offset, count, data,
    484    function() { node.replaceData(offset, count, data) },
    485    startContainer, startOffset, endContainer, endOffset);
    486 }
    487 
    488 var replaceDataTests = [];
    489 for (var i = 0; i < characterDataNodes.length; i++) {
    490  var node = characterDataNodes[i];
    491  replaceDataTests.push([node, 376, 0, '"foo"', node, 0, node, 1]);
    492  replaceDataTests.push([node, 0, 0, '"foo"', node, 0, node, 0]);
    493  replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 1]);
    494  replaceDataTests.push([node, node + ".length", 0, '"foo"', node, node + ".length", node, node + ".length"]);
    495  replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 3]);
    496  replaceDataTests.push([node, 2, 0, '"foo"', node, 1, node, 3]);
    497  replaceDataTests.push([node, 3, 0, '"foo"', node, 1, node, 3]);
    498 
    499  replaceDataTests.push([node, 376, 0, '""', node, 0, node, 1]);
    500  replaceDataTests.push([node, 0, 0, '""', node, 0, node, 0]);
    501  replaceDataTests.push([node, 1, 0, '""', node, 1, node, 1]);
    502  replaceDataTests.push([node, node + ".length", 0, '""', node, node + ".length", node, node + ".length"]);
    503  replaceDataTests.push([node, 1, 0, '""', node, 1, node, 3]);
    504  replaceDataTests.push([node, 2, 0, '""', node, 1, node, 3]);
    505  replaceDataTests.push([node, 3, 0, '""', node, 1, node, 3]);
    506 
    507  replaceDataTests.push([node, 376, 1, '"foo"', node, 0, node, 1]);
    508  replaceDataTests.push([node, 0, 1, '"foo"', node, 0, node, 0]);
    509  replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 1]);
    510  replaceDataTests.push([node, node + ".length", 1, '"foo"', node, node + ".length", node, node + ".length"]);
    511  replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 3]);
    512  replaceDataTests.push([node, 2, 1, '"foo"', node, 1, node, 3]);
    513  replaceDataTests.push([node, 3, 1, '"foo"', node, 1, node, 3]);
    514 
    515  replaceDataTests.push([node, 376, 1, '""', node, 0, node, 1]);
    516  replaceDataTests.push([node, 0, 1, '""', node, 0, node, 0]);
    517  replaceDataTests.push([node, 1, 1, '""', node, 1, node, 1]);
    518  replaceDataTests.push([node, node + ".length", 1, '""', node, node + ".length", node, node + ".length"]);
    519  replaceDataTests.push([node, 1, 1, '""', node, 1, node, 3]);
    520  replaceDataTests.push([node, 2, 1, '""', node, 1, node, 3]);
    521  replaceDataTests.push([node, 3, 1, '""', node, 1, node, 3]);
    522 
    523  replaceDataTests.push([node, 376, 47, '"foo"', node, 0, node, 1]);
    524  replaceDataTests.push([node, 0, 47, '"foo"', node, 0, node, 0]);
    525  replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 1]);
    526  replaceDataTests.push([node, node + ".length", 47, '"foo"', node, node + ".length", node, node + ".length"]);
    527  replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 3]);
    528  replaceDataTests.push([node, 2, 47, '"foo"', node, 1, node, 3]);
    529  replaceDataTests.push([node, 3, 47, '"foo"', node, 1, node, 3]);
    530 
    531  replaceDataTests.push([node, 376, 47, '""', node, 0, node, 1]);
    532  replaceDataTests.push([node, 0, 47, '""', node, 0, node, 0]);
    533  replaceDataTests.push([node, 1, 47, '""', node, 1, node, 1]);
    534  replaceDataTests.push([node, node + ".length", 47, '""', node, node + ".length", node, node + ".length"]);
    535  replaceDataTests.push([node, 1, 47, '""', node, 1, node, 3]);
    536  replaceDataTests.push([node, 2, 47, '""', node, 1, node, 3]);
    537  replaceDataTests.push([node, 3, 47, '""', node, 1, node, 3]);
    538 }
    539 
    540 replaceDataTests.push(
    541  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 0],
    542  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 1],
    543  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 1, "paras[0]", 1],
    544  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    545  ["paras[0].firstChild", 2, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    546  ["paras[0].firstChild", 3, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    547  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    548  ["paras[0].firstChild", 2, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    549  ["paras[0].firstChild", 3, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    550 
    551  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 0],
    552  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 1],
    553  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 1, "paras[0]", 1],
    554  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    555  ["paras[0].firstChild", 2, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    556  ["paras[0].firstChild", 3, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    557  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    558  ["paras[0].firstChild", 2, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    559  ["paras[0].firstChild", 3, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    560 
    561  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 0],
    562  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 1],
    563  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 1, "paras[0]", 1],
    564  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    565  ["paras[0].firstChild", 2, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    566  ["paras[0].firstChild", 3, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
    567  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    568  ["paras[0].firstChild", 2, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
    569  ["paras[0].firstChild", 3, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
    570 );
    571 
    572 
    573 // There are lots of ways to set data, so we pass a callback that does the
    574 // actual setting.
    575 function testDataChange(node, attr, op, rval, startContainer, startOffset, endContainer, endOffset) {
    576  return testReplaceDataAlgorithm(node, 0, node.length, op == "=" ? rval : node[attr] + rval,
    577    function() {
    578      if (op == "=") {
    579        node[attr] = rval;
    580      } else if (op == "+=") {
    581        node[attr] += rval;
    582      } else {
    583        throw "Unknown op " + op;
    584      }
    585    },
    586    startContainer, startOffset, endContainer, endOffset);
    587 }
    588 
    589 var dataChangeTests = [];
    590 var dataChangeTestAttrs = ["data", "textContent", "nodeValue"];
    591 for (var i = 0; i < characterDataNodes.length; i++) {
    592  var node = characterDataNodes[i];
    593  var dataChangeTestRanges = [
    594    [node, 0, node, 0],
    595    [node, 0, node, 1],
    596    [node, 1, node, 1],
    597    [node, 0, node, node + ".length"],
    598    [node, 1, node, node + ".length"],
    599    [node, node + ".length", node, node + ".length"],
    600  ];
    601 
    602  for (var j = 0; j < dataChangeTestRanges.length; j++) {
    603    for (var k = 0; k < dataChangeTestAttrs.length; k++) {
    604      dataChangeTests.push([
    605        node,
    606        '"' + dataChangeTestAttrs[k] + '"',
    607        '"="',
    608        '""',
    609      ].concat(dataChangeTestRanges[j]));
    610 
    611      dataChangeTests.push([
    612        node,
    613        '"' + dataChangeTestAttrs[k] + '"',
    614        '"="',
    615        '"foo"',
    616      ].concat(dataChangeTestRanges[j]));
    617 
    618      dataChangeTests.push([
    619        node,
    620        '"' + dataChangeTestAttrs[k] + '"',
    621        '"="',
    622        node + "." + dataChangeTestAttrs[k],
    623      ].concat(dataChangeTestRanges[j]));
    624 
    625      dataChangeTests.push([
    626        node,
    627        '"' + dataChangeTestAttrs[k] + '"',
    628        '"+="',
    629        '""',
    630      ].concat(dataChangeTestRanges[j]));
    631 
    632      dataChangeTests.push([
    633        node,
    634        '"' + dataChangeTestAttrs[k] + '"',
    635        '"+="',
    636        '"foo"',
    637      ].concat(dataChangeTestRanges[j]));
    638 
    639      dataChangeTests.push([
    640        node,
    641        '"' + dataChangeTestAttrs[k] + '"',
    642        '"+="',
    643        node + "." + dataChangeTestAttrs[k]
    644      ].concat(dataChangeTestRanges[j]));
    645    }
    646  }
    647 }
    648 
    649 
    650 // Now we test node insertions and deletions, as opposed to just data changes.
    651 // To avoid loads of repetition, we define modifyForRemove() and
    652 // modifyForInsert().
    653 
    654 // If we were to remove removedNode from its parent, what would the boundary
    655 // point [node, offset] become?  Returns [new node, new offset].  Must be
    656 // called BEFORE the node is actually removed, so its parent is not null.  (If
    657 // the parent is null, it will do nothing.)
    658 function modifyForRemove(removedNode, point) {
    659  var oldParent = removedNode.parentNode;
    660  var oldIndex = indexOf(removedNode);
    661  if (!oldParent) {
    662    return point;
    663  }
    664 
    665  // "For each boundary point whose node is removed node or a descendant of
    666  // it, set the boundary point to (old parent, old index)."
    667  if (point[0] == removedNode || isDescendant(point[0], removedNode)) {
    668    return [oldParent, oldIndex];
    669  }
    670 
    671  // "For each boundary point whose node is old parent and whose offset is
    672  // greater than old index, subtract one from its offset."
    673  if (point[0] == oldParent && point[1] > oldIndex) {
    674    return [point[0], point[1] - 1];
    675  }
    676 
    677  return point;
    678 }
    679 
    680 // Update the given boundary point [node, offset] to account for the fact that
    681 // insertedNode was just inserted into its current position.  This must be
    682 // called AFTER insertedNode was already inserted.
    683 function modifyForInsert(insertedNode, point) {
    684  // "For each boundary point whose node is the new parent of the affected
    685  // node and whose offset is greater than the new index of the affected
    686  // node, add one to the boundary point's offset."
    687  if (point[0] == insertedNode.parentNode && point[1] > indexOf(insertedNode)) {
    688    return [point[0], point[1] + 1];
    689  }
    690 
    691  return point;
    692 }
    693 
    694 
    695 function testInsertBefore(newParent, affectedNode, refNode, startContainer, startOffset, endContainer, endOffset) {
    696  var expectedStart = [startContainer, startOffset];
    697  var expectedEnd = [endContainer, endOffset];
    698 
    699  expectedStart = modifyForRemove(affectedNode, expectedStart);
    700  expectedEnd = modifyForRemove(affectedNode, expectedEnd);
    701 
    702  try {
    703    newParent.insertBefore(affectedNode, refNode);
    704  } catch (e) {
    705    // For our purposes, assume that DOM Core is true -- i.e., ignore
    706    // mutation events and similar.
    707    return [startContainer, startOffset, endContainer, endOffset];
    708  }
    709 
    710  expectedStart = modifyForInsert(affectedNode, expectedStart);
    711  expectedEnd = modifyForInsert(affectedNode, expectedEnd);
    712 
    713  return expectedStart.concat(expectedEnd);
    714 }
    715 
    716 var insertBeforeTests = [
    717  // Moving a node to its current position
    718  ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0],
    719  ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1],
    720  ["testDiv", "paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1],
    721  ["testDiv", "paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2],
    722  ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1],
    723  ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2],
    724  ["testDiv", "paras[0]", "paras[1]", "testDiv", 2, "testDiv", 2],
    725 
    726  // Stuff that actually moves something.  Note that paras[0] and paras[1]
    727  // are both children of testDiv.
    728  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
    729  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    730  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
    731  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1],
    732  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2],
    733  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1],
    734  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2],
    735  ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 0],
    736  ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 1],
    737  ["paras[0]", "paras[1]", "null", "paras[0]", 1, "paras[0]", 1],
    738  ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 1],
    739  ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 2],
    740  ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 1],
    741  ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 2],
    742  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0],
    743  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1],
    744  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2],
    745  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1],
    746  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0],
    747  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1],
    748  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2],
    749  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1],
    750  ["foreignDoc", "detachedComment", "null", "foreignDoc", 0, "foreignDoc", 1],
    751  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
    752  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    753  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
    754 
    755  // Stuff that throws exceptions
    756  ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    757  ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    758  ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    759  ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    760  ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    761 ];
    762 
    763 
    764 function testReplaceChild(newParent, newChild, oldChild, startContainer, startOffset, endContainer, endOffset) {
    765  var expectedStart = [startContainer, startOffset];
    766  var expectedEnd = [endContainer, endOffset];
    767 
    768  expectedStart = modifyForRemove(oldChild, expectedStart);
    769  expectedEnd = modifyForRemove(oldChild, expectedEnd);
    770 
    771  if (newChild != oldChild) {
    772    // Don't do this twice, if they're the same!
    773    expectedStart = modifyForRemove(newChild, expectedStart);
    774    expectedEnd = modifyForRemove(newChild, expectedEnd);
    775  }
    776 
    777  try {
    778    newParent.replaceChild(newChild, oldChild);
    779  } catch (e) {
    780    return [startContainer, startOffset, endContainer, endOffset];
    781  }
    782 
    783  expectedStart = modifyForInsert(newChild, expectedStart);
    784  expectedEnd = modifyForInsert(newChild, expectedEnd);
    785 
    786  return expectedStart.concat(expectedEnd);
    787 }
    788 
    789 var replaceChildTests = [
    790  // Moving a node to its current position.  Doesn't match most browsers'
    791  // behavior, but we probably want to keep the spec the same anyway:
    792  // https://bugzilla.mozilla.org/show_bug.cgi?id=647603
    793  ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 0],
    794  ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1],
    795  ["testDiv", "paras[0]", "paras[0]", "paras[0]", 1, "paras[0]", 1],
    796  ["testDiv", "paras[0]", "paras[0]", "testDiv", 0, "testDiv", 2],
    797  ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 1],
    798  ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 2],
    799  ["testDiv", "paras[0]", "paras[0]", "testDiv", 2, "testDiv", 2],
    800 
    801  // Stuff that actually moves something.
    802  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
    803  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    804  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
    805  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1],
    806  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2],
    807  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1],
    808  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2],
    809  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0],
    810  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1],
    811  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2],
    812  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1],
    813  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0],
    814  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1],
    815  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2],
    816  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1],
    817  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
    818  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    819  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
    820 
    821  // Stuff that throws exceptions
    822  ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    823  ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    824  ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    825  ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    826  ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
    827 ];
    828 
    829 
    830 function testAppendChild(newParent, affectedNode, startContainer, startOffset, endContainer, endOffset) {
    831  var expectedStart = [startContainer, startOffset];
    832  var expectedEnd = [endContainer, endOffset];
    833 
    834  expectedStart = modifyForRemove(affectedNode, expectedStart);
    835  expectedEnd = modifyForRemove(affectedNode, expectedEnd);
    836 
    837  try {
    838    newParent.appendChild(affectedNode);
    839  } catch (e) {
    840    return [startContainer, startOffset, endContainer, endOffset];
    841  }
    842 
    843  // These two lines will actually never do anything, if you think about it,
    844  // but let's leave them in so correctness is more obvious.
    845  expectedStart = modifyForInsert(affectedNode, expectedStart);
    846  expectedEnd = modifyForInsert(affectedNode, expectedEnd);
    847 
    848  return expectedStart.concat(expectedEnd);
    849 }
    850 
    851 var appendChildTests = [
    852  // Moving a node to its current position
    853  ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 0],
    854  ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 1],
    855  ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 1, "testDiv.lastChild", 1],
    856  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length"],
    857  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length - 1"],
    858  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length"],
    859  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length - 1"],
    860  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length", "testDiv", "testDiv.childNodes.length"],
    861  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 0],
    862  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 1],
    863  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 1, "detachedDiv.lastChild", 1],
    864  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length"],
    865  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length - 1"],
    866  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length"],
    867  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length - 1"],
    868  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length", "detachedDiv", "detachedDiv.childNodes.length"],
    869 
    870  // Stuff that actually moves something
    871  ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0],
    872  ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1],
    873  ["paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1],
    874  ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 1],
    875  ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2],
    876  ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1],
    877  ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2],
    878  ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length"],
    879  ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length - 1"],
    880  ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length", "foreignDoc", "foreignDoc.childNodes.length"],
    881  ["foreignDoc", "detachedComment", "detachedComment", 0, "detachedComment", 5],
    882  ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 0],
    883  ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 1],
    884  ["paras[0]", "xmlTextNode", "paras[0]", 1, "paras[0]", 1],
    885 
    886  // Stuff that throws exceptions
    887  ["paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1],
    888  ["paras[0]", "testDiv", "paras[0]", 0, "paras[0]", 1],
    889  ["paras[0]", "document", "paras[0]", 0, "paras[0]", 1],
    890  ["paras[0]", "foreignDoc", "paras[0]", 0, "paras[0]", 1],
    891  ["paras[0]", "document.doctype", "paras[0]", 0, "paras[0]", 1],
    892 ];
    893 
    894 
    895 function testRemoveChild(affectedNode, startContainer, startOffset, endContainer, endOffset) {
    896  var expectedStart = [startContainer, startOffset];
    897  var expectedEnd = [endContainer, endOffset];
    898 
    899  expectedStart = modifyForRemove(affectedNode, expectedStart);
    900  expectedEnd = modifyForRemove(affectedNode, expectedEnd);
    901 
    902  // We don't test cases where the parent is wrong, so this should never
    903  // throw an exception.
    904  affectedNode.parentNode.removeChild(affectedNode);
    905 
    906  return expectedStart.concat(expectedEnd);
    907 }
    908 
    909 var removeChildTests = [
    910  ["paras[0]", "paras[0]", 0, "paras[0]", 0],
    911  ["paras[0]", "paras[0]", 0, "paras[0]", 1],
    912  ["paras[0]", "paras[0]", 1, "paras[0]", 1],
    913  ["paras[0]", "testDiv", 0, "testDiv", 0],
    914  ["paras[0]", "testDiv", 0, "testDiv", 1],
    915  ["paras[0]", "testDiv", 1, "testDiv", 1],
    916  ["paras[0]", "testDiv", 0, "testDiv", 2],
    917  ["paras[0]", "testDiv", 1, "testDiv", 2],
    918  ["paras[0]", "testDiv", 2, "testDiv", 2],
    919 
    920  ["foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", "foreignDoc.childNodes.length"],
    921 ];