Upstream dom/ tests from jsdom

diff --git a/dom/attributes-are-nodes.html b/dom/attributes-are-nodes.html
new file mode 100644
index 0000000..54ff4cc
--- /dev/null
+++ b/dom/attributes-are-nodes.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Attributes are Nodes but should not be accepted outside of the `attributes` NamedNodeMap</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-core-changes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+
+  const attribute = document.createAttribute("newattribute");
+
+  assert_true(attribute instanceof Node, "attribute instances are instances of Node");
+  assert_true(Attr.prototype instanceof Node, "attribute instances are instances of Node");
+
+}, "Attrs are subclasses of Nodes");
+
+test(() => {
+
+  const parent = document.createElement("p");
+
+  const attribute = document.createAttribute("newattribute");
+  assert_throws_dom("HierarchyRequestError", () => {
+    parent.appendChild(attribute);
+  });
+
+}, "appendChild with an attribute as the child should fail");
+
+test(() => {
+
+  const parent = document.createElement("p");
+  parent.appendChild(document.createElement("span"));
+
+  const attribute = document.createAttribute("newattribute");
+  assert_throws_dom("HierarchyRequestError", () => {
+    parent.replaceChild(attribute, parent.firstChild);
+  });
+
+}, "replaceChild with an attribute as the child should fail");
+
+test(() => {
+
+  const parent = document.createElement("p");
+  parent.appendChild(document.createElement("span"));
+
+  const attribute = document.createAttribute("newattribute");
+  assert_throws_dom("HierarchyRequestError", () => {
+    parent.insertBefore(attribute, parent.firstChild);
+  });
+
+}, "insertBefore with an attribute as the child should fail");
+
+</script>
diff --git a/dom/collections/HTMLCollection-iterator.html b/dom/collections/HTMLCollection-iterator.html
new file mode 100644
index 0000000..4a3f630
--- /dev/null
+++ b/dom/collections/HTMLCollection-iterator.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="help" href="https://dom.spec.whatwg.org/#interface-htmlcollection">
+<link rel="help" href="https://heycam.github.io/webidl/#es-iterator">
+<title>HTMLCollection @@iterator Test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p id="1"></p>
+<p id="2"></p>
+<p id="3"></p>
+<p id="4"></p>
+<p id="5"></p>
+<script>
+"use strict";
+
+const paragraphs = document.getElementsByTagName("p");
+
+test(() => {
+  assert_true("length" in paragraphs);
+}, "HTMLCollection has length method.");
+
+test(() => {
+  assert_false("values" in paragraphs);
+}, "HTMLCollection does not have iterable's values method.");
+
+test(() => {
+  assert_false("entries" in paragraphs);
+}, "HTMLCollection does not have iterable's entries method.");
+
+test(() => {
+  assert_false("forEach" in paragraphs);
+}, "HTMLCollection does not have iterable's forEach method.");
+
+test(() => {
+  assert_true(Symbol.iterator in paragraphs);
+}, "HTMLCollection has Symbol.iterator.");
+
+test(() => {
+  const ids = "12345";
+  let idx = 0;
+  for (const element of paragraphs) {
+    assert_equals(element.getAttribute("id"), ids[idx++]);
+  }
+}, "HTMLCollection is iterable via for-of loop.");
+</script>
diff --git a/dom/events/Event-stopImmediatePropagation.html b/dom/events/Event-stopImmediatePropagation.html
new file mode 100644
index 0000000..b757322
--- /dev/null
+++ b/dom/events/Event-stopImmediatePropagation.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Event's stopImmediatePropagation</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation">
+<link rel="author" href="mailto:d@domenic.me" title="Domenic Denicola">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="target"></div>
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+const target = document.querySelector("#target");
+
+let timesCalled = 0;
+target.addEventListener("test", e => {
+  ++timesCalled;
+  e.stopImmediatePropagation();
+  assert_equals(e.cancelBubble, true, "The stop propagation flag must have been set");
+});
+target.addEventListener("test", () => {
+  ++timesCalled;
+});
+
+const e = new Event("test");
+target.dispatchEvent(e);
+assert_equals(timesCalled, 1, "The second listener must not have been called");
+
+done();
+</script>
diff --git a/dom/events/EventTarget-add-listener-platform-object.html b/dom/events/EventTarget-add-listener-platform-object.html
new file mode 100644
index 0000000..d5565c2
--- /dev/null
+++ b/dom/events/EventTarget-add-listener-platform-object.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>addEventListener with a platform object</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+</script>
+<my-custom-click id=click>Click me!</my-custom-click>
+<script>
+"use strict";
+setup({ single_test: true });
+
+class MyCustomClick extends HTMLElement {
+  connectedCallback() {
+    this.addEventListener("click", this);
+  }
+
+  handleEvent(event) {
+    if (event.target === this) {
+      this.dataset.yay = "It worked!";
+    }
+  }
+}
+window.customElements.define("my-custom-click", MyCustomClick);
+
+const customElement = document.getElementById("click");
+customElement.click();
+
+assert_equals(customElement.dataset.yay, "It worked!");
+
+done();
+</script>
diff --git a/dom/events/EventTarget-add-remove-listener.html b/dom/events/EventTarget-add-remove-listener.html
new file mode 100644
index 0000000..8fb9f43
--- /dev/null
+++ b/dom/events/EventTarget-add-remove-listener.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>EventTarget's addEventListener + removeEventListener</title>
+<link rel="author" title="Sebastian Mayr" href="mailto:wpt@smayr.name">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+"use strict";
+
+function listener(evt) {
+  evt.preventDefault();
+  return false;
+}
+
+test(() => {
+  document.addEventListener("x", listener, false);
+  let event = new Event("x", { cancelable: true });
+  let ret = document.dispatchEvent(event);
+  assert_false(ret);
+
+  document.removeEventListener("x", listener);
+  event = new Event("x", { cancelable: true });
+  ret = document.dispatchEvent(event);
+  assert_true(ret);
+}, "Removing an event listener without explicit capture arg should succeed");
+</script>
diff --git a/dom/events/EventTarget-this-of-listener.html b/dom/events/EventTarget-this-of-listener.html
new file mode 100644
index 0000000..506564c
--- /dev/null
+++ b/dom/events/EventTarget-this-of-listener.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>EventTarget listeners this value</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-listener-invoke">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+
+  const nodes = [
+    document.createElement("p"),
+    document.createTextNode("some text"),
+    document.createDocumentFragment(),
+    document.createComment("a comment"),
+    document.createProcessingInstruction("target", "data")
+  ];
+
+  let callCount = 0;
+  for (const node of nodes) {
+    node.addEventListener("someevent", function () {
+      ++callCount;
+      assert_equals(this, node);
+    });
+
+    node.dispatchEvent(new CustomEvent("someevent"));
+  }
+
+  assert_equals(callCount, nodes.length);
+
+}, "the this value inside the event listener callback should be the node");
+
+test(() => {
+
+  const nodes = [
+    document.createElement("p"),
+    document.createTextNode("some text"),
+    document.createDocumentFragment(),
+    document.createComment("a comment"),
+    document.createProcessingInstruction("target", "data")
+  ];
+
+  let callCount = 0;
+  for (const node of nodes) {
+    const handler = {
+      handleEvent() {
+        ++callCount;
+        assert_equals(this, handler);
+      }
+    };
+
+    node.addEventListener("someevent", handler);
+
+    node.dispatchEvent(new CustomEvent("someevent"));
+  }
+
+  assert_equals(callCount, nodes.length);
+
+}, "the this value inside the event listener object handleEvent should be the object");
+
+test(() => {
+
+  const nodes = [
+    document.createElement("p"),
+    document.createTextNode("some text"),
+    document.createDocumentFragment(),
+    document.createComment("a comment"),
+    document.createProcessingInstruction("target", "data")
+  ];
+
+  let callCount = 0;
+  for (const node of nodes) {
+    const handler = {
+      handleEvent() {
+        assert_unreached("should not call the old handleEvent method");
+      }
+    };
+
+    node.addEventListener("someevent", handler);
+    handler.handleEvent = function () {
+      ++callCount;
+      assert_equals(this, handler);
+    };
+
+    node.dispatchEvent(new CustomEvent("someevent"));
+  }
+
+  assert_equals(callCount, nodes.length);
+
+}, "dispatchEvent should invoke the current handleEvent method of the object");
+
+test(() => {
+
+  const nodes = [
+    document.createElement("p"),
+    document.createTextNode("some text"),
+    document.createDocumentFragment(),
+    document.createComment("a comment"),
+    document.createProcessingInstruction("target", "data")
+  ];
+
+  let callCount = 0;
+  for (const node of nodes) {
+    const handler = {};
+
+    node.addEventListener("someevent", handler);
+    handler.handleEvent = function () {
+      ++callCount;
+      assert_equals(this, handler);
+    };
+
+    node.dispatchEvent(new CustomEvent("someevent"));
+  }
+
+  assert_equals(callCount, nodes.length);
+
+}, "addEventListener should not require handleEvent to be defined on object listeners");
+
+test(() => {
+
+  const nodes = [
+    document.createElement("p"),
+    document.createTextNode("some text"),
+    document.createDocumentFragment(),
+    document.createComment("a comment"),
+    document.createProcessingInstruction("target", "data")
+  ];
+
+  let callCount = 0;
+  for (const node of nodes) {
+    function handler() {
+      ++callCount;
+      assert_equals(this, node);
+    }
+
+    handler.handleEvent = () => {
+      assert_unreached("should not call the handleEvent method on a function");
+    };
+
+    node.addEventListener("someevent", handler);
+
+    node.dispatchEvent(new CustomEvent("someevent"));
+  }
+
+  assert_equals(callCount, nodes.length);
+
+}, "handleEvent properties added to a function before addEventListener are not reached");
+
+test(() => {
+
+  const nodes = [
+    document.createElement("p"),
+    document.createTextNode("some text"),
+    document.createDocumentFragment(),
+    document.createComment("a comment"),
+    document.createProcessingInstruction("target", "data")
+  ];
+
+  let callCount = 0;
+  for (const node of nodes) {
+    function handler() {
+      ++callCount;
+      assert_equals(this, node);
+    }
+
+    node.addEventListener("someevent", handler);
+
+    handler.handleEvent = () => {
+      assert_unreached("should not call the handleEvent method on a function");
+    };
+
+    node.dispatchEvent(new CustomEvent("someevent"));
+  }
+
+  assert_equals(callCount, nodes.length);
+
+}, "handleEvent properties added to a function after addEventListener are not reached");
+
+</script>
diff --git a/dom/nodes/Document-createCDATASection.html b/dom/nodes/Document-createCDATASection.html
new file mode 100644
index 0000000..72b3684
--- /dev/null
+++ b/dom/nodes/Document-createCDATASection.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>document.createCDATASection must throw in HTML documents</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-document-createcdatasection">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+assert_throws_dom("NotSupportedError", () => document.createCDATASection("foo"));
+
+done();
+</script>
diff --git a/dom/nodes/Document-createCDATASection.xhtml b/dom/nodes/Document-createCDATASection.xhtml
new file mode 100644
index 0000000..b0a5a7f
--- /dev/null
+++ b/dom/nodes/Document-createCDATASection.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta charset="utf-8"/>
+  <title>document.createCDATASection</title>
+  <link rel="help" href="https://dom.spec.whatwg.org/#dom-document-createcdatasection"/>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="Document-createComment-createTextNode.js"></script>
+</head>
+
+<body>
+  <script>
+  "use strict";
+  test_create("createCDATASection", CDATASection, 4, "#cdata-section");
+
+  test(() => {
+    assert_throws_dom("InvalidCharacterError", () => document.createCDATASection(" ]" + "]>  "));
+  }, "Creating a CDATA section containing the string \"]" + "]>\" must throw");
+  </script>
+</body>
+</html>
diff --git a/dom/nodes/DocumentFragment-constructor.html b/dom/nodes/DocumentFragment-constructor.html
new file mode 100644
index 0000000..e97a7c4
--- /dev/null
+++ b/dom/nodes/DocumentFragment-constructor.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>DocumentFragment constructor</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-documentfragment-documentfragment">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+  const fragment = new DocumentFragment();
+  assert_equals(fragment.ownerDocument, document);
+}, "Sets the owner document to the current global object associated document");
+
+test(() => {
+  const fragment = new DocumentFragment();
+  const text = document.createTextNode("");
+  fragment.appendChild(text);
+  assert_equals(fragment.firstChild, text);
+}, "Create a valid document DocumentFragment");
+</script>
diff --git a/dom/nodes/DocumentFragment-getElementById.html b/dom/nodes/DocumentFragment-getElementById.html
new file mode 100644
index 0000000..ce0d302
--- /dev/null
+++ b/dom/nodes/DocumentFragment-getElementById.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>DocumentFragment.prototype.getElementById</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<template>
+  <div id="bar">
+    <span id="foo" data-yes></span>
+  </div>
+  <div id="foo">
+    <span id="foo"></span>
+    <ul id="bar">
+      <li id="foo"></li>
+    </ul>
+  </div>
+</template>
+
+<script>
+"use strict";
+
+test(() => {
+  assert_equals(typeof DocumentFragment.prototype.getElementById, "function", "It must exist on the prototype");
+  assert_equals(typeof document.createDocumentFragment().getElementById, "function", "It must exist on an instance");
+}, "The method must exist");
+
+test(() => {
+  assert_equals(document.createDocumentFragment().getElementById("foo"), null);
+  assert_equals(document.createDocumentFragment().getElementById(""), null);
+}, "It must return null when there are no matches");
+
+test(() => {
+  const frag = document.createDocumentFragment();
+  frag.appendChild(document.createElement("div"));
+  frag.appendChild(document.createElement("span"));
+  frag.childNodes[0].id = "foo";
+  frag.childNodes[1].id = "foo";
+
+  assert_equals(frag.getElementById("foo"), frag.childNodes[0]);
+}, "It must return the first element when there are matches");
+
+test(() => {
+  const frag = document.createDocumentFragment();
+  frag.appendChild(document.createElement("div"));
+  frag.childNodes[0].setAttribute("id", "");
+
+  assert_equals(
+    frag.getElementById(""),
+    null,
+    "Even if there is an element with an empty-string ID attribute, it must not be returned"
+  );
+}, "Empty string ID values");
+
+test(() => {
+  const frag = document.querySelector("template").content;
+
+  assert_true(frag.getElementById("foo").hasAttribute("data-yes"));
+}, "It must return the first element when there are matches, using a template");
+</script>
diff --git a/dom/nodes/DocumentFragment-querySelectorAll-after-modification.html b/dom/nodes/DocumentFragment-querySelectorAll-after-modification.html
new file mode 100644
index 0000000..8049363
--- /dev/null
+++ b/dom/nodes/DocumentFragment-querySelectorAll-after-modification.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelectorAll should still work on DocumentFragments after they are modified</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2290 -->
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+const frag = document.createDocumentFragment();
+frag.appendChild(document.createElement("div"));
+
+assert_array_equals(frag.querySelectorAll("img"), [], "before modification");
+
+frag.appendChild(document.createElement("div"));
+
+// If the bug is present, this will throw.
+assert_array_equals(frag.querySelectorAll("img"), [], "after modification");
+
+done();
+</script>
diff --git a/dom/nodes/Element-hasAttribute.html b/dom/nodes/Element-hasAttribute.html
new file mode 100644
index 0000000..26528d7
--- /dev/null
+++ b/dom/nodes/Element-hasAttribute.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Element.prototype.hasAttribute</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-element-hasattribute">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<span data-e2="2" data-F2="3" id="t"></span>
+
+<script>
+"use strict";
+
+test(() => {
+
+  const el = document.createElement("p");
+  el.setAttributeNS("foo", "x", "first");
+
+  assert_true(el.hasAttribute("x"));
+
+}, "hasAttribute should check for attribute presence, irrespective of namespace");
+
+test(() => {
+
+  const el = document.getElementById("t");
+
+  assert_true(el.hasAttribute("data-e2"));
+  assert_true(el.hasAttribute("data-E2"));
+  assert_true(el.hasAttribute("data-f2"));
+  assert_true(el.hasAttribute("data-F2"));
+
+}, "hasAttribute should work with all attribute casings");
+</script>
diff --git a/dom/nodes/Element-matches-namespaced-elements.html b/dom/nodes/Element-matches-namespaced-elements.html
new file mode 100644
index 0000000..e61b11c
--- /dev/null
+++ b/dom/nodes/Element-matches-namespaced-elements.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>matches/webkitMatchesSelector must work when an element has a namespace</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression tests for https://github.com/jsdom/jsdom/issues/1846, https://github.com/jsdom/jsdom/issues/2247 -->
+
+<script>
+"use strict";
+
+for (const method of ["matches", "webkitMatchesSelector"]) {
+  test(() => {
+    assert_true(document.createElementNS("", "element")[method]("element"));
+  }, `empty string namespace, ${method}`);
+
+  test(() => {
+    assert_true(document.createElementNS("urn:ns", "h")[method]("h"));
+  }, `has a namespace, ${method}`);
+
+  test(() => {
+    assert_true(document.createElementNS("urn:ns", "h")[method]("*|h"));
+  }, `has a namespace, *|, ${method}`);
+}
+</script>
diff --git a/dom/nodes/Element-removeAttribute.html b/dom/nodes/Element-removeAttribute.html
new file mode 100644
index 0000000..df79e62
--- /dev/null
+++ b/dom/nodes/Element-removeAttribute.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Element.prototype.removeAttribute</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-element-removeattribute">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+
+  const el = document.createElement("p");
+  el.setAttribute("x", "first");
+  el.setAttributeNS("foo", "x", "second");
+
+  assert_equals(el.attributes.length, 2);
+  assert_equals(el.getAttribute("x"), "first");
+  assert_equals(el.getAttributeNS(null, "x"), "first");
+  assert_equals(el.getAttributeNS("foo", "x"), "second");
+
+  // removeAttribute removes the first attribute with name "x" that
+  // we set on the element, irrespective of namespace.
+  el.removeAttribute("x");
+
+  // The only attribute remaining should be the second one.
+  assert_equals(el.getAttribute("x"), "second");
+  assert_equals(el.getAttributeNS(null, "x"), null);
+  assert_equals(el.getAttributeNS("foo", "x"), "second");
+  assert_equals(el.attributes.length, 1, "one attribute");
+
+}, "removeAttribute should remove the first attribute, irrespective of namespace, when the first attribute is " +
+   "not in a namespace");
+
+test(() => {
+
+  const el = document.createElement("p");
+  el.setAttributeNS("foo", "x", "first");
+  el.setAttributeNS("foo2", "x", "second");
+
+  assert_equals(el.attributes.length, 2);
+  assert_equals(el.getAttribute("x"), "first");
+  assert_equals(el.getAttributeNS("foo", "x"), "first");
+  assert_equals(el.getAttributeNS("foo2", "x"), "second");
+
+  // removeAttribute removes the first attribute with name "x" that
+  // we set on the element, irrespective of namespace.
+  el.removeAttribute("x");
+
+  // The only attribute remaining should be the second one.
+  assert_equals(el.getAttribute("x"), "second");
+  assert_equals(el.getAttributeNS("foo", "x"), null);
+  assert_equals(el.getAttributeNS("foo2", "x"), "second");
+  assert_equals(el.attributes.length, 1, "one attribute");
+
+}, "removeAttribute should remove the first attribute, irrespective of namespace, when the first attribute is " +
+   "in a namespace");
+</script>
diff --git a/dom/nodes/Element-setAttribute.html b/dom/nodes/Element-setAttribute.html
new file mode 100644
index 0000000..7609406
--- /dev/null
+++ b/dom/nodes/Element-setAttribute.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Element.prototype.setAttribute</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-element-setattribute">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+
+  const el = document.createElement("p");
+  el.setAttributeNS("foo", "x", "first");
+  el.setAttributeNS("foo2", "x", "second");
+
+  el.setAttribute("x", "changed");
+
+  assert_equals(el.attributes.length, 2);
+  assert_equals(el.getAttribute("x"), "changed");
+  assert_equals(el.getAttributeNS("foo", "x"), "changed");
+  assert_equals(el.getAttributeNS("foo2", "x"), "second");
+
+}, "setAttribute should change the first attribute, irrespective of namespace");
+
+test(() => {
+  // https://github.com/whatwg/dom/issues/31
+
+  const el = document.createElement("p");
+  el.setAttribute("FOO", "bar");
+
+  assert_equals(el.getAttribute("foo"), "bar");
+  assert_equals(el.getAttribute("FOO"), "bar");
+  assert_equals(el.getAttributeNS("", "foo"), "bar");
+  assert_equals(el.getAttributeNS("", "FOO"), null);
+
+}, "setAttribute should lowercase before setting");
+</script>
diff --git a/dom/nodes/Element-tagName.html b/dom/nodes/Element-tagName.html
index 035a23c..43e7a2d 100644
--- a/dom/nodes/Element-tagName.html
+++ b/dom/nodes/Element-tagName.html
@@ -17,8 +17,15 @@
   assert_equals(document.createElementNS(SVGNS, "SVG").tagName, "SVG")
   assert_equals(document.createElementNS(SVGNS, "s:svg").tagName, "s:svg")
   assert_equals(document.createElementNS(SVGNS, "s:SVG").tagName, "s:SVG")
