<!doctype html>
<meta charset=utf-8>
<title>Range.surroundContents() 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,16").  Only that test will be run.  Then you can look at the resulting
iframes 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);

function mySurroundContents(range, newParent) {
  try {
    // "If a non-Text node is partially contained in the context object,
    // throw a "InvalidStateError" exception and terminate these steps."
    var node = range.commonAncestorContainer;
    var stop = nextNodeDescendants(node);
    for (; node != stop; node = nextNode(node)) {
      if (node.nodeType != Node.TEXT_NODE
      && isPartiallyContained(node, range)) {
        return "INVALID_STATE_ERR";
      }
    }

    // "If newParent is a Document, DocumentType, or DocumentFragment node,
    // throw an "InvalidNodeTypeError" exception and terminate these
    // steps."
    if (newParent.nodeType == Node.DOCUMENT_NODE
    || newParent.nodeType == Node.DOCUMENT_TYPE_NODE
    || newParent.nodeType == Node.DOCUMENT_FRAGMENT_NODE) {
      return "INVALID_NODE_TYPE_ERR";
    }

    // "Call extractContents() on the context object, and let fragment be
    // the result."
    var fragment = myExtractContents(range);
    if (typeof fragment == "string") {
      return fragment;
    }

    // "While newParent has children, remove its first child."
    while (newParent.childNodes.length) {
      newParent.removeChild(newParent.firstChild);
    }

    // "Call insertNode(newParent) on the context object."
    var ret = myInsertNode(range, newParent);
    if (typeof ret == "string") {
      return ret;
    }

    // "Call appendChild(fragment) on newParent."
    newParent.appendChild(fragment);

    // "Call selectNode(newParent) on the context object."
    //
    // We just reimplement this in-place.
    if (!newParent.parentNode) {
      return "INVALID_NODE_TYPE_ERR";
    }
    var index = indexOf(newParent);
    range.setStart(newParent.parentNode, index);
    range.setEnd(newParent.parentNode, index + 1);
  } catch (e) {
    return getDomExceptionName(e);
  }
}

function restoreIframe(iframe, i, j) {
  // 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 = testRangesShort[i];
  iframe.contentWindow.testNodeInput = testNodesShort[j];
  iframe.contentWindow.run();
}

function testSurroundContents(i, j) {
  var actualRange;
  var expectedRange;
  var actualNode;
  var expectedNode;
  var actualRoots = [];
  var expectedRoots = [];

  domTests[i][j].step(function() {
    restoreIframe(actualIframe, i, j);
    restoreIframe(expectedIframe, i, j);

    actualRange = actualIframe.contentWindow.testRange;
    expectedRange = expectedIframe.contentWindow.testRange;
    actualNode = actualIframe.contentWindow.testNode;
    expectedNode = expectedIframe.contentWindow.testNode;

    assert_equals(actualIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for actual surroundContents()");
    assert_equals(expectedIframe.contentWindow.unexpectedException, null,
      "Unexpected exception thrown when setting up Range for simulated surroundContents()");
    assert_equals(typeof actualRange, "object",
      "typeof Range produced in actual iframe");
    assert_not_equals(actualRange, null,
      "Range produced in actual iframe was null");
    assert_equals(typeof expectedRange, "object",
      "typeof Range produced in expected iframe");
    assert_not_equals(expectedRange, null,
      "Range produced in expected iframe was null");
    assert_equals(typeof actualNode, "object",
      "typeof Node produced in actual iframe");
    assert_not_equals(actualNode, null,
      "Node produced in actual iframe was null");
    assert_equals(typeof expectedNode, "object",
      "typeof Node produced in expected iframe");
    assert_not_equals(expectedNode, null,
      "Node produced in expected iframe was null");

    // We want to test that the trees containing the ranges are equal, and
    // also the trees containing the moved nodes.  These might not be the
    // same, if we're inserting a node from a detached tree or a different
    // document.
    actualRoots.push(furthestAncestor(actualRange.startContainer));
    expectedRoots.push(furthestAncestor(expectedRange.startContainer));

    if (furthestAncestor(actualNode) != actualRoots[0]) {
      actualRoots.push(furthestAncestor(actualNode));
    }
    if (furthestAncestor(expectedNode) != expectedRoots[0]) {
      expectedRoots.push(furthestAncestor(expectedNode));
    }

    assert_equals(actualRoots.length, expectedRoots.length,
      "Either the actual node and actual range are in the same tree but the expected are in different trees, or vice versa");

    // This doctype stuff is to work around the fact that Opera 11.00 will
    // move around doctypes within a document, even to totally invalid
    // positions, but it won't allow a new doctype to be added to a
    // document in any way I can figure out.  So if we try moving a doctype
    // to some invalid place, in Opera it will actually succeed, and then
    // restoreIframe() will remove the doctype along with the root element,
    // and then nothing can re-add the doctype.  So instead, we catch it
    // during the test itself and move it back to the right place while we
    // still can.
    //
    // I spent *way* too much time debugging and working around this bug.
    var actualDoctype = actualIframe.contentDocument.doctype;
    var expectedDoctype = expectedIframe.contentDocument.doctype;

    var result;
    try {
      result = mySurroundContents(expectedRange, expectedNode);
    } catch (e) {
      if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
        expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
      }
      throw e;
    }
    if (typeof result == "string") {
      assert_throws_dom(result, actualIframe.contentWindow.DOMException, function() {
        try {
          actualRange.surroundContents(actualNode);
        } catch (e) {
          if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
            expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
          }
          if (actualDoctype != actualIframe.contentDocument.firstChild) {
            actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
          }
          throw e;
        }
      }, "A " + result + " must be thrown in this case");
      // Don't return, we still need to test DOM equality
    } else {
      try {
        actualRange.surroundContents(actualNode);
      } catch (e) {
        if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
          expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
        }
        if (actualDoctype != actualIframe.contentDocument.firstChild) {
          actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
        }
        throw e;
      }
    }

    for (var k = 0; k < actualRoots.length; k++) {
      assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
    }
  });
  domTests[i][j].done();

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

    for (var k = 0; k < actualRoots.length; k++) {
      assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
    }

    assert_equals(actualRange.startOffset, expectedRange.startOffset,
      "Unexpected startOffset after surroundContents()");
    assert_equals(actualRange.endOffset, expectedRange.endOffset,
      "Unexpected endOffset after surroundContents()");
    // 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 surroundContents(), 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][j].done();
}

var iStart = 0;
var iStop = testRangesShort.length;
var jStart = 0;
var jStop = testNodesShort.length;

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

var domTests = [];
var positionTests = [];
for (var i = iStart; i < iStop; i++) {
  domTests[i] = [];
  positionTests[i] = [];
  for (var j = jStart; j < jStop; j++) {
    domTests[i][j] = async_test(i + "," + j + ": resulting DOM for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
    positionTests[i][j] = async_test(i + "," + j + ": resulting range position for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
  }
}

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

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

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

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