<!doctype html>
<title>Range.cloneContents() tests</title>
<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
<meta name=timeout content=long>
<p>To debug test failures, add a query parameter "subtest" with the test id (like
"?subtest=5").  Only that test will be run.  Then you can look at the resulting
iframe in the DOM.
<div id=log></div>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script src=../common.js></script>
<script>
"use strict";

testDiv.parentNode.removeChild(testDiv);

var actualIframe = document.createElement("iframe");
actualIframe.style.display = "none";
document.body.appendChild(actualIframe);

var expectedIframe = document.createElement("iframe");
expectedIframe.style.display = "none";
document.body.appendChild(expectedIframe);

function myCloneContents(range) {
  // "Let frag be a new DocumentFragment whose ownerDocument is the same as
  // the ownerDocument of the context object's start node."
  var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE
    ? range.startContainer
    : range.startContainer.ownerDocument;
  var frag = ownerDoc.createDocumentFragment();

  // "If the context object's start and end are the same, abort this method,
  // returning frag."
  if (range.startContainer == range.endContainer
  && range.startOffset == range.endOffset) {
    return frag;
  }

  // "Let original start node, original start offset, original end node, and
  // original end offset be the context object's start and end nodes and
  // offsets, respectively."
  var originalStartNode = range.startContainer;
  var originalStartOffset = range.startOffset;
  var originalEndNode = range.endContainer;
  var originalEndOffset = range.endOffset;

  // "If original start node and original end node are the same, and they are
  // a Text, ProcessingInstruction, or Comment node:"
  if (range.startContainer == range.endContainer
  && (range.startContainer.nodeType == Node.TEXT_NODE
  || range.startContainer.nodeType == Node.COMMENT_NODE
  || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) {
    // "Let clone be the result of calling cloneNode(false) on original
    // start node."
    var clone = originalStartNode.cloneNode(false);

    // "Set the data of clone to the result of calling
    // substringData(original start offset, original end offset − original
    // start offset) on original start node."
    clone.data = originalStartNode.substringData(originalStartOffset,
      originalEndOffset - originalStartOffset);

    // "Append clone as the last child of frag."
    frag.appendChild(clone);

    // "Abort this method, returning frag."
    return frag;
  }

  // "Let common ancestor equal original start node."
  var commonAncestor = originalStartNode;

  // "While common ancestor is not an ancestor container of original end
  // node, set common ancestor to its own parent."
  while (!isAncestorContainer(commonAncestor, originalEndNode)) {
    commonAncestor = commonAncestor.parentNode;
  }

  // "If original start node is an ancestor container of original end node,
  // let first partially contained child be null."
  var firstPartiallyContainedChild;
  if (isAncestorContainer(originalStartNode, originalEndNode)) {
    firstPartiallyContainedChild = null;
  // "Otherwise, let first partially contained child be the first child of
  // common ancestor that is partially contained in the context object."
  } else {
    for (var i = 0; i < commonAncestor.childNodes.length; i++) {
      if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
        firstPartiallyContainedChild = commonAncestor.childNodes[i];
        break;
      }
    }
    if (!firstPartiallyContainedChild) {
      throw "Spec bug: no first partially contained child!";
    }
  }

  // "If original end node is an ancestor container of original start node,
  // let last partially contained child be null."
  var lastPartiallyContainedChild;
  if (isAncestorContainer(originalEndNode, originalStartNode)) {
    lastPartiallyContainedChild = null;
  // "Otherwise, let last partially contained child be the last child of
  // common ancestor that is partially contained in the context object."
  } else {
    for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
      if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
        lastPartiallyContainedChild = commonAncestor.childNodes[i];
        break;
      }
    }
    if (!lastPartiallyContainedChild) {
      throw "Spec bug: no last partially contained child!";
    }
  }

  // "Let contained children be a list of all children of common ancestor
  // that are contained in the context object, in tree order."
  //
  // "If any member of contained children is a DocumentType, raise a
  // HIERARCHY_REQUEST_ERR exception and abort these steps."
  var containedChildren = [];
  for (var i = 0; i < commonAncestor.childNodes.length; i++) {
    if (isContained(commonAncestor.childNodes[i], range)) {
      if (commonAncestor.childNodes[i].nodeType
      == Node.DOCUMENT_TYPE_NODE) {
        return "HIERARCHY_REQUEST_ERR";
      }
      containedChildren.push(commonAncestor.childNodes[i]);
    }
  }

  // "If first partially contained child is a Text, ProcessingInstruction, or Comment node:"
  if (firstPartiallyContainedChild
  && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
  || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE
  || firstPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) {
    // "Let clone be the result of calling cloneNode(false) on original
    // start node."
    var clone = originalStartNode.cloneNode(false);

    // "Set the data of clone to the result of calling substringData() on
    // original start node, with original start offset as the first
    // argument and (length of original start node − original start offset)
    // as the second."
    clone.data = originalStartNode.substringData(originalStartOffset,
      nodeLength(originalStartNode) - originalStartOffset);

    // "Append clone as the last child of frag."
    frag.appendChild(clone);
  // "Otherwise, if first partially contained child is not null:"
  } else if (firstPartiallyContainedChild) {
    // "Let clone be the result of calling cloneNode(false) on first
    // partially contained child."
    var clone = firstPartiallyContainedChild.cloneNode(false);

    // "Append clone as the last child of frag."
    frag.appendChild(clone);

    // "Let subrange be a new Range whose start is (original start node,
    // original start offset) and whose end is (first partially contained
    // child, length of first partially contained child)."
    var subrange = ownerDoc.createRange();
    subrange.setStart(originalStartNode, originalStartOffset);
    subrange.setEnd(firstPartiallyContainedChild,
      nodeLength(firstPartiallyContainedChild));

    // "Let subfrag be the result of calling cloneContents() on
    // subrange."
    var subfrag = myCloneContents(subrange);

    // "For each child of subfrag, in order, append that child to clone as
    // its last child."
    for (var i = 0; i < subfrag.childNodes.length; i++) {
      clone.appendChild(subfrag.childNodes[i]);
    }
  }

  // "For each contained child in contained children:"
  for (var i = 0; i < containedChildren.length; i++) {
    // "Let clone be the result of calling cloneNode(true) of contained
    // child."
    var clone = containedChildren[i].cloneNode(true);

    // "Append clone as the last child of frag."
    frag.appendChild(clone);
  }

  // "If last partially contained child is a Text, ProcessingInstruction, or Comment node:"
  if (lastPartiallyContainedChild
  && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
  || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE
  || lastPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) {
    // "Let clone be the result of calling cloneNode(false) on original
    // end node."
    var clone = originalEndNode.cloneNode(false);

    // "Set the data of clone to the result of calling substringData(0,
    // original end offset) on original end node."
    clone.data = originalEndNode.substringData(0, originalEndOffset);

    // "Append clone as the last child of frag."
    frag.appendChild(clone);
  // "Otherwise, if last partially contained child is not null:"
  } else if (lastPartiallyContainedChild) {
    // "Let clone be the result of calling cloneNode(false) on last
    // partially contained child."
    var clone = lastPartiallyContainedChild.cloneNode(false);

    // "Append clone as the last child of frag."
    frag.appendChild(clone);

    // "Let subrange be a new Range whose start is (last partially
    // contained child, 0) and whose end is (original end node, original
    // end offset)."
    var subrange = ownerDoc.createRange();
    subrange.setStart(lastPartiallyContainedChild, 0);
    subrange.setEnd(originalEndNode, originalEndOffset);

    // "Let subfrag be the result of calling cloneContents() on
    // subrange."
    var subfrag = myCloneContents(subrange);

    // "For each child of subfrag, in order, append that child to clone as
    // its last child."
    for (var i = 0; i < subfrag.childNodes.length; i++) {
      clone.appendChild(subfrag.childNodes[i]);
    }
  }

  // "Return frag."
  return frag;
}