+
+  assert_equals(document.createElementNS(SVGNS, "textPath").tagName, "textPath");
 }, "tagName should not upper-case for SVG elements in HTML documents.")
 
+test(() => {
+  const el2 = document.createElementNS("http://example.com/", "mixedCase");
+  assert_equals(el2.tagName, "mixedCase");
+}, "tagName should not upper-case for other non-HTML namespaces");
+
 test(function() {
   if ("DOMParser" in window) {
     var xmlel = new DOMParser()
diff --git a/dom/nodes/Node-cloneNode-XMLDocument.html b/dom/nodes/Node-cloneNode-XMLDocument.html
new file mode 100644
index 0000000..2c63c77
--- /dev/null
+++ b/dom/nodes/Node-cloneNode-XMLDocument.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cloning of an XMLDocument</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-node-clonenode">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-node-clone">
+
+<!-- This is testing in particular "that implements the same interfaces as node" -->
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+  const doc = document.implementation.createDocument("namespace", "");
+
+  assert_equals(
+    doc.constructor, XMLDocument,
+    "Precondition check: document.implementation.createDocument() creates an XMLDocument"
+  );
+
+  const clone = doc.cloneNode(true);
+
+  assert_equals(clone.constructor, XMLDocument);
+}, "Created with createDocument");
+
+</script>
diff --git a/dom/nodes/Node-cloneNode-document-with-doctype.html b/dom/nodes/Node-cloneNode-document-with-doctype.html
new file mode 100644
index 0000000..2196308
--- /dev/null
+++ b/dom/nodes/Node-cloneNode-document-with-doctype.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cloning of a document with a doctype</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-node-clonenode">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-node-clone">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+  const doctype = document.implementation.createDocumentType("name", "publicId", "systemId");
+  const doc = document.implementation.createDocument("namespace", "", doctype);
+
+  const clone = doc.cloneNode(true);
+
+  assert_equals(clone.childNodes.length, 1, "Only one child node");
+  assert_equals(clone.childNodes[0].nodeType, Node.DOCUMENT_TYPE_NODE, "Is a document fragment");
+  assert_equals(clone.childNodes[0].name, "name");
+  assert_equals(clone.childNodes[0].publicId, "publicId");
+  assert_equals(clone.childNodes[0].systemId, "systemId");
+}, "Created with the createDocument/createDocumentType");
+
+test(() => {
+  const doc = document.implementation.createHTMLDocument();
+
+  const clone = doc.cloneNode(true);
+
+  assert_equals(clone.childNodes.length, 2, "Two child nodes");
+  assert_equals(clone.childNodes[0].nodeType, Node.DOCUMENT_TYPE_NODE, "Is a document fragment");
+  assert_equals(clone.childNodes[0].name, "html");
+  assert_equals(clone.childNodes[0].publicId, "");
+  assert_equals(clone.childNodes[0].systemId, "");
+}, "Created with the createHTMLDocument");
+
+test(() => {
+  const parser = new window.DOMParser();
+  const doc = parser.parseFromString("<!DOCTYPE html><html></html>", "text/html");
+
+  const clone = doc.cloneNode(true);
+
+  assert_equals(clone.childNodes.length, 2, "Two child nodes");
+  assert_equals(clone.childNodes[0].nodeType, Node.DOCUMENT_TYPE_NODE, "Is a document fragment");
+  assert_equals(clone.childNodes[0].name, "html");
+  assert_equals(clone.childNodes[0].publicId, "");
+  assert_equals(clone.childNodes[0].systemId, "");
+}, "Created with DOMParser");
+
+</script>
diff --git a/dom/nodes/Node-cloneNode-external-stylesheet-no-bc.sub.html b/dom/nodes/Node-cloneNode-external-stylesheet-no-bc.sub.html
new file mode 100644
index 0000000..bce6074
--- /dev/null
+++ b/dom/nodes/Node-cloneNode-external-stylesheet-no-bc.sub.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cloneNode on a stylesheet link in a browsing-context-less document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2497 -->
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+const doc = document.implementation.createHTMLDocument();
+
+// Bug was only triggered by absolute URLs, for some reason...
+const absoluteURL = new URL("/common/canvas-frame.css", location.href);
+doc.head.innerHTML = `<link rel="stylesheet" href="${absoluteURL}">`;
+
+// Test passes if this does not throw/crash
+doc.cloneNode(true);
+
+done();
+</script>
diff --git a/dom/nodes/Node-cloneNode-svg.html b/dom/nodes/Node-cloneNode-svg.html
new file mode 100644
index 0000000..9d4704b
--- /dev/null
+++ b/dom/nodes/Node-cloneNode-svg.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Cloning of SVG elements and attributes</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-node-clonenode">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-node-clone">
+<!-- regression test for https://github.com/jsdom/jsdom/issues/1601 -->
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<svg xmlns:xlink='http://www.w3.org/1999/xlink'><use xlink:href='#test'></use></svg>
+
+<script>
+"use strict";
+
+const svg = document.querySelector("svg");
+const clone = svg.cloneNode(true);
+
+test(() => {
+
+  assert_equals(clone.namespaceURI, "http://www.w3.org/2000/svg");
+  assert_equals(clone.prefix, null);
+  assert_equals(clone.localName, "svg");
+  assert_equals(clone.tagName, "svg");
+
+}, "cloned <svg> should have the right properties");
+
+test(() => {
+
+  const attr = clone.attributes[0];
+
+  assert_equals(attr.namespaceURI, "http://www.w3.org/2000/xmlns/");
+  assert_equals(attr.prefix, "xmlns");
+  assert_equals(attr.localName, "xlink");
+  assert_equals(attr.name, "xmlns:xlink");
+  assert_equals(attr.value, "http://www.w3.org/1999/xlink");
+
+}, "cloned <svg>'s xmlns:xlink attribute should have the right properties");
+
+test(() => {
+
+  const use = clone.firstElementChild;
+  assert_equals(use.namespaceURI, "http://www.w3.org/2000/svg");
+  assert_equals(use.prefix, null);
+  assert_equals(use.localName, "use");
+  assert_equals(use.tagName, "use");
+
+}, "cloned <use> should have the right properties");
+
+test(() => {
+
+  const use = clone.firstElementChild;
+  const attr = use.attributes[0];
+
+  assert_equals(attr.namespaceURI, "http://www.w3.org/1999/xlink");
+  assert_equals(attr.prefix, "xlink");
+  assert_equals(attr.localName, "href");
+  assert_equals(attr.name, "xlink:href");
+  assert_equals(attr.value, "#test");
+
+}, "cloned <use>'s xlink:href attribute should have the right properties");
+
+</script>
diff --git a/dom/nodes/Node-isConnected-shadow-dom.html b/dom/nodes/Node-isConnected-shadow-dom.html
new file mode 100644
index 0000000..7d04dc3
--- /dev/null
+++ b/dom/nodes/Node-isConnected-shadow-dom.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test of Node.isConnected in a shadow tree</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#connected">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+function testIsConnected(mode) {
+  test(() => {
+    const host = document.createElement("div");
+    document.body.appendChild(host);
+
+    const root = host.attachShadow({ mode });
+
+    const node = document.createElement("div");
+    root.appendChild(node);
+
+    assert_true(node.isConnected);
+  }, `Node.isConnected in a ${mode} shadow tree`);
+}
+
+for (const mode of ["closed", "open"]) {
+  testIsConnected(mode);
+}
+</script>
diff --git a/dom/nodes/Node-mutation-adoptNode.html b/dom/nodes/Node-mutation-adoptNode.html
new file mode 100644
index 0000000..9c9594c
--- /dev/null
+++ b/dom/nodes/Node-mutation-adoptNode.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Node-manipulation-adopted</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-domimplementation-createhtmldocument">
+<link rel=help href="https://dom.spec.whatwg.org/#mutation-algorithms">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(() => {
+  const old = document.implementation.createHTMLDocument("");
+  const div = old.createElement("div");
+  div.appendChild(old.createTextNode("text"));
+  assert_equals(div.ownerDocument, old);
+  assert_equals(div.firstChild.ownerDocument, old);
+  document.body.appendChild(div);
+  assert_equals(div.ownerDocument, document);
+  assert_equals(div.firstChild.ownerDocument, document);
+}, "simple append of foreign div with text");
+
+</script>
diff --git a/dom/nodes/ParentNode-querySelector-case-insensitive.html b/dom/nodes/ParentNode-querySelector-case-insensitive.html
new file mode 100644
index 0000000..e461ee5
--- /dev/null
+++ b/dom/nodes/ParentNode-querySelector-case-insensitive.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelector(All) must work with the i and *= selectors</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2551 -->
+
+<input name="User" id="testInput"></input>
+
+<script>
+"use strict";
+const input = document.getElementById("testInput");
+
+test(() => {
+  assert_equals(document.querySelector("input[name*=user i]"), input);
+}, "querySelector");
+
+test(() => {
+  assert_array_equals(document.querySelectorAll("input[name*=user i]"), [input]);
+}, "querySelectorAll");
+</script>
diff --git a/dom/nodes/ParentNode-querySelector-escapes.html b/dom/nodes/ParentNode-querySelector-escapes.html
new file mode 100644
index 0000000..65a75e5
--- /dev/null
+++ b/dom/nodes/ParentNode-querySelector-escapes.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>querySelector() with CSS escapes</title>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-parentnode-queryselector">
+<link rel="help" href="https://drafts.csswg.org/css-syntax/#consume-escaped-code-point">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<link rel="author" title="bellbind" href="mailto:bellbind@gmail.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+function testMatched(id, selector) {
+  test(() => {
+    const container = document.createElement("div");
+    const child = document.createElement("span");
+    child.id = id;
+
+    container.appendChild(child);
+
+    assert_equals(container.querySelector(selector), child);
+  }, `${JSON.stringify(id)} should match with ${JSON.stringify(selector)}`);
+}
+
+function testNeverMatched(id, selector) {
+  test(() => {
+    const container = document.createElement("div");
+    const child = document.createElement("span");
+    child.id = id;
+
+    container.appendChild(child);
+
+    assert_equals(container.querySelector(selector), null);
+  }, `${JSON.stringify(id)} should never match with ${JSON.stringify(selector)}`);
+}
+
+// 4.3.7 from https://drafts.csswg.org/css-syntax/#consume-escaped-code-point
+testMatched("nonescaped", "#nonescaped");
+
+// - escape hex digit
+testMatched("0nextIsWhiteSpace", "#\\30 nextIsWhiteSpace");
+testMatched("0nextIsNotHexLetters", "#\\30nextIsNotHexLetters");
+testMatched("0connectHexMoreThan6Hex", "#\\000030connectHexMoreThan6Hex");
+testMatched("0spaceMoreThan6Hex", "#\\000030 spaceMoreThan6Hex");
+
+// - hex digit special replacement
+// 1. zero points
+testMatched("zero\u{fffd}", "#zero\\0");
+testNeverMatched("zero\u{0}", "#zero\\0");
+testMatched("zero\u{fffd}", "#zero\\000000");
+testNeverMatched("zero\u{0}", "#zero\\000000");
+// 2. surrogate points
+testMatched("\u{fffd}surrogateFirst", "#\\d83d surrogateFirst");
+testNeverMatched("\ud83dsurrogateFirst", "#\\d83d surrogateFirst");
+testMatched("surrogateSecond\u{fffd}", "#surrogateSecond\\dd11");
+testNeverMatched("surrogateSecond\udd11", "#surrogateSecond\\dd11");
+testMatched("surrogatePair\u{fffd}\u{fffd}", "#surrogatePair\\d83d\\dd11");
+testNeverMatched("surrogatePair\u{1f511}", "#surrogatePair\\d83d\\dd11");
+// 3. out of range points
+testMatched("outOfRange\u{fffd}", "#outOfRange\\110000");
+testMatched("outOfRange\u{fffd}", "#outOfRange\\110030");
+testNeverMatched("outOfRange\u{30}", "#outOfRange\\110030");
+testMatched("outOfRange\u{fffd}", "#outOfRange\\555555");
+testMatched("outOfRange\u{fffd}", "#outOfRange\\ffffff");
+
+// - escape EOF
+testNeverMatched("eof\\", "#eof\\");
+
+// - escape anythong else
+testMatched(".comma", "#\\.comma");
+testMatched("-minus", "#\\-minus");
+testMatched("g", "#\\g");
+
+// non edge cases
+testMatched("aBMPRegular", "#\\61 BMPRegular");
+testMatched("\u{1f511}nonBMP", "#\\1f511 nonBMP");
+testMatched("00continueEscapes", "#\\30\\30 continueEscapes");
+testMatched("00continueEscapes", "#\\30 \\30 continueEscapes");
+testMatched("continueEscapes00", "#continueEscapes\\30 \\30 ");
+testMatched("continueEscapes00", "#continueEscapes\\30 \\30");
+testMatched("continueEscapes00", "#continueEscapes\\30\\30 ");
+testMatched("continueEscapes00", "#continueEscapes\\30\\30");
+
+// ident tests case from CSS tests of chromium source: https://goo.gl/3Cxdov
+testMatched("hello", "#hel\\6Co");
+testMatched("&B", "#\\26 B");
+testMatched("hello", "#hel\\6C o");
+testMatched("spaces", "#spac\\65\r\ns");
+testMatched("spaces", "#sp\\61\tc\\65\fs");
+testMatched("test\u{D799}", "#test\\D799");
+testMatched("\u{E000}", "#\\E000");
+testMatched("test", "#te\\s\\t");
+testMatched("spaces in\tident", "#spaces\\ in\\\tident");
+testMatched(".,:!", "#\\.\\,\\:\\!");
+testMatched("null\u{fffd}", "#null\\0");
+testMatched("null\u{fffd}", "#null\\0000");
+testMatched("large\u{fffd}", "#large\\110000");
+testMatched("large\u{fffd}", "#large\\23456a");
+testMatched("surrogate\u{fffd}", "#surrogate\\D800");
+testMatched("surrogate\u{fffd}", "#surrogate\\0DBAC");
+testMatched("\u{fffd}surrogate", "#\\00DFFFsurrogate");
+testMatched("\u{10ffff}", "#\\10fFfF");
+testMatched("\u{10ffff}0", "#\\10fFfF0");
+testMatched("\u{100000}00", "#\\10000000");
+testMatched("eof\u{fffd}", "#eof\\");
+
+testMatched("simple-ident", "#simple-ident");
+testMatched("testing123", "#testing123");
+testMatched("_underscore", "#_underscore");
+testMatched("-text", "#-text");
+testMatched("-m", "#-\\6d");
+testMatched("--abc", "#--abc");
+testMatched("--", "#--");
+testMatched("--11", "#--11");
+testMatched("---", "#---");
+testMatched("\u{2003}", "#\u{2003}");
+testMatched("\u{A0}", "#\u{A0}");
+testMatched("\u{1234}", "#\u{1234}");
+testMatched("\u{12345}", "#\u{12345}");
+testMatched("\u{fffd}", "#\u{0}");
+testMatched("ab\u{fffd}c", "#ab\u{0}c");
+</script>
diff --git a/dom/nodes/ParentNode-querySelector-scope.html b/dom/nodes/ParentNode-querySelector-scope.html
new file mode 100644
index 0000000..b08a167
--- /dev/null
+++ b/dom/nodes/ParentNode-querySelector-scope.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelector(All) must work with :scope</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2359 -->
+
+<div><p><span>hello</span></p></div>
+
+<script>
+"use strict";
+const div = document.querySelector("div");
+const p = document.querySelector("p");
+
+test(() => {
+  assert_equals(div.querySelector(":scope > p"), p);
+  assert_equals(div.querySelector(":scope > span"), null);
+}, "querySelector");
+
+test(() => {
+  assert_array_equals(div.querySelectorAll(":scope > p"), [p]);
+  assert_array_equals(div.querySelectorAll(":scope > span"), []);
+}, "querySelectorAll");
+</script>
diff --git a/dom/nodes/ParentNode-querySelectorAll-removed-elements.html b/dom/nodes/ParentNode-querySelectorAll-removed-elements.html
new file mode 100644
index 0000000..3cefc80
--- /dev/null
+++ b/dom/nodes/ParentNode-querySelectorAll-removed-elements.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelectorAll must not return removed elements</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2519 -->
+
+<div id="container"></div>
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+const container = document.querySelector("#container");
+function getIDs() {
+  return [...container.querySelectorAll("a.test")].map(el => el.id);
+}
+
+container.innerHTML = `<a id="link-a" class="test">a link</a>`;
+assert_array_equals(getIDs(), ["link-a"], "Sanity check: initial setup");
+
+container.innerHTML = `<a id="link-b" class="test"><img src="foo.jpg"></a>`;
+assert_array_equals(getIDs(), ["link-b"], "After replacement");
+
+container.innerHTML = `<a id="link-a" class="test">a link</a>`;
+assert_array_equals(getIDs(), ["link-a"], "After changing back to the original HTML");
+
+done();
+</script>
diff --git a/dom/nodes/ParentNode-querySelectors-exclusive.html b/dom/nodes/ParentNode-querySelectors-exclusive.html
new file mode 100644
index 0000000..5cff936
--- /dev/null
+++ b/dom/nodes/ParentNode-querySelectors-exclusive.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelector/querySelectorAll should not include their thisArg</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2296 -->
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+const button = document.createElement("button");
+
+assert_equals(button.querySelector("*"), null, "querySelector, '*', before modification");
+assert_equals(button.querySelector("button"), null, "querySelector, 'button', before modification");
+assert_equals(button.querySelector("button, span"), null, "querySelector, 'button, span', before modification");
+assert_array_equals(button.querySelectorAll("*"), [], "querySelectorAll, '*', before modification");
+assert_array_equals(button.querySelectorAll("button"), [], "querySelectorAll, 'button', before modification");
+assert_array_equals(
+  button.querySelectorAll("button, span"), [],
+  "querySelectorAll, 'button, span', before modification"
+);
+
+
+button.innerHTML = "text";
+
+assert_equals(button.querySelector("*"), null, "querySelector, '*', after modification");
+assert_equals(button.querySelector("button"), null, "querySelector, 'button', after modification");
+assert_equals(button.querySelector("button, span"), null, "querySelector, 'button, span', after modification");
+assert_array_equals(button.querySelectorAll("*"), [], "querySelectorAll, '*', after modification");
+assert_array_equals(button.querySelectorAll("button"), [], "querySelectorAll, 'button', after modification");
+assert_array_equals(
+  button.querySelectorAll("button, span"), [],
+  "querySelectorAll, 'button, span', after modification"
+);
+
+done();
+</script>
diff --git a/dom/nodes/ParentNode-querySelectors-namespaces.html b/dom/nodes/ParentNode-querySelectors-namespaces.html
new file mode 100644
index 0000000..714999b
--- /dev/null
+++ b/dom/nodes/ParentNode-querySelectors-namespaces.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelectorAll must work with namespace attribute selectors on SVG</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2028 -->
+
+<svg id="thesvg" xlink:href="foo"></svg>
+
+<script>
+"use strict";
+
+setup({ single_test: true });
+
+const el = document.getElementById("thesvg");
+
+assert_equals(document.querySelector("[*|href]"), el);
+assert_array_equals(document.querySelectorAll("[*|href]"), [el]);
+
+done();
+</script>
diff --git a/dom/nodes/ParentNode-querySelectors-space-and-dash-attribute-value.html b/dom/nodes/ParentNode-querySelectors-space-and-dash-attribute-value.html
new file mode 100644
index 0000000..e08c6e6
--- /dev/null
+++ b/dom/nodes/ParentNode-querySelectors-space-and-dash-attribute-value.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelector(All) must work for attribute values that contain spaces and dashes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/2542 -->
+
+<a title="test with - dash and space" id="testme">Test One</a>
+
+<script>
+"use strict";
+const el = document.getElementById("testme");
+
+test(() => {
+  assert_equals(document.querySelector("a[title='test with - dash and space']"), el);
+}, "querySelector");
+
+test(() => {
+  assert_equals(document.querySelector("a[title='test with - dash and space']"), el);
+}, "querySelectorAll");
+</script>
diff --git a/dom/nodes/Text-wholeText.html b/dom/nodes/Text-wholeText.html
new file mode 100644
index 0000000..2467930
--- /dev/null
+++ b/dom/nodes/Text-wholeText.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Text - wholeText</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-text-wholetext">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+
+test(() => {
+  const parent = document.createElement("div");
+
+  const t1 = document.createTextNode("a");
+  const t2 = document.createTextNode("b");
+  const t3 = document.createTextNode("c");
+
+  assert_equals(t1.wholeText, t1.textContent);
+
+  parent.appendChild(t1);
+
+  assert_equals(t1.wholeText, t1.textContent);
+
+  parent.appendChild(t2);
+
+  assert_equals(t1.wholeText, t1.textContent + t2.textContent);
+  assert_equals(t2.wholeText, t1.textContent + t2.textContent);
+
+  parent.appendChild(t3);
+
+  assert_equals(t1.wholeText, t1.textContent + t2.textContent + t3.textContent);
+  assert_equals(t2.wholeText, t1.textContent + t2.textContent + t3.textContent);
+  assert_equals(t3.wholeText, t1.textContent + t2.textContent + t3.textContent);
+
+  const a = document.createElement("a");
+  a.textContent = "I'm an Anchor";
+  parent.insertBefore(a, t3);
+
+  const span = document.createElement("span");
+  span.textContent = "I'm a Span";
+  parent.appendChild(document.createElement("span"));
+
+  assert_equals(t1.wholeText, t1.textContent + t2.textContent);
+  assert_equals(t2.wholeText, t1.textContent + t2.textContent);
+  assert_equals(t3.wholeText, t3.textContent);
+}, "wholeText returns text of all Text nodes logically adjacent to the node, in document order.");
+</script>
diff --git a/dom/nodes/attributes-namednodemap.html b/dom/nodes/attributes-namednodemap.html
new file mode 100644
index 0000000..96f9d30
--- /dev/null
+++ b/dom/nodes/attributes-namednodemap.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<title>Tests of some tricky semantics around NamedNodeMap and the element.attributes collection</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<link rel="help" href="https://dom.spec.whatwg.org/#interface-namednodemap">
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-element-attributes">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+
+  const element = document.createElement("div");
+  element.setAttribute("x", "first");
+
+  assert_equals(element.attributes.length, 1, "one attribute");
+  assert_equals(element.attributes.x.value, "first");
+
+}, "an attribute set by setAttribute should be accessible as a field on the `attributes` field of an Element");
+
+test(() => {
+
+  const element = document.createElement("div");
+  const map = element.attributes;
+
+  assert_equals(map.length, 0);
+
+  const attr1 = document.createAttribute("attr1");
+  map.setNamedItem(attr1);
+  assert_equals(map.attr1, attr1);
+  assert_equals(map.length, 1);
+
+  const attr2 = document.createAttribute("attr2");
+  map.setNamedItem(attr2);
+  assert_equals(map.attr2, attr2);
+  assert_equals(map.length, 2);
+
+  const rm1 = map.removeNamedItem("attr1");
+  assert_equals(rm1, attr1);
+  assert_equals(map.length, 1);
+
+  const rm2 = map.removeNamedItem("attr2");
+  assert_equals(rm2, attr2);
+  assert_equals(map.length, 0);
+
+}, "setNamedItem and removeNamedItem on `attributes` should add and remove fields from `attributes`");
+
+test(() => {
+
+  const element = document.createElement("div");
+  const map = element.attributes;
+
+  const fooAttribute = document.createAttribute("foo");
+  map.setNamedItem(fooAttribute);
+
+  const itemAttribute = document.createAttribute("item");
+  map.setNamedItem(itemAttribute);
+
+  assert_equals(map.foo, fooAttribute);
+  assert_equals(map.item, NamedNodeMap.prototype.item);
+  assert_equals(typeof map.item, "function");
+
+  map.removeNamedItem("item");
+  assert_equals(map.item, NamedNodeMap.prototype.item);
+  assert_equals(typeof map.item, "function");
+
+}, "setNamedItem and removeNamedItem on `attributes` should not interfere with existing method names");
+
+test(() => {
+
+  const element = document.createElement("div");
+  element.setAttributeNS(null, "x", "first");
+
+  assert_equals(element.attributes.length, 1, "one attribute");
+  assert_equals(element.attributes.x.value, "first");
+
+}, "an attribute with a null namespace should be accessible as a field on the `attributes` field of an Element");
+
+test(() => {
+
+  const element = document.createElement("div");
+  element.setAttributeNS("foo", "x", "first");
+
+  assert_equals(element.attributes.length, 1, "one attribute");
+  assert_equals(element.attributes.x.value, "first");
+
+}, "an attribute with a set namespace should be accessible as a field on the `attributes` field of an Element");
+
+test(() => {
+
+  const element = document.createElement("div");
+  element.setAttributeNS("foo", "setNamedItem", "first");
+
+  assert_equals(element.attributes.length, 1, "one attribute");
+  assert_equals(typeof element.attributes.setNamedItem, "function");
+
+}, "setting an attribute should not overwrite the methods of an `NamedNodeMap` object");
+
+test(() => {
+
+  const element = document.createElement("div");
+  element.setAttributeNS("foo", "toString", "first");
+
+  assert_equals(element.attributes.length, 1, "one attribute");
+  assert_equals(typeof element.attributes.toString, "function");
+
+}, "setting an attribute should not overwrite the methods defined by prototype ancestors of an `NamedNodeMap` object");
+
+test(() => {
+
+  const element = document.createElement("div");
+  element.setAttributeNS("foo", "length", "first");
+
+  assert_equals(element.attributes.length, 1, "one attribute");
+
+}, "setting an attribute should not overwrite the the length property of an `NamedNodeMap` object");
+
+</script>
diff --git a/dom/nodes/getElementsByClassName-32.html b/dom/nodes/getElementsByClassName-32.html
new file mode 100644
index 0000000..29eb413
--- /dev/null
+++ b/dom/nodes/getElementsByClassName-32.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Node.prototype.getElementsByClassName tests imported from jsdom</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div class="df-article" id="1">
+</div>
+<div class="df-article" id="2">
+</div>
+<div class="df-article" id="3">
+</div>
+
+<script>
+"use strict";
+
+test(() => {
+
+  const p = document.createElement("p");
+  p.className = "unknown";
+  document.body.appendChild(p);
+
+  const elements = document.getElementsByClassName("first-p");
+  assert_array_equals(elements, []);
+
+}, "cannot find the class name");
+
+test(() => {
+
+  const p = document.createElement("p");
+  p.className = "first-p";
+  document.body.appendChild(p);
+
+  const elements = document.getElementsByClassName("first-p");
+  assert_array_equals(elements, [p]);
+
+}, "finds the class name");
+
+
+test(() => {
+
+  const p = document.createElement("p");
+  p.className = "the-p second third";
+  document.body.appendChild(p);
+
+  const elements1 = document.getElementsByClassName("the-p");
+  assert_array_equals(elements1, [p]);
+
+  const elements2 = document.getElementsByClassName("second");
+  assert_array_equals(elements2, [p]);
+
+  const elements3 = document.getElementsByClassName("third");
+  assert_array_equals(elements3, [p]);
+
+}, "finds the same element with multiple class names");
+
+test(() => {
+
+  const elements = document.getElementsByClassName("df-article");
+
+  assert_equals(elements.length, 3);
+  assert_array_equals(Array.prototype.map.call(elements, el => el.id), ["1", "2", "3"]);
+
+}, "does not get confused by numeric IDs");
+
+</script>
diff --git a/dom/nodes/getElementsByClassName-empty-set.html b/dom/nodes/getElementsByClassName-empty-set.html
new file mode 100644
index 0000000..75b8d5a
--- /dev/null
+++ b/dom/nodes/getElementsByClassName-empty-set.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Node.prototype.getElementsByClassName with no real class names</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<span class=" ">test</span>
+
+<script>
+"use strict";
+
+test(() => {
+  const elements = document.getElementsByClassName("");
+  assert_array_equals(elements, []);
+}, "Passing an empty string to getElementsByClassName should return an empty HTMLCollection");
+
+test(() => {
+  const elements = document.getElementsByClassName(" ");
+  assert_array_equals(elements, []);
+}, "Passing a space to getElementsByClassName should return an empty HTMLCollection");
+
+test(() => {
+  const elements = document.getElementsByClassName("   ");
+  assert_array_equals(elements, []);
+}, "Passing three spaces to getElementsByClassName should return an empty HTMLCollection");
+
+</script>
diff --git a/dom/nodes/getElementsByClassName-whitespace-class-names.html b/dom/nodes/getElementsByClassName-whitespace-class-names.html
new file mode 100644
index 0000000..59bfd2e
--- /dev/null
+++ b/dom/nodes/getElementsByClassName-whitespace-class-names.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>Node.prototype.getElementsByClassName with no real class names</title>
+<link rel=help href="https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<span class="&#x000B;">LINE TABULATION</span>
+<span class="&#x0085;">NEXT LINE</span>
+<span class="&#x00A0;">NO-BREAK SPACE</span>
+<span class="&#x1680;">OGHAM SPACE MARK</span>
+<span class="&#x2000;">EN QUAD</span>
+<span class="&#x2001;">EM QUAD</span>
+<span class="&#x2002;">EN SPACE</span>
+<span class="&#x2003;">EM SPACE</span>
+<span class="&#x2004;">THREE-PER-EM SPACE</span>
+<span class="&#x2005;">FOUR-PER-EM SPACE</span>
+<span class="&#x2006;">SIX-PER-EM SPACE</span>
+<span class="&#x2007;">FIGURE SPACE</span>
+<span class="&#x2008;">PUNCTUATION SPACE</span>
+<span class="&#x2009;">THIN SPACE</span>
+<span class="&#x200A;">HAIR SPACE</span>
+<span class="&#x2028;">LINE SEPARATOR</span>
+<span class="&#x2029;">PARAGRAPH SEPARATOR</span>
+<span class="&#x202F;">NARROW NO-BREAK SPACE</span>
+<span class="&#x205F;">MEDIUM MATHEMATICAL SPACE</span>
+<span class="&#x3000;">IDEOGRAPHIC SPACE</span>
+
+<span class="&#x180E;">MONGOLIAN VOWEL SEPARATOR</span>
+<span class="&#x200B;">ZERO WIDTH SPACE</span>
+<span class="&#x200C;">ZERO WIDTH NON-JOINER</span>
+<span class="&#x200D;">ZERO WIDTH JOINER</span>
+<span class="&#x2060;">WORD JOINER</span>
+<span class="&#xFEFF;">ZERO WIDTH NON-BREAKING SPACE</span>
+
+<script>
+"use strict";
+
+const spans = document.querySelectorAll("span");
+
+for (const span of spans) {
+  test(() => {
+    const className = span.getAttribute("class");
+    assert_equals(className.length, 1, "Sanity check: the class name was retrieved and is a single character");
+    const shouldBeSpan = document.getElementsByClassName(className);
+    assert_array_equals(shouldBeSpan, [span]);
+  }, `Passing a ${span.textContent} to getElementsByClassName still finds the span`);
+}
+</script>
diff --git a/dom/nodes/svg-template-querySelector.html b/dom/nodes/svg-template-querySelector.html
new file mode 100644
index 0000000..5d2f634
--- /dev/null
+++ b/dom/nodes/svg-template-querySelector.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>querySelector on template fragments with SVG elements</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<template id="template1"><div></div></template>
+<template id="template2"><svg></svg></template>
+<template id="template3"><div><svg></svg></div></template>
+
+<script>
+"use strict";
+
+test(() => {
+  const fragment = document.querySelector("#template1").content;
+  assert_not_equals(fragment.querySelector("div"), null);
+}, "querySelector works on template contents fragments with HTML elements (sanity check)");
+
+test(() => {
+  const fragment = document.querySelector("#template2").content;
+  assert_not_equals(fragment.querySelector("svg"), null);
+}, "querySelector works on template contents fragments with SVG elements");
+
+test(() => {
+  const fragment = document.querySelector("#template3").content;
+  assert_not_equals(fragment.firstChild.querySelector("svg"), null);
+}, "querySelector works on template contents fragments with nested SVG elements");
+</script>
diff --git a/dom/window-extends-event-target.html b/dom/window-extends-event-target.html
new file mode 100644
index 0000000..3b69032
--- /dev/null
+++ b/dom/window-extends-event-target.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Window extends EventTarget</title>
+<link rel="help" href="https://github.com/jsdom/jsdom/issues/2830">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+test(() => {
+
+  assert_equals(window.addEventListener, EventTarget.prototype.addEventListener);
+  assert_equals(window.removeEventListener, EventTarget.prototype.removeEventListener);
+  assert_equals(window.dispatchEvent, EventTarget.prototype.dispatchEvent);
+
+}, "EventTarget methods on Window instances are inherited from the EventTarget prototype");
+
+test(() => {
+
+  const kCustom = "custom-event";
+  const customEvent = new CustomEvent(kCustom, {
+    bubbles: true
+  });
+
+  let target;
+  window.addEventListener.call(document.body, kCustom, function () {
+    target = this;
+  });
+
+  document.body.dispatchEvent(customEvent);
+
+  assert_equals(target, document.body);
+
+}, "window.addEventListener respects custom `this`");
+
+test(() => {
+
+  const kCustom = "custom-event";
+  const customEvent = new CustomEvent(kCustom, {
+    bubbles: true
+  });
+
+  let target;
+  window.addEventListener.call(null, kCustom, function () {
+    target = this;
+  });
+
+  document.body.dispatchEvent(customEvent);
+
+  assert_equals(target, window);
+
+}, "window.addEventListener treats nullish `this` as `window`");
+</script>