Take selection tests too
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Wed, 05 Oct 2011 11:44:20 -0600
changeset 631 a589a8d60a6a
parent 630 197e383926ac
child 632 a100007a751b
Take selection tests too

Reported-By: Ms2ger
Report-URL: http://www.w3.org/Bugs/Public/show_bug.cgi?id=14248#c2
selecttest/Selection-addRange.html
selecttest/Selection-collapse.html
selecttest/Selection-collapseToEnd.html
selecttest/Selection-collapseToStart.html
selecttest/Selection-dir.html
selecttest/Selection-extend.html
selecttest/Selection-getRangeAt.html
selecttest/common.js
selecttest/support/testharness.css
selecttest/support/testharness.js
selecttest/support/testharnessreport.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/Selection-addRange.html	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,120 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<div id=log></div>
+<script src=support/testharness.js></script>
+<script src=support/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// TODO: Test addRange() on a non-empty Selection, once we have a usable spec
+// for that.
+
+function testAddRange(endpoints) {
+	try {
+		selection.removeAllRanges();
+	} catch (e) {
+		throw {message: "Exception thrown during removeAllRanges(): " + e};
+	}
+	try {
+		var range = ownerDocument(endpoints[0]).createRange();
+		range.setStart(endpoints[0], endpoints[1]);
+		range.setEnd(endpoints[2], endpoints[3]);
+	} catch (e) {
+		throw {message: "Exception thrown during Range creation: " + e};
+	}
+
+	assert_equals(range.startContainer, endpoints[0],
+		"Sanity check: the Range we created must have the desired startContainer");
+	assert_equals(range.startOffset, endpoints[1],
+		"Sanity check: the Range we created must have the desired startOffset");
+	assert_equals(range.endContainer, endpoints[2],
+		"Sanity check: the Range we created must have the desired endContainer");
+	assert_equals(range.endOffset, endpoints[3],
+		"Sanity check: the Range we created must have the desired endOffset");
+
+	try {
+		selection.addRange(range);
+	} catch (e) {
+		throw {message: "Exception thrown during addRange(): " + e};
+	}
+
+	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");
+
+	assert_equals(selection.rangeCount, 1,
+		"After removeAllRanges() and addRange(), rangeCount must be 1");
+	assert_not_equals(selection.getRangeAt(0), null,
+		"After addRange(), getRangeAt(0) must not return null");
+	assert_equals(typeof selection.getRangeAt(0), "object",
+		"After addRange(), getRangeAt(0) must return an object");
+
+	assert_equals(selection.getRangeAt(0).startContainer, range.startContainer,
+		"After addRange(), startContainer of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).startOffset, range.startOffset,
+		"After addRange(), startOffset of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).endContainer, range.endContainer,
+		"After addRange(), endContainer of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).endOffset, range.endOffset,
+		"After addRange(), endOffset of the Range returned by getRangeAt(0) must match the added Range");
+
+	assert_equals(selection.getRangeAt(0), range,
+		"getRangeAt(0) must return the same object we added");
+
+	// Let's not test many different modifications -- one should be enough.
+	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);
+	}
+
+	assert_equals(selection.getRangeAt(0).startContainer, range.startContainer,
+		"After mutating the added Range, startContainer of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).startOffset, range.startOffset,
+		"After mutating the added Range, startOffset of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).endContainer, range.endContainer,
+		"After mutating the added Range, endContainer of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).endOffset, range.endOffset,
+		"After mutating the added Range, endOffset of the Range returned by getRangeAt(0) must match the added Range");
+
+	// Now test the other way too.
+	if (selection.getRangeAt(0).startContainer == paras[0].firstChild
+	&& selection.getRangeAt(0).startOffset == 4
+	&& selection.getRangeAt(0).endContainer == paras[0].firstChild
+	&& selection.getRangeAt(0).endOffset == 6) {
+		selection.getRangeAt(0).setStart(paras[0].firstChild, 5);
+	} else {
+		selection.getRangeAt(0).setStart(paras[0].firstChild, 4);
+		selection.getRangeAt(0).setStart(paras[0].firstChild, 6);
+	}
+
+	assert_equals(selection.getRangeAt(0).startContainer, range.startContainer,
+		"After mutating the result of getRangeAt(0), startContainer of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).startOffset, range.startOffset,
+		"After mutating the result of getRangeAt(0), startOffset of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).endContainer, range.endContainer,
+		"After mutating the result of getRangeAt(0), endContainer of the Range returned by getRangeAt(0) must match the added Range");
+	assert_equals(selection.getRangeAt(0).endOffset, range.endOffset,
+		"After mutating the result of getRangeAt(0), endOffset of the Range returned by getRangeAt(0) must match the added Range");
+}
+
+var tests = [];
+for (var i = 0; i < testRanges.length; i++) {
+	tests.push(["Range " + i + " " + testRanges[i], eval(testRanges[i])]);
+}
+
+generate_tests(testAddRange, tests);
+
+testDiv.style.display = "none";
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/Selection-collapse.html	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,90 @@
+<!doctype html>
+<title>Selection.collapse() tests</title>
+<div id=log></div>
+<script src=support/testharness.js></script>
+<script src=support/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// TODO: Test a Selection with multiple Ranges.
+
+function testCollapse(range, point) {
+	selection.removeAllRanges();
+	var addedRange;
+	if (range) {
+		addedRange = range.cloneRange();
+		selection.addRange(addedRange);
+	}
+
+	if (point[0].nodeType == Node.DOCUMENT_TYPE_NODE
+	|| point[0].nodeType == Node.PROCESSING_INSTRUCTION_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 or ProcessingInstruction");
+		return;
+	}
+
+	if (point[1] < 0 || point[1] > nodeLength(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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/Selection-collapseToEnd.html	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>The collapseToEnd method</title>
+<div id=log></div>
+<script src=support/testharness.js></script>
+<script src=support/testharnessreport.js></script>
+<script>
+test(function() {
+  var sel = getSelection();
+  sel.removeAllRanges();
+  assert_throws("INVALID_STATE_ERR", function() { sel.collapseToEnd(); })
+});
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/Selection-collapseToStart.html	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>The collapseToStart method</title>
+<div id=log></div>
+<script src=support/testharness.js></script>
+<script src=support/testharnessreport.js></script>
+<script>
+test(function() {
+  var sel = getSelection();
+  sel.removeAllRanges();
+  assert_throws("INVALID_STATE_ERR", function() { sel.collapseToStart(); })
+});
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/Selection-dir.html	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,103 @@
+<!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=support/testharness.js></script>
+<script src=support/testharnessreport.js></script>
+<script>
+function testDirection() {
+	var testDiv = document.getElementById("test");
+	var p = testDiv.getElementsByTagName("p")[1].firstChild;
+	var selection = getSelection();
+	// Spec says the last Range is the one that counts
+	var range = selection.getRangeAt(selection.rangeCount - 1);
+	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_unsorted([selection.anchorOffset, selection.focusOffset], [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_unsorted([selection.anchorOffset, selection.focusOffset], [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_unsorted([selection.anchorOffset, selection.focusOffset], [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()");
+}
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/Selection-extend.html	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,246 @@
+<!doctype html>
+<title>Selection extend() tests</title>
+<meta charset=utf-8>
+<body>
+<script src=support/testharness.js></script>
+<script src=support/testharnessreport.js></script>
+<script src=common.js></script>
+<div id=log></div>
+<script>
+/**
+ * Returns "forwards" if the selection direction is forwards, "backwards" if
+ * it's backwards.  This appears not to work in WebKit at all, because there
+ * seems to be no way of adding a range or replacing the current range without
+ * calling removeAllRanges(), which resets the direction.  So we're nice and
+ * look at the current range if possible; otherwise we do some stuff that
+ * involves calling removeRange(), which doesn't exist in WebKit, so it will
+ * fail the test.
+ */
+function getSelectionDirection() {
+	if (selection.anchorNode != selection.focusNode
+	|| selection.anchorOffset != selection.focusOffset) {
+		var range = selection.getRangeAt(selection.rangeCount - 1);
+		// We can determine the direction without mangling anything.
+		if (selection.anchorNode == range.startContainer
+		&& selection.anchorOffset == range.startOffset) {
+			return "forwards";
+		}
+		if (selection.anchorNode == range.endContainer
+		&& selection.anchorOffset == range.endOffset) {
+			return "backwards";
+		}
+		throw "Something buggy with directions";
+	}
+
+	var range = document.createRange();
+	range.setStart(paras[0].firstChild, 0);
+	range.setEnd(paras[0].firstChild, 1);
+	selection.addRange(range);
+	if (selection.anchorOffset == range.startOffset) {
+		selection.removeRange(range);
+		return "forwards";
+	}
+	if (selection.anchorOffset == range.endOffset) {
+		selection.removeRange(range);
+		return "backwards";
+	}
+}
+
+/**
+ * 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.
+ */
+
+var originalSelectionDirection;
+
+function testExtendForwards(initialRanges, extendTarget) {
+	originalSelectionDirection = "forwards";
+	selection.removeAllRanges();
+
+	for (var i = 0; i < initialRanges.length; i += 4) {
+		var range = ownerDocument(initialRanges[i]).createRange();
+		range.setStart(initialRanges[i], initialRanges[i + 1]);
+		range.setEnd(initialRanges[i + 2], initialRanges[i + 3]);
+		selection.addRange(range);
+	}
+
+	testExtend(extendTarget, initialRanges.length/4);
+}
+
+function testExtendBackwards(initialRanges, extendTarget) {
+	originalSelectionDirection = "backwards";
+	selection.removeAllRanges();
+
+	for (var i = 0; i < initialRanges.length; i += 4) {
+		// To get a backwards selection, we add ranges by appending a
+		// zero-length range at the end, then extend()ing backwards to the
+		// start.  This fails in Opera, since Opera ignores addRange() on a
+		// collapsed range.  FIXME: This doesn't actually make the initial
+		// selection backwards, if the range we're given is collapsed.
+		var range = ownerDocument(initialRanges[i]).createRange();
+		range.setStart(initialRanges[i + 2], initialRanges[i + 3]);
+		range.setEnd(initialRanges[i + 2], initialRanges[i + 3]);
+		selection.addRange(range);
+		selection.extend(initialRanges[i], initialRanges[i + 1]);
+	}
+
+	testExtend(extendTarget, initialRanges.length/4);
+}
+
+function testExtend(extendTarget, numRanges) {
+	assert_equals(selection.rangeCount, numRanges,
+		"Failed sanity check: selection.rangeCount is wrong.  Perhaps addRange() failed.");
+
+	var node = extendTarget[0];
+	var offset = extendTarget[1];
+
+	if (node === null) {
+		assert_throws("TYPE_MISMATCH_ERR", function() {
+			selection.extend(node, offset);
+		}, "extend(null, foo) must throw TYPE_MISMATCH_ERR");
+		return;
+	}
+
+	if (selection.rangeCount == 0) {
+		assert_throws("INVALID_STATE_ERR", function() {
+			selection.extend(node, offset);
+		}, "extend() when rangeCount is 0 must throw INVALID_STATE_ERR");
+		return;
+	}
+
+	if (node.nodeType == Node.DOCUMENT_TYPE_NODE
+	|| node.nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
+		assert_throws("INVALID_NODE_TYPE_ERR", function() {
+			selection.extend(node, offset);
+		}, "extend() to a doctype or PI must throw INVALID_NODE_TYPE_ERR");
+		return;
+	}
+
+	if (offset < 0 || offset > nodeLength(node)) {
+		assert_throws("INDEX_SIZE_ERR", function() {
+			selection.extend(node, offset);
+		}, "extend() to an offset that's negative or greater than node length (" + nodeLength(node) + ") must throw INDEX_SIZE_ERR");
+		return;
+	}
+
+	var range = selection.getRangeAt(selection.rangeCount - 1);
+	var rangeRoot = furthestAncestor(range.startContainer);
+	var nodeRoot = furthestAncestor(node);
+
+	assert_equals(rangeRoot, furthestAncestor(range.endContainer),
+		"The furthest ancestor of a Range's start and end must always be the same (I think)");
+
+	if (rangeRoot != nodeRoot) {
+		selection.extend(node, offset);
+		assert_equals(selection.anchorNode, node,
+			"If the furthest ancestors of the range and extend() target differ, anchorNode must be set to the target node");
+		assert_equals(selection.anchorOffset, offset,
+			"If the furthest ancestors of the range and extend() target differ, anchorOffset must be set to the target offset");
+		assert_equals(selection.focusNode, node,
+			"If the furthest ancestors of the range and extend() target differ, focusNode must be set to the target node");
+		assert_equals(selection.focusOffset, offset,
+			"If the furthest ancestors of the range and extend() target differ, focusOffset must be set to the target offset");
+		assert_equals(getSelectionDirection(), "backwards",
+			"If the furthest ancestors of the range and extent() target differ, the new selection must be backwards");
+		return;
+	}
+
+	if (selection.focusNode == node && selection.focusOffset == offset) {
+		// extend() must do nothing.
+		var oldFocusNode = selection.focusNode;
+		var oldFocusOffset = selection.focusOffset;
+		var oldAnchorNode = selection.anchorNode;
+		var oldAnchorOffset = selection.anchorOffset;
+		var oldRanges = [];
+		for (var i = 0; i < selection.rangeCount; i++) {
+			oldRanges.push(selection.getRangeAt(i));
+		}
+		selection.extend(node, offset);
+		assert_equals(selection.focusNode, oldFocusNode,
+			"extend() to the current focus must not change focusNode");
+		assert_equals(selection.focusOffset, oldFocusOffset,
+			"extend() to the current focus must not change focusOffset");
+		assert_equals(selection.anchorNode, oldAnchorNode,
+			"extend() to the current focus must not change anchorNode");
+		assert_equals(selection.anchorOffset, oldAnchorOffset,
+			"extend() to the current focus must not change anchorOffset");
+		assert_equals(selection.rangeCount, oldRanges.length,
+			"extend() to the current focus must not change rangeCount");
+		for (var i = 0; i < oldRanges.length; i++) {
+			assert_equals(selection.getRangeAt(i), oldRanges[i],
+				"extend() to the current focus must not change any Ranges");
+		}
+		assert_equals(getSelectionDirection(), originalSelectionDirection,
+			"extend() of a selection to the current focus must not change direction");
+		return;
+	}
+
+	var oldAnchorNode = selection.anchorNode;
+	var oldAnchorOffset = selection.anchorOffset;
+	var oldFocusNode = selection.focusNode;
+	var oldFocusOffset = selection.focusOffset;
+	var oldRanges = [];
+	for (var i = 0; i < selection.rangeCount; i++) {
+		oldRanges.push(selection.getRangeAt(i));
+	}
+	selection.extend(node, offset);
+	assert_equals(selection.anchorNode, oldAnchorNode,
+		"extend() must not change anchorNode in the usual case");
+	assert_equals(selection.anchorOffset, oldAnchorOffset,
+		"extend() must not change anchorOffset in the usual case");
+	assert_equals(selection.rangeCount, oldRanges.length,
+		"extend() must not change rangeCount in the usual case");
+	for (var i = 0; i < oldRanges.length - 1; i++) {
+		assert_equals(selection.getRangeAt(i), oldRanges[i],
+			"extend() must not change any Range but the last in the usual case");
+	}
+	assert_equals(selection.focusNode, node,
+		"extend() must update focusNode to the target node in the usual case");
+	assert_equals(selection.focusOffset, offset,
+		"extend() must update focusOffset to the target offset in the usual case");
+
+	var expectedDirection;
+	var range = document.createRange();
+	range.setStart(oldAnchorNode, oldAnchorOffset);
+	range.setEnd(oldAnchorNode, oldAnchorOffset);
+	if (range.comparePoint(node, offset) >= 0) {
+		expectedDirection = "forwards";
+	} else {
+		expectedDirection = "backwards";
+	}
+	assert_equals(getSelectionDirection(), expectedDirection,
+		"extend() must set direction appropriately in the usual case");
+}
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+var tests = [];
+for (var i = 0; i < testRanges.length; i++) {
+	for (var j = 0; j < testPoints.length; j++) {
+		tests.push([
+			"extend() forwards with range " + i + " " + testRanges[i] + " and point " + j + " " + testPoints[j],
+			eval(testRanges[i]),
+			eval(testPoints[j])
+		]);
+	}
+}
+generate_tests(testExtendForwards, tests);
+
+// Copy-pasted with "forwards" changed to "backwards" :/
+var tests = [];
+for (var i = 0; i < testRanges.length; i++) {
+	for (var j = 0; j < testPoints.length; j++) {
+		tests.push([
+			"extend() backwards with range " + i + " " + testRanges[i] + " and point " + j + " " + testPoints[j],
+			eval(testRanges[i]),
+			eval(testPoints[j])
+		]);
+	}
+}
+generate_tests(testExtendBackwards, tests);
+
+// Let's be tidy.
+testDiv.style.display = "none";
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/Selection-getRangeAt.html	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>The getRangeAt method</title>
+<div id=log></div>
+<script src=support/testharness.js></script>
+<script src=support/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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/common.js	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,912 @@
+"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, 4, document.body, 5]",
+		"[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 nodeLength(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, nodeLength(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,
+			nodeLength(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,
+			nodeLength(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,
+			nodeLength(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;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/support/testharness.css	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,70 @@
+html {
+    font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;
+}
+
+section#summary {
+    margin-bottom:1em;
+}
+
+table#results {
+    border-collapse:collapse;
+    table-layout:fixed;
+    width:100%;
+}
+
+table#results th:first-child,
+table#results td:first-child {
+    width:4em;
+}
+
+table#results th:last-child,
+table#results td:last-child {
+    width:50%;
+}
+
+table#results th {
+    padding:0;
+    padding-bottom:0.5em;
+    border-bottom:medium solid black;
+}
+
+table#results td {
+    padding:1em;
+    padding-bottom:0.5em;
+    border-bottom:thin solid black;
+}
+
+tr.pass > td:first-child {
+    color:green;
+}
+
+tr.fail > td:first-child {
+    color:red;
+}
+
+tr.timeout > td:first-child {
+    color:red;
+}
+
+tr.notrun > td:first-child {
+    color:blue;
+}
+
+.pass .fail .timeout .notrun > td:first-child {
+    font-variant:small-caps;
+}
+
+table#results span {
+    display:block;
+}
+
+table#results span.expected {
+    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;
+    white-space:pre;
+}
+
+table#results span.actual {
+    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;
+    white-space:pre;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/selecttest/support/testharness.js	Wed Oct 05 11:44:20 2011 -0600
@@ -0,0 +1,1574 @@
+/*
+Distributed under both the W3C Test Suite License [1] and the W3C
+3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
+policies and contribution forms [3].
+
+[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
+[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
+[3] http://www.w3.org/2004/10/27-testcases
+*/
+
+/*
+ * == Introduction ==
+ *
+ * This file provides a framework for writing testcases. It is intended to
+ * provide a convenient API for making common assertions, and to work both
+ * for testing synchronous and asynchronous DOM features in a way that
+ * promotes clear, robust, tests.
+ *
+ * == Basic Usage ==
+ *
+ * To use this file, import the script into the test document:
+ * <script src="http://w3c-test.org/resources/testharness.js"></script>
+ *
+ * Within each file one may define one or more tests. Each test is atomic
+ * in the sense that a single test has a single result (pass/fail/timeout).
+ * Within each test one may have a number of asserts. The test fails at the
+ * first failing assert, and the remainder of the test is (typically) not run.
+ *
+ * If the file containing the tests is a HTML file with an element of id "log"
+ * this will be populated with a table containing the test results after all
+ * the tests have run.
+ *
+ * == Synchronous Tests ==
+ *
+ * To create a synchronous test use the test() function:
+ *
+ * test(test_function, name, properties)
+ *
+ * test_function is a function that contains the code to test. For example a
+ * trivial passing test would be:
+ *
+ * test(function() {assert_true(true)}, "assert_true with true")
+ *
+ * The function passed in is run in the test() call.
+ *
+ * properties is an object that overrides default test properties. The recognised properties
+ * are:
+ *    timeout - the test timeout in ms
+ *
+ * e.g.
+ * test(test_function, "Sample test", {timeout:1000})
+ *
+ * would run test_function with a timeout of 1s.
+ *
+ * == Asynchronous Tests ==
+ *
+ * Testing asynchronous features is somewhat more complex since the result of
+ * a test may depend on one or more events or other callbacks. The API provided
+ * for testing these features is indended to be rather low-level but hopefully
+ * applicable to many situations.
+ *
+ * To create a test, one starts by getting a Test object using async_test:
+ *
+ * async_test(name, properties)
+ *
+ * e.g.
+ * var t = async_test("Simple async test")
+ *
+ * Assertions can be added to the test by calling the step method of the test
+ * object with a function containing the test assertions:
+ *
+ * t.step(function() {assert_true(true)});
+ *
+ * When all the steps are complete, the done() method must be called:
+ *
+ * t.done();
+ *
+ * The properties argument is identical to that for test().
+ *
+ * In many cases it is convenient to run a step in response to an event or a
+ * callback. A convenient method of doing this is through the step_func method
+ * which returns a function that, when called runs a test step. For example
+ *
+ * object.some_event = t.step_func(function(e) {assert_true(e.a)});
+ *
+ * == Making assertions ==
+ *
+ * Functions for making assertions start assert_
+ * The best way to get a list is to look in this file for functions names
+ * matching that pattern. The general signature is
+ *
+ * assert_something(actual, expected, description)
+ *
+ * although not all assertions precisely match this pattern e.g. assert_true
+ * only takes actual and description as arguments.
+ *
+ * The description parameter is used to present more useful error messages when
+ * a test fails
+ *
+ * == Setup ==
+ *
+ * Sometimes tests require non-trivial setup that may fail. For this purpose
+ * there is a setup() function, that may be called with one or two arguments.
+ * The two argument version is:
+ *
+ * setup(func, properties)
+ *
+ * The one argument versions may omit either argument.
+ * func is a function to be run synchronously. setup() becomes a no-op once
+ * any tests have returned results. Properties are global properties of the test
+ * harness. Currently recognised properties are:
+ *
+ * timeout - The time in ms after which the harness should stop waiting for
+ *           tests to complete (this is different to the per-test timeout
+ *           because async tests do not start their timer until .step is called)
+ *
+ * explicit_done - Wait for an explicit call to done() before declaring all tests
+ *                 complete (see below)
+ *
+ * == Determining when all tests are complete ==
+ *
+ * By default the test harness will assume there are no more results to come
+ * when:
+ * 1) There are no Test objects that have been created but not completed
+ * 2) The load event on the document has fired
+ *
+ * This behaviour can be overridden by setting the explicit_done property to true
+ * in a call to setup(). If explicit_done is true, the test harness will not assume
+ * it is done until the global done() function is called. Once done() is called, the
+ * two conditions above apply like normal.
+ *
+ * == Generating tests ==
+ *
+ * NOTE: this functionality may be removed
+ *
+ * There are scenarios in which is is desirable to create a large number of
+ * (synchronous) tests that are internally similar but vary in the parameters
+ * used. To make this easier, the generate_tests function allows a single
+ * function to be called with each set of parameters in a list:
+ *
+ * generate_tests(test_function, parameter_lists)
+ *
+ * For example:
+ *
+ * generate_tests(assert_equals, [
+ *     ["Sum one and one", 1+1, 2],
+ *     ["Sum one and zero", 1+0, 1]
+ *     ])
+ *
+ * Is equivalent to:
+ *
+ * test(function() {assert_equals(1+1, 2)}, "Sum one and one")
+ * test(function() {assert_equals(1+0, 1)}, "Sum one and zero")
+ *
+ * Note that the first item in each parameter list corresponds to the name of
+ * the test.
+ *
+ * == Callback API ==
+ *
+ * The framework provides callbacks corresponding to 3 events:
+ *
+ * start - happens when the first Test is created
+ * result - happens when a test result is recieved
+ * complete - happens when all results are recieved
+ *
+ * The page defining the tests may add callbacks for these events by calling
+ * the following methods:
+ *
+ *   add_start_callback(callback) - callback called with no arguments
+ *   add_result_callback(callback) - callback called with a test argument
+ *   add_completion_callback(callback) - callback called with an array of tests
+ *                                       and an status object
+ *
+ * tests have the following properties:
+ *   status: A status code. This can be compared to the PASS, FAIL, TIMEOUT and
+ *           NOTRUN properties on the test object
+ *   message: A message indicating the reason for failure. In the future this
+ *            will always be a string
+ *
+ *  The status object gives the overall status of the harness. It has the
+ *  following properties:
+ *    status: Can be compared to the OK, ERROR and TIMEOUT properties
+ *    message: An error message set when the status is ERROR
+ *
+ * == External API ==
+ *
+ * In order to collect the results of multiple pages containing tests, the test
+ * harness will, when loaded in a nested browsing context, attempt to call
+ * certain functions in the top level browsing context:
+ *
+ * start - top.start_callback
+ * result - top.result_callback
+ * complete - top.completion_callback
+ *
+ * These are given the same arguments as the corresponding internal callbacks
+ * described above.
+ *
+ * == List of assertions ==
+ *
+ * assert_true(actual, description)
+ *   asserts that /actual/ is strictly true
+ *
+ * assert_false(actual, description)
+ *   asserts that /actual/ is strictly false
+ *
+ * assert_equals(actual, expected, description)
+ *   asserts that /actual/ is the same value as /expected/
+ *
+ * assert_not_equals(actual, expected, description)
+ *   asserts that /actual/ is a different value to /expected/. Yes, this means
+ *   that "expected" is a misnomer
+ *
+ * assert_array_equals(actual, expected, description)
+ *   asserts that /actual/ and /expected/ have the same length and the value of
+ *   each indexed property in /actual/ is the strictly equal to the corresponding
+ *   property value in /expected/
+ *
+ * assert_regexp_match(actual, expected, description)
+ *   asserts that /actual/ matches the regexp /expected/
+ *
+ * assert_own_property(object, property_name, description)
+ *   assert that object has own property property_name
+ *
+ * assert_inherits(object, property_name, description)
+ *   assert that object does not have an own property named property_name
+ *   but that property_name is present in the prototype chain for object
+ *
+ * assert_idl_attribute(object, attribute_name, description)
+ *   assert that an object that is an instance of some interface has the
+ *   attribute attribute_name following the conditions specified by WebIDL
+ *
+ * assert_readonly(object, property_name, description)
+ *   assert that property property_name on object is readonly
+ *
+ * assert_throws(code, func, description)
+ *   code - a DOMException/RangeException code as a string, e.g. "HIERARCHY_REQUEST_ERR"
+ *   func - a function that should throw
+ *
+ *   assert that func throws a DOMException or RangeException (as appropriate)
+ *   with the given code.  If an object is passed for code instead of a string,
+ *   checks that the thrown exception has a property called "name" that matches
+ *   the property of code called "name".  Note, this function will probably be
+ *   rewritten sometime to make more sense.
+ *
+ * assert_unreached(description)
+ *   asserts if called. Used to ensure that some codepath is *not* taken e.g.
+ *   an event does not fire.
+ *
+ * assert_exists(object, property_name, description)
+ *   *** deprecated ***
+ *   asserts that object has an own property property_name
+ *
+ * assert_not_exists(object, property_name, description)
+ *   *** deprecated ***
+ *   assert that object does not have own property property_name
+ */
+
+(function ()
+{
+    var debug = false;
+    // default timeout is 5 seconds, test can override if needed
+    var default_timeout = 5000;
+    var default_test_timeout = 2000;
+
+    /*
+     * API functions
+     */
+
+    var name_counter = 0;
+    function next_default_name()
+    {
+        //Don't use document.title to work around an Opera bug in XHTML documents
+        var prefix = document.getElementsByTagName("title").length > 0 ?
+                         document.getElementsByTagName("title")[0].firstChild.data :
+                         "Untitled";
+        var suffix = name_counter > 0 ? " " + name_counter : "";
+        name_counter++;
+        return prefix + suffix;
+    }
+
+    function test(func, name, properties)
+    {
+        var test_name = name ? name : next_default_name();
+        properties = properties ? properties : {};
+        var test_obj = new Test(test_name, properties);
+        test_obj.step(func);
+        if (test_obj.status === test_obj.NOTRUN) {
+            test_obj.done();
+        }
+    }
+
+    function async_test(name, properties)
+    {
+        var test_name = name ? name : next_default_name();
+        properties = properties ? properties : {};
+        var test_obj = new Test(test_name, properties);
+        return test_obj;
+    }
+
+    function setup(func_or_properties, properties_or_func)
+    {
+        var func = null;
+        var properties = {};
+        if (arguments.length === 2) {
+            func = func_or_properties;
+            properties = properties_or_func;
+        } else if (func_or_properties instanceof Function){
+            func = func_or_properties;
+        } else {
+            properties = func_or_properties;
+
+        }
+        tests.setup(func, properties);
+    }
+
+    function done() {
+        tests.end_wait();
+    }
+
+    function generate_tests(func, args) {
+        forEach(args, function(x)
+                {
+                    var name = x[0];
+                    test(function()
+                         {
+                             func.apply(this, x.slice(1));
+                         }, name);
+                });
+    }
+
+    function on_event(object, event, callback)
+    {
+      object.addEventListener(event, callback, false);
+    }
+
+    expose(test, 'test');
+    expose(async_test, 'async_test');
+    expose(generate_tests, 'generate_tests');
+    expose(setup, 'setup');
+    expose(done, 'done');
+    expose(on_event, 'on_event');
+
+    /*
+     * Convert a value to a nice, human-readable string
+     */
+    function format_value(val)
+    {
+        if (val === null)
+        {
+            // typeof is object, so the switch isn't useful
+            return "null";
+        }
+        // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
+        // special-case.
+        if (val === -0 && 1/val === -Infinity)
+        {
+            return "-0";
+        }
+        // Special-case Node objects, since those come up a lot in my tests.  I
+        // ignore namespaces.  I use duck-typing instead of instanceof, because
+        // instanceof doesn't work if the node is from another window (like an
+        // iframe's contentWindow):
+        // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+        if (typeof val == "object"
+        && "nodeType" in val
+        && "nodeName" in val
+        && "nodeValue" in val
+        && "childNodes" in val)
+        {
+            switch (val.nodeType)
+            {
+            case Node.ELEMENT_NODE:
+                var ret = "Element node <";
+                if (val.namespaceURI == "http://www.w3.org/1999/xhtml" || val.namespaceURI === null)
+                {
+                    ret += val.tagName.toLowerCase();
+                }
+                else
+                {
+                    ret += val.tagName;
+                }
+                for (var i = 0; i < val.attributes.length; i++)
+                {
+                    ret += " " + val.attributes[i].name + "=" + format_value(val.attributes[i].value);
+                }
+                ret += "> with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+                return ret;
+            case Node.TEXT_NODE:
+                return "Text node with data " + format_value(val.data) + " and parent " + format_value(val.parentNode);
+            case Node.PROCESSING_INSTRUCTION_NODE:
+                return "ProcessingInstruction node with target " + format_value(val.target) + " and data " + format_value(val.data);
+            case Node.COMMENT_NODE:
+                return "Comment node with data " + format_value(val.data);
+            case Node.DOCUMENT_NODE:
+                return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+            case Node.DOCUMENT_TYPE_NODE:
+                return "DocumentType node";
+            case Node.DOCUMENT_FRAGMENT_NODE:
+                return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+            default:
+                return "Node object of unknown type";
+            }
+        }
+        switch (typeof val)
+        {
+        case "string":
+            for (var i = 0; i < 32; i++)
+            {
+                var replace = "\\";
+                switch (i) {
+                case 0: replace += "0"; break;
+                case 1: replace += "x01"; break;
+                case 2: replace += "x02"; break;
+                case 3: replace += "x03"; break;
+                case 4: replace += "x04"; break;
+                case 5: replace += "x05"; break;
+                case 6: replace += "x06"; break;
+                case 7: replace += "x07"; break;
+                case 8: replace += "b"; break;
+                case 9: replace += "t"; break;
+                case 10: replace += "n"; break;
+                case 11: replace += "v"; break;
+                case 12: replace += "f"; break;
+                case 13: replace += "r"; break;
+                case 14: replace += "x0e"; break;
+                case 15: replace += "x0f"; break;
+                case 16: replace += "x10"; break;
+                case 17: replace += "x11"; break;
+                case 18: replace += "x12"; break;
+                case 19: replace += "x13"; break;
+                case 20: replace += "x14"; break;
+                case 21: replace += "x15"; break;
+                case 22: replace += "x16"; break;
+                case 23: replace += "x17"; break;
+                case 24: replace += "x18"; break;
+                case 25: replace += "x19"; break;
+                case 26: replace += "x1a"; break;
+                case 27: replace += "x1b"; break;
+                case 28: replace += "x1c"; break;
+                case 29: replace += "x1d"; break;
+                case 30: replace += "x1e"; break;
+                case 31: replace += "x1f"; break;
+                }
+                val = val.replace(String.fromCharCode(i), replace);
+            }
+            return '"' + val.replace('"', '\\"') + '"';
+        case "boolean":
+        case "undefined":
+        case "number":
+            return String(val);
+        default:
+            return typeof val + ' "' + val + '"';
+        }
+    }
+    expose(format_value, "format_value");
+
+    /*
+     * Assertions
+     */
+
+    function assert_true(actual, description)
+    {
+        var message = make_message("assert_true", description,
+                                   "expected true got ${actual}", {actual:actual});
+        assert(actual === true, message);
+    };
+    expose(assert_true, "assert_true");
+
+    function assert_false(actual, description)
+    {
+        var message = make_message("assert_false", description,
+                                   "expected false got ${actual}", {actual:actual});
+        assert(actual === false, message);
+    };
+    expose(assert_false, "assert_false");
+
+    function same_value(x, y) {
+        if (y !== y)
+        {
+            //NaN case
+            return x !== x;
+        }
+        else if (x === 0 && y === 0) {
+            //Distinguish +0 and -0
+            return 1/x === 1/y;
+        }
+        else
+        {
+            //typical case
+            return x === y;
+        }
+    }
+
+    function assert_equals(actual, expected, description)
+    {
+         /*
+          * Test if two primitives are equal or two objects
+          * are the same object
+          */
+        var message = make_message("assert_equals", description,
+                                    "expected ${expected} but got ${actual}",
+                                    {expected:expected, actual:actual});
+
+        assert(same_value(actual, expected), message);
+    };
+    expose(assert_equals, "assert_equals");
+
+    function assert_not_equals(actual, expected, description)
+    {
+         /*
+          * Test if two primitives are unequal or two objects
+          * are different objects
+          */
+         var message = make_message("assert_not_equals", description,
+                                    "got disallowed value ${actual}",
+                                    {actual:actual});
+
+        assert(!same_value(actual, expected), message);
+    };
+    expose(assert_not_equals, "assert_not_equals");
+
+    function assert_object_equals(actual, expected, description)
+    {
+         //This needs to be improved a great deal
+         function check_equal(expected, actual, stack)
+         {
+             stack.push(actual);
+
+             var p;
+             for (p in actual)
+             {
+                 var message = make_message(
+                     "assert_object_equals", description,
+                     "unexpected property ${p}", {p:p});
+
+                 assert(expected.hasOwnProperty(p), message);
+
+                 if (typeof actual[p] === "object" && actual[p] !== null)
+                 {
+                     if (stack.indexOf(actual[p]) === -1)
+                     {
+                         check_equal(actual[p], expected[p], stack);
+                     }
+                 }
+                 else
+                 {
+                     message = make_message(
+                         "assert_object_equals", description,
+                         "property ${p} expected ${expected} got ${actual}",
+                         {p:p, expected:expected, actual:actual});
+
+                     assert(actual[p] === expected[p], message);
+                 }
+             }
+             for (p in expected)
+             {
+                 var message = make_message(
+                     "assert_object_equals", description,
+                     "expected property ${p} missing", {p:p});
+
+                 assert(actual.hasOwnProperty(p), message);
+             }
+             stack.pop();
+         }
+         check_equal(actual, expected, []);
+    };
+    expose(assert_object_equals, "assert_object_equals");
+
+    function assert_array_equals(actual, expected, description)
+    {
+        var message = make_message(
+            "assert_array_equals", description,
+            "lengths differ, expected ${expected} got ${actual}",
+            {expected:expected.length, actual:actual.length});
+
+        assert(actual.length === expected.length, message);
+
+        for (var i=0; i < actual.length; i++)
+        {
+            message = make_message(
+                "assert_array_equals", description,
+                "property ${i}, property expected to be $expected but was $actual",
+                {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+                 actual:actual.hasOwnProperty(i) ? "present" : "missing"});
+            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), message);
+            message = make_message(
+                          "assert_array_equals", description,
+                          "property ${i}, expected ${expected} but got ${actual}",
+                          {i:i, expected:expected[i], actual:actual[i]});
+            assert(expected[i] === actual[i], message);
+        }
+    }
+    expose(assert_array_equals, "assert_array_equals");
+
+    function assert_regexp_match(actual, expected, description) {
+        /*
+         * Test if a string (actual) matches a regexp (expected)
+         */
+        var message = make_message("assert_regexp_match", description,
+                                   "expected ${expected} but got ${actual}",
+                                   {expected:expected, actual:actual});
+        assert(expected.test(actual), message);
+    }
+    expose(assert_regexp_match, "assert_regexp_match");
+
+
+    function _assert_own_property(name) {
+        return function(object, property_name, description)
+        {
+            var message = make_message(
+                name, description,
+                "expected property ${p} missing", {p:property_name});
+
+            assert(object.hasOwnProperty(property_name), message);
+        };
+    }
+    expose(_assert_own_property("assert_exists"), "assert_exists");
+    expose(_assert_own_property("assert_own_property"), "assert_own_property");
+
+    function assert_not_exists(object, property_name, description)
+    {
+        var message = make_message(
+            "assert_not_exists", description,
+            "unexpected property ${p} found", {p:property_name});
+
+        assert(!object.hasOwnProperty(property_name), message);
+    };
+    expose(assert_not_exists, "assert_not_exists");
+
+    function _assert_inherits(name) {
+        return function (object, property_name, description)
+        {
+            var message = make_message(
+                name, description,
+                "property ${p} found on object expected in prototype chain",
+                {p:property_name});
+            assert(!object.hasOwnProperty(property_name), message);
+
+            message = make_message(
+                name, description,
+                "property ${p} not found in prototype chain",
+                {p:property_name});
+            assert(property_name in object, message);
+        };
+    }
+    expose(_assert_inherits("assert_inherits"), "assert_inherits");
+    expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
+
+    function assert_readonly(object, property_name, description)
+    {
+         var initial_value = object[property_name];
+         try {
+             var message = make_message(
+                 "assert_readonly", description,
+                 "deleting property ${p} succeeded", {p:property_name});
+             assert(delete object[property_name] === false, message);
+             assert(object[property_name] === initial_value, message);
+             //Note that this can have side effects in the case where
+             //the property has PutForwards
+             object[property_name] = initial_value + "a"; //XXX use some other value here?
+             message = make_message("assert_readonly", description,
+                                    "changing property ${p} succeeded",
+                                    {p:property_name});
+             assert(object[property_name] === initial_value, message);
+         }
+         finally
+         {
+             object[property_name] = initial_value;
+         }
+    };
+    expose(assert_readonly, "assert_readonly");
+
+    function assert_throws(code, func, description)
+    {
+        try
+        {
+            func.call(this);
+            assert(false, make_message("assert_throws", description,
+                                      "${func} did not throw", {func:func}));
+        }
+        catch(e)
+        {
+            if (e instanceof AssertionError) {
+                throw(e);
+            }
+            if (typeof code === "object")
+            {
+                assert(typeof e == "object" && "name" in e && e.name == code.name,
+                       make_message("assert_throws", description,
+                           "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
+                                    {func:func, actual:e, actual_name:e.name,
+                                     expected:code,
+                                     expected_name:code.name}));
+                return;
+            }
+            var required_props = {};
+            var expected_type;
+            if (code in DOMException)
+            {
+                expected_type = "DOMException";
+                required_props[code] = DOMException[code];
+                required_props.code = DOMException[code];
+                //Uncomment this when the latest version of every browser
+                //actually implements the spec; otherwise it just creates
+                //zillions of failures
+                //required_props.name = code;
+            }
+            else if (code in RangeException)
+            {
+                expected_type = "RangeException";
+                required_props[code] = RangeException[code];
+                required_props.code = RangeException[code];
+                //As above
+                //required_props.name = code;
+            }
+            else
+            {
+                throw new AssertionError('Test bug: unrecognized code "' + code + '" passed to assert_throws()');
+            }
+            //We'd like to test that e instanceof the appropriate interface,
+            //but we can't, because we don't know what window it was created
+            //in.  It might be an instanceof the appropriate interface on some
+            //unknown other window.  TODO: Work around this somehow?
+
+            assert(typeof e == "object",
+                    make_message("assert_throws", description,
+                        "${func} threw ${e} with type ${type}, not an object",
+                                {func:func, e:e, type:typeof e}));
+
+            for (var prop in required_props)
+            {
+                assert(typeof e == "object" && prop in e && e[prop] == required_props[prop],
+                        make_message("assert_throws", description,
+                            "${func} threw ${e} that is not a " + expected_type + " " + code + ": property ${prop} is equal to ${actual}, expected ${expected}",
+                                {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}));
+            }
+        }
+    }
+    expose(assert_throws, "assert_throws");
+
+    function assert_unreached(description) {
+         var message = make_message("assert_unreached", description,
+                                    "Reached unreachable code");
+
+         assert(false, message);
+    }
+    expose(assert_unreached, "assert_unreached");
+
+    function Test(name, properties)
+    {
+        this.name = name;
+        this.status = this.NOTRUN;
+        this.timeout_id = null;
+        this.is_done = false;
+
+        this.timeout_length = properties.timeout ? properties.timeout : default_test_timeout;
+
+        this.message = null;
+
+        var this_obj = this;
+        this.steps = [];
+
+        tests.push(this);
+    }
+
+    Test.prototype = {
+        PASS:0,
+        FAIL:1,
+        TIMEOUT:2,
+        NOTRUN:3
+    };
+
+
+    Test.prototype.step = function(func, this_obj)
+    {
+        //In case the test has already failed
+        if (this.status !== this.NOTRUN)
+        {
+          return;
+        }
+
+        tests.started = true;
+
+        if (this.timeout_id === null) {
+            this.set_timeout();
+        }
+
+        this.steps.push(func);
+
+        try
+        {
+            func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+        }
+        catch(e)
+        {
+            //This can happen if something called synchronously invoked another
+            //step
+            if (this.status !== this.NOTRUN)
+            {
+                return;
+            }
+            this.status = this.FAIL;
+            this.message = e.message;
+            if (typeof e.stack != "undefined" && typeof e.message == "string") {
+                //Try to make it more informative for some exceptions, at least
+                //in Gecko and WebKit.  This results in a stack dump instead of
+                //just errors like "Cannot read property 'parentNode' of null"
+                //or "root is null".  Makes it a lot longer, of course.
+                this.message += "(stack: " + e.stack + ")";
+            }
+            this.done();
+            if (debug && e.constructor !== AssertionError) {
+                throw e;
+            }
+        }
+    };
+
+    Test.prototype.step_func = function(func, this_obj)
+    {
+        var test_this = this;
+        return function()
+        {
+            test_this.step.apply(test_this, [func, this_obj].concat(
+                                     Array.prototype.slice.call(arguments)));
+        };
+    };
+
+    Test.prototype.set_timeout = function()
+    {
+        var this_obj = this;
+        this.timeout_id = setTimeout(function()
+                                     {
+                                         this_obj.timeout();
+                                     }, this.timeout_length);
+    };
+
+    Test.prototype.timeout = function()
+    {
+        this.status = this.TIMEOUT;
+        this.timeout_id = null;
+        this.message = "Test timed out";
+        this.done();
+    };
+
+    Test.prototype.done = function()
+    {
+        if (this.is_done) {
+            return;
+        }
+        clearTimeout(this.timeout_id);
+        if (this.status === this.NOTRUN)
+        {
+            this.status = this.PASS;
+        }
+        this.is_done = true;
+        tests.result(this);
+    };
+
+
+    /*
+     * Harness
+     */
+
+    function TestsStatus()
+    {
+        this.status = null;
+        this.message = null;
+    }
+    TestsStatus.prototype = {
+        OK:0,
+        ERROR:1,
+        TIMEOUT:2
+    };
+
+    function Tests()
+    {
+        this.tests = [];
+        this.num_pending = 0;
+
+        this.phases = {
+            INITIAL:0,
+            SETUP:1,
+            HAVE_TESTS:2,
+            HAVE_RESULTS:3,
+            COMPLETE:4
+        };
+        this.phase = this.phases.INITIAL;
+
+        //All tests can't be done until the load event fires
+        this.all_loaded = false;
+        this.wait_for_finish = false;
+        this.processing_callbacks = false;
+
+        this.timeout_length = default_timeout;
+        this.timeout_id = null;
+        this.set_timeout();
+
+        this.start_callbacks = [];
+        this.test_done_callbacks = [];
+        this.all_done_callbacks = [];
+
+        this.status = new TestsStatus();
+
+        var this_obj = this;
+
+        on_event(window, "load",
+                 function()
+                 {
+                     this_obj.all_loaded = true;
+                     if (this_obj.all_done())
+                     {
+                         this_obj.complete();
+                     }
+                 });
+    }
+
+    Tests.prototype.setup = function(func, properties)
+    {
+        if (this.phase >= this.phases.HAVE_RESULTS) {
+            return;
+        }
+        if (this.phase < this.phases.SETUP) {
+            this.phase = this.phases.SETUP;
+        }
+
+        if (properties.timeout)
+        {
+            this.timeout_length = properties.timeout;
+            this.set_timeout();
+        }
+        if (properties.explicit_done)
+        {
+            this.wait_for_finish = true;
+        }
+
+        if (func)
+        {
+            try
+            {
+                func();
+            } catch(e)
+            {
+                this.status.status = this.status.ERROR;
+                this.status.message = e;
+            };
+        }
+    };
+
+    Tests.prototype.set_timeout = function()
+    {
+        var this_obj = this;
+        clearTimeout(this.timeout_id);
+        this.timeout_id = setTimeout(function() {
+                                         this_obj.timeout();
+                                     }, this.timeout_length);
+    };
+
+    Tests.prototype.timeout = function() {
+        this.status.status = this.status.TIMEOUT;
+        this.complete();
+    };
+
+    Tests.prototype.end_wait = function()
+    {
+        this.wait_for_finish = false;
+        if (this.all_done()) {
+            this.complete();
+        }
+    };
+
+    Tests.prototype.push = function(test)
+    {
+        if (this.phase < this.phases.HAVE_TESTS) {
+            this.notify_start();
+        }
+        this.num_pending++;
+        this.tests.push(test);
+    };
+
+    Tests.prototype.all_done = function() {
+        return (this.all_loaded && this.num_pending === 0 &&
+                !this.wait_for_finish && !this.processing_callbacks);
+    };
+
+    Tests.prototype.start = function() {
+        this.phase = this.phases.HAVE_TESTS;
+        this.notify_start();
+    };
+
+    Tests.prototype.notify_start = function() {
+        var this_obj = this;
+        forEach (this.start_callbacks,
+                 function(callback)
+                 {
+                     callback(this_obj);
+                 });
+        if(top !== window && top.start_callback)
+        {
+            try
+            {
+                top.start_callback.call(this_obj);
+            }
+            catch(e)
+            {
+                if (debug)
+                {
+                    throw(e);
+                }
+            }
+        }
+    };
+
+    Tests.prototype.result = function(test)
+    {
+        if (this.phase > this.phases.HAVE_RESULTS)
+        {
+            return;
+        }
+        this.phase = this.phases.HAVE_RESULTS;
+        this.num_pending--;
+        this.notify_result(test);
+    };
+
+    Tests.prototype.notify_result = function(test) {
+        var this_obj = this;
+        this.processing_callbacks = true;
+        forEach(this.test_done_callbacks,
+                function(callback)
+                {
+                    callback(test, this_obj);
+                });
+
+        if(top !== window && top.result_callback)
+        {
+            try
+            {
+                top.result_callback.call(this_obj, test);
+            }
+            catch(e)
+            {
+                if(debug) {
+                    throw e;
+                }
+            }
+        }
+        this.processing_callbacks = false;
+        if (this.all_done())
+        {
+            this.complete();
+        }
+
+    };
+
+    Tests.prototype.complete = function() {
+        if (this.phase === this.phases.COMPLETE) {
+            return;
+        }
+        this.phase = this.phases.COMPLETE;
+        this.notify_complete();
+    };
+
+    Tests.prototype.notify_complete = function()
+    {
+        clearTimeout(this.timeout_id);
+        var this_obj = this;
+        if (this.status.status === null)
+        {
+            this.status.status = this.status.OK;
+        }
+
+        forEach (this.all_done_callbacks,
+                 function(callback)
+                 {
+                     callback(this_obj.tests, this_obj.status);
+                 });
+        if(top !== window && top.completion_callback)
+        {
+            try
+            {
+                top.completion_callback(this_obj.tests, this.status);
+            }
+            catch(e)
+            {
+                if (debug)
+                {
+                    throw e;
+                }
+            }
+
+        }
+    };
+
+    var tests = new Tests();
+    add_completion_callback(output_results);
+
+
+    function add_start_callback(callback) {
+        tests.start_callbacks.push(callback);
+    }
+
+    function add_result_callback(callback)
+    {
+        tests.test_done_callbacks.push(callback);
+    }
+
+    function add_completion_callback(callback)
+    {
+       tests.all_done_callbacks.push(callback);
+    }
+
+    expose(add_start_callback, 'add_start_callback');
+    expose(add_result_callback, 'add_result_callback');
+    expose(add_completion_callback, 'add_completion_callback');
+
+    /*
+     * Output listener
+    */
+
+    (function show_status() {
+        var done_count = 0;
+         function on_done(test, tests) {
+             var log = document.getElementById("log");
+             done_count++;
+             if (log)
+             {
+                 if (log.lastChild) {
+                     log.removeChild(log.lastChild);
+                 }
+                 var nodes = render([["{text}", "Running, ${done} complete"],
+                                 function() {
+                                     if (tests.all_done) {
+                                         return ["{text}", " ${pending} remain"];
+                                     } else {
+                                         return null;
+                                     }
+                                 }
+                                    ], {done:done_count,
+                                        pending:tests.num_pending});
+                 forEach(nodes, function(node) {
+                             log.appendChild(node);
+                         });
+                 log.normalize();
+             }
+         }
+         if (document.getElementById("log"))
+         {
+             add_result_callback(on_done);
+         }
+     })();
+
+    function output_results(tests, harness_status)
+    {
+        var log = document.getElementById("log");
+        if (!log)
+        {
+            return;
+        }
+        while (log.lastChild)
+        {
+            log.removeChild(log.lastChild);
+        }
+        var prefix = null;
+        var scripts = document.getElementsByTagName("script");
+        for (var i=0; i<scripts.length; i++)
+        {
+            var src = scripts[i].src;
+            if (src.slice(src.length - "testharness.js".length) === "testharness.js")
+            {
+                prefix = src.slice(0, src.length - "testharness.js".length);
+                break;
+            }
+        }
+        if (prefix != null) {
+            var stylesheet = document.createElement("link");
+            stylesheet.setAttribute("rel", "stylesheet");
+            stylesheet.setAttribute("href", prefix + "testharness.css");
+            var heads = document.getElementsByTagName("head");
+            if (heads) {
+                heads[0].appendChild(stylesheet);
+            }
+        }
+
+        var status_text = {};
+        status_text[Test.prototype.PASS] = "Pass";
+        status_text[Test.prototype.FAIL] = "Fail";
+        status_text[Test.prototype.TIMEOUT] = "Timeout";
+        status_text[Test.prototype.NOTRUN] = "Not Run";
+
+        var status_number = {};
+        forEach(tests, function(test) {
+                    var status = status_text[test.status];
+                    if (status_number.hasOwnProperty(status))
+                    {
+                        status_number[status] += 1;
+                    } else {
+                        status_number[status] = 1;
+                    }
+                });
+
+        function status_class(status)
+        {
+            return status.replace(/\s/g, '').toLowerCase();
+        }
+
+        var summary_template = ["section", {"id":"summary"},
+                                ["h2", {}, "Summary"],
+                                ["p", {}, "Found ${num_tests} tests"],
+                                function(vars) {
+                                    var rv = [["div", {}]];
+                                    var i=0;
+                                    while (status_text.hasOwnProperty(i)) {
+                                        if (status_number.hasOwnProperty(status_text[i])) {
+                                            var status = status_text[i];
+                                            rv[0].push(["div", {"class":status_class(status)},
+                                                        ["input", {type:"checkbox", checked:"checked"}],
+                                                       status_number[status] + " " + status]);
+                                        }
+                                        i++;
+                                    }
+                                    return rv;
+                                }];
+
+        log.appendChild(render(summary_template, {num_tests:tests.length}));
+
+        forEach(document.querySelectorAll("section#summary input"),
+                function(element)
+                {
+                    on_event(element, "click",
+                             function(e)
+                             {
+                                 if (document.getElementById("results") === null)
+                                 {
+                                     e.preventDefault();
+                                     return;
+                                 }
+                                 var result_class = element.parentNode.getAttribute("class");
+                                 var style_element = document.querySelector("style#hide-" + result_class);
+                                 if (!style_element && !element.checked) {
+                                     style_element = document.createElement("style");
+                                     style_element.id = "hide-" + result_class;
+                                     style_element.innerHTML = "table#results > tbody > tr."+result_class+"{display:none}";
+                                     document.body.appendChild(style_element);
+                                 } else if (style_element && element.checked) {
+                                     style_element.parentNode.removeChild(style_element);
+                                 }
+                             });
+                });
+
+        var template = ["section", {},
+                        ["h2", {}, "Details"],
+                        ["table", {"id":"results"},
+                        ["tr", {},
+                         ["th", {}, "Result"],
+                         ["th", {}, "Test Name"],
+                         ["th", {}, "Message"]
+                        ],
+                        ["tbody", {},
+                        function(vars) {
+                            var rv = map(vars.tests, function(test) {
+                                             var status = status_text[test.status];
+                                             return  ["tr", {"class":status_class(status)},
+                                                      ["td", {}, status],
+                                                      ["td", {}, test.name],
+                                                      ["td", {}, test.message ? test.message : " "]
+                                                     ];
+                                         });
+                            return rv;
+                        }]
+                       ]];
+
+        log.appendChild(render(template, {tests:tests}));
+
+    }
+
+
+    /*
+     * Template code
+     *
+     * A template is just a javascript structure. An element is represented as:
+     *
+     * [tag_name, {attr_name:attr_value}, child1, child2]
+     *
+     * the children can either be strings (which act like text nodes), other templates or
+     * functions (see below)
+     *
+     * A text node is represented as
+     *
+     * ["{text}", value]
+     *
+     * String values have a simple substitution syntax; ${foo} represents a variable foo.
+     *
+     * It is possible to embed logic in templates by using a function in a place where a
+     * node would usually go. The function must either return part of a template or null.
+     *
+     * In cases where a set of nodes are required as output rather than a single node
+     * with children it is possible to just use a list
+     * [node1, node2, node3]
+     *
+     * Usage:
+     *
+     * render(template, substitutions) - take a template and an object mapping
+     * variable names to parameters and return either a DOM node or a list of DOM nodes
+     *
+     * substitute(template, substitutions) - take a template and variable mapping object,
+     * make the variable substitutions and return the substituted template
+     *
+     */
+
+    function is_single_node(template)
+    {
+        return typeof template[0] === "string";
+    }
+
+    function substitute(template, substitutions)
+    {
+        if (typeof template === "function") {
+            var replacement = template(substitutions);
+            if (replacement)
+            {
+                var rv = substitute(replacement, substitutions);
+                return rv;
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else if (is_single_node(template))
+        {
+            return substitute_single(template, substitutions);
+        }
+        else
+        {
+            return filter(map(template, function(x) {
+                                  return substitute(x, substitutions);
+                              }), function(x) {return x !== null;});
+        }
+    }
+
+    function substitute_single(template, substitutions)
+    {
+        var substitution_re = /\${([^ }]*)}/g;
+
+        function do_substitution(input) {
+            var components = input.split(substitution_re);
+            var rv = [];
+            for (var i=0; i<components.length; i+=2)
+            {
+                rv.push(components[i]);
+                if (components[i+1])
+                {
+                    rv.push(String(substitutions[components[i+1]]));
+                }
+            }
+            return rv;
+        }
+
+        var rv = [];
+        rv.push(do_substitution(String(template[0])).join(""));
+
+        if (template[0] === "{text}") {
+            substitute_children(template.slice(1), rv);
+        } else {
+            substitute_attrs(template[1], rv);
+            substitute_children(template.slice(2), rv);
+        }
+
+        function substitute_attrs(attrs, rv)
+        {
+            rv[1] = {};
+            for (var name in template[1])
+            {
+                if (attrs.hasOwnProperty(name))
+                {
+                    var new_name = do_substitution(name).join("");
+                    var new_value = do_substitution(attrs[name]).join("");
+                    rv[1][new_name] = new_value;
+                };
+            }
+        }
+
+        function substitute_children(children, rv)
+        {
+            for (var i=0; i<children.length; i++)
+            {
+                if (children[i] instanceof Object) {
+                    var replacement = substitute(children[i], substitutions);
+                    if (replacement !== null)
+                    {
+                        if (is_single_node(replacement))
+                        {
+                            rv.push(replacement);
+                        }
+                        else
+                        {
+                            extend(rv, replacement);
+                        }
+                    }
+                }
+                else
+                {
+                    extend(rv, do_substitution(String(children[i])));
+                }
+            }
+            return rv;
+        }
+
+        return rv;
+    }
+
+    function make_dom_single(template)
+    {
+        if (template[0] === "{text}")
+        {
+            var element = document.createTextNode("");
+            for (var i=1; i<template.length; i++)
+            {
+                element.data += template[i];
+            }
+        }
+        else
+        {
+            var element = document.createElement(template[0]);
+            for (var name in template[1]) {
+                if (template[1].hasOwnProperty(name))
+                {
+                    element.setAttribute(name, template[1][name]);
+                }
+            }
+            for (var i=2; i<template.length; i++)
+            {
+                if (template[i] instanceof Object)
+                {
+                    var sub_element = make_dom(template[i]);
+                    element.appendChild(sub_element);
+                }
+                else
+                {
+                    var text_node = document.createTextNode(template[i]);
+                    element.appendChild(text_node);
+                }
+            }
+        }
+
+        return element;
+    }
+
+
+
+    function make_dom(template, substitutions)
+    {
+        if (is_single_node(template))
+        {
+            return make_dom_single(template);
+        }
+        else
+        {
+            return map(template, function(x) {
+                           return make_dom_single(x);
+                       });
+        }
+    }
+
+    function render(template, substitutions)
+    {
+        return make_dom(substitute(template, substitutions));
+    }
+
+    /*
+     * Utility funcions
+     */
+    function assert(expected_true, message)
+    {
+        if (expected_true !== true)
+        {
+            throw new AssertionError(message);
+        }
+    }
+
+    function AssertionError(message)
+    {
+        this.message = message;
+    }
+
+    function make_message(function_name, description, error, substitutions)
+    {
+        for (var p in substitutions) {
+            if (substitutions.hasOwnProperty(p)) {
+                substitutions[p] = format_value(substitutions[p]);
+            }
+        }
+        var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
+                                   merge({function_name:function_name,
+                                          description:(description?description + " ":"")},
+                                          substitutions));
+        return node_form.slice(1).join("");
+    }
+
+    function filter(array, callable, thisObj) {
+        var rv = [];
+        for (var i=0; i<array.length; i++)
+        {
+            if (array.hasOwnProperty(i))
+            {
+                var pass = callable.call(thisObj, array[i], i, array);
+                if (pass) {
+                    rv.push(array[i]);
+                }
+            }
+        }
+        return rv;
+    }
+
+    function map(array, callable, thisObj)
+    {
+        var rv = [];
+        rv.length = array.length;
+        for (var i=0; i<array.length; i++)
+        {
+            if (array.hasOwnProperty(i))
+            {
+                rv[i] = callable.call(thisObj, array[i], i, array);
+            }
+        }
+        return rv;
+    }
+
+    function extend(array, items)
+    {
+        Array.prototype.push.apply(array, items);
+    }
+
+    function forEach (array, callback, thisObj)
+    {
+        for (var i=0; i<array.length; i++)
+        {
+            if (array.hasOwnProperty(i))
+            {
+                callback.call(thisObj, array[i], i, array);
+            }
+        }
+    }
+
+    function merge(a,b)
+    {
+        var rv = {};
+        var p;
+        for (p in a)
+        {
+            rv[p] = a[p];
+        }
+        for (p in b) {
+            rv[p] = b[p];
+        }
+        return rv;
+    }
+
+    function expose(object, name)
+    {
+        var components = name.split(".");
+        var target = window;
+        for (var i=0; i<components.length - 1; i++)
+        {
+            if (!(components[i] in target))
+            {
+                target[components[i]] = {};
+            }
+            target = target[components[i]];
+        }
+        target[components[components.length - 1]] = object;
+    }
+
+})();
+// vim: set expandtab shiftwidth=4 tabstop=4: