Test Resource State Pseudo-classes (#36565)

Ported over from WebKit's internal tests.

https://w3c.github.io/csswg-drafts/selectors/#resource-pseudos
diff --git a/css/selectors/media/media-loading-state.html b/css/selectors/media/media-loading-state.html
new file mode 100644
index 0000000..b48cfc1
--- /dev/null
+++ b/css/selectors/media/media-loading-state.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long" />
+<title>Media Loading State: the :buffering and :stalled pseudo-classes</title>
+<link
+  rel="help"
+  href="https://w3c.github.io/csswg-drafts/selectors/#media-loading-state"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+  <video width="300" height="300" muted loop controls></video>
+  <script type="module">
+    test((t) => {
+      for (const pseudo of [":buffering", ":stalled"]) {
+        try {
+          document.querySelector(`.not-a-thing${pseudo}`);
+        } catch (e) {
+          assert_unreached(`${pseudo} is not supported`);
+        }
+      }
+    }, "Test :pseudo-class syntax is supported without throwing a SyntaxError");
+
+    promise_test(async (t) => {
+      const video = document.querySelector("video");
+      await new Promise((r) => {
+        video.addEventListener("stalled", r, { once: true });
+        video.src = `/media/counting.mp4?pipe=trickle(100:d1:r2)&random=${Math.random()}`;
+      });
+      const promise = video.play();
+      assert_equals(
+        document.querySelector("video:stalled"),
+        video,
+        "video is stalled"
+      );
+      video.src = "";
+      // Wait for the video to abort trying to play
+      try {
+        await promise;
+      } catch (err) {}
+    }, "Test :stalled pseudo-class");
+
+    promise_test(async (t) => {
+      const video = document.querySelector("video");
+      await new Promise((r) => {
+        video.addEventListener("stalled", r, { once: true });
+        video.src = `/media/counting.mp4?pipe=trickle(100:d1:r2)&random=${Math.random()}`;
+      });
+      video.currentTime = 10;
+      const promise = video.play();
+      assert_equals(
+        document.querySelector("video:buffering"),
+        video,
+        "video is buffering"
+      );
+      video.src = "";
+      // Wait for the video to abort trying to play
+      try {
+        await promise;
+      } catch (err) {}
+    }, "Test :buffering pseudo-class");
+  </script>
+</body>
diff --git a/css/selectors/media/media-playback-state.html b/css/selectors/media/media-playback-state.html
new file mode 100644
index 0000000..628daa2
--- /dev/null
+++ b/css/selectors/media/media-playback-state.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>
+  Media Playback State: the :playing, :paused, and :seeking pseudo-classes
+</title>
+<link
+  rel="help"
+  href="https://w3c.github.io/csswg-drafts/selectors/#video-state"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+  <video width="300" height="300" muted loop></video>
+  <script>
+    test((t) => {
+      for (const pseudo of [":playing", ":paused", ":seeking"]) {
+        try {
+          document.querySelector(`.not-a-thing${pseudo}`);
+        } catch (e) {
+          assert_unreached(`${pseudo} is not supported`);
+        }
+      }
+    }, "Test :pseudo-class syntax is supported without throwing a SyntaxError");
+
+    promise_test(async (t) => {
+      const video = document.querySelector("video");
+      await new Promise((r) => {
+        video.addEventListener("canplay", r, { once: true });
+        video.src = "/media/counting.mp4";
+      });
+      video.muted = true; // allows us to play the video
+      assert_true(video.muted, "video is muted");
+      assert_true(video.paused, "video is paused");
+      await new Promise((r) => {
+        video.addEventListener("playing", r, { once: true });
+        video.play();
+      });
+      assert_false(video.paused, "video is playing");
+      assert_equals(document.querySelector("video:playing"), video);
+      assert_equals(document.querySelector("video:not(:playing)"), null);
+      assert_equals(document.querySelector("video:paused"), null);
+      assert_equals(document.querySelector("video:not(:paused)"), video);
+    }, "Test :playing pseudo-classes");
+
+    promise_test(async (t) => {
+      const video = document.querySelector("video");
+      await new Promise((r) => {
+        video.addEventListener("canplay", r, { once: true });
+        video.src = "/media/counting.mp4";
+      });
+      assert_equals(video.paused, true);
+      assert_equals(document.querySelector("video:playing"), null);
+      assert_equals(document.querySelector("video:not(:playing)"), video);
+      assert_equals(document.querySelector("video:paused"), video);
+      assert_equals(document.querySelector("video:not(:paused)"), null);
+    }, "Test :paused pseudo-classes");
+
+    promise_test(async (t) => {
+      const video = document.querySelector("video");
+      await new Promise((r) => {
+        video.addEventListener("canplay", r, { once: true });
+        video.src = "/media/counting.mp4";
+      });
+      assert_equals(document.querySelector("video:seeking"), null);
+      assert_equals(document.querySelector("video:not(:seeking)"), video);
+      await new Promise((r) => {
+        video.addEventListener("seeking", r, { once: true });
+        video.currentTime = 10;
+      });
+      assert_equals(document.querySelector("video:seeking"), video);
+      assert_equals(document.querySelector("video:not(:seeking)"), null);
+    }, "Test :seeking pseudo-class");
+  </script>
+</body>
diff --git a/css/selectors/media/sound-state.html b/css/selectors/media/sound-state.html
new file mode 100644
index 0000000..d9eb86a
--- /dev/null
+++ b/css/selectors/media/sound-state.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Sound State: the :muted and :volume-locked pseudo-classes</title>
+<link
+  rel="help"
+  href="https://w3c.github.io/csswg-drafts/selectors/#sound-state"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+  <video width="300" height="300" loop></video>
+  <script type="module">
+    // Unfortunately, we can't test the volume-locked state because it's not
+    // possible to lock the volume of a video element with JS.
+
+    test((t) => {
+      for (const pseudo of [":muted", ":volume-locked"]) {
+        try {
+          document.querySelector(`.not-a-thing${pseudo}`);
+        } catch (e) {
+          assert_unreached(`${pseudo} is not supported`);
+        }
+      }
+    }, "Test :pseudo-class syntax is supported without throwing a SyntaxError");
+
+    promise_test(async (t) => {
+      assert_equals(
+        document.querySelector("video:muted"),
+        null,
+        "must know :muted"
+      );
+      const video = document.querySelector("video");
+      await new Promise((r) => {
+        video.addEventListener("canplay", r, { once: true });
+        video.src = "/media/counting.mp4";
+      });
+      video.muted = false;
+      assert_false(video.muted, "video is unmuted");
+      assert_equals(document.querySelector("video:muted"), null);
+      assert_equals(document.querySelector("video:not(:muted)"), video);
+      video.muted = true;
+      assert_equals(document.querySelector("video:muted"), video);
+      assert_equals(document.querySelector("video:not(:muted)"), null);
+    }, "Test :muted pseudo-class");
+  </script>
+</body>