Import Cobalt 19.master.0.203780

Includes the following patches:
  https://cobalt-review.googlesource.com/c/cobalt/+/5210
    by errong.leng@samsung.com
  https://cobalt-review.googlesource.com/c/cobalt/+/5270
    by linus.wang@samsung.com
diff --git a/src/third_party/web_platform_tests/selection/Document-open.html b/src/third_party/web_platform_tests/selection/Document-open.html
new file mode 100644
index 0000000..9d17091
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/Document-open.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<title>Selection Document.open() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+// This tests the HTML spec requirement "Replace the Document's singleton
+// objects with new instances of those objects. (This includes in particular
+// the Window, Location, History, ApplicationCache, and Navigator, objects, the
+// various BarProp objects, the two Storage objects, the various HTMLCollection
+// objects, and objects defined by other specifications, like Selection and the
+// document's UndoManager. It also includes all the Web IDL prototypes in the
+// JavaScript binding, including the Document object's prototype.)" in the
+// document.open() algorithm.
+
+var iframe = document.createElement("iframe");
+var t = async_test("Selection must be replaced with a new object after document.open()");
+iframe.onload = function() {
+    t.step(function() {
+        var originalSelection = iframe.contentWindow.getSelection();
+        assert_equals(originalSelection.rangeCount, 0,
+            "Sanity check: rangeCount must be initially 0");
+        iframe.contentDocument.body.appendChild(
+            iframe.contentDocument.createTextNode("foo"));
+        var range = iframe.contentDocument.createRange();
+        range.selectNodeContents(iframe.contentDocument.body);
+        iframe.contentWindow.getSelection().addRange(range);
+        assert_equals(originalSelection.rangeCount, 1,
+            "Sanity check: rangeCount must be 1 after adding a range");
+
+        iframe.contentDocument.open();
+
+        assert_not_equals(iframe.contentWindow.getSelection(), originalSelection,
+            "After document.open(), the Selection object must no longer be the same");
+        assert_equals(iframe.contentWindow.getSelection().rangeCount, 0,
+            "After document.open(), rangeCount must be 0 again");
+    });
+    t.done();
+    document.body.removeChild(iframe);
+};
+document.body.appendChild(iframe);
+</script>
diff --git a/src/third_party/web_platform_tests/selection/addRange.html b/src/third_party/web_platform_tests/selection/addRange.html
new file mode 100644
index 0000000..5fe8409
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/addRange.html
@@ -0,0 +1,178 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<div id=log></div>
+<meta name="timeout" content="long">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+function testAddRange(exception, range, endpoints, qualifier, testName) {
+    test(function() {
+        assert_equals(exception, null, "Test setup must not throw exceptions");
+
+        selection.addRange(range);
+
+        assert_equals(range.startContainer, endpoints[0],
+            "addRange() must not modify the startContainer of the Range it's given");
+        assert_equals(range.startOffset, endpoints[1],
+            "addRange() must not modify the startOffset of the Range it's given");
+        assert_equals(range.endContainer, endpoints[2],
+            "addRange() must not modify the endContainer of the Range it's given");
+        assert_equals(range.endOffset, endpoints[3],
+            "addRange() must not modify the endOffset of the Range it's given");
+    }, testName + ": " + qualifier + " addRange() must not throw exceptions or modify the range it's given");
+
+    test(function() {
+        assert_equals(exception, null, "Test setup must not throw exceptions");
+
+        assert_equals(selection.rangeCount, 1, "rangeCount must be 1");
+    }, testName + ": " + qualifier + " addRange() must result in rangeCount being 1");
+
+    // From here on out we check selection.getRangeAt(selection.rangeCount - 1)
+    // so as not to double-fail Gecko.
+
+    test(function() {
+        assert_equals(exception, null, "Test setup must not throw exceptions");
+        assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+        var newRange = selection.getRangeAt(selection.rangeCount - 1);
+
+        assert_not_equals(newRange, null,
+            "getRangeAt(rangeCount - 1) must not return null");
+        assert_equals(typeof newRange, "object",
+            "getRangeAt(rangeCount - 1) must return an object");
+
+        assert_equals(newRange.startContainer, range.startContainer,
+            "startContainer of the Selection's last Range must match the added Range");
+        assert_equals(newRange.startOffset, range.startOffset,
+            "startOffset of the Selection's last Range must match the added Range");
+        assert_equals(newRange.endContainer, range.endContainer,
+            "endContainer of the Selection's last Range must match the added Range");
+        assert_equals(newRange.endOffset, range.endOffset,
+            "endOffset of the Selection's last Range must match the added Range");
+    }, testName + ": " + qualifier + " addRange() must result in the selection's last range having the specified endpoints");
+
+    test(function() {
+        assert_equals(exception, null, "Test setup must not throw exceptions");
+        assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+        assert_equals(selection.getRangeAt(selection.rangeCount - 1), range,
+            "getRangeAt(rangeCount - 1) must return the same object we added");
+    }, testName + ": " + qualifier + " addRange() must result in the selection's last range being the same object we added");
+
+    // Let's not test many different modifications -- one should be enough.
+    test(function() {
+        assert_equals(exception, null, "Test setup must not throw exceptions");
+        assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+        if (range.startContainer == paras[0].firstChild
+        && range.startOffset == 0
+        && range.endContainer == paras[0].firstChild
+        && range.endOffset == 2) {
+            // Just in case . . .
+            range.setStart(paras[0].firstChild, 1);
+        } else {
+            range.setStart(paras[0].firstChild, 0);
+            range.setEnd(paras[0].firstChild, 2);
+        }
+
+        var newRange = selection.getRangeAt(selection.rangeCount - 1);
+
+        assert_equals(newRange.startContainer, range.startContainer,
+            "After mutating the " + qualifier + " added Range, startContainer of the Selection's last Range must match the added Range");
+        assert_equals(newRange.startOffset, range.startOffset,
+            "After mutating the " + qualifier + " added Range, startOffset of the Selection's last Range must match the added Range");
+        assert_equals(newRange.endContainer, range.endContainer,
+            "After mutating the " + qualifier + " added Range, endContainer of the Selection's last Range must match the added Range");
+        assert_equals(newRange.endOffset, range.endOffset,
+            "After mutating the " + qualifier + " added Range, endOffset of the Selection's last Range must match the added Range");
+    }, testName + ": modifying the " + qualifier + " added range must modify the Selection's last Range");
+
+    // Now test the other way too.
+    test(function() {
+        assert_equals(exception, null, "Test setup must not throw exceptions");
+        assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+        var newRange = selection.getRangeAt(selection.rangeCount - 1);
+
+        if (newRange.startContainer == paras[0].firstChild
+        && newRange.startOffset == 4
+        && newRange.endContainer == paras[0].firstChild
+        && newRange.endOffset == 6) {
+            newRange.setStart(paras[0].firstChild, 5);
+        } else {
+            newRange.setStart(paras[0].firstChild, 4);
+            newRange.setStart(paras[0].firstChild, 6);
+        }
+
+        assert_equals(newRange.startContainer, range.startContainer,
+            "After " + qualifier + " addRange(), after mutating the Selection's last Range, startContainer of the Selection's last Range must match the added Range");
+        assert_equals(newRange.startOffset, range.startOffset,
+            "After " + qualifier + " addRange(), after mutating the Selection's last Range, startOffset of the Selection's last Range must match the added Range");
+        assert_equals(newRange.endContainer, range.endContainer,
+            "After " + qualifier + " addRange(), after mutating the Selection's last Range, endContainer of the Selection's last Range must match the added Range");
+        assert_equals(newRange.endOffset, range.endOffset,
+            "After " + qualifier + " addRange(), after mutating the Selection's last Range, endOffset of the Selection's last Range must match the added Range");
+    }, testName + ": modifying the Selection's last Range must modify the " + qualifier + " added Range");
+}
+
+// Do only n evals, not n^2
+var testRangesEvaled = testRanges.map(eval);
+
+for (var i = 0; i < testRanges.length; i++) {
+    for (var j = 0; j < testRanges.length; j++) {
+        var testName = "Range " + i + " " + testRanges[i]
+            + " followed by Range " + j + " " + testRanges[j];
+
+        var exception = null;
+        try {
+            selection.removeAllRanges();
+
+            var endpoints1 = testRangesEvaled[i];
+            var range1 = ownerDocument(endpoints1[0]).createRange();
+            range1.setStart(endpoints1[0], endpoints1[1]);
+            range1.setEnd(endpoints1[2], endpoints1[3]);
+
+            if (range1.startContainer !== endpoints1[0]) {
+                throw "Sanity check: the first Range we created must have the desired startContainer";
+            }
+            if (range1.startOffset !== endpoints1[1]) {
+                throw "Sanity check: the first Range we created must have the desired startOffset";
+            }
+            if (range1.endContainer !== endpoints1[2]) {
+                throw "Sanity check: the first Range we created must have the desired endContainer";
+            }
+            if (range1.endOffset !== endpoints1[3]) {
+                throw "Sanity check: the first Range we created must have the desired endOffset";
+            }
+
+            var endpoints2 = testRangesEvaled[j];
+            var range2 = ownerDocument(endpoints2[0]).createRange();
+            range2.setStart(endpoints2[0], endpoints2[1]);
+            range2.setEnd(endpoints2[2], endpoints2[3]);
+
+            if (range2.startContainer !== endpoints2[0]) {
+                throw "Sanity check: the second Range we created must have the desired startContainer";
+            }
+            if (range2.startOffset !== endpoints2[1]) {
+                throw "Sanity check: the second Range we created must have the desired startOffset";
+            }
+            if (range2.endContainer !== endpoints2[2]) {
+                throw "Sanity check: the second Range we created must have the desired endContainer";
+            }
+            if (range2.endOffset !== endpoints2[3]) {
+                throw "Sanity check: the second Range we created must have the desired endOffset";
+            }
+        } catch (e) {
+            exception = e;
+        }
+
+        testAddRange(exception, range1, endpoints1, "first", testName);
+        testAddRange(exception, range2, endpoints2, "second", testName);
+    }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/src/third_party/web_platform_tests/selection/collapse.html b/src/third_party/web_platform_tests/selection/collapse.html
new file mode 100644
index 0000000..f1268f9
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/collapse.html
@@ -0,0 +1,87 @@
+<!doctype html>
+<title>Selection.collapse() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+function testCollapse(range, point) {
+    selection.removeAllRanges();
+    var addedRange;
+    if (range) {
+        addedRange = range.cloneRange();
+        selection.addRange(addedRange);
+    }
+
+    if (point[0].nodeType == Node.DOCUMENT_TYPE_NODE) {
+        assert_throws("INVALID_NODE_TYPE_ERR", function() {
+            selection.collapse(point[0], point[1]);
+        }, "Must throw INVALID_NODE_TYPE_ERR when collapse()ing if the node is a DocumentType");
+        return;
+    }
+
+    if (point[1] < 0 || point[1] > getNodeLength(point[0])) {
+        assert_throws("INDEX_SIZE_ERR", function() {
+            selection.collapse(point[0], point[1]);
+        }, "Must throw INDEX_SIZE_ERR when collapse()ing if the offset is negative or greater than the node's length");
+        return;
+    }
+
+    selection.collapse(point[0], point[1]);
+
+    assert_equals(selection.rangeCount, 1,
+        "selection.rangeCount must equal 1 after collapse()");
+    assert_equals(selection.focusNode, point[0],
+        "focusNode must equal the node we collapse()d to");
+    assert_equals(selection.focusOffset, point[1],
+        "focusOffset must equal the offset we collapse()d to");
+    assert_equals(selection.focusNode, selection.anchorNode,
+        "focusNode and anchorNode must be equal after collapse()");
+    assert_equals(selection.focusOffset, selection.anchorOffset,
+        "focusOffset and anchorOffset must be equal after collapse()");
+    if (range) {
+        assert_equals(addedRange.startContainer, range.startContainer,
+            "collapse() must not change the startContainer of a preexisting Range");
+        assert_equals(addedRange.endContainer, range.endContainer,
+            "collapse() must not change the endContainer of a preexisting Range");
+        assert_equals(addedRange.startOffset, range.startOffset,
+            "collapse() must not change the startOffset of a preexisting Range");
+        assert_equals(addedRange.endOffset, range.endOffset,
+            "collapse() must not change the endOffset of a preexisting Range");
+    }
+}
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+// Don't want to eval() each point a bazillion times
+var testPointsCached = [];
+for (var i = 0; i < testPoints.length; i++) {
+    testPointsCached.push(eval(testPoints[i]));
+}
+
+var tests = [];
+for (var i = 0; i < testRanges.length; i++) {
+    var endpoints = eval(testRanges[i]);
+    var range;
+    test(function() {
+        if (endpoints.length) {
+            range = ownerDocument(endpoints[0]).createRange();
+            range.setStart(endpoints[0], endpoints[1]);
+            range.setEnd(endpoints[2], endpoints[3]);
+        } else {
+            // Empty selection
+            range = null;
+        }
+    }, "Set up range " + i + " " + testRanges[i]);
+    for (var j = 0; j < testPoints.length; j++) {
+        tests.push(["Range " + i + " " + testRanges[i] + ", point " + j + " " + testPoints[j], range, testPointsCached[j]]);
+    }
+}
+
+generate_tests(testCollapse, tests);
+
+testDiv.style.display = "none";
+</script>
diff --git a/src/third_party/web_platform_tests/selection/collapseToStartEnd.html b/src/third_party/web_platform_tests/selection/collapseToStartEnd.html
new file mode 100644
index 0000000..37c57fa
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/collapseToStartEnd.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<title>Selection.collapseTo(Start|End)() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+for (var i = 0; i < testRanges.length; i++) {
+    test(function() {
+        selection.removeAllRanges();
+        var endpoints = eval(testRanges[i]);
+        if (!endpoints.length) {
+            assert_throws("INVALID_STATE_ERR", function() {
+                selection.collapseToStart();
+            }, "Must throw InvalidStateErr if the selection's range is null");
+            return;
+        }
+
+        var addedRange = ownerDocument(endpoints[0]).createRange();
+        addedRange.setStart(endpoints[0], endpoints[1]);
+        addedRange.setEnd(endpoints[2], endpoints[3]);
+        selection.addRange(addedRange);
+
+        // We don't penalize browsers here for mishandling addRange() and
+        // adding a different range than we specified.  They fail addRange()
+        // tests for that, and don't have to fail collapseToStart/End() tests
+        // too.  They do fail if they throw unexpectedly, though.  I also fail
+        // them if there's no range at all, because otherwise they could pass
+        // all tests if addRange() always does nothing and collapseToStart()
+        // always throws.
+        assert_equals(selection.rangeCount, 1,
+            "Sanity check: rangeCount must equal 1 after addRange()");
+
+        var expectedEndpoint = [
+            selection.getRangeAt(0).startContainer,
+            selection.getRangeAt(0).startOffset
+        ];
+
+        selection.collapseToStart();
+
+        assert_equals(selection.rangeCount, 1,
+            "selection.rangeCount must equal 1");
+        assert_equals(selection.focusNode, expectedEndpoint[0],
+            "focusNode must equal the original start node");
+        assert_equals(selection.focusOffset, expectedEndpoint[1],
+            "focusOffset must equal the original start offset");
+        assert_equals(selection.anchorNode, expectedEndpoint[0],
+            "anchorNode must equal the original start node");
+        assert_equals(selection.anchorOffset, expectedEndpoint[1],
+            "anchorOffset must equal the original start offset");
+        assert_equals(addedRange.startContainer, endpoints[0],
+            "collapseToStart() must not change the startContainer of the selection's original range");
+        assert_equals(addedRange.startOffset, endpoints[1],
+            "collapseToStart() must not change the startOffset of the selection's original range");
+        assert_equals(addedRange.endContainer, endpoints[2],
+            "collapseToStart() must not change the endContainer of the selection's original range");
+        assert_equals(addedRange.endOffset, endpoints[3],
+            "collapseToStart() must not change the endOffset of the selection's original range");
+    }, "Range " + i + " " + testRanges[i] + " collapseToStart()");
+
+    // Copy-paste of above
+    test(function() {
+        selection.removeAllRanges();
+        var endpoints = eval(testRanges[i]);
+        if (!endpoints.length) {
+            assert_throws("INVALID_STATE_ERR", function() {
+                selection.collapseToEnd();
+            }, "Must throw InvalidStateErr if the selection's range is null");
+            return;
+        }
+
+        var addedRange = ownerDocument(endpoints[0]).createRange();
+        addedRange.setStart(endpoints[0], endpoints[1]);
+        addedRange.setEnd(endpoints[2], endpoints[3]);
+        selection.addRange(addedRange);
+
+        // We don't penalize browsers here for mishandling addRange() and
+        // adding a different range than we specified.  They fail addRange()
+        // tests for that, and don't have to fail collapseToStart/End() tests
+        // too.  They do fail if they throw unexpectedly, though.  I also fail
+        // them if there's no range at all, because otherwise they could pass
+        // all tests if addRange() always does nothing and collapseToStart()
+        // always throws.
+        assert_equals(selection.rangeCount, 1,
+            "Sanity check: rangeCount must equal 1 after addRange()");
+
+        var expectedEndpoint = [
+            selection.getRangeAt(0).endContainer,
+            selection.getRangeAt(0).endOffset
+        ];
+
+        selection.collapseToEnd();
+
+        assert_equals(selection.rangeCount, 1,
+            "selection.rangeCount must equal 1");
+        assert_equals(selection.focusNode, expectedEndpoint[0],
+            "focusNode must equal the original end node");
+        assert_equals(selection.focusOffset, expectedEndpoint[1],
+            "focusOffset must equal the original end offset");
+        assert_equals(selection.anchorNode, expectedEndpoint[0],
+            "anchorNode must equal the original end node");
+        assert_equals(selection.anchorOffset, expectedEndpoint[1],
+            "anchorOffset must equal the original end offset");
+        assert_equals(addedRange.startContainer, endpoints[0],
+            "collapseToEnd() must not change the startContainer of the selection's original range");
+        assert_equals(addedRange.startOffset, endpoints[1],
+            "collapseToEnd() must not change the startOffset of the selection's original range");
+        assert_equals(addedRange.endContainer, endpoints[2],
+            "collapseToEnd() must not change the endContainer of the selection's original range");
+        assert_equals(addedRange.endOffset, endpoints[3],
+            "collapseToEnd() must not change the endOffset of the selection's original range");
+    }, "Range " + i + " " + testRanges[i] + " collapseToEnd()");
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/src/third_party/web_platform_tests/selection/common.js b/src/third_party/web_platform_tests/selection/common.js
new file mode 100644
index 0000000..c0b622f
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/common.js
@@ -0,0 +1,952 @@
+"use strict";
+// TODO: iframes, contenteditable/designMode
+
+// Everything is done in functions in this test harness, so we have to declare
+// all the variables before use to make sure they can be reused.
+var selection;
+var testDiv, paras, detachedDiv, detachedPara1, detachedPara2,
+    foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement,
+    detachedXmlElement, detachedTextNode, foreignTextNode,
+    detachedForeignTextNode, xmlTextNode, detachedXmlTextNode,
+    processingInstruction, detachedProcessingInstruction, comment,
+    detachedComment, foreignComment, detachedForeignComment, xmlComment,
+    detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype,
+    foreignDoctype, xmlDoctype;
+var testRanges, testPoints, testNodes;
+
+function setupRangeTests() {
+    selection = getSelection();
+    testDiv = document.querySelector("#test");
+    if (testDiv) {
+        testDiv.parentNode.removeChild(testDiv);
+    }
+    testDiv = document.createElement("div");
+    testDiv.id = "test";
+    document.body.insertBefore(testDiv, document.body.firstChild);
+    // Test some diacritics, to make sure browsers are using code units here
+    // and not something like grapheme clusters.
+    testDiv.innerHTML = "<p id=a>A&#x308;b&#x308;c&#x308;d&#x308;e&#x308;f&#x308;g&#x308;h&#x308;\n"
+        + "<p id=b style=display:none>Ijklmnop\n"
+        + "<p id=c>Qrstuvwx"
+        + "<p id=d style=display:none>Yzabcdef"
+        + "<p id=e style=display:none>Ghijklmn";
+    paras = testDiv.querySelectorAll("p");
+
+    detachedDiv = document.createElement("div");
+    detachedPara1 = document.createElement("p");
+    detachedPara1.appendChild(document.createTextNode("Opqrstuv"));
+    detachedPara2 = document.createElement("p");
+    detachedPara2.appendChild(document.createTextNode("Wxyzabcd"));
+    detachedDiv.appendChild(detachedPara1);
+    detachedDiv.appendChild(detachedPara2);
+
+    // Opera doesn't automatically create a doctype for a new HTML document,
+    // contrary to spec.  It also doesn't let you add doctypes to documents
+    // after the fact through any means I've tried.  So foreignDoc in Opera
+    // will have no doctype, foreignDoctype will be null, and Opera will fail
+    // some tests somewhat mysteriously as a result.
+    foreignDoc = document.implementation.createHTMLDocument("");
+    foreignPara1 = foreignDoc.createElement("p");
+    foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl"));
+    foreignPara2 = foreignDoc.createElement("p");
+    foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst"));
+    foreignDoc.body.appendChild(foreignPara1);
+    foreignDoc.body.appendChild(foreignPara2);
+
+    // Now we get to do really silly stuff, which nobody in the universe is
+    // ever going to actually do, but the spec defines behavior, so too bad.
+    // Testing is fun!
+    xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcde", "x\"'y");
+    xmlDoc = document.implementation.createDocument(null, null, xmlDoctype);
+    detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element-names");
+    detachedTextNode = document.createTextNode("Uvwxyzab");
+    detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij");
+    detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr");
+    // PIs only exist in XML documents, so don't bother with document or
+    // foreignDoc.
+    detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoorwill", "chirp chirp chirp");
+    detachedComment = document.createComment("Stuvwxyz");
+    // Hurrah, we finally got to "z" at the end!
+    detachedForeignComment = foreignDoc.createComment("אריה יהודה");
+    detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר");
+
+    // We should also test with document fragments that actually contain stuff
+    // . . . but, maybe later.
+    docfrag = document.createDocumentFragment();
+    foreignDocfrag = foreignDoc.createDocumentFragment();
+    xmlDocfrag = xmlDoc.createDocumentFragment();
+
+    xmlElement = xmlDoc.createElement("igiveuponcreativenames");
+    xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti");
+    xmlElement.appendChild(xmlTextNode);
+    processingInstruction = xmlDoc.createProcessingInstruction("somePI", 'Did you know that ":syn sync fromstart" is very useful when using vim to edit large amounts of JavaScript embedded in HTML?');
+    xmlDoc.appendChild(xmlElement);
+    xmlDoc.appendChild(processingInstruction);
+    xmlComment = xmlDoc.createComment("I maliciously created a comment that will break incautious XML serializers, but Firefox threw an exception, so all I got was this lousy T-shirt");
+    xmlDoc.appendChild(xmlComment);
+
+    comment = document.createComment("Alphabet soup?");
+    testDiv.appendChild(comment);
+
+    foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mean different things.  I\'ve seen non-native speakers trip up on this.');
+    foreignDoc.appendChild(foreignComment);
+    foreignTextNode = foreignDoc.createTextNode("I admit that I harbor doubts about whether we really need so many things to test, but it's too late to stop now.");
+    foreignDoc.body.appendChild(foreignTextNode);
+
+    doctype = document.doctype;
+    foreignDoctype = foreignDoc.doctype;
+
+    testRanges = [
+        // Various ranges within the text node children of different
+        // paragraphs.  All should be valid.
+        "[paras[0].firstChild, 0, paras[0].firstChild, 0]",
+        "[paras[0].firstChild, 0, paras[0].firstChild, 1]",
+        "[paras[0].firstChild, 2, paras[0].firstChild, 8]",
+        "[paras[0].firstChild, 2, paras[0].firstChild, 9]",
+        "[paras[1].firstChild, 0, paras[1].firstChild, 0]",
+        "[paras[1].firstChild, 0, paras[1].firstChild, 1]",
+        "[paras[1].firstChild, 2, paras[1].firstChild, 8]",
+        "[paras[1].firstChild, 2, paras[1].firstChild, 9]",
+        "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]",
+        "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]",
+        "[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]",
+        "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]",
+        "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]",
+        "[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]",
+        // Now try testing some elements, not just text nodes.
+        "[document.documentElement, 0, document.documentElement, 1]",
+        "[document.documentElement, 0, document.documentElement, 2]",
+        "[document.documentElement, 1, document.documentElement, 2]",
+        "[document.head, 1, document.head, 1]",
+        "[document.body, 0, document.body, 1]",
+        "[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]",
+        "[foreignDoc.head, 1, foreignDoc.head, 1]",
+        "[foreignDoc.body, 0, foreignDoc.body, 0]",
+        "[paras[0], 0, paras[0], 0]",
+        "[paras[0], 0, paras[0], 1]",
+        "[detachedPara1, 0, detachedPara1, 0]",
+        "[detachedPara1, 0, detachedPara1, 1]",
+        // Now try some ranges that span elements.
+        "[paras[0].firstChild, 0, paras[1].firstChild, 0]",
+        "[paras[0].firstChild, 0, paras[1].firstChild, 8]",
+        "[paras[0].firstChild, 3, paras[3], 1]",
+        // How about something that spans a node and its descendant?
+        "[paras[0], 0, paras[0].firstChild, 7]",
+        "[testDiv, 2, paras[4], 1]",
+        "[testDiv, 1, paras[2].firstChild, 5]",
+        "[document.documentElement, 1, document.body, 0]",
+        "[foreignDoc.documentElement, 1, foreignDoc.body, 0]",
+        // Then a few more interesting things just for good measure.
+        "[document, 0, document, 1]",
+        "[document, 0, document, 2]",
+        "[document, 1, document, 2]",
+        "[testDiv, 0, comment, 5]",
+        "[paras[2].firstChild, 4, comment, 2]",
+        "[paras[3], 1, comment, 8]",
+        "[foreignDoc, 0, foreignDoc, 0]",
+        "[foreignDoc, 1, foreignComment, 2]",
+        "[foreignDoc.body, 0, foreignTextNode, 36]",
+        "[xmlDoc, 0, xmlDoc, 0]",
+        // Opera 11 crashes if you extractContents() a range that ends at offset
+        // zero in a comment.  Comment out this line to run the tests successfully.
+        "[xmlDoc, 1, xmlComment, 0]",
+        "[detachedTextNode, 0, detachedTextNode, 8]",
+        "[detachedForeignTextNode, 7, detachedForeignTextNode, 7]",
+        "[detachedForeignTextNode, 0, detachedForeignTextNode, 8]",
+        "[detachedXmlTextNode, 7, detachedXmlTextNode, 7]",
+        "[detachedXmlTextNode, 0, detachedXmlTextNode, 8]",
+        "[detachedComment, 3, detachedComment, 4]",
+        "[detachedComment, 5, detachedComment, 5]",
+        "[detachedForeignComment, 0, detachedForeignComment, 1]",
+        "[detachedForeignComment, 4, detachedForeignComment, 4]",
+        "[detachedXmlComment, 2, detachedXmlComment, 6]",
+        "[docfrag, 0, docfrag, 0]",
+        "[foreignDocfrag, 0, foreignDocfrag, 0]",
+        "[xmlDocfrag, 0, xmlDocfrag, 0]",
+    ];
+
+    testPoints = [
+        // Various positions within the page, some invalid.  Remember that
+        // paras[0] is visible, and paras[1] is display: none.
+        "[paras[0].firstChild, -1]",
+        "[paras[0].firstChild, 0]",
+        "[paras[0].firstChild, 1]",
+        "[paras[0].firstChild, 2]",
+        "[paras[0].firstChild, 8]",
+        "[paras[0].firstChild, 9]",
+        "[paras[0].firstChild, 10]",
+        "[paras[0].firstChild, 65535]",
+        "[paras[1].firstChild, -1]",
+        "[paras[1].firstChild, 0]",
+        "[paras[1].firstChild, 1]",
+        "[paras[1].firstChild, 2]",
+        "[paras[1].firstChild, 8]",
+        "[paras[1].firstChild, 9]",
+        "[paras[1].firstChild, 10]",
+        "[paras[1].firstChild, 65535]",
+        "[detachedPara1.firstChild, 0]",
+        "[detachedPara1.firstChild, 1]",
+        "[detachedPara1.firstChild, 8]",
+        "[detachedPara1.firstChild, 9]",
+        "[foreignPara1.firstChild, 0]",
+        "[foreignPara1.firstChild, 1]",
+        "[foreignPara1.firstChild, 8]",
+        "[foreignPara1.firstChild, 9]",
+        // Now try testing some elements, not just text nodes.
+        "[document.documentElement, -1]",
+        "[document.documentElement, 0]",
+        "[document.documentElement, 1]",
+        "[document.documentElement, 2]",
+        "[document.documentElement, 7]",
+        "[document.head, 1]",
+        "[document.body, 3]",
+        "[foreignDoc.documentElement, 0]",
+        "[foreignDoc.documentElement, 1]",
+        "[foreignDoc.head, 0]",
+        "[foreignDoc.body, 1]",
+        "[paras[0], 0]",
+        "[paras[0], 1]",
+        "[paras[0], 2]",
+        "[paras[1], 0]",
+        "[paras[1], 1]",
+        "[paras[1], 2]",
+        "[detachedPara1, 0]",
+        "[detachedPara1, 1]",
+        "[testDiv, 0]",
+        "[testDiv, 3]",
+        // Then a few more interesting things just for good measure.
+        "[document, -1]",
+        "[document, 0]",
+        "[document, 1]",
+        "[document, 2]",
+        "[document, 3]",
+        "[comment, -1]",
+        "[comment, 0]",
+        "[comment, 4]",
+        "[comment, 96]",
+        "[foreignDoc, 0]",
+        "[foreignDoc, 1]",
+        "[foreignComment, 2]",
+        "[foreignTextNode, 0]",
+        "[foreignTextNode, 36]",
+        "[xmlDoc, -1]",
+        "[xmlDoc, 0]",
+        "[xmlDoc, 1]",
+        "[xmlDoc, 5]",
+        "[xmlComment, 0]",
+        "[xmlComment, 4]",
+        "[processingInstruction, 0]",
+        "[processingInstruction, 5]",
+        "[processingInstruction, 9]",
+        "[detachedTextNode, 0]",
+        "[detachedTextNode, 8]",
+        "[detachedForeignTextNode, 0]",
+        "[detachedForeignTextNode, 8]",
+        "[detachedXmlTextNode, 0]",
+        "[detachedXmlTextNode, 8]",
+        "[detachedProcessingInstruction, 12]",
+        "[detachedComment, 3]",
+        "[detachedComment, 5]",
+        "[detachedForeignComment, 0]",
+        "[detachedForeignComment, 4]",
+        "[detachedXmlComment, 2]",
+        "[docfrag, 0]",
+        "[foreignDocfrag, 0]",
+        "[xmlDocfrag, 0]",
+        "[doctype, 0]",
+        "[doctype, -17]",
+        "[doctype, 1]",
+        "[foreignDoctype, 0]",
+        "[xmlDoctype, 0]",
+    ];
+
+    testNodes = [
+        "paras[0]",
+        "paras[0].firstChild",
+        "paras[1]",
+        "paras[1].firstChild",
+        "foreignPara1",
+        "foreignPara1.firstChild",
+        "detachedPara1",
+        "detachedPara1.firstChild",
+        "detachedPara1",
+        "detachedPara1.firstChild",
+        "testDiv",
+        "document",
+        "detachedDiv",
+        "detachedPara2",
+        "foreignDoc",
+        "foreignPara2",
+        "xmlDoc",
+        "xmlElement",
+        "detachedXmlElement",
+        "detachedTextNode",
+        "foreignTextNode",
+        "detachedForeignTextNode",
+        "xmlTextNode",
+        "detachedXmlTextNode",
+        "processingInstruction",
+        "detachedProcessingInstruction",
+        "comment",
+        "detachedComment",
+        "foreignComment",
+        "detachedForeignComment",
+        "xmlComment",
+        "detachedXmlComment",
+        "docfrag",
+        "foreignDocfrag",
+        "xmlDocfrag",
+        "doctype",
+        "foreignDoctype",
+        "xmlDoctype",
+    ];
+}
+if ("setup" in window) {
+    setup(setupRangeTests);
+} else {
+    // Presumably we're running from within an iframe or something
+    setupRangeTests();
+}
+
+/**
+ * Return the length of a node as specified in DOM Range.
+ */
+function getNodeLength(node) {
+    if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+        return 0;
+    }
+    if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) {
+        return node.length;
+    }
+    return node.childNodes.length;
+}
+
+/**
+ * Returns the furthest ancestor of a Node as defined by the spec.
+ */
+function furthestAncestor(node) {
+    var root = node;
+    while (root.parentNode != null) {
+        root = root.parentNode;
+    }
+    return root;
+}
+
+/**
+ * "The ancestor containers of a Node are the Node itself and all its
+ * ancestors."
+ *
+ * Is node1 an ancestor container of node2?
+ */
+function isAncestorContainer(node1, node2) {
+    return node1 == node2 ||
+        (node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS);
+}
+
+/**
+ * Returns the first Node that's after node in tree order, or null if node is
+ * the last Node.
+ */
+function nextNode(node) {
+    if (node.hasChildNodes()) {
+        return node.firstChild;
+    }
+    return nextNodeDescendants(node);
+}
+
+/**
+ * Returns the last Node that's before node in tree order, or null if node is
+ * the first Node.
+ */
+function previousNode(node) {
+    if (node.previousSibling) {
+        node = node.previousSibling;
+        while (node.hasChildNodes()) {
+            node = node.lastChild;
+        }
+        return node;
+    }
+    return node.parentNode;
+}
+
+/**
+ * Returns the next Node that's after node and all its descendants in tree
+ * order, or null if node is the last Node or an ancestor of it.
+ */
+function nextNodeDescendants(node) {
+    while (node && !node.nextSibling) {
+        node = node.parentNode;
+    }
+    if (!node) {
+        return null;
+    }
+    return node.nextSibling;
+}
+
+/**
+ * Returns the ownerDocument of the Node, or the Node itself if it's a
+ * Document.
+ */
+function ownerDocument(node) {
+    return node.nodeType == Node.DOCUMENT_NODE
+        ? node
+        : node.ownerDocument;
+}
+
+/**
+ * Returns true if ancestor is an ancestor of descendant, false otherwise.
+ */
+function isAncestor(ancestor, descendant) {
+    if (!ancestor || !descendant) {
+        return false;
+    }
+    while (descendant && descendant != ancestor) {
+        descendant = descendant.parentNode;
+    }
+    return descendant == ancestor;
+}
+
+/**
+ * Returns true if descendant is a descendant of ancestor, false otherwise.
+ */
+function isDescendant(descendant, ancestor) {
+    return isAncestor(ancestor, descendant);
+}
+
+/**
+ * The position of two boundary points relative to one another, as defined by
+ * the spec.
+ */
+function getPosition(nodeA, offsetA, nodeB, offsetB) {
+    // "If node A is the same as node B, return equal if offset A equals offset
+    // B, before if offset A is less than offset B, and after if offset A is
+    // greater than offset B."
+    if (nodeA == nodeB) {
+        if (offsetA == offsetB) {
+            return "equal";
+        }
+        if (offsetA < offsetB) {
+            return "before";
+        }
+        if (offsetA > offsetB) {
+            return "after";
+        }
+    }
+
+    // "If node A is after node B in tree order, compute the position of (node
+    // B, offset B) relative to (node A, offset A). If it is before, return
+    // after. If it is after, return before."
+    if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
+        var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
+        if (pos == "before") {
+            return "after";
+        }
+        if (pos == "after") {
+            return "before";
+        }
+    }
+
+    // "If node A is an ancestor of node B:"
+    if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
+        // "Let child equal node B."
+        var child = nodeB;
+
+        // "While child is not a child of node A, set child to its parent."
+        while (child.parentNode != nodeA) {
+            child = child.parentNode;
+        }
+
+        // "If the index of child is less than offset A, return after."
+        if (indexOf(child) < offsetA) {
+            return "after";
+        }
+    }
+
+    // "Return before."
+    return "before";
+}
+
+/**
+ * "contained" as defined by DOM Range: "A Node node is contained in a range
+ * range if node's furthest ancestor is the same as range's root, and (node, 0)
+ * is after range's start, and (node, length of node) is before range's end."
+ */
+function isContained(node, range) {
+    var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
+    var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
+
+    return furthestAncestor(node) == furthestAncestor(range.startContainer)
+        && pos1 == "after"
+        && pos2 == "before";
+}
+
+/**
+ * "partially contained" as defined by DOM Range: "A Node is partially
+ * contained in a range if it is an ancestor container of the range's start but
+ * not its end, or vice versa."
+ */
+function isPartiallyContained(node, range) {
+    var cond1 = isAncestorContainer(node, range.startContainer);
+    var cond2 = isAncestorContainer(node, range.endContainer);
+    return (cond1 && !cond2) || (cond2 && !cond1);
+}
+
+/**
+ * Index of a node as defined by the spec.
+ */
+function indexOf(node) {
+    if (!node.parentNode) {
+        // No preceding sibling nodes, right?
+        return 0;
+    }
+    var i = 0;
+    while (node != node.parentNode.childNodes[i]) {
+        i++;
+    }
+    return i;
+}
+
+/**
+ * extractContents() implementation, following the spec.  If an exception is
+ * supposed to be thrown, will return a string with the name (e.g.,
+ * "HIERARCHY_REQUEST_ERR") instead of a document fragment.  It might also
+ * return an arbitrary human-readable string if a condition is hit that implies
+ * a spec bug.
+ */
+function myExtractContents(range) {
+    // "If the context object's detached flag is set, raise an
+    // INVALID_STATE_ERR exception and abort these steps."
+    try {
+        range.collapsed;
+    } catch (e) {
+        return "INVALID_STATE_ERR";
+    }
+
+    // "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 or Comment node:"
+    if (range.startContainer == range.endContainer
+    && (range.startContainer.nodeType == Node.TEXT_NODE
+    || range.startContainer.nodeType == Node.COMMENT_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);
+
+        // "Call deleteData(original start offset, original end offset −
+        // original start offset) on original start node."
+        originalStartNode.deleteData(originalStartOffset,
+            originalEndOffset - originalStartOffset);
+
+        // "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 original start node is an ancestor container of original end node,
+    // set new node to original start node and new offset to original start
+    // offset."
+    var newNode, newOffset;
+    if (isAncestorContainer(originalStartNode, originalEndNode)) {
+        newNode = originalStartNode;
+        newOffset = originalStartOffset;
+    // "Otherwise:"
+    } else {
+        // "Let reference node equal original start node."
+        var referenceNode = originalStartNode;
+
+        // "While reference node's parent is not null and is not an ancestor
+        // container of original end node, set reference node to its parent."
+        while (referenceNode.parentNode
+        && !isAncestorContainer(referenceNode.parentNode, originalEndNode)) {
+            referenceNode = referenceNode.parentNode;
+        }
+
+        // "Set new node to the parent of reference node, and new offset to one
+        // plus the index of reference node."
+        newNode = referenceNode.parentNode;
+        newOffset = 1 + indexOf(referenceNode);
+    }
+
+    // "If first partially contained child is a Text or Comment node:"
+    if (firstPartiallyContainedChild
+    && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
+    || firstPartiallyContainedChild.nodeType == Node.COMMENT_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,
+            getNodeLength(originalStartNode) - originalStartOffset);
+
+        // "Append clone as the last child of frag."
+        frag.appendChild(clone);
+
+        // "Call deleteData() on original start node, with original start
+        // offset as the first argument and (length of original start node −
+        // original start offset) as the second."
+        originalStartNode.deleteData(originalStartOffset,
+            getNodeLength(originalStartNode) - originalStartOffset);
+    // "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,
+            getNodeLength(firstPartiallyContainedChild));
+
+        // "Let subfrag be the result of calling extractContents() on
+        // subrange."
+        var subfrag = myExtractContents(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, append contained child
+    // as the last child of frag."
+    for (var i = 0; i < containedChildren.length; i++) {
+        frag.appendChild(containedChildren[i]);
+    }
+
+    // "If last partially contained child is a Text or Comment node:"
+    if (lastPartiallyContainedChild
+    && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
+    || lastPartiallyContainedChild.nodeType == Node.COMMENT_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);
+
+        // "Call deleteData(0, original end offset) on original end node."
+        originalEndNode.deleteData(0, originalEndOffset);
+    // "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 extractContents() on
+        // subrange."
+        var subfrag = myExtractContents(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]);
+        }
+    }
+
+    // "Set the context object's start and end to (new node, new offset)."
+    range.setStart(newNode, newOffset);
+    range.setEnd(newNode, newOffset);
+
+    // "Return frag."
+    return frag;
+}
+
+/**
+ * insertNode() implementation, following the spec.  If an exception is
+ * supposed to be thrown, will return a string with the name (e.g.,
+ * "HIERARCHY_REQUEST_ERR") instead of a document fragment.  It might also
+ * return an arbitrary human-readable string if a condition is hit that implies
+ * a spec bug.
+ */
+function myInsertNode(range, newNode) {
+    // "If the context object's detached flag is set, raise an
+    // INVALID_STATE_ERR exception and abort these steps."
+    //
+    // Assume that if accessing collapsed throws, it's detached.
+    try {
+        range.collapsed;
+    } catch (e) {
+        return "INVALID_STATE_ERR";
+    }
+
+    // "If the context object's start node is a Text or Comment node and its
+    // parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these
+    // steps."
+    if ((range.startContainer.nodeType == Node.TEXT_NODE
+    || range.startContainer.nodeType == Node.COMMENT_NODE)
+    && !range.startContainer.parentNode) {
+        return "HIERARCHY_REQUEST_ERR";
+    }
+
+    // "If the context object's start node is a Text node, run splitText() on
+    // it with the context object's start offset as its argument, and let
+    // reference node be the result."
+    var referenceNode;
+    if (range.startContainer.nodeType == Node.TEXT_NODE) {
+        // We aren't testing how ranges vary under mutations, and browsers vary
+        // in how they mutate for splitText, so let's just force the correct
+        // way.
+        var start = [range.startContainer, range.startOffset];
+        var end = [range.endContainer, range.endOffset];
+
+        referenceNode = range.startContainer.splitText(range.startOffset);
+
+        if (start[0] == end[0]
+        && end[1] > start[1]) {
+            end[0] = referenceNode;
+            end[1] -= start[1];
+        } else if (end[0] == start[0].parentNode
+        && end[1] > indexOf(referenceNode)) {
+            end[1]++;
+        }
+        range.setStart(start[0], start[1]);
+        range.setEnd(end[0], end[1]);
+    // "Otherwise, if the context object's start node is a Comment, let
+    // reference node be the context object's start node."
+    } else if (range.startContainer.nodeType == Node.COMMENT_NODE) {
+        referenceNode = range.startContainer;
+    // "Otherwise, let reference node be the child of the context object's
+    // start node with index equal to the context object's start offset, or
+    // null if there is no such child."
+    } else {
+        referenceNode = range.startContainer.childNodes[range.startOffset];
+        if (typeof referenceNode == "undefined") {
+            referenceNode = null;
+        }
+    }
+
+    // "If reference node is null, let parent node be the context object's
+    // start node."
+    var parentNode;
+    if (!referenceNode) {
+        parentNode = range.startContainer;
+    // "Otherwise, let parent node be the parent of reference node."
+    } else {
+        parentNode = referenceNode.parentNode;
+    }
+
+    // "Call insertBefore(newNode, reference node) on parent node, re-raising
+    // any exceptions that call raised."
+    try {
+        parentNode.insertBefore(newNode, referenceNode);
+    } catch (e) {
+        return getDomExceptionName(e);
+    }
+}
+
+/**
+ * Asserts that two nodes are equal, in the sense of isEqualNode().  If they
+ * aren't, tries to print a relatively informative reason why not.  TODO: Move
+ * this to testharness.js?
+ */
+function assertNodesEqual(actual, expected, msg) {
+    if (!actual.isEqualNode(expected)) {
+        msg = "Actual and expected mismatch for " + msg + ".  ";
+
+        while (actual && expected) {
+            assert_true(actual.nodeType === expected.nodeType
+                && actual.nodeName === expected.nodeName
+                && actual.nodeValue === expected.nodeValue
+                && actual.childNodes.length === expected.childNodes.length,
+                "First differing node: expected " + format_value(expected)
+                + ", got " + format_value(actual));
+            actual = nextNode(actual);
+            expected = nextNode(expected);
+        }
+
+        assert_unreached("DOMs were not equal but we couldn't figure out why");
+    }
+}
+
+/**
+ * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR").  In
+ * theory this should be just e.name, but in practice it's not.  So I could
+ * legitimately just return e.name, but then every engine but WebKit would fail
+ * every test, since no one seems to care much for standardizing DOMExceptions.
+ * Instead I mangle it to account for browser bugs, so as not to fail
+ * insertNode() tests (for instance) for insertBefore() bugs.  Of course, a
+ * standards-compliant browser will work right in any event.
+ *
+ * If the exception has no string property called "name" or "message", we just
+ * re-throw it.
+ */
+function getDomExceptionName(e) {
+    if (typeof e.name == "string"
+    && /^[A-Z_]+_ERR$/.test(e.name)) {
+        // Either following the standard, or prefixing NS_ERROR_DOM (I'm
+        // looking at you, Gecko).
+        return e.name.replace(/^NS_ERROR_DOM_/, "");
+    }
+
+    if (typeof e.message == "string"
+    && /^[A-Z_]+_ERR$/.test(e.message)) {
+        // Opera
+        return e.message;
+    }
+
+    if (typeof e.message == "string"
+    && /^DOM Exception:/.test(e.message)) {
+        // IE
+        return /[A-Z_]+_ERR/.exec(e.message)[0];
+    }
+
+    throw e;
+}
+
+/**
+ * Given an array of endpoint data [start container, start offset, end
+ * container, end offset], returns a Range with those endpoints.
+ */
+function rangeFromEndpoints(endpoints) {
+    // If we just use document instead of the ownerDocument of endpoints[0],
+    // WebKit will throw on setStart/setEnd.  This is a WebKit bug, but it's in
+    // range, not selection, so we don't want to fail anything for it.
+    var range = ownerDocument(endpoints[0]).createRange();
+    range.setStart(endpoints[0], endpoints[1]);
+    range.setEnd(endpoints[2], endpoints[3]);
+    return range;
+}
+
+/**
+ * Given an array of endpoint data [start container, start offset, end
+ * container, end offset], sets the selection to have those endpoints.  Uses
+ * addRange, so the range will be forwards.  Accepts an empty array for
+ * endpoints, in which case the selection will just be emptied.
+ */
+function setSelectionForwards(endpoints) {
+    selection.removeAllRanges();
+    if (endpoints.length) {
+        selection.addRange(rangeFromEndpoints(endpoints));
+    }
+}
+
+/**
+ * Given an array of endpoint data [start container, start offset, end
+ * container, end offset], sets the selection to have those endpoints, with the
+ * direction backwards.  Uses extend, so it will throw in IE.  Accepts an empty
+ * array for endpoints, in which case the selection will just be emptied.
+ */
+function setSelectionBackwards(endpoints) {
+    selection.removeAllRanges();
+    if (endpoints.length) {
+        selection.collapse(endpoints[2], endpoints[3]);
+        selection.extend(endpoints[0], endpoints[1]);
+    }
+}
diff --git a/src/third_party/web_platform_tests/selection/deleteFromDocument.html b/src/third_party/web_platform_tests/selection/deleteFromDocument.html
new file mode 100644
index 0000000..b7e0360
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/deleteFromDocument.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<title>Selection.deleteFromDocument() tests</title>
+<link rel=author title="Aryeh Gregor" href=ayg@aryeh.name>
+<p>To debug test failures, add a query parameter with the test id (like
+"?5").  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";
+
+// We need to use explicit_done, because in Chrome 16 dev and Opera 12.00, the
+// second iframe doesn't block the load event -- even though it is added before
+// the load event.
+setup({explicit_done: true});
+
+// Specified by WebIDL
+test(function() {
+    assert_equals(Selection.prototype.deleteFromDocument.length, 0,
+        "Selection.prototype.deleteFromDocument.length must equal 0");
+}, "Selection.prototype.deleteFromDocument.length must equal 0");
+
+testDiv.parentNode.removeChild(testDiv);
+
+// Test an empty selection too
+testRanges.unshift("empty");
+
+var actualIframe = document.createElement("iframe");
+
+var expectedIframe = document.createElement("iframe");
+
+var referenceDoc = document.implementation.createHTMLDocument("");
+referenceDoc.removeChild(referenceDoc.documentElement);
+
+actualIframe.onload = function() {
+    expectedIframe.onload = function() {
+        for (var i = 0; i < testRanges.length; i++) {
+            if (location.search && i != Number(location.search)) {
+                continue;
+            }
+
+            test(function() {
+                initializeIframe(actualIframe, testRanges[i]);
+                initializeIframe(expectedIframe, testRanges[i]);
+
+                var actualRange = actualIframe.contentWindow.testRange;
+                var expectedRange = expectedIframe.contentWindow.testRange;
+
+                assert_equals(actualIframe.contentWindow.unexpectedException, null,
+                    "Unexpected exception thrown when setting up Range for actual deleteFromDocument");
+                assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+                    "Unexpected exception thrown when setting up Range for simulated deleteFromDocument");
+
+                actualIframe.contentWindow.getSelection().removeAllRanges();
+                if (testRanges[i] != "empty") {
+                    assert_equals(typeof actualRange, "object",
+                        "Range produced in actual iframe must be an object");
+                    assert_equals(typeof expectedRange, "object",
+                        "Range produced in expected iframe must be an object");
+                    assert_true(actualRange instanceof actualIframe.contentWindow.Range,
+                        "Range produced in actual iframe must be instanceof Range");
+                    assert_true(expectedRange instanceof expectedIframe.contentWindow.Range,
+                        "Range produced in expected iframe must be instanceof Range");
+                    actualIframe.contentWindow.getSelection().addRange(actualIframe.contentWindow.testRange);
+                    expectedIframe.contentWindow.testRange.deleteContents();
+                }
+                actualIframe.contentWindow.getSelection().deleteFromDocument();
+
+                assertNodesEqual(actualIframe.contentDocument, expectedIframe.contentDocument, "DOM contents");
+            }, "Range " + i + ": " + testRanges[i]);
+        }
+        actualIframe.style.display = "none";
+        expectedIframe.style.display = "none";
+        done();
+    };
+    expectedIframe.src = "test-iframe.html";
+    document.body.appendChild(expectedIframe);
+    referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
+};
+actualIframe.src = "test-iframe.html";
+document.body.appendChild(actualIframe);
+
+function initializeIframe(iframe, endpoints) {
+    while (iframe.contentDocument.firstChild) {
+        iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
+    }
+    iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
+    iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
+    iframe.contentWindow.setupRangeTests();
+    if (endpoints != "empty") {
+        iframe.contentWindow.testRangeInput = endpoints;
+        iframe.contentWindow.run();
+    }
+}
+</script>
diff --git a/src/third_party/web_platform_tests/selection/dir-manual.html b/src/third_party/web_platform_tests/selection/dir-manual.html
new file mode 100644
index 0000000..39cf655
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/dir-manual.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<title>Selection direction tests</title>
+<meta charset=utf-8>
+<div id=test>
+    <p>This is a manual test, since there's no way to synthesize keyboard or
+    mouse input.  Click after the letter "c" in the following paragraph and
+    drag backwards so that both the "b" and the "c" are highlighted, then click
+    the "Test" button:
+
+    <p>abcd <button onclick=testDirection()>Test</button>
+
+    <p>efghi
+</div>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+setup({explicit_done: true});
+
+function testDirection() {
+    var testDiv = document.getElementById("test");
+    var p = testDiv.getElementsByTagName("p")[1].firstChild;
+    var selection = getSelection();
+    var range = selection.getRangeAt(0);
+    test(function() {
+        assert_equals(range.toString(), "bc");
+    }, "The expected range is selected");
+    test(function() {
+        assert_equals(selection.anchorNode, p);
+        assert_equals(selection.focusNode, p);
+    }, "Expected node is initially selected");
+    test(function() {
+        assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [1, 3]);
+    }, "Expected offsets are initially selected (maybe not in order)");
+    test(function() {
+        assert_equals(selection.anchorOffset, 3);
+        assert_equals(selection.focusOffset, 1);
+    }, "Offsets are backwards for initial selection"),
+    test(function() {
+        assert_equals(selection.anchorNode, range.endContainer);
+        assert_equals(selection.anchorOffset, range.endOffset);
+        assert_equals(selection.focusNode, range.startContainer);
+        assert_equals(selection.focusOffset, range.startOffset);
+    }, "Offsets match the range for initial selection");
+
+    // Per spec, the direction of the selection remains even if you zap a range
+    // and add a new one.
+    test(function() {
+        selection.removeRange(range);
+        range = document.createRange();
+        p = testDiv.getElementsByTagName("p")[0].firstChild;
+        range.setStart(p, 0);
+        range.setEnd(p, 4);
+        assert_equals(range.toString(), "This");
+        selection.addRange(range);
+    }, "removeRange()/addRange() successful");
+    test(function() {
+        assert_equals(selection.anchorNode, p);
+        assert_equals(selection.focusNode, p);
+    }, "Expected node is selected after remove/addRange()");
+    test(function() {
+        assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [0, 4]);
+    }, "Expected offsets are selected after remove/addRange() (maybe not in order)");
+    test(function() {
+        assert_equals(selection.anchorOffset, 4);
+        assert_equals(selection.focusOffset, 0);
+    }, "Offsets are backwards after remove/addRange()"),
+    test(function() {
+        assert_equals(selection.anchorNode, range.endContainer);
+        assert_equals(selection.anchorOffset, range.endOffset);
+        assert_equals(selection.focusNode, range.startContainer);
+        assert_equals(selection.focusOffset, range.startOffset);
+    }, "Offsets match the range after remove/addRange()");
+
+    // But if you call removeAllRanges(), the direction should reset to
+    // forwards.
+    test(function() {
+        selection.removeAllRanges();
+        range = document.createRange();
+        p = testDiv.getElementsByTagName("p")[2].firstChild;
+        range.setStart(p, 2);
+        range.setEnd(p, 5);
+        assert_equals(range.toString(), "ghi");
+        selection.addRange(range);
+    }, "removeAllRanges() successful");
+    test(function() {
+        assert_equals(selection.anchorNode, p);
+        assert_equals(selection.focusNode, p);
+    }, "Expected node is selected after removeAllRanges()");
+    test(function() {
+        assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [2, 5]);
+    }, "Expected offsets are selected after removeAllRanges() (maybe not in order)");
+    test(function() {
+        assert_equals(selection.anchorOffset, 2);
+        assert_equals(selection.focusOffset, 5);
+    }, "Offsets are forwards after removeAllRanges()");
+    test(function() {
+        assert_equals(selection.anchorNode, range.startContainer);
+        assert_equals(selection.anchorOffset, range.startOffset);
+        assert_equals(selection.focusNode, range.endContainer);
+        assert_equals(selection.focusOffset, range.endOffset);
+    }, "Offsets match the range after removeAllRanges()");
+
+    done();
+}
+</script>
diff --git a/src/third_party/web_platform_tests/selection/extend.html b/src/third_party/web_platform_tests/selection/extend.html
new file mode 100644
index 0000000..bc8fe2c
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/extend.html
@@ -0,0 +1,148 @@
+<!doctype html>
+<title>Selection extend() tests</title>
+<meta charset=utf-8>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<div id=log></div>
+<script>
+"use strict";
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+/**
+ * We test Selections that go both forwards and backwards here.  In the latter
+ * case we need to use extend() to force it to go backwards, which is fair
+ * enough, since that's what we're testing.  We test collapsed selections only
+ * once.
+ */
+for (var i = 0; i < testRanges.length; i++) {
+    var endpoints = eval(testRanges[i]);
+    for (var j = 0; j < testPoints.length; j++) {
+        if (endpoints[0] == endpoints[2]
+        && endpoints[1] == endpoints[3]) {
+            // Test collapsed selections only once
+            test(function() {
+                setSelectionForwards(endpoints);
+                testExtend(endpoints, eval(testPoints[j]));
+            }, "extend() with range " + i + " " + testRanges[i]
+            + " and point " + j + " " + testPoints[j]);
+        } else {
+            test(function() {
+                setSelectionForwards(endpoints);
+                testExtend(endpoints, eval(testPoints[j]));
+            }, "extend() forwards with range " + i + " " + testRanges[i]
+            + " and point " + j + " " + testPoints[j]);
+
+            test(function() {
+                setSelectionBackwards(endpoints);
+                testExtend(endpoints, eval(testPoints[j]));
+            }, "extend() backwards with range " + i + " " + testRanges[i]
+            + " and point " + j + " " + testPoints[j]);
+        }
+    }
+}
+
+function testExtend(endpoints, target) {
+    assert_equals(getSelection().rangeCount, endpoints.length/4,
+        "Sanity check: rangeCount must be correct");
+
+    var node = target[0];
+    var offset = target[1];
+
+    // "If the context object's range is null, throw an InvalidStateError
+    // exception and abort these steps."
+    if (getSelection().rangeCount == 0) {
+        assert_throws("INVALID_STATE_ERR", function() {
+            selection.extend(node, offset);
+        }, "extend() when rangeCount is 0 must throw InvalidStateError");
+        return;
+    }
+
+    assert_equals(getSelection().getRangeAt(0).startContainer, endpoints[0],
+        "Sanity check: startContainer must be correct");
+    assert_equals(getSelection().getRangeAt(0).startOffset, endpoints[1],
+        "Sanity check: startOffset must be correct");
+    assert_equals(getSelection().getRangeAt(0).endContainer, endpoints[2],
+        "Sanity check: endContainer must be correct");
+    assert_equals(getSelection().getRangeAt(0).endOffset, endpoints[3],
+        "Sanity check: endOffset must be correct");
+
+    // "Let anchor and focus be the context object's anchor and focus, and let
+    // new focus be the boundary point (node, offset)."
+    var anchorNode = getSelection().anchorNode;
+    var anchorOffset = getSelection().anchorOffset;
+    var focusNode = getSelection().focusNode;
+    var focusOffset = getSelection().focusOffset;
+
+    // "Let new range be a new range."
+    //
+    // We'll always be setting either new range's start or its end to new
+    // focus, so we'll always throw at some point.  Test that now.
+    //
+    // From DOM4's "set the start or end of a range": "If node is a doctype,
+    // throw an "InvalidNodeTypeError" exception and terminate these steps."
+    if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+        assert_throws("INVALID_NODE_TYPE_ERR", function() {
+            selection.extend(node, offset);
+        }, "extend() to a doctype must throw InvalidNodeTypeError");
+        return;
+    }
+
+    // From DOM4's "set the start or end of a range": "If offset is greater
+    // than node's length, throw an "IndexSizeError" exception and terminate
+    // these steps."
+    //
+    // FIXME: We should be casting offset to an unsigned int per WebIDL.  Until
+    // we do, we need the offset < 0 check too.
+    if (offset < 0 || offset > getNodeLength(node)) {
+        assert_throws("INDEX_SIZE_ERR", function() {
+            selection.extend(node, offset);
+        }, "extend() to an offset that's greater than node length (" + getNodeLength(node) + ") must throw IndexSizeError");
+        return;
+    }
+
+    // Now back to the editing spec.
+    var originalRange = getSelection().getRangeAt(0);
+
+    // "If node's root is not the same as the context object's range's root,
+    // set new range's start and end to (node, offset)."
+    //
+    // "Otherwise, if anchor is before or equal to new focus, set new range's
+    // start to anchor, then set its end to new focus."
+    //
+    // "Otherwise, set new range's start to new focus, then set its end to
+    // anchor."
+    //
+    // "Set the context object's range to new range."
+    //
+    // "If new focus is before anchor, set the context object's direction to
+    // backwards. Otherwise, set it to forwards."
+    //
+    // The upshot of all these is summed up by just testing the anchor and
+    // offset.
+    getSelection().extend(node, offset);
+
+    if (furthestAncestor(anchorNode) == furthestAncestor(node)) {
+        assert_equals(getSelection().anchorNode, anchorNode,
+            "anchorNode must not change if the node passed to extend() has the same root as the original range");
+        assert_equals(getSelection().anchorOffset, anchorOffset,
+            "anchorOffset must not change if the node passed to extend() has the same root as the original range");
+    } else {
+        assert_equals(getSelection().anchorNode, node,
+            "anchorNode must be the node passed to extend() if it has a different root from the original range");
+        assert_equals(getSelection().anchorOffset, offset,
+            "anchorOffset must be the offset passed to extend() if the node has a different root from the original range");
+    }
+    assert_equals(getSelection().focusNode, node,
+        "focusNode must be the node passed to extend()");
+    assert_equals(getSelection().focusOffset, offset,
+        "focusOffset must be the offset passed to extend()");
+    assert_not_equals(getSelection().getRangeAt(0), originalRange,
+        "extend() must replace any existing range with a new one, not mutate the existing one");
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/src/third_party/web_platform_tests/selection/getRangeAt.html b/src/third_party/web_platform_tests/selection/getRangeAt.html
new file mode 100644
index 0000000..3c6d797
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/getRangeAt.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>The getRangeAt method</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(function() {
+  var sel = getSelection();
+  var range = document.createRange();
+  sel.addRange(range);
+  assert_throws("INDEX_SIZE_ERR", function() { sel.getRangeAt(-1); })
+  assert_throws("INDEX_SIZE_ERR", function() { sel.getRangeAt(1); })
+});
+</script>
diff --git a/src/third_party/web_platform_tests/selection/getSelection.html b/src/third_party/web_platform_tests/selection/getSelection.html
new file mode 100644
index 0000000..ea119f2
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/getSelection.html
@@ -0,0 +1,160 @@
+<!doctype html>
+<title>getSelection() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+// TODO: Figure out more places where defaultView is or is not guaranteed to be
+// null, and test whether getSelection() is null.
+//
+// TODO: Figure out a good way to test display: none iframes.
+
+test(function() {
+    // Sanity checks like this are to flag known browser bugs with clearer
+    // error messages, instead of throwing inscrutable exceptions.
+    assert_true("Selection" in window,
+        "Sanity check: window must have Selection property");
+
+    assert_true(window.getSelection() instanceof Selection);
+}, "window.getSelection() instanceof Selection");
+
+test(function() {
+    assert_equals(window.getSelection(), window.getSelection());
+}, "window.getSelection() === window.getSelection()");
+
+test(function() {
+    assert_true("Selection" in window,
+        "Sanity check: window must have Selection property");
+    // This sanity check (which occurs a number of times below, too) is because
+    // document.getSelection() is supposed to return null if defaultView is
+    // null, so we need to figure out whether defaultView is null or not before
+    // we can make correct assertions about getSelection().
+    assert_not_equals(document.defaultView, null,
+        "Sanity check: document.defaultView must not be null");
+
+    assert_equals(typeof document.getSelection(), "object",
+        "document.getSelection() must be an object");
+    assert_true(document.getSelection() instanceof Selection);
+}, "document.getSelection() instanceof Selection");
+
+test(function() {
+    assert_not_equals(document.defaultView, null,
+        "Sanity check: document.defaultView must not be null");
+    assert_equals(document.getSelection(), document.getSelection());
+}, "document.getSelection() === document.getSelection()");
+
+test(function() {
+    assert_not_equals(document.defaultView, null,
+        "Sanity check: document.defaultView must not be null");
+    assert_equals(window.getSelection(), document.getSelection());
+}, "window.getSelection() === document.getSelection()");
+
+// "Each selection is associated with a single range, which may be null and is
+// initially null."
+//
+// "The rangeCount attribute must return 0 if the context object's range is
+// null, otherwise 1."
+test(function() {
+    assert_equals(window.getSelection().rangeCount, 0,
+        "window.getSelection().rangeCount must initially be 0");
+    assert_equals(typeof document.getSelection(), "object",
+        "Sanity check: document.getSelection() must be an object");
+    assert_equals(document.getSelection().rangeCount, 0,
+        "document.getSelection().rangeCount must initially be 0");
+}, "Selection's range must initially be null");
+
+test(function() {
+    var doc = document.implementation.createHTMLDocument("");
+    assert_equals(doc.defaultView, null,
+        "Sanity check: defaultView of created HTML document must be null");
+    assert_equals(doc.getSelection(), null);
+}, "getSelection() on HTML document with null defaultView must be null");
+
+test(function() {
+    var xmlDoc = document.implementation.createDocument(null, "", null);
+
+    assert_true("getSelection" in xmlDoc, "XML document must have getSelection()");
+
+    assert_equals(xmlDoc.defaultView, null,
+        "Sanity check: defaultView of created XML document must be null");
+    assert_equals(xmlDoc.getSelection(), null);
+}, "getSelection() on XML document with null defaultView must be null");
+
+
+// Run a bunch of iframe tests, once immediately after the iframe is appended
+// to the document and once onload.  This makes a difference, because browsers
+// differ (at the time of this writing) in whether they load about:blank in
+// iframes synchronously or not.  Per the HTML spec, there must be a browsing
+// context associated with the iframe as soon as it's appended to the document,
+// so there should be a selection too.
+var iframe = document.createElement("iframe");
+add_completion_callback(function() {
+    document.body.removeChild(iframe);
+});
+
+var testDescs = [];
+var testFuncs = [];
+testDescs.push("window.getSelection() instanceof Selection in an iframe");
+testFuncs.push(function() {
+    assert_true("Selection" in iframe.contentWindow,
+        "Sanity check: window must have Selection property");
+    assert_not_equals(iframe.contentWindow.document.defaultView, null,
+        "Sanity check: document.defaultView must not be null");
+    assert_not_equals(iframe.contentWindow.getSelection(), null,
+        "window.getSelection() must not be null");
+    assert_true(iframe.contentWindow.getSelection() instanceof iframe.contentWindow.Selection);
+});
+
+testDescs.push("document.getSelection() instanceof Selection in an iframe");
+testFuncs.push(function() {
+    assert_true("Selection" in iframe.contentWindow,
+        "Sanity check: window must have Selection property");
+    assert_not_equals(iframe.contentDocument.defaultView, null,
+        "Sanity check: document.defaultView must not be null");
+    assert_not_equals(iframe.contentDocument.getSelection(), null,
+        "document.getSelection() must not be null");
+    assert_equals(typeof iframe.contentDocument.getSelection(), "object",
+        "document.getSelection() must be an object");
+    assert_true(iframe.contentDocument.getSelection() instanceof iframe.contentWindow.Selection);
+});
+
+testDescs.push("window.getSelection() === document.getSelection() in an iframe");
+testFuncs.push(function() {
+    assert_not_equals(iframe.contentDocument.defaultView, null,
+        "Sanity check: document.defaultView must not be null");
+    assert_equals(iframe.contentWindow.getSelection(), iframe.contentDocument.getSelection());
+});
+
+testDescs.push("getSelection() inside and outside iframe must return different objects");
+testFuncs.push(function() {
+    assert_not_equals(iframe.contentWindow.getSelection(), getSelection());
+});
+
+testDescs.push("getSelection() on HTML document with null defaultView must be null inside an iframe");
+testFuncs.push(function() {
+    var doc = iframe.contentDocument.implementation.createHTMLDocument("");
+    assert_equals(doc.defaultView, null,
+        "Sanity check: defaultView of created HTML document must be null");
+    assert_equals(doc.getSelection(), null);
+});
+
+var asyncTests = [];
+testDescs.forEach(function(desc) {
+    asyncTests.push(async_test(desc + " onload"));
+});
+
+iframe.onload = function() {
+    asyncTests.forEach(function(t, i) {
+        t.step(testFuncs[i]);
+        t.done();
+    });
+};
+
+document.body.appendChild(iframe);
+
+testDescs.forEach(function(desc, i) {
+    test(testFuncs[i], desc + " immediately after appendChild");
+});
+</script>
diff --git a/src/third_party/web_platform_tests/selection/interfaces.html b/src/third_party/web_platform_tests/selection/interfaces.html
new file mode 100644
index 0000000..7174686
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/interfaces.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<title>Selection interface tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/resources/WebIDLParser.js></script>
+<script src=/resources/idlharness.js></script>
+<script type=text/plain>
+interface Selection {
+  readonly attribute Node? anchorNode;
+  readonly attribute unsigned long anchorOffset;
+  readonly attribute Node? focusNode;
+  readonly attribute unsigned long focusOffset;
+
+  readonly attribute boolean isCollapsed;
+  void               collapse(Node node, unsigned long offset);
+  void               collapseToStart();
+  void               collapseToEnd();
+
+  void               extend(Node node, unsigned long offset);
+
+  void               selectAllChildren(Node node);
+  void               deleteFromDocument();
+
+  readonly attribute unsigned long rangeCount;
+  Range              getRangeAt(unsigned long index);
+  void               addRange(Range range);
+  void               removeRange(Range range);
+  void               removeAllRanges();
+
+  stringifier;
+};
+</script>
+<script>
+"use strict";
+
+var idlArray = new IdlArray();
+idlArray.add_idls(document.querySelector("script[type=text\\/plain]").textContent);
+idlArray.add_objects({Selection: ['getSelection()']});
+idlArray.test();
+</script>
diff --git a/src/third_party/web_platform_tests/selection/isCollapsed.html b/src/third_party/web_platform_tests/selection/isCollapsed.html
new file mode 100644
index 0000000..113a16d
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/isCollapsed.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>Selection.isCollapsed tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+test(function() {
+    selection.removeAllRanges();
+    assert_true(selection.isCollapsed, "isCollapsed must be true if both anchor and focus are null");
+}, "Empty selection");
+
+for (var i = 0; i < testRanges.length; i++) {
+    test(function() {
+        selection.removeAllRanges();
+        var endpoints = eval(testRanges[i]);
+        var range = ownerDocument(endpoints[0]).createRange();
+        range.setStart(endpoints[0], endpoints[1]);
+        range.setEnd(endpoints[2], endpoints[3]);
+        selection.addRange(range);
+
+        assert_equals(selection.isCollapsed,
+            endpoints[0] === endpoints[2] && endpoints[1] === endpoints[3],
+            "Value of isCollapsed");
+    }, "Range " + i + " " + testRanges[i]);
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/src/third_party/web_platform_tests/selection/removeAllRanges.html b/src/third_party/web_platform_tests/selection/removeAllRanges.html
new file mode 100644
index 0000000..cc6b692
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/removeAllRanges.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<title>Selection.removeAllRanges() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+var range = rangeFromEndpoints([paras[0].firstChild, 0, paras[0].firstChild, 1]);
+
+for (var i = 0; i < testRanges.length; i++) {
+    test(function() {
+        setSelectionForwards(eval(testRanges[i]));
+        selection.removeAllRanges();
+        assert_equals(selection.rangeCount, 0,
+            "After removeAllRanges(), rangeCount must be 0");
+        // Test that it's forwards
+        selection.addRange(range);
+        assert_equals(selection.anchorOffset, selection.getRangeAt(0).startOffset,
+            "After removeAllRanges(), addRange() must be forwards, so anchorOffset must equal startOffset rather than endOffset");
+        assert_equals(selection.focusOffset, selection.getRangeAt(0).endOffset,
+            "After removeAllRanges(), addRange() must be forwards, so focusOffset must equal endOffset rather than startOffset");
+    }, "Range " + i + " " + testRanges[i] + " forwards");
+
+    // Copy-pasted from above
+    test(function() {
+        setSelectionBackwards(eval(testRanges[i]));
+        selection.removeAllRanges();
+        assert_equals(selection.rangeCount, 0,
+            "After removeAllRanges(), rangeCount must be 0");
+        // Test that it's forwards
+        selection.addRange(range);
+        assert_equals(selection.anchorOffset, selection.getRangeAt(0).startOffset,
+            "After removeAllRanges(), addRange() must be forwards, so anchorOffset must equal startOffset rather than endOffset");
+        assert_equals(selection.focusOffset, selection.getRangeAt(0).endOffset,
+            "After removeAllRanges(), addRange() must be forwards, so focusOffset must equal endOffset rather than startOffset");
+    }, "Range " + i + " " + testRanges[i] + " backwards");
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/src/third_party/web_platform_tests/selection/selectAllChildren.html b/src/third_party/web_platform_tests/selection/selectAllChildren.html
new file mode 100644
index 0000000..904e0cb
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/selectAllChildren.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<title>Selection.selectAllChildren tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+testRanges.unshift("[]");
+
+for (var i = 0; i < testRanges.length; i++) {
+    var endpoints = eval(testRanges[i]);
+
+    for (var j = 0; j < testNodes.length; j++) {
+        var node = eval(testNodes[j]);
+
+        test(function() {
+            setSelectionForwards(endpoints);
+            var originalRange = getSelection().rangeCount
+                ? getSelection().getRangeAt(0)
+                : null;
+
+            if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+                assert_throws("INVALID_NODE_TYPE_ERR", function() {
+                    selection.selectAllChildren(node);
+                }, "selectAllChildren() on a DocumentType must throw InvalidNodeTypeError");
+                return;
+            }
+
+            selection.selectAllChildren(node);
+            // This implicitly tests that the selection is forwards, by using
+            // anchorOffset/focusOffset instead of getRangeAt.
+            assert_equals(selection.rangeCount, 1,
+                "After selectAllChildren, rangeCount must be 1");
+            assert_equals(selection.anchorNode, node,
+                "After selectAllChildren, anchorNode must be the given node");
+            assert_equals(selection.anchorOffset, 0,
+                "After selectAllChildren, anchorOffset must be 0");
+            assert_equals(selection.focusNode, node,
+                "After selectAllChildren, focusNode must be the given node");
+            assert_equals(selection.focusOffset, node.childNodes.length,
+                "After selectAllChildren, focusOffset must be the given node's number of children");
+            if (originalRange) {
+                assert_not_equals(getSelection().getRangeAt(0), originalRange,
+                    "selectAllChildren must replace any existing range, not mutate it");
+            }
+        }, "Range " + i + " " + testRanges[i] + ", node " + j + " " + testNodes[j]);
+    }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/src/third_party/web_platform_tests/selection/test-iframe.html b/src/third_party/web_platform_tests/selection/test-iframe.html
new file mode 100644
index 0000000..42b9823
--- /dev/null
+++ b/src/third_party/web_platform_tests/selection/test-iframe.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>Selection test iframe</title>
+<link rel=author title="Aryeh Gregor" href=ayg@aryeh.name>
+<body>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// This script only exists because we want to evaluate the range endpoints
+// in each iframe using that iframe's local variables set up by common.js.  It
+// just creates a range with the endpoints given by
+// eval(window.testRangeInput), and assigns the result to window.testRange.  If
+// there's an exception, it's assigned to window.unexpectedException.
+// Everything else is to be done by the script that created the iframe.
+window.unexpectedException = null;
+
+function run() {
+    window.unexpectedException = null;
+    try {
+        window.testRange = rangeFromEndpoints(eval(window.testRangeInput));
+    } catch(e) {
+        window.unexpectedException = e;
+    }
+}
+
+// Remove the scripts so they don't run repeatedly when the iframe is
+// reinitialized
+[].forEach.call(document.querySelectorAll("script"), function(script) {
+    script.parentNode.removeChild(script);
+});
+
+testDiv.style.display = "none";
+</script>