function restoreIframe(iframe, i) {
  // Most of this function is designed to work around the fact that Opera
  // doesn't let you add a doctype to a document that no longer has one, in
  // any way I can figure out.  I eventually compromised on something that
  // will still let Opera pass most tests that don't actually involve
  // doctypes.
  while (iframe.contentDocument.firstChild
  && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
    iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
  }

  while (iframe.contentDocument.lastChild
  && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
    iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
  }

  if (!iframe.contentDocument.firstChild) {
    // This will throw an exception in Opera if we reach here, which is why
    // I try to avoid it.  It will never happen in a browser that obeys the
    // spec, so it's really just insurance.  I don't think it actually gets
    // hit by anything.
    iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
  }
  iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
  iframe.contentWindow.setupRangeTests();
  iframe.contentWindow.testRangeInput = testRanges[i];
  iframe.contentWindow.run();
}

function testCloneContents(i) {
  restoreIframe(actualIframe, i);
  restoreIframe(expectedIframe, i);

  var actualRange = actualIframe.contentWindow.testRange;
  var expectedRange = expectedIframe.contentWindow.testRange;
  var actualFrag, expectedFrag;
  var actualRoots, expectedRoots;

  domTests[i].step(function() {
    assert_equals(actualIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for actual cloneContents()");
    assert_equals(expectedIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for simulated cloneContents()");
    assert_equals(typeof actualRange, "object",
      "typeof Range produced in actual iframe");
    assert_equals(typeof expectedRange, "object",
      "typeof Range produced in expected iframe");

    // NOTE: We could just assume that cloneContents() doesn't change
    // anything.  That would simplify these tests, taken in isolation.  But
    // once we've already set up the whole apparatus for extractContents()
    // and deleteContents(), we just reuse it here, on the theory of "why
    // not test some more stuff if it's easy to do".
    //
    // Just to be pedantic, we'll test not only that the tree we're
    // modifying is the same in expected vs. actual, but also that all the
    // nodes originally in it were the same.  Typically some nodes will
    // become detached when the algorithm is run, but they still exist and
    // references can still be kept to them, so they should also remain the
    // same.
    //
    // We initialize the list to all nodes, and later on remove all the
    // ones which still have parents, since the parents will presumably be
    // tested for isEqualNode() and checking the children would be
    // redundant.
    var actualAllNodes = [];
    var node = furthestAncestor(actualRange.startContainer);
    do {
      actualAllNodes.push(node);
    } while (node = nextNode(node));

    var expectedAllNodes = [];
    var node = furthestAncestor(expectedRange.startContainer);
    do {
      expectedAllNodes.push(node);
    } while (node = nextNode(node));

    expectedFrag = myCloneContents(expectedRange);
    if (typeof expectedFrag == "string") {
      assert_throws_dom(
        expectedFrag,
        actualIframe.contentWindow.DOMException,
        function() {
          actualRange.cloneContents();
        }
      );
    } else {
      actualFrag = actualRange.cloneContents();
    }

    actualRoots = [];
    for (var j = 0; j < actualAllNodes.length; j++) {
      if (!actualAllNodes[j].parentNode) {
        actualRoots.push(actualAllNodes[j]);
      }
    }

    expectedRoots = [];
    for (var j = 0; j < expectedAllNodes.length; j++) {
      if (!expectedAllNodes[j].parentNode) {
        expectedRoots.push(expectedAllNodes[j]);
      }
    }

    for (var j = 0; j < actualRoots.length; j++) {
      assertNodesEqual(actualRoots[j], expectedRoots[j], j ? "detached node #" + j : "tree root");

      if (j == 0) {
        // Clearly something is wrong if the node lists are different
        // lengths.  We want to report this only after we've already
        // checked the main tree for equality, though, so it doesn't
        // mask more interesting errors.
        assert_equals(actualRoots.length, expectedRoots.length,
          "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)");
      }
    }
  });
  domTests[i].done();

  positionTests[i].step(function() {
    assert_equals(actualIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for actual cloneContents()");
    assert_equals(expectedIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for simulated cloneContents()");
    assert_equals(typeof actualRange, "object",
      "typeof Range produced in actual iframe");
    assert_equals(typeof expectedRange, "object",
      "typeof Range produced in expected iframe");

    assert_true(actualRoots[0].isEqualNode(expectedRoots[0]),
      "The resulting DOMs were not equal, so comparing positions makes no sense");

    if (typeof expectedFrag == "string") {
      // It's no longer true that, e.g., startContainer and endContainer
      // must always be the same
      return;
    }

    assert_equals(actualRange.startOffset, expectedRange.startOffset,
      "Unexpected startOffset after cloneContents()");
    // How do we decide that the two nodes are equal, since they're in
    // different trees?  Since the DOMs are the same, it's enough to check
    // that the index in the parent is the same all the way up the tree.
    // But we can first cheat by just checking they're actually equal.
    assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
      "Unexpected startContainer after cloneContents(), expected " +
      expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
      actualRange.startContainer.nodeName.toLowerCase());
    var currentActual = actualRange.startContainer;
    var currentExpected = expectedRange.startContainer;
    var actual = "";
    var expected = "";
    while (currentActual && currentExpected) {
      actual = indexOf(currentActual) + "-" + actual;
      expected = indexOf(currentExpected) + "-" + expected;

      currentActual = currentActual.parentNode;
      currentExpected = currentExpected.parentNode;
    }
    actual = actual.substr(0, actual.length - 1);
    expected = expected.substr(0, expected.length - 1);
    assert_equals(actual, expected,
      "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");
  });
  positionTests[i].done();

  fragTests[i].step(function() {
    assert_equals(actualIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for actual cloneContents()");
    assert_equals(expectedIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for simulated cloneContents()");
    assert_equals(typeof actualRange, "object",
      "typeof Range produced in actual iframe");
    assert_equals(typeof expectedRange, "object",
      "typeof Range produced in expected iframe");

    if (typeof expectedFrag == "string") {
      // Comparing makes no sense
      return;
    }
    assertNodesEqual(actualFrag, expectedFrag,
      "returned fragment");
  });
  fragTests[i].done();
}

// First test a Range that has the no-op detach() called on it, synchronously
test(function() {
  var range = document.createRange();
  range.detach();
  assert_array_equals(range.cloneContents().childNodes, []);
}, "Range.detach()");

var iStart = 0;
var iStop = testRanges.length;

if (/subtest=[0-9]+/.test(location.search)) {
  var matches = /subtest=([0-9]+)/.exec(location.search);
  iStart = Number(matches[1]);
  iStop = Number(matches[1]) + 1;
}

var domTests = [];
var positionTests = [];
var fragTests = [];

for (var i = iStart; i < iStop; i++) {
  domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i]);
  positionTests[i] = async_test("Resulting cursor position for range " + i + " " + testRanges[i]);
  fragTests[i] = async_test("Returned fragment for range " + i + " " + testRanges[i]);
}

var referenceDoc = document.implementation.createHTMLDocument("");
referenceDoc.removeChild(referenceDoc.documentElement);

actualIframe.onload = function() {
  expectedIframe.onload = function() {
    for (var i = iStart; i < iStop; i++) {
      testCloneContents(i);
    }
  }
  expectedIframe.src = "Range-test-iframe.html";
  referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
}
actualIframe.src = "Range-test-iframe.html";
</script>
