tor-browser

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

Range-cloneContents.html (18714B)


      1 <!doctype html>
      2 <title>Range.cloneContents() tests</title>
      3 <link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
      4 <meta name=timeout content=long>
      5 <p>To debug test failures, add a query parameter "subtest" with the test id (like
      6 "?subtest=5").  Only that test will be run.  Then you can look at the resulting
      7 iframe in the DOM.
      8 <div id=log></div>
      9 <script src=/resources/testharness.js></script>
     10 <script src=/resources/testharnessreport.js></script>
     11 <script src=../common.js></script>
     12 <script>
     13 "use strict";
     14 
     15 testDiv.parentNode.removeChild(testDiv);
     16 
     17 var actualIframe = document.createElement("iframe");
     18 actualIframe.style.display = "none";
     19 document.body.appendChild(actualIframe);
     20 
     21 var expectedIframe = document.createElement("iframe");
     22 expectedIframe.style.display = "none";
     23 document.body.appendChild(expectedIframe);
     24 
     25 function myCloneContents(range) {
     26  // "Let frag be a new DocumentFragment whose ownerDocument is the same as
     27  // the ownerDocument of the context object's start node."
     28  var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE
     29    ? range.startContainer
     30    : range.startContainer.ownerDocument;
     31  var frag = ownerDoc.createDocumentFragment();
     32 
     33  // "If the context object's start and end are the same, abort this method,
     34  // returning frag."
     35  if (range.startContainer == range.endContainer
     36  && range.startOffset == range.endOffset) {
     37    return frag;
     38  }
     39 
     40  // "Let original start node, original start offset, original end node, and
     41  // original end offset be the context object's start and end nodes and
     42  // offsets, respectively."
     43  var originalStartNode = range.startContainer;
     44  var originalStartOffset = range.startOffset;
     45  var originalEndNode = range.endContainer;
     46  var originalEndOffset = range.endOffset;
     47 
     48  // "If original start node and original end node are the same, and they are
     49  // a Text, ProcessingInstruction, or Comment node:"
     50  if (range.startContainer == range.endContainer
     51  && (isText(range.startContainer)
     52  || range.startContainer.nodeType == Node.COMMENT_NODE
     53  || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) {
     54    // "Let clone be the result of calling cloneNode(false) on original
     55    // start node."
     56    var clone = originalStartNode.cloneNode(false);
     57 
     58    // "Set the data of clone to the result of calling
     59    // substringData(original start offset, original end offset − original
     60    // start offset) on original start node."
     61    clone.data = originalStartNode.substringData(originalStartOffset,
     62      originalEndOffset - originalStartOffset);
     63 
     64    // "Append clone as the last child of frag."
     65    frag.appendChild(clone);
     66 
     67    // "Abort this method, returning frag."
     68    return frag;
     69  }
     70 
     71  // "Let common ancestor equal original start node."
     72  var commonAncestor = originalStartNode;
     73 
     74  // "While common ancestor is not an ancestor container of original end
     75  // node, set common ancestor to its own parent."
     76  while (!isAncestorContainer(commonAncestor, originalEndNode)) {
     77    commonAncestor = commonAncestor.parentNode;
     78  }
     79 
     80  // "If original start node is an ancestor container of original end node,
     81  // let first partially contained child be null."
     82  var firstPartiallyContainedChild;
     83  if (isAncestorContainer(originalStartNode, originalEndNode)) {
     84    firstPartiallyContainedChild = null;
     85  // "Otherwise, let first partially contained child be the first child of
     86  // common ancestor that is partially contained in the context object."
     87  } else {
     88    for (var i = 0; i < commonAncestor.childNodes.length; i++) {
     89      if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
     90        firstPartiallyContainedChild = commonAncestor.childNodes[i];
     91        break;
     92      }
     93    }
     94    if (!firstPartiallyContainedChild) {
     95      throw "Spec bug: no first partially contained child!";
     96    }
     97  }
     98 
     99  // "If original end node is an ancestor container of original start node,
    100  // let last partially contained child be null."
    101  var lastPartiallyContainedChild;
    102  if (isAncestorContainer(originalEndNode, originalStartNode)) {
    103    lastPartiallyContainedChild = null;
    104  // "Otherwise, let last partially contained child be the last child of
    105  // common ancestor that is partially contained in the context object."
    106  } else {
    107    for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
    108      if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
    109        lastPartiallyContainedChild = commonAncestor.childNodes[i];
    110        break;
    111      }
    112    }
    113    if (!lastPartiallyContainedChild) {
    114      throw "Spec bug: no last partially contained child!";
    115    }
    116  }
    117 
    118  // "Let contained children be a list of all children of common ancestor
    119  // that are contained in the context object, in tree order."
    120  //
    121  // "If any member of contained children is a DocumentType, raise a
    122  // HIERARCHY_REQUEST_ERR exception and abort these steps."
    123  var containedChildren = [];
    124  for (var i = 0; i < commonAncestor.childNodes.length; i++) {
    125    if (isContained(commonAncestor.childNodes[i], range)) {
    126      if (commonAncestor.childNodes[i].nodeType
    127      == Node.DOCUMENT_TYPE_NODE) {
    128        return "HIERARCHY_REQUEST_ERR";
    129      }
    130      containedChildren.push(commonAncestor.childNodes[i]);
    131    }
    132  }
    133 
    134  // "If first partially contained child is a Text, ProcessingInstruction, or Comment node:"
    135  if (firstPartiallyContainedChild
    136  && (isText(firstPartiallyContainedChild)
    137  || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE
    138  || firstPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) {
    139    // "Let clone be the result of calling cloneNode(false) on original
    140    // start node."
    141    var clone = originalStartNode.cloneNode(false);
    142 
    143    // "Set the data of clone to the result of calling substringData() on
    144    // original start node, with original start offset as the first
    145    // argument and (length of original start node − original start offset)
    146    // as the second."
    147    clone.data = originalStartNode.substringData(originalStartOffset,
    148      nodeLength(originalStartNode) - originalStartOffset);
    149 
    150    // "Append clone as the last child of frag."
    151    frag.appendChild(clone);
    152  // "Otherwise, if first partially contained child is not null:"
    153  } else if (firstPartiallyContainedChild) {
    154    // "Let clone be the result of calling cloneNode(false) on first
    155    // partially contained child."
    156    var clone = firstPartiallyContainedChild.cloneNode(false);
    157 
    158    // "Append clone as the last child of frag."
    159    frag.appendChild(clone);
    160 
    161    // "Let subrange be a new Range whose start is (original start node,
    162    // original start offset) and whose end is (first partially contained
    163    // child, length of first partially contained child)."
    164    var subrange = ownerDoc.createRange();
    165    subrange.setStart(originalStartNode, originalStartOffset);
    166    subrange.setEnd(firstPartiallyContainedChild,
    167      nodeLength(firstPartiallyContainedChild));
    168 
    169    // "Let subfrag be the result of calling cloneContents() on
    170    // subrange."
    171    var subfrag = myCloneContents(subrange);
    172 
    173    // "For each child of subfrag, in order, append that child to clone as
    174    // its last child."
    175    for (var i = 0; i < subfrag.childNodes.length; i++) {
    176      clone.appendChild(subfrag.childNodes[i]);
    177    }
    178  }
    179 
    180  // "For each contained child in contained children:"
    181  for (var i = 0; i < containedChildren.length; i++) {
    182    // "Let clone be the result of calling cloneNode(true) of contained
    183    // child."
    184    var clone = containedChildren[i].cloneNode(true);
    185 
    186    // "Append clone as the last child of frag."
    187    frag.appendChild(clone);
    188  }
    189 
    190  // "If last partially contained child is a Text, ProcessingInstruction, or Comment node:"
    191  if (lastPartiallyContainedChild
    192  && (isText(lastPartiallyContainedChild)
    193  || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE
    194  || lastPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) {
    195    // "Let clone be the result of calling cloneNode(false) on original
    196    // end node."
    197    var clone = originalEndNode.cloneNode(false);
    198 
    199    // "Set the data of clone to the result of calling substringData(0,
    200    // original end offset) on original end node."
    201    clone.data = originalEndNode.substringData(0, originalEndOffset);
    202 
    203    // "Append clone as the last child of frag."
    204    frag.appendChild(clone);
    205  // "Otherwise, if last partially contained child is not null:"
    206  } else if (lastPartiallyContainedChild) {
    207    // "Let clone be the result of calling cloneNode(false) on last
    208    // partially contained child."
    209    var clone = lastPartiallyContainedChild.cloneNode(false);
    210 
    211    // "Append clone as the last child of frag."
    212    frag.appendChild(clone);
    213 
    214    // "Let subrange be a new Range whose start is (last partially
    215    // contained child, 0) and whose end is (original end node, original
    216    // end offset)."
    217    var subrange = ownerDoc.createRange();
    218    subrange.setStart(lastPartiallyContainedChild, 0);
    219    subrange.setEnd(originalEndNode, originalEndOffset);
    220 
    221    // "Let subfrag be the result of calling cloneContents() on
    222    // subrange."
    223    var subfrag = myCloneContents(subrange);
    224 
    225    // "For each child of subfrag, in order, append that child to clone as
    226    // its last child."
    227    for (var i = 0; i < subfrag.childNodes.length; i++) {
    228      clone.appendChild(subfrag.childNodes[i]);
    229    }
    230  }
    231 
    232  // "Return frag."
    233  return frag;
    234 }
    235 
    236 function restoreIframe(iframe, i) {
    237  // Most of this function is designed to work around the fact that Opera
    238  // doesn't let you add a doctype to a document that no longer has one, in
    239  // any way I can figure out.  I eventually compromised on something that
    240  // will still let Opera pass most tests that don't actually involve
    241  // doctypes.
    242  while (iframe.contentDocument.firstChild
    243  && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
    244    iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
    245  }
    246 
    247  while (iframe.contentDocument.lastChild
    248  && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
    249    iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
    250  }
    251 
    252  if (!iframe.contentDocument.firstChild) {
    253    // This will throw an exception in Opera if we reach here, which is why
    254    // I try to avoid it.  It will never happen in a browser that obeys the
    255    // spec, so it's really just insurance.  I don't think it actually gets
    256    // hit by anything.
    257    iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
    258  }
    259  iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
    260  iframe.contentWindow.setupRangeTests();
    261  iframe.contentWindow.testRangeInput = testRanges[i];
    262  iframe.contentWindow.run();
    263 }
    264 
    265 function testCloneContents(i) {
    266  restoreIframe(actualIframe, i);
    267  restoreIframe(expectedIframe, i);
    268 
    269  var actualRange = actualIframe.contentWindow.testRange;
    270  var expectedRange = expectedIframe.contentWindow.testRange;
    271  var actualFrag, expectedFrag;
    272  var actualRoots, expectedRoots;
    273 
    274  domTests[i].step(function() {
    275    assert_equals(actualIframe.contentWindow.unexpectedException, null,
    276      "Unexpected exception thrown when setting up Range for actual cloneContents()");
    277    assert_equals(expectedIframe.contentWindow.unexpectedException, null,
    278      "Unexpected exception thrown when setting up Range for simulated cloneContents()");
    279    assert_equals(typeof actualRange, "object",
    280      "typeof Range produced in actual iframe");
    281    assert_equals(typeof expectedRange, "object",
    282      "typeof Range produced in expected iframe");
    283 
    284    // NOTE: We could just assume that cloneContents() doesn't change
    285    // anything.  That would simplify these tests, taken in isolation.  But
    286    // once we've already set up the whole apparatus for extractContents()
    287    // and deleteContents(), we just reuse it here, on the theory of "why
    288    // not test some more stuff if it's easy to do".
    289    //
    290    // Just to be pedantic, we'll test not only that the tree we're
    291    // modifying is the same in expected vs. actual, but also that all the
    292    // nodes originally in it were the same.  Typically some nodes will
    293    // become detached when the algorithm is run, but they still exist and
    294    // references can still be kept to them, so they should also remain the
    295    // same.
    296    //
    297    // We initialize the list to all nodes, and later on remove all the
    298    // ones which still have parents, since the parents will presumably be
    299    // tested for isEqualNode() and checking the children would be
    300    // redundant.
    301    var actualAllNodes = [];
    302    var node = furthestAncestor(actualRange.startContainer);
    303    do {
    304      actualAllNodes.push(node);
    305    } while (node = nextNode(node));
    306 
    307    var expectedAllNodes = [];
    308    var node = furthestAncestor(expectedRange.startContainer);
    309    do {
    310      expectedAllNodes.push(node);
    311    } while (node = nextNode(node));
    312 
    313    expectedFrag = myCloneContents(expectedRange);
    314    if (typeof expectedFrag == "string") {
    315      assert_throws_dom(
    316        expectedFrag,
    317        actualIframe.contentWindow.DOMException,
    318        function() {
    319          actualRange.cloneContents();
    320        }
    321      );
    322    } else {
    323      actualFrag = actualRange.cloneContents();
    324    }
    325 
    326    actualRoots = [];
    327    for (var j = 0; j < actualAllNodes.length; j++) {
    328      if (!actualAllNodes[j].parentNode) {
    329        actualRoots.push(actualAllNodes[j]);
    330      }
    331    }
    332 
    333    expectedRoots = [];
    334    for (var j = 0; j < expectedAllNodes.length; j++) {
    335      if (!expectedAllNodes[j].parentNode) {
    336        expectedRoots.push(expectedAllNodes[j]);
    337      }
    338    }
    339 
    340    for (var j = 0; j < actualRoots.length; j++) {
    341      assertNodesEqual(actualRoots[j], expectedRoots[j], j ? "detached node #" + j : "tree root");
    342 
    343      if (j == 0) {
    344        // Clearly something is wrong if the node lists are different
    345        // lengths.  We want to report this only after we've already
    346        // checked the main tree for equality, though, so it doesn't
    347        // mask more interesting errors.
    348        assert_equals(actualRoots.length, expectedRoots.length,
    349          "Actual and expected DOMs were broken up into a different number of pieces by cloneContents() (this probably means you created or detached nodes when you weren't supposed to)");
    350      }
    351    }
    352  });
    353  domTests[i].done();
    354 
    355  positionTests[i].step(function() {
    356    assert_equals(actualIframe.contentWindow.unexpectedException, null,
    357      "Unexpected exception thrown when setting up Range for actual cloneContents()");
    358    assert_equals(expectedIframe.contentWindow.unexpectedException, null,
    359      "Unexpected exception thrown when setting up Range for simulated cloneContents()");
    360    assert_equals(typeof actualRange, "object",
    361      "typeof Range produced in actual iframe");
    362    assert_equals(typeof expectedRange, "object",
    363      "typeof Range produced in expected iframe");
    364 
    365    assert_true(actualRoots[0].isEqualNode(expectedRoots[0]),
    366      "The resulting DOMs were not equal, so comparing positions makes no sense");
    367 
    368    if (typeof expectedFrag == "string") {
    369      // It's no longer true that, e.g., startContainer and endContainer
    370      // must always be the same
    371      return;
    372    }
    373 
    374    assert_equals(actualRange.startOffset, expectedRange.startOffset,
    375      "Unexpected startOffset after cloneContents()");
    376    // How do we decide that the two nodes are equal, since they're in
    377    // different trees?  Since the DOMs are the same, it's enough to check
    378    // that the index in the parent is the same all the way up the tree.
    379    // But we can first cheat by just checking they're actually equal.
    380    assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
    381      "Unexpected startContainer after cloneContents(), expected " +
    382      expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
    383      actualRange.startContainer.nodeName.toLowerCase());
    384    var currentActual = actualRange.startContainer;
    385    var currentExpected = expectedRange.startContainer;
    386    var actual = "";
    387    var expected = "";
    388    while (currentActual && currentExpected) {
    389      actual = indexOf(currentActual) + "-" + actual;
    390      expected = indexOf(currentExpected) + "-" + expected;
    391 
    392      currentActual = currentActual.parentNode;
    393      currentExpected = currentExpected.parentNode;
    394    }
    395    actual = actual.substr(0, actual.length - 1);
    396    expected = expected.substr(0, expected.length - 1);
    397    assert_equals(actual, expected,
    398      "startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened");
    399  });
    400  positionTests[i].done();
    401 
    402  fragTests[i].step(function() {
    403    assert_equals(actualIframe.contentWindow.unexpectedException, null,
    404      "Unexpected exception thrown when setting up Range for actual cloneContents()");
    405    assert_equals(expectedIframe.contentWindow.unexpectedException, null,
    406      "Unexpected exception thrown when setting up Range for simulated cloneContents()");
    407    assert_equals(typeof actualRange, "object",
    408      "typeof Range produced in actual iframe");
    409    assert_equals(typeof expectedRange, "object",
    410      "typeof Range produced in expected iframe");
    411 
    412    if (typeof expectedFrag == "string") {
    413      // Comparing makes no sense
    414      return;
    415    }
    416    assertNodesEqual(actualFrag, expectedFrag,
    417      "returned fragment");
    418  });
    419  fragTests[i].done();
    420 }
    421 
    422 // First test a Range that has the no-op detach() called on it, synchronously
    423 test(function() {
    424  var range = document.createRange();
    425  range.detach();
    426  assert_array_equals(range.cloneContents().childNodes, []);
    427 }, "Range.detach()");
    428 
    429 var iStart = 0;
    430 var iStop = testRanges.length;
    431 
    432 if (/subtest=[0-9]+/.test(location.search)) {
    433  var matches = /subtest=([0-9]+)/.exec(location.search);
    434  iStart = Number(matches[1]);
    435  iStop = Number(matches[1]) + 1;
    436 }
    437 
    438 var domTests = [];
    439 var positionTests = [];
    440 var fragTests = [];
    441 
    442 for (var i = iStart; i < iStop; i++) {
    443  domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i]);
    444  positionTests[i] = async_test("Resulting cursor position for range " + i + " " + testRanges[i]);
    445  fragTests[i] = async_test("Returned fragment for range " + i + " " + testRanges[i]);
    446 }
    447 
    448 var referenceDoc = document.implementation.createHTMLDocument("");
    449 referenceDoc.removeChild(referenceDoc.documentElement);
    450 
    451 actualIframe.onload = function() {
    452  expectedIframe.onload = function() {
    453    for (var i = iStart; i < iStop; i++) {
    454      testCloneContents(i);
    455    }
    456  }
    457  expectedIframe.src = "Range-test-iframe.html";
    458  referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
    459 }
    460 actualIframe.src = "Range-test-iframe.html";
    461 </script>