summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/css/selectors/invalidation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/css/selectors/invalidation
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/css/selectors/invalidation')
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/any-link-pseudo.html36
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/attribute-or-elemental-selectors-in-has.html149
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/attribute.html234
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/child-indexed-pseudo-classes-in-has.html116
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/class-id-attr-ref.html9
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/class-id-attr.html24
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/defined.html76
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/empty-pseudo-in-has.html37
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/enabled-disabled.html51
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/first-child-last-child.html57
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/fullscreen-pseudo-class-in-has.html53
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-complexity.html80
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-in-adjacent-position.html312
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-in-ancestor-position.html320
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-in-parent-position.html300
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-in-sibling-position.html312
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-invalidation-after-removing-non-first-element.html31
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-invalidation-for-wiping-an-element.html42
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-sibling.html149
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-with-not.html107
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/has-with-pseudo-class.html99
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/host-pseudo-class-in-has.html65
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/input-pseudo-classes-in-has.html141
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/insert-sibling-001.html39
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/insert-sibling-002.html41
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/insert-sibling-003.html44
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/insert-sibling-004.html43
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/is-pseudo-containing-complex-in-has.html379
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/is.html139
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/link-pseudo-in-has.html97
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/location-pseudo-classes-in-has.html29
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/media-pseudo-classes-in-has.html114
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/modal-pseudo-class-in-has.html80
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/not-001.html41
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/not-002.html133
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/not-pseudo-containing-complex-in-has.html375
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/quirks-mode-stylesheet-dynamic-add-001.html27
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/selectorText-dynamic-001.html24
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-001.html26
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002-ref.html7
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002.html23
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/sibling.html139
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/subject-has-invalidation-with-display-none-anchor-element.html29
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/target-pseudo-in-has.html90
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/typed-child-indexed-pseudo-classes-in-has.html131
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/user-action-pseudo-classes-in-has.html115
-rw-r--r--testing/web-platform/tests/css/selectors/invalidation/where.html108
47 files changed, 5073 insertions, 0 deletions
diff --git a/testing/web-platform/tests/css/selectors/invalidation/any-link-pseudo.html b/testing/web-platform/tests/css/selectors/invalidation/any-link-pseudo.html
new file mode 100644
index 0000000000..9792fd0ebe
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/any-link-pseudo.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Selectors Invalidation: :any-link</title>
+ <link rel="author" title="Victoria Su" href="mailto:victoriaytsu@google.com">
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#the-any-link-pseudo">
+ <meta name="assert" content="This tests that the :any-link selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ #link { background-color: red }
+ #link:any-link { background-color: green }
+ #link + div { color: pink }
+ </style>
+ <a id="link">This link should have a green background.</a>
+ <div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+ <script>
+ test(function() {
+ var red = "rgb(255, 0, 0)";
+ var green = "rgb(0, 128, 0)";
+
+ assert_equals(getComputedStyle(link).backgroundColor, red);
+
+ link.href = "not-visited.html";
+
+ assert_equals(getComputedStyle(link).backgroundColor, green);
+ }, "Style was recalculated for the :any-link pseudo class.");
+
+ </script>
+ </head>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/attribute-or-elemental-selectors-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/attribute-or-elemental-selectors-in-has.html
new file mode 100644
index 0000000000..d0edb1e5e4
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/attribute-or-elemental-selectors-in-has.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selectors Invalidation: :has() invalidation basic</title>
+<link rel="author" title="Byungwoo Lee" href="mailto:blee@igalia.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+.subject:has(> .child) { color: red }
+.subject:has(.descendant) { color: green }
+.subject:has([attrname=descendant]) { color: blue }
+.subject:has(#div_descendant) { color: yellow }
+.subject:has(descendant) { color: yellowgreen }
+</style>
+<main id=main>
+ <div id=div_subject class="subject">
+ <div id=div_child>
+ <div id=div_grandchild></div>
+ </div>
+ </div>
+</main>
+<script>
+ let grey = 'rgb(128, 128, 128)';
+ let red = 'rgb(255, 0, 0)';
+ let green = 'rgb(0, 128, 0)';
+ let blue = 'rgb(0, 0, 255)';
+ let yellow = 'rgb(255, 255, 0)';
+ let yellowgreen = 'rgb(154, 205, 50)';
+
+ function test_div(test_name, el, color) {
+ test(function() {
+ assert_equals(getComputedStyle(el).color, color);
+ }, test_name + ': div#' + el.id + '.color');
+ }
+
+ test_div('initial_color', div_subject, grey);
+ test_div('initial_color', div_child, grey);
+ test_div('initial_color', div_grandchild, grey);
+
+ div_child.classList.add('child');
+ test_div('add .child to #div_child', div_subject, red);
+ div_child.classList.remove('child');
+ test_div('remove .child from #div_child', div_subject, grey);
+
+ div_grandchild.classList.add('child');
+ test_div('add .child to #div_grandchild', div_subject, grey);
+ div_grandchild.classList.remove('child');
+ test_div('remove .child from #div_grandchild', div_subject, grey);
+
+ div_child.classList.add('descendant');
+ test_div('add .descendant to #div_child', div_subject, green);
+ div_child.classList.remove('descendant');
+ test_div('remove .descendant from #div_child', div_subject, grey);
+
+ div_grandchild.classList.add('descendant');
+ test_div('add .descendant to #div_grandchild', div_subject, green);
+ div_grandchild.classList.remove('descendant');
+ test_div('remove .descendant from #div_grandchild', div_subject, grey);
+
+ div_grandchild.setAttribute('attrname', 'descendant');
+ test_div('set descendant to #div_grandchild[attrname]', div_subject, blue);
+ div_grandchild.setAttribute('attrname', '');
+ test_div('clear #div_grandchild[attrname]', div_subject, grey);
+
+ div_grandchild.id = 'div_descendant';
+ test_div('change #div_grandchild to #div_descendant', div_subject, yellow);
+ div_descendant.id = 'div_grandchild';
+ test_div('change #div_descendant to #div_grandchild', div_subject, grey);
+
+ {
+ const descendant = document.createElement('descendant');
+ div_subject.appendChild(descendant);
+ test_div('add descendant to #div_subject', div_subject, yellowgreen);
+ div_subject.removeChild(descendant);
+ test_div('remove descendant from #div_subject', div_subject, grey);
+ }
+
+ {
+ const div = document.createElement('div');
+ const descendant = document.createElement('descendant');
+ div.appendChild(descendant);
+ div_subject.appendChild(div);
+ test_div('add "div > descendant" to #div_subject', div_subject, yellowgreen);
+ div_subject.removeChild(div);
+ test_div('remove "div > descendant" from #div_subject', div_subject, grey);
+ }
+
+ {
+ const child = document.createElement('div');
+ child.classList.add('child');
+ div_subject.appendChild(child);
+ test_div('add div.child to #div_subject', div_subject, red);
+ div_subject.removeChild(child);
+ test_div('remove div.child from #div_subject', div_subject, grey);
+ }
+
+ {
+ const descendant = document.createElement('div');
+ descendant.classList.add('descendant');
+ const div = document.createElement('div');
+ div.appendChild(descendant);
+ div_subject.appendChild(div);
+ test_div('add "div > div.descendant" to #div_subject', div_subject, green);
+ div_subject.removeChild(div);
+ test_div('remove "div > div.descendant" from #div_subject', div_subject, grey);
+ }
+
+ {
+ const child = document.createElement('div');
+ child.id = 'div_descendant';
+ div_subject.appendChild(child);
+ test_div('add div#div_descendant to #div_subject', div_subject, yellow);
+ div_subject.removeChild(child);
+ test_div('remove div#div_descendant from #div_subject', div_subject, grey);
+ }
+
+ {
+ const descendant = document.createElement('div');
+ descendant.id = 'div_descendant';
+ const div = document.createElement('div');
+ div.appendChild(descendant);
+ div_subject.appendChild(div);
+ test_div('add "div#div_descendant" to #div_subject', div_subject, yellow);
+ div_subject.removeChild(div);
+ test_div('remove "div#div_descendant" from #div_subject', div_subject, grey);
+ }
+
+ {
+ const child = document.createElement('div');
+ child.setAttribute('attrname', 'descendant');
+ div_subject.appendChild(child);
+ test_div('add div[attrname] to #div_subject', div_subject, blue);
+ div_subject.removeChild(child);
+ test_div('remove div[attrname] from #div_subject', div_subject, grey);
+ }
+
+ {
+ const descendant = document.createElement('div');
+ descendant.setAttribute('attrname', 'descendant');
+ const div = document.createElement('div');
+ div.appendChild(descendant);
+ div_subject.appendChild(div);
+ test_div('add "div > div[attrname]" to #div_subject', div_subject, blue);
+ div_subject.removeChild(div);
+ test_div('remove "div > div[attrname]" from #div_subject', div_subject, grey);
+ }
+
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/attribute.html b/testing/web-platform/tests/css/selectors/invalidation/attribute.html
new file mode 100644
index 0000000000..3bda52c91d
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/attribute.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Selectors Invalidation: attribute</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#attribute-selectors">
+ <meta name="assert" content="This tests that the attribute selectors are effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ div {
+ color: gray;
+ }
+
+ #a1[style] {
+ color: blue;
+ }
+ #a1[style] > #b1 {
+ color: green;
+ }
+ #a1[style] #c1 {
+ color: red;
+ }
+ #a1[style] + #d1 {
+ color: yellow;
+ }
+
+ [id=a2] {
+ color: blue;
+ }
+ [id=a2] > #b2 {
+ color: green;
+ }
+ [id=a2] #c2 {
+ color: red;
+ }
+ [id=a2] + #d2 {
+ color: yellow;
+ }
+
+ #a3[class~=q] {
+ color: blue;
+ }
+ #a3[class~=q] > #b3 {
+ color: green;
+ }
+ #a3[class~=q] #c3 {
+ color: red;
+ }
+ #a3[class~=q] + #d3 {
+ color: yellow;
+ }
+
+ #a4[run|=one] {
+ color: blue;
+ }
+ #a4[run|=one] > #b4 {
+ color: green;
+ }
+ #a4[run|=one] #c4 {
+ color: red;
+ }
+ #a4[run|=one] + #d4 {
+ color: yellow;
+ }
+
+ #a5 {
+ color: blue;
+ }
+ #a5 > #b5 {
+ color: green;
+ }
+ #a5 #c5 {
+ color: red;
+ }
+ #a5 + #d5 {
+ color: yellow;
+ }
+
+ #a6.q {
+ color: blue;
+ }
+ #a6.q > #b6 {
+ color: green;
+ }
+ #a6.q #c6 {
+ color: red;
+ }
+ #a6.q + #d6 {
+ color: yellow;
+ }
+
+ </style>
+ </head>
+ <body>
+
+ <div id="a1">
+ <div id="b1">
+ <div id="c1">
+ </div>
+ </div>
+ </div>
+ <div id="d1">
+ </div>
+
+ <div>
+ <div id="b2">
+ <div id="c2">
+ </div>
+ </div>
+ </div>
+ <div id="d2">
+ </div>
+
+ <div id="a3">
+ <div id="b3">
+ <div id="c3">
+ </div>
+ </div>
+ </div>
+ <div id="d3">
+ </div>
+
+ <div id="a4">
+ <div id="b4">
+ <div id="c4">
+ </div>
+ </div>
+ </div>
+ <div id="d4">
+ </div>
+
+ <div>
+ <div id="b5">
+ <div id="c5">
+ </div>
+ </div>
+ </div>
+ <div id="d5">
+ </div>
+
+ <div id="a6">
+ <div id="b6">
+ <div id="c6">
+ </div>
+ </div>
+ </div>
+ <div id="d6">
+ </div>
+
+ <script>
+ const gray = "rgb(128, 128, 128)";
+ const blue = "rgb(0, 0, 255)";
+ const green = "rgb(0, 128, 0)";
+ const red = "rgb(255, 0, 0)";
+ const yellow = "rgb(255, 255, 0)";
+
+ function assertGray(a, b, c, d) {
+ assert_equals(getComputedStyle(a).color, gray);
+ assert_equals(getComputedStyle(b).color, gray);
+ assert_equals(getComputedStyle(c).color, gray);
+ assert_equals(getComputedStyle(d).color, gray);
+ }
+
+ function assertColorful(a, b, c, d) {
+ assert_equals(getComputedStyle(a).color, blue);
+ assert_equals(getComputedStyle(b).color, green);
+ assert_equals(getComputedStyle(c).color, red);
+ assert_equals(getComputedStyle(d).color, yellow);
+ }
+
+ test(() => {
+ assertGray(a1, b1, c1, d1);
+ a1.style.visibility = "visible";
+ assertColorful(a1, b1, c1, d1);
+ a1.removeAttribute('style');
+ assertGray(a1, b1, c1, d1);
+ }, "[att] selector is effective");
+
+ test(() => {
+ const a2 = b2.parentElement;
+ assertGray(a2, b2, c2, d2);
+ a2.id = 'x-a2';
+ assertGray(a2, b2, c2, d2);
+ a2.id = 'a2';
+ assertColorful(a2, b2, c2, d2);
+ a2.id = 'a2-y';
+ assertGray(a2, b2, c2, d2);
+ }, "[att=val] selector is effective");
+
+ test(() => {
+ assertGray(a3, b3, c3, d3);
+ a3.setAttribute('class', 'p q r');
+ assertColorful(a3, b3, c3, d3);
+ a3.setAttribute('class', 'q-r');
+ assertGray(a3, b3, c3, d3);
+ }, "[att~=val] selector is effective");
+
+ test(() => {
+ assertGray(a4, b4, c4, d4);
+ a4.setAttribute('run', 'one');
+ assertColorful(a4, b4, c4, d4);
+ a4.setAttribute('run', 'one two three');
+ assertGray(a4, b4, c4, d4);
+ a4.setAttribute('run', 'one-two-three');
+ assertColorful(a4, b4, c4, d4);
+ a4.setAttribute('run', 'zero-one');
+ assertGray(a4, b4, c4, d4);
+ }, "[att|=val] selector is effective");
+
+ test(() => {
+ const a5 = b5.parentElement;
+ assertGray(a5, b5, c5, d5);
+ a5.setAttribute('id', 'x-a5');
+ assertGray(a5, b5, c5, d5);
+ a5.setAttribute('id', 'a5');
+ assertColorful(a5, b5, c5, d5);
+ a5.setAttribute('id', 'a5-y');
+ assertGray(a5, b5, c5, d5);
+ }, "#id selector is effective");
+
+ test(() => {
+ assertGray(a6, b6, c6, d6);
+ a6.classList.add('p');
+ a6.classList.add('q');
+ a6.classList.add('r');
+ assertColorful(a6, b6, c6, d6);
+ a6.classList.remove('q');
+ a6.classList.add('q-r');
+ assertGray(a6, b6, c6, d6);
+ }, ".class selector is effective");
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/child-indexed-pseudo-classes-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/child-indexed-pseudo-classes-in-has.html
new file mode 100644
index 0000000000..5c689e2333
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/child-indexed-pseudo-classes-in-has.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: child-indexed pseudo classes in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #container ~ div { color: grey }
+ #container:has(:only-child) ~ #only_child { color: red }
+ #container:has(.orange:first-child) ~ #first_child { color: orange }
+ #container:has(.yellow:first-child) ~ #first_child { color: yellow }
+ #container:has(.green:first-child) ~ #first_child { color: green }
+ #container:has(.orange:last-child) ~ #last_child { color: orange }
+ #container:has(.yellow:last-child) ~ #last_child { color: yellow }
+ #container:has(.green:last-child) ~ #last_child { color: green }
+ #container:has(.orange:nth-child(3n)) ~ #nth_child_3n { color: orange }
+ #container:has(.yellow:nth-child(3n)) ~ #nth_child_3n { color: yellow }
+ #container:has(.green:nth-child(3n)) ~ #nth_child_3n { color: green }
+ #container:has(.orange:nth-child(3n+1)) ~ #nth_child_3n_1 { color: orange }
+ #container:has(.yellow:nth-child(3n+1)) ~ #nth_child_3n_1 { color: yellow }
+ #container:has(.green:nth-child(3n+1)) ~ #nth_child_3n_1 { color: green }
+ #container:has(.orange:nth-child(3n+2)) ~ #nth_child_3n_2 { color: orange }
+ #container:has(.yellow:nth-child(3n+2)) ~ #nth_child_3n_2 { color: yellow }
+ #container:has(.green:nth-child(3n+2)) ~ #nth_child_3n_2 { color: green }
+ #container:has(.orange:nth-child(3n)) ~ #nth_child_3n { color: orange }
+ #container:has(.yellow:nth-child(3n)) ~ #nth_child_3n { color: yellow }
+ #container:has(.green:nth-child(3n)) ~ #nth_child_3n { color: green }
+</style>
+<div id="container">
+</div>
+<div id="only_child"></div>
+<div id="first_child"></div>
+<div id="last_child"></div>
+<div id="nth_child_3n_1"></div>
+<div id="nth_child_3n_2"></div>
+<div id="nth_child_3n"></div>
+<script>
+const grey = "rgb(128, 128, 128)";
+const red = "rgb(255, 0, 0)";
+const orange = "rgb(255, 165, 0)";
+const yellow = "rgb(255, 255, 0)";
+const green = "rgb(0, 128, 0)";
+
+function testColors(test_name,
+ only_child_color,
+ first_child_color,
+ last_child_color,
+ nth_child_3n_1_color,
+ nth_child_3n_2_color,
+ nth_child_3n_color) {
+ test(function() {
+ assert_equals(getComputedStyle(only_child).color, only_child_color);
+ }, test_name + ": #only_child");
+ test(function() {
+ assert_equals(getComputedStyle(first_child).color, first_child_color);
+ }, test_name + ": #first_child");
+ test(function() {
+ assert_equals(getComputedStyle(last_child).color, last_child_color);
+ }, test_name + ": #last_child");
+ test(function() {
+ assert_equals(getComputedStyle(nth_child_3n_1).color, nth_child_3n_1_color);
+ }, test_name + ": #nth_child_3n_1");
+ test(function() {
+ assert_equals(getComputedStyle(nth_child_3n_2).color, nth_child_3n_2_color);
+ }, test_name + ": #nth_child_3n_2");
+ test(function() {
+ assert_equals(getComputedStyle(nth_child_3n).color, nth_child_3n_color);
+ }, test_name + ": #nth_child_3n");
+}
+
+testColors("Initial colors", grey, grey, grey, grey, grey, grey);
+
+let div1 = document.createElement("div");
+div1.id = "div1";
+div1.classList.add("green");
+container.insertBefore(div1, container.firstChild);
+testColors("Prepend #div1.green", red, green, green, green, grey, grey);
+
+let div2 = document.createElement("div");
+div2.id = "div2";
+div2.classList.add("yellow");
+container.insertBefore(div2, container.firstChild);
+testColors("Prepend #div2.yellow", grey, yellow, green, yellow, green, grey);
+
+let div3 = document.createElement("div");
+div3.id = "div3";
+div3.classList.add("orange");
+container.insertBefore(div3, container.firstChild);
+testColors("Prepend #div3.orange", grey, orange, green, orange, yellow, green);
+
+let div4 = document.createElement("div");
+div4.id = "div4";
+container.insertBefore(div4, container.firstChild);
+testColors("Prepend #div4", grey, grey, green, green, orange, yellow);
+
+let div5 = document.createElement("div");
+div5.id = "div5";
+container.insertBefore(div5, container.firstChild);
+testColors("Prepend #div5", grey, grey, green, yellow, green, orange);
+
+div1.remove();
+testColors("Remove #div1", grey, grey, yellow, yellow, grey, orange);
+
+div2.remove();
+testColors("Remove #div2", grey, grey, orange, grey, grey, orange);
+
+div3.remove();
+testColors("Remove #div3", grey, grey, grey, grey, grey, grey);
+
+div4.remove();
+testColors("Remove #div4", red, grey, grey, grey, grey, grey);
+
+div5.remove();
+testColors("Remove #div5", grey, grey, grey, grey, grey, grey);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/class-id-attr-ref.html b/testing/web-platform/tests/css/selectors/invalidation/class-id-attr-ref.html
new file mode 100644
index 0000000000..a3cc6d3d8f
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/class-id-attr-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS test reference</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<style>
+div { color: green; }
+</style>
+<div>This should be green</div>
+<div>And this too</div>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/class-id-attr.html b/testing/web-platform/tests/css/selectors/invalidation/class-id-attr.html
new file mode 100644
index 0000000000..91a6f1a3a5
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/class-id-attr.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: [id] and [class] attribute selectors are invalidated correctly.</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#attribute-selectors">
+<link rel="match" href="class-id-attr-ref.html">
+<style>
+[class="foo"] {
+ color: green;
+}
+[id="baz"] {
+ color: green;
+}
+</style>
+<div id="foo">This should be green</div>
+<div id="bar">And this too</div>
+<script>
+onload = function() {
+ document.documentElement.offsetTop;
+ foo.classList.add("foo");
+ bar.setAttribute("id", "baz");
+ document.documentElement.offsetTop;
+}
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/defined.html b/testing/web-platform/tests/css/selectors/invalidation/defined.html
new file mode 100644
index 0000000000..565ed552e1
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/defined.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Selectors Invalidation: :defined</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#the-defined-pseudo">
+ <meta name="assert" content="This tests that the :defined selector is effective">
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/semantics-other.html#selector-defined">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ #container {
+ color: gray;
+ }
+
+ #a1:defined {
+ color: blue;
+ }
+ :defined + #b1 {
+ color: green;
+ }
+ :defined > #c1 {
+ color: red;
+ }
+ div + :defined + * #d1 {
+ color: yellow;
+ }
+ </style>
+ </head>
+ <body>
+ <section id="container">
+ <elucidate-late id="a1"></elucidate-late>
+ <div id="b1"></div>
+ <elucidate-late>
+ <div id="c1"></div>
+ </elucidate-late>
+ <div>
+ <div id="d1"></div>
+ </div>
+ </section>
+
+ <script>
+ const gray = "rgb(128, 128, 128)";
+ const blue = "rgb(0, 0, 255)";
+ const green = "rgb(0, 128, 0)";
+ const red = "rgb(255, 0, 0)";
+ const yellow = "rgb(255, 255, 0)";
+
+ function assertGray(a, b, c, d) {
+ assert_equals(getComputedStyle(a).color, gray);
+ assert_equals(getComputedStyle(b).color, gray);
+ assert_equals(getComputedStyle(c).color, gray);
+ assert_equals(getComputedStyle(d).color, gray);
+ }
+
+ function assertColorful(a, b, c, d) {
+ assert_equals(getComputedStyle(a).color, blue);
+ assert_equals(getComputedStyle(b).color, green);
+ assert_equals(getComputedStyle(c).color, red);
+ assert_equals(getComputedStyle(d).color, yellow);
+ }
+
+ class ElucidateLate extends HTMLElement {
+ constructor() {
+ super();
+ }
+ }
+
+ test(() => {
+ assertGray(a1, b1, c1, d1);
+ customElements.define('elucidate-late', ElucidateLate);
+ assertColorful(a1, b1, c1, d1);
+ }, ":defined selector is effective");
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/empty-pseudo-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/empty-pseudo-in-has.html
new file mode 100644
index 0000000000..83cf051565
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/empty-pseudo-in-has.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: :empty in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #subject { color: red }
+ #subject:has(:empty) { color: green }
+ #subject:has(:not(:empty)) { color: blue }
+</style>
+<div id="subject"></div>
+<script>
+const red = 'rgb(255, 0, 0)';
+const green = 'rgb(0, 128, 0)';
+const blue = 'rgb(0, 0, 255)';
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+testColor("Empty #subject", red);
+
+let child = document.createElement("div");
+child.id = "child";
+subject.appendChild(child);
+
+testColor("Insert div#child to #subject", green);
+
+child.appendChild(document.createElement("div"));
+
+testColor("Insert div to div.#child", blue);
+
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/enabled-disabled.html b/testing/web-platform/tests/css/selectors/invalidation/enabled-disabled.html
new file mode 100644
index 0000000000..bac3067d00
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/enabled-disabled.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Selectors Invalidation: :enabled and :disabled</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#enableddisabled">
+ <link rel="help" href="https://html.spec.whatwg.org/#enabling-and-disabling-form-controls:-the-disabled-attribute">
+ <meta name="assert" content="This tests that the :enabled and :disabled selectors are effective, and that the enabled/disabled status of an element is changed when updating its 'disabled' attribute by script.">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ input {
+ position: absolute;
+ left: 200px;
+ top: 300px;
+ }
+
+ :enabled {
+ left: 100px;
+ }
+
+ :disabled {
+ top: 400px;
+ }
+ </style>
+ </head>
+ <body>
+ <input id="first" type="button" value="First"></input>
+ <input id="second" type="button" value="Second" disabled></input>
+
+ <script>
+ test(() => {
+ assert_equals(getComputedStyle(first).left, '100px');
+ assert_equals(getComputedStyle(first).top, '300px');
+
+ first.disabled = true;
+ assert_equals(getComputedStyle(first).left, '200px');
+ assert_equals(getComputedStyle(first).top, '400px');
+ }, "Element updates when disabled");
+
+ test(() => {
+ assert_equals(getComputedStyle(second).left, '200px');
+ assert_equals(getComputedStyle(second).top, '400px');
+
+ second.disabled = false;
+ assert_equals(getComputedStyle(second).left, '100px');
+ assert_equals(getComputedStyle(second).top, '300px');
+ }, "Element updates when enabled");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/first-child-last-child.html b/testing/web-platform/tests/css/selectors/invalidation/first-child-last-child.html
new file mode 100644
index 0000000000..4a2ed4570e
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/first-child-last-child.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CSS Selectors Invalidation: :first-child :last-child</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#the-first-child-pseudo">
+ <meta name="assert" content="This tests that the :first-child and :last-child selectors are effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ div {
+ color: black;
+ }
+ span:first-child {
+ color: blue;
+ }
+ span:last-child {
+ color: red;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"><span>first-initially</span><span>last-initially</span></div>
+ <script>
+
+'use strict';
+const black = 'rgb(0, 0, 0)';
+const blue = 'rgb(0, 0, 255)';
+const red = 'rgb(255, 0, 0)';
+
+test(() => {
+ const target = document.querySelector('#target');
+ const first = target.firstChild;
+ assert_equals(getComputedStyle(first).color, blue);
+ target.insertAdjacentHTML('afterbegin', '\n<span>foo</span><span>bar</span>');
+ assert_equals(getComputedStyle(target.firstElementChild).color, blue);
+ assert_equals(getComputedStyle(first).color, black);
+ while (target.firstElementChild !== first)
+ target.removeChild(target.firstElementChild);
+ assert_equals(getComputedStyle(first).color, blue);
+}, 'Adding multiple nodes at once should invalidate :first-child correctly.');
+
+test(() => {
+ const target = document.querySelector('#target');
+ const last = target.lastChild;
+ assert_equals(getComputedStyle(last).color, red);
+ target.insertAdjacentHTML('beforeend', '\n<span>foo</span><span>bar</span>');
+ assert_equals(getComputedStyle(target.lastChild).color, red);
+ assert_equals(getComputedStyle(last).color, black);
+ while (target.lastChild !== last)
+ target.removeChild(target.lastChild);
+ assert_equals(getComputedStyle(last).color, red);
+}, 'Adding multiple nodes at once should invalidate :last-child correctly.');
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/fullscreen-pseudo-class-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/fullscreen-pseudo-class-in-has.html
new file mode 100644
index 0000000000..dbbcea2502
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/fullscreen-pseudo-class-in-has.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: :fullscreen pseudo class in :has()</title>
+<link rel="author" title="Tim Nguyen" href="https://github.com/nt1m">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<style>
+ #subject:has(#target:fullscreen) { color: green; }
+</style>
+
+<div id="subject">
+ This is some text.
+ <div id="target">This is going to be fullscreened</div>
+</div>
+
+<script>
+let waitForFullscreenChange = () => {
+ return new Promise((resolve) => {
+ document.addEventListener("fullscreenchange", resolve, { once: true });
+ });
+};
+
+promise_test(async function() {
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ test_driver.bless("fullscreen", () => target.requestFullscreen());
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 128, 0)",
+ "ancestor should be green since target is fullscreen");
+ document.exitFullscreen();
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since target is no longer fullscreen");
+}, ":fullscreen pseudo-class invalidation with requestFullscreen + exitFullscreen");
+
+promise_test(async function() {
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ test_driver.bless("fullscreen", () => target.requestFullscreen());
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 128, 0)",
+ "ancestor should be green since target is fullscreen");
+ target.remove();
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since target is removed");
+}, ":fullscreen pseudo-class invalidation with requestFullscreen + remove");
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-complexity.html b/testing/web-platform/tests/css/selectors/invalidation/has-complexity.html
new file mode 100644
index 0000000000..0bdcdec13b
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-complexity.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() invalidation should not be O(n^2)</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+main:has(span) .subject { color: red }
+main:has(span + span) .subject { color: green }
+main:has(span + final) .subject { color: blue }
+main:has(nonexistent + span) .subject { color: black }
+main:has(span) span { color: black }
+main:has(nonexistent) span { color: black }
+main:has(div div span) .subject { color: purple }
+</style>
+<main>
+ <div id=container>
+ <span></span>
+ </div>
+ <div id=subject class=subject></div>
+</main>
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const green = 'rgb(0, 128, 0)';
+const blue = 'rgb(0, 0, 255)';
+const purple = 'rgb(128, 0, 128)';
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+const count = 25000;
+
+testColor(`Before appending ${count} elements`, red);
+
+for (let i = 0; i < count; ++i) {
+ const span = document.createElement("span");
+ container.appendChild(span);
+}
+
+testColor(`After appending ${count} elements. This should not time out.`, green);
+
+for (let i = 0; i < count - 1; ++i) {
+ const span = document.createElement("span");
+ container.appendChild(span);
+}
+
+const final = document.createElement("final");
+container.appendChild(final);
+
+testColor(`After appending another ${count} elements. This should not time out.`, blue);
+
+const div = document.createElement("div");
+for (let i = 0; i < count; ++i) {
+ const span = document.createElement("span");
+ div.appendChild(span);
+}
+container.appendChild(div);
+
+testColor(`After appending div with ${count} elements. This should not time out.`, purple);
+
+div.remove();
+
+testColor(`After removing div with ${count} elements. This should not time out.`, blue);
+
+for (let i = 0; i < count; ++i)
+ container.lastChild.remove();
+
+testColor(`After removing ${count} elements one-by-one. This should not time out.`, green);
+
+container.replaceChildren();
+
+testColor(`After removing the remaining elements. This should not time out.`, grey);
+
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-in-adjacent-position.html b/testing/web-platform/tests/css/selectors/invalidation/has-in-adjacent-position.html
new file mode 100644
index 0000000000..5c1a1ecf78
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-in-adjacent-position.html
@@ -0,0 +1,312 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() in adjacent position</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+div:has(.test) + #subject { color: red }
+div:has([test_attr]) + #subject { color: orangered }
+div:has(> .test) + #subject { color: green }
+div:has(> [test_attr]) + #subject { color: lightgreen }
+div:has(~ .test) + #subject { color: yellow }
+div:has(~ [test_attr]) + #subject { color: ivory }
+div:has(+ .test) + #subject { color: blue }
+div:has(+ [test_attr]) + #subject { color: skyblue }
+div:has(~ div .test) + #subject { color: purple }
+div:has(~ div [test_attr]) + #subject { color: violet }
+div:has(+ div .test) + #subject { color: pink }
+div:has(+ div [test_attr]) + #subject { color: lightpink }
+</style>
+
+<main id=main>
+ <div id=previous_sibling>
+ <div id=previous_sibling_child>
+ <div id=previous_sibling_descendant></div>
+ </div>
+ </div>
+ <div id=subject></div>
+ <div id=next_sibling>
+ <div id=next_sibling_child>
+ <div id=next_sibling_descendant></div>
+ </div>
+ </div>
+</main>
+
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const orangered = 'rgb(255, 69, 0)';
+const green = 'rgb(0, 128, 0)';
+const lightgreen = 'rgb(144, 238, 144)';
+const blue = 'rgb(0, 0, 255)';
+const skyblue = 'rgb(135, 206, 235)';
+const yellow = 'rgb(255, 255, 0)';
+const ivory = 'rgb(255, 255, 240)';
+const purple = 'rgb(128, 0, 128)';
+const violet = 'rgb(238, 130, 238)';
+const pink = 'rgb(255, 192, 203)';
+const lightpink = 'rgb(255, 182, 193)';
+const colors = {
+ grey: {
+ classTest: grey,
+ attributeTest: grey,
+ },
+ red: {
+ classTest: red,
+ attributeTest: orangered,
+ },
+ green: {
+ classTest: green,
+ attributeTest: lightgreen,
+ },
+ blue: {
+ classTest: blue,
+ attributeTest: skyblue,
+ },
+ yellow: {
+ classTest: yellow,
+ attributeTest: ivory,
+ },
+ purple: {
+ classTest: purple,
+ attributeTest: violet,
+ },
+ pink: {
+ classTest: pink,
+ attributeTest: lightpink,
+ },
+};
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+function testClassChange(element, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ element.classList.add('test');
+ testColor(`add .test to ${element.id}`, expectedColorForClassTest);
+ element.classList.remove('test');
+ testColor(`remove .test from ${element.id}`, grey);
+}
+
+function testElementInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ beforeElement.before(newElement);
+ testColor(`insert element div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test before ${beforeElement.id}`, grey);
+
+ newElement.classList.remove('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div before ${beforeElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testElementInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ afterElement.after(newElement);
+ testColor(`insert element div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test after ${afterElement.id}`, grey);
+
+ newElement.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert element div after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div after ${afterElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] after ${afterElement.id}`, grey);
+}
+
+function testTreeInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test before ${beforeElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div>div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testTreeInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test after ${afterElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div after ${afterElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div>div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] after ${afterElement.id}`, grey);
+}
+
+testColor('Initial color', grey);
+
+testClassChange(previous_sibling, "grey");
+testClassChange(previous_sibling_child, "green");
+testClassChange(previous_sibling_descendant, "red");
+testClassChange(subject, "blue");
+testClassChange(next_sibling, "yellow");
+testClassChange(next_sibling_child, "purple");
+testClassChange(next_sibling_descendant, "purple");
+
+testElementInsertionBefore(previous_sibling, "grey");
+testElementInsertionBefore(previous_sibling_child, "green");
+testElementInsertionBefore(previous_sibling_descendant, "red");
+testElementInsertionBefore(subject, "grey");
+testElementInsertionBefore(next_sibling, "yellow");
+testElementInsertionBefore(next_sibling_child, "purple");
+testElementInsertionBefore(next_sibling_descendant, "purple");
+
+testElementInsertionAfter(previous_sibling, "grey");
+testElementInsertionAfter(previous_sibling_child, "green");
+testElementInsertionAfter(previous_sibling_descendant, "red");
+testElementInsertionAfter(subject, "yellow");
+testElementInsertionAfter(next_sibling, "yellow");
+testElementInsertionAfter(next_sibling_child, "purple");
+testElementInsertionAfter(next_sibling_descendant, "purple");
+
+testTreeInsertionBefore(previous_sibling, "grey");
+testTreeInsertionBefore(previous_sibling_child, "red");
+testTreeInsertionBefore(previous_sibling_descendant, "red");
+testTreeInsertionBefore(subject, "green");
+testTreeInsertionBefore(next_sibling, "purple");
+testTreeInsertionBefore(next_sibling_child, "purple");
+testTreeInsertionBefore(next_sibling_descendant, "purple");
+
+testTreeInsertionAfter(previous_sibling, "green");
+testTreeInsertionAfter(previous_sibling_child, "red");
+testTreeInsertionAfter(previous_sibling_descendant, "red");
+testTreeInsertionAfter(subject, "purple");
+testTreeInsertionAfter(next_sibling, "purple");
+testTreeInsertionAfter(next_sibling_child, "purple");
+testTreeInsertionAfter(next_sibling_descendant, "purple");
+
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-in-ancestor-position.html b/testing/web-platform/tests/css/selectors/invalidation/has-in-ancestor-position.html
new file mode 100644
index 0000000000..7662fbde69
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-in-ancestor-position.html
@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() in ancestor position</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+div:has(.test) #subject { color: red }
+div:has([test_attr]) #subject { color: orangered }
+div:has(> .test) #subject { color: green }
+div:has(> [test_attr]) #subject { color: lightgreen }
+div:has(~ .test) #subject { color: yellow }
+div:has(~ [test_attr]) #subject { color: ivory }
+div:has(+ .test) #subject { color: blue }
+div:has(+ [test_attr]) #subject { color: skyblue }
+div:has(~ div .test) #subject { color: purple }
+div:has(~ div [test_attr]) #subject { color: violet }
+div:has(+ div .test) #subject { color: pink }
+div:has(+ div [test_attr]) #subject { color: lightpink }
+</style>
+
+<main id=main>
+ <div id=subject_ancestor>
+ <div id=subject_parent>
+ <div id=subject>
+ <div id=subject_child>
+ <div id=subject_descendant></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id=next_sibling>
+ <div id=next_sibling_child>
+ <div id=next_sibling_descendant></div>
+ </div>
+ </div>
+</main>
+
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const orangered = 'rgb(255, 69, 0)';
+const green = 'rgb(0, 128, 0)';
+const lightgreen = 'rgb(144, 238, 144)';
+const blue = 'rgb(0, 0, 255)';
+const skyblue = 'rgb(135, 206, 235)';
+const yellow = 'rgb(255, 255, 0)';
+const ivory = 'rgb(255, 255, 240)';
+const purple = 'rgb(128, 0, 128)';
+const violet = 'rgb(238, 130, 238)';
+const pink = 'rgb(255, 192, 203)';
+const lightpink = 'rgb(255, 182, 193)';
+const colors = {
+ grey: {
+ classTest: grey,
+ attributeTest: grey,
+ },
+ red: {
+ classTest: red,
+ attributeTest: orangered,
+ },
+ green: {
+ classTest: green,
+ attributeTest: lightgreen,
+ },
+ blue: {
+ classTest: blue,
+ attributeTest: skyblue,
+ },
+ yellow: {
+ classTest: yellow,
+ attributeTest: ivory,
+ },
+ purple: {
+ classTest: purple,
+ attributeTest: violet,
+ },
+ pink: {
+ classTest: pink,
+ attributeTest: lightpink,
+ },
+};
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+function testClassChange(element, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ element.classList.add('test');
+ testColor(`add .test to ${element.id}`, expectedColorForClassTest);
+ element.classList.remove('test');
+ testColor(`remove .test from ${element.id}`, grey);
+}
+
+function testElementInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ beforeElement.before(newElement);
+ testColor(`insert element div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test before ${beforeElement.id}`, grey);
+
+ newElement.classList.remove('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div before ${beforeElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testElementInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ afterElement.after(newElement);
+ testColor(`insert element div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test after ${afterElement.id}`, grey);
+
+ newElement.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert element div after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div after ${afterElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] after ${afterElement.id}`, grey);
+}
+
+function testTreeInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test before ${beforeElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div>div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testTreeInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test after ${afterElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div after ${afterElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div>div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] after ${afterElement.id}`, grey);
+}
+
+testColor('Initial color', grey);
+
+testClassChange(subject_ancestor, "grey");
+testClassChange(subject_parent, "green");
+testClassChange(subject, "green");
+testClassChange(subject_child, "red");
+testClassChange(subject_descendant, "red");
+testClassChange(next_sibling, "blue");
+testClassChange(next_sibling_child, "pink");
+testClassChange(next_sibling_descendant, "pink");
+
+testElementInsertionBefore(subject_ancestor, "grey");
+testElementInsertionBefore(subject_parent, "green");
+testElementInsertionBefore(subject, "green");
+testElementInsertionBefore(subject_child, "red");
+testElementInsertionBefore(subject_descendant, "red");
+testElementInsertionBefore(next_sibling, "blue");
+testElementInsertionBefore(next_sibling_child, "pink");
+testElementInsertionBefore(next_sibling_descendant, "pink");
+
+testElementInsertionAfter(subject_ancestor, "blue");
+testElementInsertionAfter(subject_parent, "blue");
+testElementInsertionAfter(subject, "green");
+testElementInsertionAfter(subject_child, "red");
+testElementInsertionAfter(subject_descendant, "red");
+testElementInsertionAfter(next_sibling, "yellow");
+testElementInsertionAfter(next_sibling_child, "pink");
+testElementInsertionAfter(next_sibling_descendant, "pink");
+
+testTreeInsertionBefore(subject_ancestor, "grey");
+testTreeInsertionBefore(subject_parent, "red");
+testTreeInsertionBefore(subject, "red");
+testTreeInsertionBefore(subject_child, "red");
+testTreeInsertionBefore(subject_descendant, "red");
+testTreeInsertionBefore(next_sibling, "pink");
+testTreeInsertionBefore(next_sibling_child, "pink");
+testTreeInsertionBefore(next_sibling_descendant, "pink");
+
+testTreeInsertionAfter(subject_ancestor, "pink");
+testTreeInsertionAfter(subject_parent, "pink");
+testTreeInsertionAfter(subject, "red");
+testTreeInsertionAfter(subject_child, "red");
+testTreeInsertionAfter(subject_descendant, "red");
+testTreeInsertionAfter(next_sibling, "purple");
+testTreeInsertionAfter(next_sibling_child, "pink");
+testTreeInsertionAfter(next_sibling_descendant, "pink");
+
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-in-parent-position.html b/testing/web-platform/tests/css/selectors/invalidation/has-in-parent-position.html
new file mode 100644
index 0000000000..4690e6929f
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-in-parent-position.html
@@ -0,0 +1,300 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() in parent position</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+div:has(.test) > #subject { color: red }
+div:has([test_attr]) > #subject { color: orangered }
+div:has(> .test) > #subject { color: green }
+div:has(> [test_attr]) > #subject { color: lightgreen }
+div:has(~ .test) > #subject { color: yellow }
+div:has(~ [test_attr]) > #subject { color: ivory }
+div:has(+ .test) > #subject { color: blue }
+div:has(+ [test_attr]) > #subject { color: skyblue }
+div:has(~ div .test) > #subject { color: purple }
+div:has(~ div [test_attr]) > #subject { color: violet }
+div:has(+ div .test) > #subject { color: pink }
+div:has(+ div [test_attr]) > #subject { color: lightpink }
+</style>
+
+<main id=main>
+ <div id=subject_ancestor>
+ <div id=subject_parent>
+ <div id=subject>
+ <div id=subject_child>
+ <div id=subject_descendant></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</main>
+
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const orangered = 'rgb(255, 69, 0)';
+const green = 'rgb(0, 128, 0)';
+const lightgreen = 'rgb(144, 238, 144)';
+const blue = 'rgb(0, 0, 255)';
+const skyblue = 'rgb(135, 206, 235)';
+const yellow = 'rgb(255, 255, 0)';
+const ivory = 'rgb(255, 255, 240)';
+const purple = 'rgb(128, 0, 128)';
+const violet = 'rgb(238, 130, 238)';
+const pink = 'rgb(255, 192, 203)';
+const lightpink = 'rgb(255, 182, 193)';
+const colors = {
+ grey: {
+ classTest: grey,
+ attributeTest: grey,
+ },
+ red: {
+ classTest: red,
+ attributeTest: orangered,
+ },
+ green: {
+ classTest: green,
+ attributeTest: lightgreen,
+ },
+ blue: {
+ classTest: blue,
+ attributeTest: skyblue,
+ },
+ yellow: {
+ classTest: yellow,
+ attributeTest: ivory,
+ },
+ purple: {
+ classTest: purple,
+ attributeTest: violet,
+ },
+ pink: {
+ classTest: pink,
+ attributeTest: lightpink,
+ },
+};
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+function testClassChange(element, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ element.classList.add('test');
+ testColor(`add .test to ${element.id}`, expectedColorForClassTest);
+ element.classList.remove('test');
+ testColor(`remove .test from ${element.id}`, grey);
+}
+
+function testElementInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test before ${beforeElement.id}`, grey);
+
+ newElement.classList.remove('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div before ${beforeElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testElementInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test');
+
+ afterElement.after(newElement);
+ testColor(`insert element div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test after ${afterElement.id}`, grey);
+
+ newElement.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert element div after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div after ${afterElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] after ${afterElement.id}`, grey);
+}
+
+function testTreeInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test before ${beforeElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div>div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testTreeInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test after ${afterElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div after ${afterElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div>div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] after ${afterElement.id}`, grey);
+}
+
+testColor('Initial color', grey);
+
+testClassChange(subject_ancestor, "grey");
+testClassChange(subject_parent, "grey");
+testClassChange(subject, "green");
+testClassChange(subject_child, "red");
+testClassChange(subject_descendant, "red");
+
+testElementInsertionBefore(subject_ancestor, "grey");
+testElementInsertionBefore(subject_parent, "grey");
+testElementInsertionBefore(subject, "green");
+testElementInsertionBefore(subject_child, "red");
+testElementInsertionBefore(subject_descendant, "red");
+
+testElementInsertionAfter(subject_ancestor, "grey");
+testElementInsertionAfter(subject_parent, "blue");
+testElementInsertionAfter(subject, "green");
+testElementInsertionAfter(subject_child, "red");
+testElementInsertionAfter(subject_descendant, "red");
+
+testTreeInsertionBefore(subject_ancestor, "grey");
+testTreeInsertionBefore(subject_parent, "grey");
+testTreeInsertionBefore(subject, "red");
+testTreeInsertionBefore(subject_child, "red");
+testTreeInsertionBefore(subject_descendant, "red");
+
+testTreeInsertionAfter(subject_ancestor, "grey");
+testTreeInsertionAfter(subject_parent, "pink");
+testTreeInsertionAfter(subject, "red");
+testTreeInsertionAfter(subject_child, "red");
+testTreeInsertionAfter(subject_descendant, "red");
+
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-in-sibling-position.html b/testing/web-platform/tests/css/selectors/invalidation/has-in-sibling-position.html
new file mode 100644
index 0000000000..8b35940f87
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-in-sibling-position.html
@@ -0,0 +1,312 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() in sibling position</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+div:has(.test) ~ #subject { color: red }
+div:has([test_attr]) ~ #subject { color: orangered }
+div:has(> .test) ~ #subject { color: green }
+div:has(> [test_attr]) ~ #subject { color: lightgreen }
+div:has(~ .test) ~ #subject { color: yellow }
+div:has(~ [test_attr]) ~ #subject { color: ivory }
+div:has(+ .test) ~ #subject { color: blue }
+div:has(+ [test_attr]) ~ #subject { color: skyblue }
+div:has(~ div .test) ~ #subject { color: purple }
+div:has(~ div [test_attr]) ~ #subject { color: violet }
+div:has(+ div .test) ~ #subject { color: pink }
+div:has(+ div [test_attr]) ~ #subject { color: lightpink }
+</style>
+
+<main id=main>
+ <div id=previous_sibling>
+ <div id=previous_sibling_child>
+ <div id=previous_sibling_descendant></div>
+ </div>
+ </div>
+ <div id=subject></div>
+ <div id=next_sibling>
+ <div id=next_sibling_child>
+ <div id=next_sibling_descendant></div>
+ </div>
+ </div>
+</main>
+
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const orangered = 'rgb(255, 69, 0)';
+const green = 'rgb(0, 128, 0)';
+const lightgreen = 'rgb(144, 238, 144)';
+const blue = 'rgb(0, 0, 255)';
+const skyblue = 'rgb(135, 206, 235)';
+const yellow = 'rgb(255, 255, 0)';
+const ivory = 'rgb(255, 255, 240)';
+const purple = 'rgb(128, 0, 128)';
+const violet = 'rgb(238, 130, 238)';
+const pink = 'rgb(255, 192, 203)';
+const lightpink = 'rgb(255, 182, 193)';
+const colors = {
+ grey: {
+ classTest: grey,
+ attributeTest: grey,
+ },
+ red: {
+ classTest: red,
+ attributeTest: orangered,
+ },
+ green: {
+ classTest: green,
+ attributeTest: lightgreen,
+ },
+ blue: {
+ classTest: blue,
+ attributeTest: skyblue,
+ },
+ yellow: {
+ classTest: yellow,
+ attributeTest: ivory,
+ },
+ purple: {
+ classTest: purple,
+ attributeTest: violet,
+ },
+ pink: {
+ classTest: pink,
+ attributeTest: lightpink,
+ },
+};
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+function testClassChange(element, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ element.classList.add('test');
+ testColor(`add .test to ${element.id}`, expectedColorForClassTest);
+ element.classList.remove('test');
+ testColor(`remove .test from ${element.id}`, grey);
+}
+
+function testElementInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ beforeElement.before(newElement);
+ testColor(`insert element div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test before ${beforeElement.id}`, grey);
+
+ newElement.classList.remove('test')
+
+ beforeElement.before(newElement);
+ testColor(`insert element div before ${beforeElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div before ${beforeElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testElementInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ afterElement.after(newElement);
+ testColor(`insert element div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' again to the element inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove element div.test after ${afterElement.id}`, grey);
+
+ newElement.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert element div after ${afterElement.id}`, grey);
+
+ newElement.classList.add('test');
+ testColor(`add the class 'test' to the element inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.classList.remove('test');
+ testColor(`remove the class 'test' from the element inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove element div after ${afterElement.id}`, grey);
+
+ newElement.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div[test_attr] after ${afterElement.id}`, grey);
+}
+
+function testTreeInsertionBefore(beforeElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div.test before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test before ${beforeElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again before ${beforeElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again before ${beforeElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div before ${beforeElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div>div[test_attr] before ${beforeElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] before ${beforeElement.id}`, grey);
+}
+
+function testTreeInsertionAfter(afterElement, expectedColorName)
+{
+ const expectedColorForClassTest = colors[expectedColorName].classTest;
+ const expectedColorForAttributeTest = colors[expectedColorName].attributeTest;
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div.test after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' again to the element in the tree inserted after ${afterElement.id}`, expectedColorForClassTest);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test after ${afterElement.id}`, grey);
+
+ newChild.classList.remove('test');
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div after ${afterElement.id}`, grey);
+
+ newChild.classList.add('test');
+ testColor(`add the class 'test' to the element in the tree inserted again after ${afterElement.id}`, expectedColorForClassTest);
+
+ newChild.classList.remove('test');
+ testColor(`remove the class 'test' from the element in the tree inserted again after ${afterElement.id}`, grey);
+
+ newElement.remove();
+ testColor(`remove tree div>div after ${afterElement.id}`, grey);
+
+ newChild.setAttribute('test_attr', '');
+
+ afterElement.after(newElement);
+ testColor(`insert element div>div[test_attr] after ${afterElement.id}`, expectedColorForAttributeTest);
+
+ newElement.remove();
+ testColor(`remove element div>div[test_attr] after ${afterElement.id}`, grey);
+}
+
+testColor('Initial color', grey);
+
+testClassChange(previous_sibling, "grey");
+testClassChange(previous_sibling_child, "green");
+testClassChange(previous_sibling_descendant, "red");
+testClassChange(subject, "blue");
+testClassChange(next_sibling, "yellow");
+testClassChange(next_sibling_child, "purple");
+testClassChange(next_sibling_descendant, "purple");
+
+testElementInsertionBefore(previous_sibling, "grey");
+testElementInsertionBefore(previous_sibling_child, "green");
+testElementInsertionBefore(previous_sibling_descendant, "red");
+testElementInsertionBefore(subject, "blue");
+testElementInsertionBefore(next_sibling, "yellow");
+testElementInsertionBefore(next_sibling_child, "purple");
+testElementInsertionBefore(next_sibling_descendant, "purple");
+
+testElementInsertionAfter(previous_sibling, "blue");
+testElementInsertionAfter(previous_sibling_child, "green");
+testElementInsertionAfter(previous_sibling_descendant, "red");
+testElementInsertionAfter(subject, "yellow");
+testElementInsertionAfter(next_sibling, "yellow");
+testElementInsertionAfter(next_sibling_child, "purple");
+testElementInsertionAfter(next_sibling_descendant, "purple");
+
+testTreeInsertionBefore(previous_sibling, "green");
+testTreeInsertionBefore(previous_sibling_child, "red");
+testTreeInsertionBefore(previous_sibling_descendant, "red");
+testTreeInsertionBefore(subject, "pink");
+testTreeInsertionBefore(next_sibling, "purple");
+testTreeInsertionBefore(next_sibling_child, "purple");
+testTreeInsertionBefore(next_sibling_descendant, "purple");
+
+testTreeInsertionAfter(previous_sibling, "pink");
+testTreeInsertionAfter(previous_sibling_child, "red");
+testTreeInsertionAfter(previous_sibling_descendant, "red");
+testTreeInsertionAfter(subject, "purple");
+testTreeInsertionAfter(next_sibling, "purple");
+testTreeInsertionAfter(next_sibling_child, "purple");
+testTreeInsertionAfter(next_sibling_descendant, "purple");
+
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-invalidation-after-removing-non-first-element.html b/testing/web-platform/tests/css/selectors/invalidation/has-invalidation-after-removing-non-first-element.html
new file mode 100644
index 0000000000..482f07b8af
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-invalidation-after-removing-non-first-element.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>:has() invalidation after removing non-first element</title>
+<link rel="author" title="Byungwoo Lee" href="mailto:blee@igalia.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+#subject:has(descendant) { color: red }
+</style>
+<main id="main">
+ <div id="subject">
+ <div></div>
+ <descendant id="descendant"></descendant>
+ </div>
+</main>
+<script>
+ let grey = 'rgb(128, 128, 128)';
+ let red = 'rgb(255, 0, 0)';
+
+ function test_div(test_name, el, color) {
+ test(function() {
+ assert_equals(getComputedStyle(el).color, color);
+ }, test_name + ': div#' + el.id + '.color');
+ }
+
+ test_div('initial_color', subject, red);
+ subject.removeChild(descendant);
+ test_div('remove descendant', subject, grey);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-invalidation-for-wiping-an-element.html b/testing/web-platform/tests/css/selectors/invalidation/has-invalidation-for-wiping-an-element.html
new file mode 100644
index 0000000000..03fa9a5554
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-invalidation-for-wiping-an-element.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>:has() invalidation for wiping an element by means of innerHTML</title>
+<link rel="author" title="Byungwoo Lee" href="mailto:blee@igalia.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+.subject:has(.descendant) { color: green}
+</style>
+<main id=main>
+ <div id="subject" class="subject"></div>
+</main>
+<script>
+ let grey = 'rgb(128, 128, 128)';
+ let green = 'rgb(0, 128, 0)';
+
+ function test_div(test_name, el, color) {
+ test(function() {
+ assert_equals(getComputedStyle(el).color, color);
+ }, test_name + ': div#' + el.id + '.color');
+ }
+
+ test_div('initial color', subject, grey);
+
+ subject.innerHTML = "This is a text <div><div class='descendant'></div></div>";
+
+ test_div('color after inserting text and div > .descendant', subject, green);
+
+ subject.innerHTML = "This is a text";
+
+ test_div('color after wiping #child to remove div > .descendant', subject, grey);
+
+ subject.innerHTML = "<div id='child'> This is a text <div class='descendant'></div></div>";
+
+ test_div('color after inserting text and #child > .descendant', subject, green);
+
+ child.innerHTML = "This is a text";
+
+ test_div('color after wiping #child to remove .descendant', subject, grey);
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-sibling.html b/testing/web-platform/tests/css/selectors/invalidation/has-sibling.html
new file mode 100644
index 0000000000..7c56b2e7b3
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-sibling.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() with sibling combinator argument</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+#subject:has(~ .test) { color: red }
+#subject:has(+ .test) { color: green }
+#subject:has(~ div .test) { color: blue }
+#subject:has(~ div > .test) { color: purple }
+#subject:has(+ div .test) { color: yellow }
+#subject:has(+ div > .test) { color: pink }
+</style>
+
+<main id=main>
+ <div id=subject></div>
+ <div id=first_sibling>
+ <div id=first_sibling_child>
+ <div id=first_sibling_descendant></div>
+ </div>
+ </div>
+ <div id=second_sibling></div>
+ <div id=third_sibling>
+ <div id=third_sibling_child>
+ <div id=third_sibling_descendant></div>
+ </div>
+ </div>
+</main>
+<script>
+
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const green = 'rgb(0, 128, 0)';
+const blue = 'rgb(0, 0, 255)';
+const yellow = 'rgb(255, 255, 0)';
+const purple = 'rgb(128, 0, 128)';
+const pink = 'rgb(255, 192, 203)';
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+function testClassChange(element, expectedColor)
+{
+ element.classList.add('test');
+ testColor(`add .test to ${element.id}`, expectedColor);
+ element.classList.remove('test');
+ testColor(`remove .test from ${element.id}`, grey);
+}
+
+function testElementInsertionBefore(beforeElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ beforeElement.before(newElement);
+ testColor(`insert element div.test before ${beforeElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove element div.test before ${beforeElement.id}`, grey);
+}
+
+function testElementInsertionAfter(afterElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+ newElement.classList.add('test')
+
+ afterElement.after(newElement);
+ testColor(`insert element div.test after ${afterElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove element div.test after ${afterElement.id}`, grey);
+}
+
+function testTreeInsertionBefore(beforeElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div.test before ${beforeElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test before ${beforeElement.id}`, grey);
+}
+
+function testTreeInsertionAfter(afterElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newChild.classList.add('test');
+ newElement.appendChild(newChild);
+
+ afterElement.after(newElement);
+ testColor(`insert tree div>div.test after ${afterElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove tree div>div.test after ${afterElement.id}`, grey);
+}
+
+testColor('initial_color', grey);
+
+testClassChange(first_sibling, green);
+testClassChange(second_sibling, red);
+testClassChange(third_sibling, red);
+testClassChange(first_sibling_child, pink);
+testClassChange(first_sibling_descendant, yellow);
+testClassChange(third_sibling_child, purple);
+testClassChange(third_sibling_descendant, blue);
+
+testElementInsertionBefore(first_sibling, green);
+testElementInsertionBefore(second_sibling, red);
+testElementInsertionBefore(third_sibling, red);
+testElementInsertionBefore(first_sibling_child, pink);
+testElementInsertionBefore(first_sibling_descendant, yellow);
+testElementInsertionBefore(third_sibling_child, purple);
+testElementInsertionBefore(third_sibling_descendant, blue);
+
+testElementInsertionAfter(first_sibling, red);
+testElementInsertionAfter(second_sibling, red);
+testElementInsertionAfter(third_sibling, red);
+testElementInsertionAfter(first_sibling_child, pink);
+testElementInsertionAfter(first_sibling_descendant, yellow);
+testElementInsertionAfter(third_sibling_child, purple);
+testElementInsertionAfter(third_sibling_descendant, blue);
+
+testTreeInsertionBefore(first_sibling, pink);
+testTreeInsertionBefore(second_sibling, purple);
+testTreeInsertionBefore(third_sibling, purple);
+testTreeInsertionBefore(first_sibling_child, yellow);
+testTreeInsertionBefore(first_sibling_descendant, yellow);
+testTreeInsertionBefore(third_sibling_child, blue);
+testTreeInsertionBefore(third_sibling_descendant, blue);
+
+testTreeInsertionAfter(first_sibling, purple);
+testTreeInsertionAfter(second_sibling, purple);
+testTreeInsertionAfter(third_sibling, purple);
+testTreeInsertionAfter(first_sibling_child, yellow);
+testTreeInsertionAfter(first_sibling_descendant, yellow);
+testTreeInsertionAfter(third_sibling_child, blue);
+testTreeInsertionAfter(third_sibling_descendant, blue);
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-with-not.html b/testing/web-platform/tests/css/selectors/invalidation/has-with-not.html
new file mode 100644
index 0000000000..b67ec5e3b7
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-with-not.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() with :not()</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+div, main { color: grey }
+#subject:has(:not(.test)) { color: green }
+#subject:has(.test :not(.test)) { color: red }
+</style>
+
+<main id=main>
+ <div id=subject>
+ <div id=subject_child class=test>
+ <div id=subject_descendant class=test></div>
+ </div>
+ </div>
+</main>
+
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const green = 'rgb(0, 128, 0)';
+const blue = 'rgb(0, 0, 255)';
+
+function testColor(test_name, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, color);
+ }, test_name);
+}
+
+function testClassChange(element, expectedColor)
+{
+ element.classList.remove('test');
+ testColor(`remove .test to ${element.id}`, expectedColor);
+ element.classList.add('test');
+ testColor(`add .test from ${element.id}`, grey);
+}
+
+function testElementInsertionBefore(beforeElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+
+ beforeElement.before(newElement);
+ testColor(`insert element div before ${beforeElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove element div before ${beforeElement.id}`, grey);
+}
+
+function testElementInsertionAfter(afterElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+
+ afterElement.after(newElement);
+ testColor(`insert element div after ${afterElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove element div after ${afterElement.id}`, grey);
+}
+
+function testTreeInsertionBefore(beforeElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newElement.appendChild(newChild);
+
+ beforeElement.before(newElement);
+ testColor(`insert tree div>div before ${beforeElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove tree div>div before ${beforeElement.id}`, grey);
+}
+
+function testTreeInsertionAfter(afterElement, expectedColor)
+{
+ const newElement = document.createElement('div');
+ const newChild = document.createElement('div');
+ newElement.appendChild(newChild);
+
+ afterElement.after(newElement);
+ testColor(`insert tree div.test after ${afterElement.id}`, expectedColor);
+
+ newElement.remove();
+ testColor(`remove tree div.test after ${afterElement.id}`, grey);
+}
+
+testColor('Initial color', grey);
+
+testClassChange(subject_child, green);
+testClassChange(subject_descendant, red);
+
+testElementInsertionBefore(subject_child, green);
+testElementInsertionBefore(subject_descendant, red);
+
+testElementInsertionAfter(subject_child, green);
+testElementInsertionAfter(subject_descendant, red);
+
+testTreeInsertionBefore(subject_child, green);
+testTreeInsertionBefore(subject_descendant, red);
+
+testTreeInsertionAfter(subject_child, green);
+testTreeInsertionAfter(subject_descendant, red);
+
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/has-with-pseudo-class.html b/testing/web-platform/tests/css/selectors/invalidation/has-with-pseudo-class.html
new file mode 100644
index 0000000000..4dc4c1a6a2
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/has-with-pseudo-class.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() with pseudo-classes</title>
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<style>
+main:has(input) div { color: grey }
+main:has(#checkbox:checked) > #subject { color: red }
+main:has(#option:checked) > #subject { color: red }
+main:has(#checkbox:disabled) > #subject { color: green }
+main:has(#option:disabled) > :is(#subject, #subject2) { color: green }
+main:has(#optgroup:disabled) > #subject { color: blue }
+main:not(:has(#checkbox:enabled)) > #subject3 { color: green }
+main:not(:has(#option:enabled)) :is(#subject3, #subject4) { color: green }
+main:not(:has(#optgroup:enabled)) > #subject3 { color: blue }
+main:has(#text_input:valid) > #subject { color: yellow }
+main:not(:has(#text_input:invalid)) > #subject2 { color: yellow }
+main:has(#form:valid) > #subject3 { color: yellow }
+main:not(:has(#form:invalid)) > #subject4 { color: yellow }
+</style>
+
+<main id=main>
+ <form id=form>
+ <input type=checkbox id=checkbox></input>
+ <select id=select><optgroup id=optgroup><option>a</option><option id=option>b</option></optgroup></select>
+ <input id=text_input type=text required></input>
+ </form>
+ <div id=subject></div>
+ <div id=subject2></div>
+ <div id=subject3></div>
+ <div id=subject4></div>
+</main>
+
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const green = 'rgb(0, 128, 0)';
+const blue = 'rgb(0, 0, 255)';
+const yellow = 'rgb(255, 255, 0)';
+const purple = 'rgb(128, 0, 128)';
+const pink = 'rgb(255, 192, 203)';
+
+function testColor(test_name, subject_element, color) {
+ test(function() {
+ assert_equals(getComputedStyle(subject_element).color, color);
+ }, test_name);
+}
+
+function testPseudoClassChange(element, property, subject_element, expectedColor)
+{
+ testColor(`Before set ${property} on ${element.id}, testing ${subject_element.id}`, subject_element, grey);
+
+ element[property] = true;
+ testColor(`Set ${property} on ${element.id}, testing ${subject_element.id}`, subject_element, expectedColor);
+
+ element[property] = false;
+ testColor(`Unset ${property} on ${element.id}, testing ${subject_element.id}`, subject_element, grey);
+}
+
+function testSelectedChange(option, subject_element, expectedColor)
+{
+ const oldOption = select.selectedOptions[0];
+ option.selected = true;
+ testColor(`Set select on ${option.id}`, subject_element, expectedColor);
+ oldOption.selected = true;
+ testColor(`Reset select`, subject, grey);
+}
+
+function testValueChange(input, subject_element, expectedColor)
+{
+ testColor(`Before setting value of ${input.id}, testing ${subject_element.id}`, subject_element, grey);
+
+ input.value = "value";
+ testColor(`Set value of ${input.id}, testing ${subject_element.id}`, subject_element, expectedColor);
+
+ input.value = "";
+ testColor(`Clear value of ${input.id}, testing ${subject_element.id}`, subject_element, grey);
+}
+
+testPseudoClassChange(checkbox, "checked", subject, red);
+testSelectedChange(option, subject, red);
+
+testPseudoClassChange(checkbox, "disabled", subject, green);
+testPseudoClassChange(checkbox, "disabled", subject3, green);
+testPseudoClassChange(option, "disabled", subject, green);
+testPseudoClassChange(option, "disabled", subject3, green);
+
+testPseudoClassChange(optgroup, "disabled", subject, blue);
+testPseudoClassChange(optgroup, "disabled", subject2, green);
+testPseudoClassChange(optgroup, "disabled", subject3, blue);
+testPseudoClassChange(optgroup, "disabled", subject4, green);
+
+testValueChange(text_input, subject, yellow);
+testValueChange(text_input, subject2, yellow);
+testValueChange(text_input, subject3, yellow);
+testValueChange(text_input, subject4, yellow);
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/host-pseudo-class-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/host-pseudo-class-in-has.html
new file mode 100644
index 0000000000..a2c63d5463
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/host-pseudo-class-in-has.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: Invalidation for :host() and :host-context() inside :has()</title>
+<link rel="author" title="Byungwoo" href="mailto:blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="host_parent"><div id="host"></div></div>
+<script>
+ var shadow = host.attachShadow({ mode: 'open' });
+ shadow.innerHTML = `
+ <style>
+ .subject {
+ color: red;
+ }
+ .subject:has(:is(:host-context(.a) > .foo .bar)) { color: green }
+ .subject:has(:is(:host(.a) > .foo .bar)) { color: yellowgreen }
+ .subject:has(:is(:host-context(.a) .bar)) { color: blue }
+ .subject:has(:is(:host(.a) .bar)) { color: skyblue }
+ </style>
+ <div class="foo">
+ <div id="subject1" class="subject">
+ <div class="bar"></div>
+ </div>
+ </div>
+ <div>
+ <div class="foo">
+ <div id="subject2" class="subject">
+ <div class="bar"></div>
+ </div>
+ </div>
+ </div>
+ `;
+
+ const red = "rgb(255, 0, 0)";
+ const green = "rgb(0, 128, 0)";
+ const yellowgreen = "rgb(154, 205, 50)";
+ const blue = "rgb(0, 0, 255)";
+ const skyblue = "rgb(135, 206, 235)";
+
+ function checkColor(test_name, subject_id, subject_color) {
+ test(function() {
+ let subject = shadow.querySelector("#" + subject_id);
+ assert_equals(getComputedStyle(subject).color, subject_color);
+ }, test_name + ": Check #" + subject_id + " color");
+ }
+
+ checkColor("Before adding 'a' to #host_parent", "subject1", red);
+ checkColor("Before adding 'a' to #host_parent", "subject2", red);
+
+ host_parent.classList.add('a');
+
+ checkColor("After adding 'a' to #host_parent", "subject1", green);
+ checkColor("After adding 'a' to #host_parent", "subject2", blue);
+
+ host_parent.classList.remove('a');
+
+ checkColor("After removing 'a' from #host_parent", "subject1", red);
+ checkColor("After removing 'a' from #host_parent", "subject2", red);
+
+ host.classList.add('a');
+
+ checkColor("After adding 'a' to #host", "subject1", yellowgreen);
+ checkColor("After adding 'a' to #host", "subject2", skyblue);
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/input-pseudo-classes-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/input-pseudo-classes-in-has.html
new file mode 100644
index 0000000000..67aeedf315
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/input-pseudo-classes-in-has.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: input pseudo classes in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ .ancestor:has(#checkme:checked) { color: green }
+ .ancestor:has(#checkme:indeterminate) { color: yellowgreen }
+ .ancestor:has(#checkme:disabled) { color: blue }
+ .ancestor:has(#textinput:read-only) { color: skyblue }
+ .ancestor:has(#textinput:placeholder-shown) { color: navy }
+ .ancestor:has(#radioinput:default) { color: lightblue }
+ .ancestor:has(#textinput:valid) { color: lightgreen }
+ .ancestor:has(#numberinput:out-of-range) { color: darkgreen }
+ .ancestor:has(#numberinput:required) { color: pink }
+ .ancestor:has(#progress:indeterminate) { color: orange }
+</style>
+<div id=subject class=ancestor>
+ <input type="checkbox" name="my-checkbox" id="checkme">
+ <label for="checkme">Check me!</label>
+ <input type="text" id="textinput" required>
+ <input id="radioinput" checked>
+ <input id="numberinput" type="number" min="1" max="10" value="5">
+ <progress id="progress" value="50" max="100"></progress>
+</div>
+<script>
+ test(function() {
+ this.add_cleanup(() => {
+ checkme.checked = false;
+ });
+ checkme.checked = false;
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ checkme.checked = true;
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 128, 0)",
+ "ancestor should be green");
+ checkme.indeterminate = true;
+ assert_equals(getComputedStyle(subject).color, "rgb(154, 205, 50)",
+ "ancestor should be yellowgreen");
+ const input = checkme;
+ checkme.remove();
+ input.indeterminate = false;
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+
+ subject.prepend(input);
+ checkme.checked = true;
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 128, 0)",
+ "ancestor should be green");
+ }, ":checked & :indeterminate invalidation on <input>");
+
+ test(function() {
+ this.add_cleanup(() => {
+ progress.setAttribute("value", "50");
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ progress.removeAttribute("value");
+ assert_equals(getComputedStyle(subject).color, "rgb(255, 165, 0)",
+ "ancestor should be orange");
+ }, ":indeterminate invalidation on <progress>");
+
+ test(function() {
+ this.add_cleanup(() => {
+ checkme.disabled = false;
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ checkme.disabled = true;
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 255)",
+ "ancestor should be blue");
+ }, ":disabled invalidation");
+
+ test(function() {
+ this.add_cleanup(() => {
+ textinput.readOnly = false;
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ textinput.readOnly = true;
+ assert_equals(getComputedStyle(subject).color, "rgb(135, 206, 235)",
+ "ancestor should be skyblue");
+ }, ":read-only invalidation");
+
+ test(function() {
+ this.add_cleanup(() => {
+ textinput.value = "";
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ textinput.value = "text input";
+ assert_equals(getComputedStyle(subject).color, "rgb(144, 238, 144)",
+ "ancestor should be lightgreen");
+ }, ":valid invalidation");
+
+ test(function() {
+ this.add_cleanup(() => {
+ radioinput.removeAttribute("type");
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ radioinput.type = 'radio';
+ assert_equals(getComputedStyle(subject).color, "rgb(173, 216, 230)",
+ "ancestor should be lightblue");
+ }, ":default invalidation with input[type=radio]");
+
+ test(function() {
+ this.add_cleanup(() => {
+ numberinput.required = false;
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ numberinput.required = true;
+ assert_equals(getComputedStyle(subject).color, "rgb(255, 192, 203)",
+ "ancestor should be pink");
+ }, ":required invalidation");
+
+ test(function() {
+ this.add_cleanup(() => {
+ numberinput.value = 5;
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ numberinput.value = 12;
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 100, 0)",
+ "ancestor should be darkgreen");
+ }, ":out-of-range invalidation");
+
+ test(function() {
+ this.add_cleanup(() => {
+ textinput.removeAttribute("placeholder");
+ });
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ textinput.placeholder = 'placeholder text';
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 128)",
+ "ancestor should be navy");
+ }, ":placeholder-shown invalidation");
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-001.html b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-001.html
new file mode 100644
index 0000000000..fa966d3f25
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-001.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>CSS Selectors Invalidation: insert sibling</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#adjacent-sibling-combinators">
+ <meta name="assert" content="This tests that the + next-sibling selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .c { background-color: blue; }
+ .a + * + .c { background-color: green; }
+ </style>
+</head>
+<body>
+ <div>
+ <div id="first" class="a"></div>
+ <div></div>
+ <div id="target" class="c"></div>
+ </div>
+ <script>
+ 'use strict';
+ const green = 'rgb(0, 128, 0)';
+ const blue = 'rgb(0, 0, 255)';
+
+ test(function() {
+ const first = document.getElementById('first');
+ const target = document.getElementById('target');
+ const parent = first.parentElement;
+ assert_equals(getComputedStyle(target).backgroundColor, green, "initial color");
+
+ parent.removeChild(first);
+ assert_equals(getComputedStyle(target).backgroundColor, blue, "color after removal");
+
+ parent.insertBefore(first, parent.firstChild);
+ assert_equals(getComputedStyle(target).backgroundColor, green, "color after insert")
+ }, "Remove/Insert earlier sibling");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-002.html b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-002.html
new file mode 100644
index 0000000000..7e1eac37ea
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-002.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>CSS Selectors Invalidation: insert adjacent sibling of parent</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#adjacent-sibling-combinators">
+ <meta name="assert" content="This tests that the + next-sibling selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .d { background-color: blue; }
+
+ .a + .c > .d { background-color: green; }
+ </style>
+</head>
+<body>
+ <div>
+ <div id="first" class="a"></div>
+ <div class="c">
+ <div id="target" class="d"></div>
+ </div>
+ </div>
+ <script>
+ 'use strict';
+ const green = 'rgb(0, 128, 0)';
+ const blue = 'rgb(0, 0, 255)';
+
+ test(function() {
+ const first = document.getElementById('first');
+ const target = document.getElementById('target');
+ const parent = first.parentElement;
+ assert_equals(getComputedStyle(target).backgroundColor, green, "initial color");
+
+ parent.removeChild(first);
+ assert_equals(getComputedStyle(target).backgroundColor, blue, "color after removal");
+
+ parent.insertBefore(first, parent.firstChild);
+ assert_equals(getComputedStyle(target).backgroundColor, green, "color after insert")
+ }, "Remove/Insert adjacent sibling of parent");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-003.html b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-003.html
new file mode 100644
index 0000000000..c7c51eaf6a
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-003.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>CSS Selectors Invalidation: insert sibling of ancestor</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#adjacent-sibling-combinators">
+ <meta name="assert" content="This tests that the + next-sibling selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .c * { background-color: blue; }
+
+ .a + * + .c * { background-color: green; }
+ </style>
+</head>
+<body>
+ <div>
+ <div id="first" class="a"></div>
+ <div></div>
+ <div class="c">
+ <div>
+ <div id="target"></div>
+ </div>
+ </div>
+ </div>
+ <script>
+ 'use strict';
+ const green = 'rgb(0, 128, 0)';
+ const blue = 'rgb(0, 0, 255)';
+
+ test(function() {
+ const first = document.getElementById('first');
+ const target = document.getElementById('target');
+ const parent = first.parentElement;
+ assert_equals(getComputedStyle(target).backgroundColor, green, "initial color");
+
+ parent.removeChild(first);
+ assert_equals(getComputedStyle(target).backgroundColor, blue, "color after removal");
+
+ parent.insertBefore(first, parent.firstChild);
+ assert_equals(getComputedStyle(target).backgroundColor, green, "color after insert")
+ }, "Remove/Insert earlier sibling of ancestor");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-004.html b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-004.html
new file mode 100644
index 0000000000..aa3fb1c9fc
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/insert-sibling-004.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>CSS Selectors Invalidation: insert sibling of parent</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#general-sibling-combinators">
+ <meta name="assert" content="This tests that the ~ subsequent-sibling selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ span { background-color: blue; }
+
+ .a ~ .c > span { background-color: green; }
+ </style>
+</head>
+<body>
+ <div>
+ <div id="first" class="a"></div>
+ <div></div>
+ <div></div>
+ <div class="c">
+ <span id="target"></span>
+ </div>
+ </div>
+ <script>
+ 'use strict';
+ const green = 'rgb(0, 128, 0)';
+ const blue = 'rgb(0, 0, 255)';
+
+ test(function() {
+ const first = document.getElementById('first');
+ const target = document.getElementById('target');
+ const parent = first.parentElement;
+ assert_equals(getComputedStyle(target).backgroundColor, green, "initial color");
+
+ parent.removeChild(first);
+ assert_equals(getComputedStyle(target).backgroundColor, blue, "color after removal");
+
+ parent.insertBefore(first, parent.firstChild);
+ assert_equals(getComputedStyle(target).backgroundColor, green, "color after insert")
+ }, "Remove/Insert earlier sibling of parent");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/is-pseudo-containing-complex-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/is-pseudo-containing-complex-in-has.html
new file mode 100644
index 0000000000..4e6d4c8832
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/is-pseudo-containing-complex-in-has.html
@@ -0,0 +1,379 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: :is() in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div { color: grey }
+.red:has(#descendant:is(.a_has_scope .b)) { color: red }
+.orangered:has(#descendant:is(.a_descendant .b)) #descendant { color: orangered }
+.darkred:has(#descendant:is(.a_indirect_next .b)) ~ #indirect_next { color: darkred }
+.pink:has(#descendant:is(.a_indirect_next_child .b)) ~ #indirect_next #indirect_next_child { color: pink }
+.green:has(#descendant:is(.p + .c_has_scope ~ .d .e)) { color: green }
+.lightgreen:has(#descendant:is(.p + .c_descendant ~ .d .e)) #descendant { color: lightgreen }
+.darkgreen:has(#descendant:is(.p + .c_indirect_next ~ .d .e)) ~ #indirect_next { color: darkgreen }
+.yellowgreen:has(#descendant:is(.p + .c_indirect_next_child ~ .d .e)) ~ #indirect_next #indirect_next_child { color: yellowgreen }
+.blue:has(~ #indirect_next:is(.p + .f_has_scope ~ .g)) { color: blue }
+.skyblue:has(~ #indirect_next:is(.p + .f_descendant ~ .g)) #descendant { color: skyblue }
+.lightblue:has(~ #indirect_next:is(.p + .f_indirect_next ~ .g)) ~ #indirect_next { color: lightblue }
+.darkblue:has(~ #indirect_next:is(.p + .f_indirect_next_child ~ .g)) ~ #indirect_next #indirect_next_child { color: darkblue }
+.yellow:has(~ #indirect_next:is(.h_has_scope .i)) { color: yellow }
+.ivory:has(~ #indirect_next:is(.h_descendant .i)) #descendant { color: ivory }
+.greenyellow:has(~ #indirect_next:is(.h_indirect_next .i)) ~ #indirect_next { color: greenyellow }
+.khaki:has(~ #indirect_next:is(.h_indirect_next_child .i)) ~ #indirect_next #indirect_next_child { color: khaki }
+.purple:has(~ #indirect_next:is(.p + .j_has_scope ~ .k .l)) { color: purple }
+.violet:has(~ #indirect_next:is(.p + .j_descendant ~ .k .l)) #descendant { color: violet }
+.orchid:has(~ #indirect_next:is(.p + .j_indirect_next ~ .k .l)) ~ #indirect_next { color: orchid }
+.plum:has(~ #indirect_next:is(.p + .j_indirect_next_child ~ .k .l)) ~ #indirect_next #indirect_next_child { color: plum }
+.orange:has(#descendant:is(:is(.m, .n) .o)) { color: orange }
+</style>
+<div>
+ <div class="p"></div>
+ <div id="parent_previous"></div>
+ <div id="parent" class="d k">
+ <div class="p"></div>
+ <div id="previous"></div>
+ <div class="p"></div>
+ <div id="has_scope" class="d">
+ <div class="p"></div>
+ <div id="child_previous"></div>
+ <div id="child" class="d">
+ <div id="descendant" class="b e o"></div>
+ </div>
+ </div>
+ <div class="p"></div>
+ <div id="direct_next"></div>
+ <div id="indirect_next" class="g i l">
+ <div id="indirect_next_child"></div>
+ </div>
+ </div>
+</div>
+<script>
+const grey = "rgb(128, 128, 128)";
+const red = "rgb(255, 0, 0)";
+const orangered = "rgb(255, 69, 0)";
+const darkred = "rgb(139, 0, 0)";
+const pink = "rgb(255, 192, 203)";
+const green = "rgb(0, 128, 0)";
+const lightgreen = "rgb(144, 238, 144)";
+const darkgreen = "rgb(0, 100, 0)";
+const yellowgreen = "rgb(154, 205, 50)";
+const blue = "rgb(0, 0, 255)";
+const skyblue = "rgb(135, 206, 235)";
+const lightblue = "rgb(173, 216, 230)";
+const darkblue = "rgb(0, 0, 139)";
+const yellow = "rgb(255, 255, 0)";
+const ivory = "rgb(255, 255, 240)";
+const greenyellow = "rgb(173, 255, 47)";
+const khaki = "rgb(240, 230, 140)";
+const purple = "rgb(128, 0, 128)";
+const violet = "rgb(238, 130, 238)";
+const orchid = "rgb(218, 112, 214)";
+const plum = "rgb(221, 160, 221)";
+const orange = "rgb(255, 165, 0)";
+
+function addClass(element, class_name) {
+ element.classList.add(class_name);
+}
+
+function removeClass(element, class_name) {
+ element.classList.remove(class_name);
+}
+
+function testClassChange(operation, class_name, element_id,
+ selector, matches_result,
+ subject_id, subject_color) {
+ let element = document.getElementById(element_id);
+ assert_equals(element ? element.id : "", element_id);
+ let subject = document.getElementById(subject_id);
+ assert_equals(subject ? subject.id : "", subject_id);
+ let message_prefix = [
+ "[", selector, "]",
+ ["#", element.id, ".classList.",
+ (operation == addClass ? "add" : "remove"),
+ "('", class_name, "')"].join(""),
+ ": "].join(" ");
+ operation(element, class_name);
+ test(function() {
+ assert_equals(subject.matches(selector), matches_result);
+ }, message_prefix + "check matches (" + matches_result + ")");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, subject_color);
+ }, message_prefix + "check #" + subject_id + " color");
+}
+
+function testSiblingInsertionRemoval(class_name, insert_before_id, selector,
+ subject_id,
+ insertion_matches_result,
+ insertion_subject_color,
+ removal_matches_result,
+ removal_subject_color) {
+ let insert_before = document.getElementById(insert_before_id);
+ assert_equals(insert_before ? insert_before.id : "", insert_before_id);
+ let parent = insert_before.parentElement;
+ let subject = document.getElementById(subject_id);
+ assert_equals(subject ? subject.id : "", subject_id);
+ let message_prefix = [
+ "[", selector, "]",
+ ["insert/remove .",
+ class_name, " before #", insert_before.id, ")"].join(""),
+ ": "].join(" ");
+
+ let div = document.createElement("div");
+ div.classList.add(class_name);
+
+ parent.insertBefore(div, insert_before);
+ test(function() {
+ assert_equals(subject.matches(selector), insertion_matches_result);
+ }, message_prefix + "(insertion) check matches (" +
+ insertion_matches_result + ")");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, insertion_subject_color);
+ }, message_prefix + "(insertion) check #" + subject_id + " color");
+
+ div.remove();
+ test(function() {
+ assert_equals(subject.matches(selector), removal_matches_result);
+ }, message_prefix + "(removal) check matches (" +
+ removal_matches_result + ")");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, removal_subject_color);
+ }, message_prefix + "(removal) check #" + subject_id + " color");
+}
+
+assert_equals(getComputedStyle(has_scope).color, grey);
+
+let selector = ".red:has(#descendant:is(.a_has_scope .b))";
+testClassChange(addClass, "red", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "a_has_scope", "parent", selector, true, "has_scope", red);
+testClassChange(removeClass, "a_has_scope", "parent", selector, false, "has_scope", grey);
+testClassChange(addClass, "a_has_scope", "has_scope", selector, true, "has_scope", red);
+testClassChange(removeClass, "a_has_scope", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "a_has_scope", "child", selector, true, "has_scope", red);
+testClassChange(removeClass, "a_has_scope", "child", selector, false, "has_scope", grey);
+testClassChange(removeClass, "red", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".orangered:has(#descendant:is(.a_descendant .b)) #descendant";
+testClassChange(addClass, "orangered", "has_scope", selector, false, "descendant", grey);
+testClassChange(addClass, "a_descendant", "parent", selector, true, "descendant", orangered);
+testClassChange(removeClass, "a_descendant", "parent", selector, false, "descendant", grey);
+testClassChange(addClass, "a_descendant", "has_scope", selector, true, "descendant", orangered);
+testClassChange(removeClass, "a_descendant", "has_scope", selector, false, "descendant", grey);
+testClassChange(addClass, "a_descendant", "child", selector, true, "descendant", orangered);
+testClassChange(removeClass, "a_descendant", "child", selector, false, "descendant", grey);
+testClassChange(removeClass, "orangered", "has_scope", selector, false, "descendant", grey);
+
+selector = ".darkred:has(#descendant:is(.a_indirect_next .b)) ~ #indirect_next";
+testClassChange(addClass, "darkred", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(addClass, "a_indirect_next", "parent", selector, true, "indirect_next", darkred);
+testClassChange(removeClass, "a_indirect_next", "parent", selector, false, "indirect_next", grey);
+testClassChange(addClass, "a_indirect_next", "has_scope", selector, true, "indirect_next", darkred);
+testClassChange(removeClass, "a_indirect_next", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(addClass, "a_indirect_next", "child", selector, true, "indirect_next", darkred);
+testClassChange(removeClass, "a_indirect_next", "child", selector, false, "indirect_next", grey);
+testClassChange(removeClass, "darkred", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".pink:has(#descendant:is(.a_indirect_next_child .b)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "pink", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "a_indirect_next_child", "parent", selector, true, "indirect_next_child", pink);
+testClassChange(removeClass, "a_indirect_next_child", "parent", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "a_indirect_next_child", "has_scope", selector, true, "indirect_next_child", pink);
+testClassChange(removeClass, "a_indirect_next_child", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "a_indirect_next_child", "child", selector, true, "indirect_next_child", pink);
+testClassChange(removeClass, "a_indirect_next_child", "child", selector, false, "indirect_next_child", grey);
+testClassChange(removeClass, "pink", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".green:has(#descendant:is(.p + .c_has_scope ~ .d .e))";
+testClassChange(addClass, "green", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "c_has_scope", "parent_previous", selector, true, "has_scope", green);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "has_scope", false, grey, true, green);
+testClassChange(removeClass, "c_has_scope", "parent_previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("c_has_scope", "parent_previous", selector, "has_scope", true, green, false, grey);
+testClassChange(addClass, "c_has_scope", "previous", selector, true, "has_scope", green);
+testSiblingInsertionRemoval("invalid", "previous", selector, "has_scope", false, grey, true, green);
+testClassChange(removeClass, "c_has_scope", "previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("c_has_scope", "previous", selector, "has_scope", true, green, false, grey);
+testClassChange(addClass, "c_has_scope", "child_previous", selector, true, "has_scope", green);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "has_scope", false, grey, true, green);
+testClassChange(removeClass, "c_has_scope", "child_previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("c_has_scope", "child_previous", selector, "has_scope", true, green, false, grey);
+testClassChange(removeClass, "green", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".lightgreen:has(#descendant:is(.p + .c_descendant ~ .d .e)) #descendant";
+testClassChange(addClass, "lightgreen", "has_scope", selector, false, "descendant", grey);
+testClassChange(addClass, "c_descendant", "parent_previous", selector, true, "descendant", lightgreen);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "descendant", false, grey, true, lightgreen);
+testClassChange(removeClass, "c_descendant", "parent_previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("c_descendant", "parent_previous", selector, "descendant", true, lightgreen, false, grey);
+testClassChange(addClass, "c_descendant", "previous", selector, true, "descendant", lightgreen);
+testSiblingInsertionRemoval("invalid", "previous", selector, "descendant", false, grey, true, lightgreen);
+testClassChange(removeClass, "c_descendant", "previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("c_descendant", "previous", selector, "descendant", true, lightgreen, false, grey);
+testClassChange(addClass, "c_descendant", "child_previous", selector, true, "descendant", lightgreen);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "descendant", false, grey, true, lightgreen);
+testClassChange(removeClass, "c_descendant", "child_previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("c_descendant", "child_previous", selector, "descendant", true, lightgreen, false, grey);
+testClassChange(removeClass, "lightgreen", "has_scope", selector, false, "descendant", grey);
+
+selector = ".darkgreen:has(#descendant:is(.p + .c_indirect_next ~ .d .e)) ~ #indirect_next";
+testClassChange(addClass, "darkgreen", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(addClass, "c_indirect_next", "parent_previous", selector, true, "indirect_next", darkgreen);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next", false, grey, true, darkgreen);
+testClassChange(removeClass, "c_indirect_next", "parent_previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("c_indirect_next", "parent_previous", selector, "indirect_next", true, darkgreen, false, grey);
+testClassChange(addClass, "c_indirect_next", "previous", selector, true, "indirect_next", darkgreen);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next", false, grey, true, darkgreen);
+testClassChange(removeClass, "c_indirect_next", "previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("c_indirect_next", "previous", selector, "indirect_next", true, darkgreen, false, grey);
+testClassChange(addClass, "c_indirect_next", "child_previous", selector, true, "indirect_next", darkgreen);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "indirect_next", false, grey, true, darkgreen);
+testClassChange(removeClass, "c_indirect_next", "child_previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("c_indirect_next", "child_previous", selector, "indirect_next", true, darkgreen, false, grey);
+testClassChange(removeClass, "darkgreen", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".yellowgreen:has(#descendant:is(.p + .c_indirect_next_child ~ .d .e)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "yellowgreen", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "c_indirect_next_child", "parent_previous", selector, true, "indirect_next_child", yellowgreen);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next_child", false, grey, true, yellowgreen);
+testClassChange(removeClass, "c_indirect_next_child", "parent_previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("c_indirect_next_child", "parent_previous", selector, "indirect_next_child", true, yellowgreen, false, grey);
+testClassChange(addClass, "c_indirect_next_child", "previous", selector, true, "indirect_next_child", yellowgreen);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next_child", false, grey, true, yellowgreen);
+testClassChange(removeClass, "c_indirect_next_child", "previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("c_indirect_next_child", "previous", selector, "indirect_next_child", true, yellowgreen, false, grey);
+testClassChange(addClass, "c_indirect_next_child", "child_previous", selector, true, "indirect_next_child", yellowgreen);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "indirect_next_child", false, grey, true, yellowgreen);
+testClassChange(removeClass, "c_indirect_next_child", "child_previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("c_indirect_next_child", "child_previous", selector, "indirect_next_child", true, yellowgreen, false, grey);
+testClassChange(removeClass, "yellowgreen", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".blue:has(~ #indirect_next:is(.p + .f_has_scope ~ .g))";
+testClassChange(addClass, "blue", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "f_has_scope", "previous", selector, true, "has_scope", blue);
+testSiblingInsertionRemoval("invalid", "previous", selector, "has_scope", false, grey, true, blue);
+testClassChange(removeClass, "f_has_scope", "previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("f_has_scope", "previous", selector, "has_scope", true, blue, false, grey);
+testClassChange(addClass, "f_has_scope", "has_scope", selector, true, "has_scope", blue);
+testClassChange(removeClass, "f_has_scope", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "f_has_scope", "direct_next", selector, true, "has_scope", blue);
+testSiblingInsertionRemoval("invalid", "direct_next", selector, "has_scope", false, grey, true, blue);
+testClassChange(removeClass, "f_has_scope", "direct_next", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("f_has_scope", "direct_next", selector, "has_scope", true, blue, false, grey);
+testClassChange(removeClass, "blue", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".skyblue:has(~ #indirect_next:is(.p + .f_descendant ~ .g)) #descendant";
+testClassChange(addClass, "skyblue", "has_scope", selector, false, "descendant", grey);
+testClassChange(addClass, "f_descendant", "previous", selector, true, "descendant", skyblue);
+testSiblingInsertionRemoval("invalid", "previous", selector, "descendant", false, grey, true, skyblue);
+testClassChange(removeClass, "f_descendant", "previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("f_descendant", "previous", selector, "descendant", true, skyblue, false, grey);
+testClassChange(addClass, "f_descendant", "has_scope", selector, true, "descendant", skyblue);
+testClassChange(removeClass, "f_descendant", "has_scope", selector, false, "descendant", grey);
+testClassChange(addClass, "f_descendant", "direct_next", selector, true, "descendant", skyblue);
+testSiblingInsertionRemoval("invalid", "direct_next", selector, "descendant", false, grey, true, skyblue);
+testClassChange(removeClass, "f_descendant", "direct_next", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("f_descendant", "direct_next", selector, "descendant", true, skyblue, false, grey);
+testClassChange(removeClass, "skyblue", "has_scope", selector, false, "descendant", grey);
+
+selector = ".lightblue:has(~ #indirect_next:is(.p + .f_indirect_next ~ .g)) ~ #indirect_next";
+testClassChange(addClass, "lightblue", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(addClass, "f_indirect_next", "previous", selector, true, "indirect_next", lightblue);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next", false, grey, true, lightblue);
+testClassChange(removeClass, "f_indirect_next", "previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("f_indirect_next", "previous", selector, "indirect_next", true, lightblue, false, grey);
+testClassChange(addClass, "f_indirect_next", "has_scope", selector, true, "indirect_next", lightblue);
+testClassChange(removeClass, "f_indirect_next", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(addClass, "f_indirect_next", "direct_next", selector, true, "indirect_next", lightblue);
+testSiblingInsertionRemoval("invalid", "direct_next", selector, "indirect_next", false, grey, true, lightblue);
+testClassChange(removeClass, "f_indirect_next", "direct_next", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("f_indirect_next", "direct_next", selector, "indirect_next", true, lightblue, false, grey);
+testClassChange(removeClass, "lightblue", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".darkblue:has(~ #indirect_next:is(.p + .f_indirect_next_child ~ .g)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "darkblue", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "f_indirect_next_child", "previous", selector, true, "indirect_next_child", darkblue);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next_child", false, grey, true, darkblue);
+testClassChange(removeClass, "f_indirect_next_child", "previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("f_indirect_next_child", "previous", selector, "indirect_next_child", true, darkblue, false, grey);
+testClassChange(addClass, "f_indirect_next_child", "has_scope", selector, true, "indirect_next_child", darkblue);
+testClassChange(removeClass, "f_indirect_next_child", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "f_indirect_next_child", "direct_next", selector, true, "indirect_next_child", darkblue);
+testSiblingInsertionRemoval("invalid", "direct_next", selector, "indirect_next_child", false, grey, true, darkblue);
+testClassChange(removeClass, "f_indirect_next_child", "direct_next", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("f_indirect_next_child", "direct_next", selector, "indirect_next_child", true, darkblue, false, grey);
+testClassChange(removeClass, "darkblue", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".yellow:has(~ #indirect_next:is(.h_has_scope .i))"
+testClassChange(addClass, "yellow", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "h_has_scope", "parent", selector, true, "has_scope", yellow);
+testClassChange(removeClass, "h_has_scope", "parent", selector, false, "has_scope", grey);
+testClassChange(removeClass, "yellow", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".ivory:has(~ #indirect_next:is(.h_descendant .i)) #descendant";
+testClassChange(addClass, "ivory", "has_scope", selector, false, "descendant", grey);
+testClassChange(addClass, "h_descendant", "parent", selector, true, "descendant", ivory);
+testClassChange(removeClass, "h_descendant", "parent", selector, false, "descendant", grey);
+testClassChange(removeClass, "ivory", "has_scope", selector, false, "descendant", grey);
+
+selector = ".greenyellow:has(~ #indirect_next:is(.h_indirect_next .i)) ~ #indirect_next";
+testClassChange(addClass, "greenyellow", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(addClass, "h_indirect_next", "parent", selector, true, "indirect_next", greenyellow);
+testClassChange(removeClass, "h_indirect_next", "parent", selector, false, "indirect_next", grey);
+testClassChange(removeClass, "greenyellow", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".khaki:has(~ #indirect_next:is(.h_indirect_next_child .i)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "khaki", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "h_indirect_next_child", "parent", selector, true, "indirect_next_child", khaki);
+testClassChange(removeClass, "h_indirect_next_child", "parent", selector, false, "indirect_next_child", grey);
+testClassChange(removeClass, "khaki", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".purple:has(~ #indirect_next:is(.p + .j_has_scope ~ .k .l))"
+testClassChange(addClass, "purple", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "j_has_scope", "parent_previous", selector, true, "has_scope", purple);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "has_scope", false, grey, true, purple);
+testClassChange(removeClass, "j_has_scope", "parent_previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("j_has_scope", "parent_previous", selector, "has_scope", true, purple, false, grey);
+testClassChange(removeClass, "purple", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".violet:has(~ #indirect_next:is(.p + .j_descendant ~ .k .l)) #descendant";
+testClassChange(addClass, "violet", "has_scope", selector, false, "descendant", grey);
+testClassChange(addClass, "j_descendant", "parent_previous", selector, true, "descendant", violet);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "descendant", false, grey, true, violet);
+testClassChange(removeClass, "j_descendant", "parent_previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("j_descendant", "parent_previous", selector, "descendant", true, violet, false, grey);
+testClassChange(removeClass, "violet", "has_scope", selector, false, "descendant", grey);
+
+selector = ".orchid:has(~ #indirect_next:is(.p + .j_indirect_next ~ .k .l)) ~ #indirect_next";
+testClassChange(addClass, "orchid", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(addClass, "j_indirect_next", "parent_previous", selector, true, "indirect_next", orchid);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next", false, grey, true, orchid);
+testClassChange(removeClass, "j_indirect_next", "parent_previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("j_indirect_next", "parent_previous", selector, "indirect_next", true, orchid, false, grey);
+testClassChange(removeClass, "orchid", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".plum:has(~ #indirect_next:is(.p + .j_indirect_next_child ~ .k .l)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "plum", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(addClass, "j_indirect_next_child", "parent_previous", selector, true, "indirect_next_child", plum);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next_child", false, grey, true, plum);
+testClassChange(removeClass, "j_indirect_next_child", "parent_previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("j_indirect_next_child", "parent_previous", selector, "indirect_next_child", true, plum, false, grey);
+testClassChange(removeClass, "plum", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".orange:has(#descendant:is(:is(.m, .n) .o))";
+testClassChange(addClass, "orange", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "m", "parent", selector, true, "has_scope", orange);
+testClassChange(removeClass, "m", "parent", selector, false, "has_scope", grey);
+testClassChange(addClass, "n", "parent", selector, true, "has_scope", orange);
+testClassChange(removeClass, "n", "parent", selector, false, "has_scope", grey);
+testClassChange(addClass, "m", "has_scope", selector, true, "has_scope", orange);
+testClassChange(removeClass, "m", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "n", "has_scope", selector, true, "has_scope", orange);
+testClassChange(removeClass, "n", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "m", "child", selector, true, "has_scope", orange);
+testClassChange(removeClass, "m", "child", selector, false, "has_scope", grey);
+testClassChange(addClass, "n", "child", selector, true, "has_scope", orange);
+testClassChange(removeClass, "n", "child", selector, false, "has_scope", grey);
+testClassChange(removeClass, "orange", "has_scope", selector, false, "has_scope", grey);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/is.html b/testing/web-platform/tests/css/selectors/invalidation/is.html
new file mode 100644
index 0000000000..8490f09697
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/is.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Selectors Invalidation: :is()</title>
+ <link rel="author" title="Victoria Su" href="mailto:victoriaytsu@google.com">
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#matches">
+ <meta name="assert" content="This tests that the :is() selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .b {
+ color: yellow;
+ }
+ /*Simple selector arguments */
+ .a :is(.b, .c) {
+ color: red;
+ }
+ /*Compound selector arguments */
+ .a :is(.c#d, .e) {
+ color: green;
+ }
+ /* Complex selector arguments */
+ .a .g>.b {
+ color: black;
+ }
+ .a :is(.e+.f, .g>.b, .h) {
+ color: blue;
+ }
+ .g>.b {
+ color: black;
+ }
+ .a .h {
+ color: black;
+ }
+ /* Nested */
+ .a+.c>.e {
+ color: black;
+ }
+ .c>.a+.e {
+ color: black;
+ }
+ .a+:is(.b+.f, :is(.c>.e, .g)) {
+ color: red;
+ }
+ .c>.e {
+ color: black;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="a1">
+ <div class="b" id="b1">
+ Red
+ </div>
+ <div class="c" id="c1">
+ Red
+ </div>
+ <div class="c" id="d">
+ Green
+ </div>
+ <div class="e" id="e1">
+ Green
+ </div>
+ <div class="f" id="f1">
+ Blue
+ </div>
+ <div class="g">
+ <div class="b" id="b2">
+ Blue
+ <div class="b" id="b3">
+ Red
+ </div>
+ </div>
+ </div>
+ <div class="h" id="h1">
+ Blue
+ </div>
+ </div>
+ <div class="c" id="c2">
+ <div id="a2"></div>
+ <div class="e" id="e2">
+ Red
+ </div>
+ </div>
+ <script>
+ document.body.offsetTop;
+
+ var black = "rgb(0, 0, 0)";
+ var blue = "rgb(0, 0, 255)";
+ var green = "rgb(0, 128, 0)";
+ var red = "rgb(255, 0, 0)";
+ var yellow = "rgb(255, 255, 0)";
+
+ test(() => {
+ assert_equals(getComputedStyle(b1).color, yellow);
+ assert_equals(getComputedStyle(b2).color, black);
+ assert_equals(getComputedStyle(b3).color, yellow);
+ assert_equals(getComputedStyle(c1).color, black);
+ assert_equals(getComputedStyle(d).color, black);
+ assert_equals(getComputedStyle(e1).color, black);
+ assert_equals(getComputedStyle(e2).color, black);
+ assert_equals(getComputedStyle(f1).color, black);
+ assert_equals(getComputedStyle(h1).color, black);
+ }, "Preconditions.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(b1).color, red);
+ assert_equals(getComputedStyle(b3).color, red);
+ assert_equals(getComputedStyle(c1).color, red);
+ }, "Invalidate :is() for simple selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(d).color, green);
+ }, "Invalidate :is() for compound selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(b2).color, blue);
+ assert_equals(getComputedStyle(b3).color, red);
+ assert_equals(getComputedStyle(f1).color, blue);
+ }, "Invalidate :is() for complex selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(e2).color, black);
+ a2.className = "a";
+ assert_equals(getComputedStyle(e2).color, red);
+ }, "Invalidate nested :is().");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(b2).color, blue);
+ assert_equals(getComputedStyle(h1).color, blue);
+ }, "Test specificity of :is().");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/link-pseudo-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/link-pseudo-in-has.html
new file mode 100644
index 0000000000..0ff879768b
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/link-pseudo-in-has.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: :link, :visited :any-link, pseudo-class in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #parent { color: blue; }
+ #grandparent { color: blue; }
+ #parent:has(> :not(:link)) { color: grey; }
+ #parent:has(> :link) { color: green; }
+ #parent:has(> :visited) { color: red; }
+ #grandparent:has(:not(:any-link)) { color: grey; }
+ #grandparent:has(:any-link) { color: green; }
+</style>
+<div id="grandparent"></div>
+<script>
+ const BLUE = "rgb(0, 0, 255)";
+ const GREY = "rgb(128, 128, 128)";
+ const GREEN = "rgb(0, 128, 0)";
+ const RED = "rgb(255, 0, 0)";
+
+ function checkColor(id, color, target_matches) {
+ let element = document.getElementById(id);
+ let message = ["location.hash ==", location.hash, ": #" + id, "should be",
+ color, (target_matches ? "with" : "without"),
+ ":target"].join(" ");
+ assert_equals(getComputedStyle(element).color, color, message);
+ }
+
+ promise_test(async () => {
+ assert_equals(getComputedStyle(grandparent).color, BLUE,
+ "grandparent should be blue without any element");
+
+ let parent = document.createElement("div");
+ parent.id = "parent";
+ grandparent.appendChild(parent);
+
+ assert_equals(getComputedStyle(grandparent).color, GREY,
+ "grandparent should be grey after parent added");
+ assert_equals(getComputedStyle(parent).color, BLUE,
+ "parent should be blue without any link");
+
+ let div = document.createElement("div");
+ parent.appendChild(div);
+
+ assert_equals(getComputedStyle(grandparent).color, GREY,
+ "grandparent should be grey after div added");
+ assert_equals(getComputedStyle(parent).color, GREY,
+ "parent should be grey after div added");
+
+ let visited = document.createElement("a");
+ visited.href = "";
+ parent.appendChild(visited);
+
+ assert_equals(getComputedStyle(grandparent).color, GREEN,
+ "grandparent should be green after visited link added");
+ assert_equals(getComputedStyle(parent).color, GREEN,
+ "parent should be green after visited link added");
+
+ let unvisited = document.createElement("a");
+ unvisited.href = "unvisited";
+ parent.appendChild(unvisited);
+
+ assert_equals(getComputedStyle(grandparent).color, GREEN,
+ "grandparent should be green after unvisited link added");
+ assert_equals(getComputedStyle(parent).color, GREEN,
+ "parent should be green after unvisited link added");
+
+ unvisited.remove();
+
+ assert_equals(getComputedStyle(grandparent).color, GREEN,
+ "grandparent should be green after unvisited link removed");
+ assert_equals(getComputedStyle(parent).color, GREEN,
+ "parent should be blue after unvisited link removed");
+
+ visited.remove();
+
+ assert_equals(getComputedStyle(grandparent).color, GREY,
+ "grandparent should be grey after visited link removed");
+ assert_equals(getComputedStyle(parent).color, GREY,
+ "parent should be grey after visited link removed");
+
+ div.remove();
+
+ assert_equals(getComputedStyle(grandparent).color, GREY,
+ "grandparent should be grey after div removed");
+ assert_equals(getComputedStyle(parent).color, BLUE,
+ "parent should be blue after div removed");
+
+ parent.remove();
+
+ assert_equals(getComputedStyle(grandparent).color, BLUE,
+ "grandparent should be blue after parent removed");
+ });
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/location-pseudo-classes-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/location-pseudo-classes-in-has.html
new file mode 100644
index 0000000000..697fc70ae6
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/location-pseudo-classes-in-has.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: location pseudo classes in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #parent1:has(:link) { color: green }
+ #parent1:has(:visited) { color: yellowgreen }
+</style>
+<div id=parent1>
+ <div>parent color must be blue with
+ <a id=link1 href="#unvisited">visited link</a>
+ </div>
+</div>
+<script>
+ test(() => {
+ history.replaceState({}, "", "#visited");
+
+ assert_equals(getComputedStyle(parent1).color, "rgb(0, 128, 0)",
+ "parent should be green");
+
+ link1.href = "#visited";
+
+ assert_equals(getComputedStyle(parent1).color, "rgb(0, 128, 0)",
+ "parent should be still green");
+ });
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/media-pseudo-classes-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/media-pseudo-classes-in-has.html
new file mode 100644
index 0000000000..f0df71534a
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/media-pseudo-classes-in-has.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<title>:has() invalidation with :playing, :paused, :seeking and :muted pseudo-classes</title>
+<link rel="author" title="Tim Nguyen" href="https://github.com/nt1m">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<link rel="help" href="https://w3c.github.io/csswg-drafts/selectors/#video-state">
+<style>
+ #subject {
+ background-color: black;
+ accent-color: black;
+ color: black;
+ border: 2px solid black;
+ }
+ #subject:has(:muted) {
+ background-color: red;
+ }
+ #subject:has(:playing) {
+ border-color: green;
+ }
+ #subject:has(:paused) {
+ color: orange;
+ }
+ #subject:has(:seeking) {
+ accent-color: blue;
+ }
+</style>
+<body>
+ <div id="subject">
+ Test media pseudo-classes invalidation with :has()
+ <input type="checkbox">
+ <video width="300" height="300" loop></video>
+ </div>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script>
+ const GREEN = "rgb(0, 128, 0)";
+ const ORANGE = "rgb(255, 165, 0)";
+ const BLUE = "rgb(0, 0, 255)";
+ const RED = "rgb(255, 0, 0)";
+ const BLACK = "rgb(0, 0, 0)";
+
+ function assert_matches_muted(muted) {
+ assert_equals(getComputedStyle(subject).backgroundColor, muted ? RED : BLACK);
+ }
+
+ function assert_matches_playing(playing) {
+ assert_equals(getComputedStyle(subject).borderColor, playing ? GREEN : BLACK);
+ assert_equals(getComputedStyle(subject).color, !playing ? ORANGE : BLACK);
+ }
+
+ function assert_matches_seeking(seeking) {
+ assert_equals(getComputedStyle(subject).accentColor, seeking ? BLUE : BLACK);
+ }
+
+ promise_test(async (t) => {
+ t.add_cleanup(() => {
+ video.muted = false;
+ video.pause();
+ video.removeAttribute("src");
+ });
+ const video = document.querySelector("video");
+ assert_matches_muted(false);
+ assert_matches_playing(false);
+ assert_matches_seeking(false);
+ 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_matches_muted(true);
+ await new Promise((r) => {
+ video.addEventListener("playing", r, { once: true });
+ video.play();
+ });
+ assert_matches_playing(true);
+ }, "Test :playing pseudo-classes");
+
+ promise_test(async (t) => {
+ t.add_cleanup(() => {
+ video.removeAttribute("src");
+ });
+ const video = document.querySelector("video");
+ assert_matches_muted(false);
+ assert_matches_playing(false);
+ assert_matches_seeking(false);
+ await new Promise((r) => {
+ video.addEventListener("canplay", r, { once: true });
+ video.src = "/media/counting.mp4";
+ });
+
+ assert_matches_seeking(false);
+ await new Promise((r) => {
+ video.addEventListener("seeking", r, { once: true });
+ video.currentTime = 10;
+ });
+ assert_matches_seeking(true);
+ }, "Test :seeking pseudo-class");
+
+ promise_test(async (t) => {
+ t.add_cleanup(() => {
+ video.removeAttribute("src");
+ });
+ const video = document.querySelector("video");
+ await new Promise((r) => {
+ video.addEventListener("canplay", r, { once: true });
+ video.src = "/media/counting.mp4";
+ });
+ assert_matches_muted(false);
+ video.muted = true;
+ assert_matches_muted(true);
+ video.muted = false;
+ assert_matches_muted(false);
+ }, "Test :muted pseudo-class");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/modal-pseudo-class-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/modal-pseudo-class-in-has.html
new file mode 100644
index 0000000000..1bff896d49
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/modal-pseudo-class-in-has.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: :modal pseudo class in :has()</title>
+<link rel="author" title="Tim Nguyen" href="https://github.com/nt1m">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+ #subject:has(#dialog:modal) { color: green; }
+ #subject:has(#fullscreenTarget:modal) { color: blue; }
+</style>
+<div id="subject">
+ This is some text.
+ <dialog id="dialog">This is a dialog</dialog>
+ <div id="fullscreenTarget">This is going to be fullscreened</div>
+</div>
+<script>
+ // Dialog tests
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since dialog is not modal");
+ dialog.show();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since dialog is not modal");
+ dialog.close();
+ }, ":modal pseudo-class is not active with dialog.show()");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ dialog.showModal();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 128, 0)",
+ "ancestor should be green since dialog is shown modally");
+ dialog.close();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since dialog is closed");
+ }, ":modal pseudo-class invalidation with showModal + close");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ dialog.showModal();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 128, 0)",
+ "ancestor should be green since dialog is shown modally");
+ dialog.remove();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since dialog is removed");
+ }, ":modal pseudo-class invalidation with showModal + remove");
+
+ // Fullscreen tests
+ let waitForFullscreenChange = () => {
+ return new Promise((resolve) => {
+ document.addEventListener("fullscreenchange", resolve, { once: true });
+ });
+ };
+ promise_test(async function() {
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ test_driver.bless("fullscreen", () => fullscreenTarget.requestFullscreen());
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 255)",
+ "ancestor should be blue since target is fullscreen");
+ document.exitFullscreen();
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since target is no longer fullscreen");
+ }, ":modal pseudo-class invalidation with requestFullscreen + exitFullscreen");
+ promise_test(async function() {
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black");
+ test_driver.bless("fullscreen", () => fullscreenTarget.requestFullscreen());
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 255)",
+ "ancestor should be blue since target is fullscreen");
+ fullscreenTarget.remove();
+ await waitForFullscreenChange();
+ assert_equals(getComputedStyle(subject).color, "rgb(0, 0, 0)",
+ "ancestor should be black since target is removed");
+ }, ":modal pseudo-class invalidation with requestFullscreen + remove");
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/not-001.html b/testing/web-platform/tests/css/selectors/invalidation/not-001.html
new file mode 100644
index 0000000000..db1cf291c7
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/not-001.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>CSS Selectors Invalidation: complex :not()</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#negation">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ * {
+ color: yellow;
+ }
+ :not(.b ~ *) {
+ color: green;
+ }
+</style>
+<div id="b">
+</div>
+<div class="c">
+</div>
+<div class="d">
+</div>
+<script>
+ var black = "rgb(0, 0, 0)";
+ var blue = "rgb(0, 0, 255)";
+ var green = "rgb(0, 128, 0)";
+ var red = "rgb(255, 0, 0)";
+ var yellow = "rgb(255, 255, 0)";
+
+ test(() => {
+ assert_equals(getComputedStyle(document.querySelector("#b")).color, green);
+ assert_equals(getComputedStyle(document.querySelector(".c")).color, green);
+ assert_equals(getComputedStyle(document.querySelector(".d")).color, green);
+ }, "precondition");
+
+ test(() => {
+ document.getElementById("b").className = "b";
+ assert_equals(getComputedStyle(document.querySelector("#b")).color, green);
+ assert_equals(getComputedStyle(document.querySelector(".c")).color, yellow);
+ assert_equals(getComputedStyle(document.querySelector(".d")).color, yellow);
+ }, "Invalidation of sibling combinators in :not()");
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/not-002.html b/testing/web-platform/tests/css/selectors/invalidation/not-002.html
new file mode 100644
index 0000000000..811b125f8c
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/not-002.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<title>CSS Selectors Invalidation: complex :not()</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#negation">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ .b {
+ color: yellow;
+ }
+ /*Simple selector arguments */
+ .a :not(:not(.b, .c)) {
+ color: red;
+ }
+ /*Compound selector arguments */
+ .a :not(:not(.c#d, .e)) {
+ color: green;
+ }
+ /* Complex selector arguments */
+ .a .g>.b {
+ color: black;
+ }
+ .a :not(:not(.e+.f, .g>.b, .h)) {
+ color: blue;
+ }
+ .g>.b {
+ color: black;
+ }
+ .a .h {
+ color: black;
+ }
+ /* Nested */
+ .a+.c>.e {
+ color: black;
+ }
+ .c>.a+.e {
+ color: black;
+ }
+ .a+:not(:not(.b+.f, :is(.c>.e, .g))) {
+ color: red;
+ }
+ .c>.e {
+ color: black;
+ }
+</style>
+<div id="a1">
+ <div class="b" id="b1">
+ Red
+ </div>
+ <div class="c" id="c1">
+ Red
+ </div>
+ <div class="c" id="d">
+ Green
+ </div>
+ <div class="e" id="e1">
+ Green
+ </div>
+ <div class="f" id="f1">
+ Blue
+ </div>
+ <div class="g">
+ <div class="b" id="b2">
+ Blue
+ <div class="b" id="b3">
+ Red
+ </div>
+ </div>
+ </div>
+ <div class="h" id="h1">
+ Blue
+ </div>
+</div>
+<div class="c" id="c2">
+ <div id="a2"></div>
+ <div class="e" id="e2">
+ Red
+ </div>
+</div>
+<script>
+ document.body.offsetTop;
+
+ var black = "rgb(0, 0, 0)";
+ var blue = "rgb(0, 0, 255)";
+ var green = "rgb(0, 128, 0)";
+ var red = "rgb(255, 0, 0)";
+ var yellow = "rgb(255, 255, 0)";
+
+ test(() => {
+ assert_equals(getComputedStyle(b1).color, yellow);
+ assert_equals(getComputedStyle(b2).color, black);
+ assert_equals(getComputedStyle(b3).color, yellow);
+ assert_equals(getComputedStyle(c1).color, black);
+ assert_equals(getComputedStyle(d).color, black);
+ assert_equals(getComputedStyle(e1).color, black);
+ assert_equals(getComputedStyle(e2).color, black);
+ assert_equals(getComputedStyle(f1).color, black);
+ assert_equals(getComputedStyle(h1).color, black);
+ }, "Preconditions.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(b1).color, red);
+ assert_equals(getComputedStyle(b3).color, red);
+ assert_equals(getComputedStyle(c1).color, red);
+ }, "Invalidate :not() for simple selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(d).color, green);
+ }, "Invalidate :not() for compound selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(b2).color, blue);
+ assert_equals(getComputedStyle(b3).color, red);
+ assert_equals(getComputedStyle(f1).color, blue);
+ }, "Invalidate :not() for complex selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(e2).color, black);
+ a2.className = "a";
+ assert_equals(getComputedStyle(e2).color, red);
+ }, "Invalidate nested :is() inside :not().");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(b2).color, blue);
+ assert_equals(getComputedStyle(h1).color, blue);
+ }, "Test specificity of :not().");
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/not-pseudo-containing-complex-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/not-pseudo-containing-complex-in-has.html
new file mode 100644
index 0000000000..d24abf69f7
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/not-pseudo-containing-complex-in-has.html
@@ -0,0 +1,375 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: :not(&lt;complex-selector&gt;) in :has() argument (complex selector)</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div { color: grey }
+.red:has(#descendant:not(.a_has_scope .b)) { color: red }
+.orangered:has(#descendant:not(.a_descendant .b)) #descendant { color: orangered }
+.darkred:has(#descendant:not(.a_indirect_next .b)) ~ #indirect_next { color: darkred }
+.pink:has(#descendant:not(.a_indirect_next_child .b)) ~ #indirect_next #indirect_next_child { color: pink }
+.green:has(#descendant:not(.p + .c_has_scope ~ .d .e)) { color: green }
+.lightgreen:has(#descendant:not(.p + .c_descendant ~ .d .e)) #descendant { color: lightgreen }
+.darkgreen:has(#descendant:not(.p + .c_indirect_next ~ .d .e)) ~ #indirect_next { color: darkgreen }
+.yellowgreen:has(#descendant:not(.p + .c_indirect_next_child ~ .d .e)) ~ #indirect_next #indirect_next_child { color: yellowgreen }
+.blue:has(~ #indirect_next:not(.p + .f_has_scope ~ .g)) { color: blue }
+.skyblue:has(~ #indirect_next:not(.p + .f_descendant ~ .g)) #descendant { color: skyblue }
+.lightblue:has(~ #indirect_next:not(.p + .f_indirect_next ~ .g)) ~ #indirect_next { color: lightblue }
+.darkblue:has(~ #indirect_next:not(.p + .f_indirect_next_child ~ .g)) ~ #indirect_next #indirect_next_child { color: darkblue }
+.yellow:has(~ #indirect_next:not(.h_has_scope .i)) { color: yellow }
+.ivory:has(~ #indirect_next:not(.h_descendant .i)) #descendant { color: ivory }
+.greenyellow:has(~ #indirect_next:not(.h_indirect_next .i)) ~ #indirect_next { color: greenyellow }
+.khaki:has(~ #indirect_next:not(.h_indirect_next_child .i)) ~ #indirect_next #indirect_next_child { color: khaki }
+.purple:has(~ #indirect_next:not(.p + .j_has_scope ~ .k .l)) { color: purple }
+.violet:has(~ #indirect_next:not(.p + .j_descendant ~ .k .l)) #descendant { color: violet }
+.orchid:has(~ #indirect_next:not(.p + .j_indirect_next ~ .k .l)) ~ #indirect_next { color: orchid }
+.plum:has(~ #indirect_next:not(.p + .j_indirect_next_child ~ .k .l)) ~ #indirect_next #indirect_next_child { color: plum }
+.orange:has(#descendant:not(.m:not(.n) .o)) { color: orange }
+</style>
+<div>
+ <div class="p"></div>
+ <div id="parent_previous"></div>
+ <div id="parent" class="d k">
+ <div class="p"></div>
+ <div id="previous"></div>
+ <div class="p"></div>
+ <div id="has_scope" class="d">
+ <div class="p"></div>
+ <div id="child_previous"></div>
+ <div id="child" class="d">
+ <div id="descendant" class="b e o"></div>
+ </div>
+ </div>
+ <div class="p"></div>
+ <div id="direct_next"></div>
+ <div id="indirect_next" class="g i l">
+ <div id="indirect_next_child"></div>
+ </div>
+ </div>
+</div>
+<script>
+const grey = "rgb(128, 128, 128)";
+const red = "rgb(255, 0, 0)";
+const orangered = "rgb(255, 69, 0)";
+const darkred = "rgb(139, 0, 0)";
+const pink = "rgb(255, 192, 203)";
+const green = "rgb(0, 128, 0)";
+const lightgreen = "rgb(144, 238, 144)";
+const darkgreen = "rgb(0, 100, 0)";
+const yellowgreen = "rgb(154, 205, 50)";
+const blue = "rgb(0, 0, 255)";
+const skyblue = "rgb(135, 206, 235)";
+const lightblue = "rgb(173, 216, 230)";
+const darkblue = "rgb(0, 0, 139)";
+const yellow = "rgb(255, 255, 0)";
+const ivory = "rgb(255, 255, 240)";
+const greenyellow = "rgb(173, 255, 47)";
+const khaki = "rgb(240, 230, 140)";
+const purple = "rgb(128, 0, 128)";
+const violet = "rgb(238, 130, 238)";
+const orchid = "rgb(218, 112, 214)";
+const plum = "rgb(221, 160, 221)";
+const orange = "rgb(255, 165, 0)";
+
+function addClass(element, class_name) {
+ element.classList.add(class_name);
+}
+
+function removeClass(element, class_name) {
+ element.classList.remove(class_name);
+}
+
+function testClassChange(operation, class_name, element_id,
+ selector, matches_result,
+ subject_id, subject_color) {
+ let element = document.getElementById(element_id);
+ assert_equals(element ? element.id : "", element_id);
+ let subject = document.getElementById(subject_id);
+ assert_equals(subject ? subject.id : "", subject_id);
+ let message_prefix = [
+ "[", selector, "]",
+ ["#", element.id, ".classList.",
+ (operation == addClass ? "add" : "remove"),
+ "('", class_name, "')"].join(""),
+ ": "].join(" ");
+ operation(element, class_name);
+ test(function() {
+ assert_equals(subject.matches(selector), matches_result);
+ }, message_prefix + "check matches (" + matches_result + ")");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, subject_color);
+ }, message_prefix + "check #" + subject_id + " color");
+}
+
+function testSiblingInsertionRemoval(class_name, insert_before_id, selector,
+ subject_id,
+ insertion_matches_result,
+ insertion_subject_color,
+ removal_matches_result,
+ removal_subject_color) {
+ let insert_before = document.getElementById(insert_before_id);
+ assert_equals(insert_before ? insert_before.id : "", insert_before_id);
+ let parent = insert_before.parentElement;
+ let subject = document.getElementById(subject_id);
+ assert_equals(subject ? subject.id : "", subject_id);
+ let message_prefix = [
+ "[", selector, "]",
+ ["insert/remove .",
+ class_name, " before #", insert_before.id, ")"].join(""),
+ ": "].join(" ");
+
+ let div = document.createElement("div");
+ div.classList.add(class_name);
+
+ parent.insertBefore(div, insert_before);
+ test(function() {
+ assert_equals(subject.matches(selector), insertion_matches_result);
+ }, message_prefix + "(insertion) check matches (" +
+ insertion_matches_result + ")");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, insertion_subject_color);
+ }, message_prefix + "(insertion) check #" + subject_id + " color");
+
+ div.remove();
+ test(function() {
+ assert_equals(subject.matches(selector), removal_matches_result);
+ }, message_prefix + "(removal) check matches (" +
+ removal_matches_result + ")");
+ test(function() {
+ assert_equals(getComputedStyle(subject).color, removal_subject_color);
+ }, message_prefix + "(removal) check #" + subject_id + " color");
+}
+
+assert_equals(getComputedStyle(has_scope).color, grey);
+
+let selector = ".red:has(#descendant:not(.a_has_scope .b))";
+testClassChange(addClass, "red", "has_scope", selector, true, "has_scope", red);
+testClassChange(addClass, "a_has_scope", "parent", selector, false, "has_scope", grey);
+testClassChange(removeClass, "a_has_scope", "parent", selector, true, "has_scope", red);
+testClassChange(addClass, "a_has_scope", "has_scope", selector, false, "has_scope", grey);
+testClassChange(removeClass, "a_has_scope", "has_scope", selector, true, "has_scope", red);
+testClassChange(addClass, "a_has_scope", "child", selector, false, "has_scope", grey);
+testClassChange(removeClass, "a_has_scope", "child", selector, true, "has_scope", red);
+testClassChange(removeClass, "red", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".orangered:has(#descendant:not(.a_descendant .b)) #descendant";
+testClassChange(addClass, "orangered", "has_scope", selector, true, "descendant", orangered);
+testClassChange(addClass, "a_descendant", "parent", selector, false, "descendant", grey);
+testClassChange(removeClass, "a_descendant", "parent", selector, true, "descendant", orangered);
+testClassChange(addClass, "a_descendant", "has_scope", selector, false, "descendant", grey);
+testClassChange(removeClass, "a_descendant", "has_scope", selector, true, "descendant", orangered);
+testClassChange(addClass, "a_descendant", "child", selector, false, "descendant", grey);
+testClassChange(removeClass, "a_descendant", "child", selector, true, "descendant", orangered);
+testClassChange(removeClass, "orangered", "has_scope", selector, false, "descendant", grey);
+
+selector = ".darkred:has(#descendant:not(.a_indirect_next .b)) ~ #indirect_next";
+testClassChange(addClass, "darkred", "has_scope", selector, true, "indirect_next", darkred);
+testClassChange(addClass, "a_indirect_next", "parent", selector, false, "indirect_next", grey);
+testClassChange(removeClass, "a_indirect_next", "parent", selector, true, "indirect_next", darkred);
+testClassChange(addClass, "a_indirect_next", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(removeClass, "a_indirect_next", "has_scope", selector, true, "indirect_next", darkred);
+testClassChange(addClass, "a_indirect_next", "child", selector, false, "indirect_next", grey);
+testClassChange(removeClass, "a_indirect_next", "child", selector, true, "indirect_next", darkred);
+testClassChange(removeClass, "darkred", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".pink:has(#descendant:not(.a_indirect_next_child .b)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "pink", "has_scope", selector, true, "indirect_next_child", pink);
+testClassChange(addClass, "a_indirect_next_child", "parent", selector, false, "indirect_next_child", grey);
+testClassChange(removeClass, "a_indirect_next_child", "parent", selector, true, "indirect_next_child", pink);
+testClassChange(addClass, "a_indirect_next_child", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(removeClass, "a_indirect_next_child", "has_scope", selector, true, "indirect_next_child", pink);
+testClassChange(addClass, "a_indirect_next_child", "child", selector, false, "indirect_next_child", grey);
+testClassChange(removeClass, "a_indirect_next_child", "child", selector, true, "indirect_next_child", pink);
+testClassChange(removeClass, "pink", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".green:has(#descendant:not(.p + .c_has_scope ~ .d .e))";
+testClassChange(addClass, "green", "has_scope", selector, true, "has_scope", green);
+testClassChange(addClass, "c_has_scope", "parent_previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "has_scope", true, green, false, grey);
+testClassChange(removeClass, "c_has_scope", "parent_previous", selector, true, "has_scope", green);
+testSiblingInsertionRemoval("c_has_scope", "parent_previous", selector, "has_scope", false, grey, true, green);
+testClassChange(addClass, "c_has_scope", "previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("invalid", "previous", selector, "has_scope", true, green, false, grey);
+testClassChange(removeClass, "c_has_scope", "previous", selector, true, "has_scope", green);
+testSiblingInsertionRemoval("c_has_scope", "previous", selector, "has_scope", false, grey, true, green);
+testClassChange(addClass, "c_has_scope", "child_previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "has_scope", true, green, false, grey);
+testClassChange(removeClass, "c_has_scope", "child_previous", selector, true, "has_scope", green);
+testSiblingInsertionRemoval("c_has_scope", "child_previous", selector, "has_scope", false, grey, true, green);
+testClassChange(removeClass, "green", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".lightgreen:has(#descendant:not(.p + .c_descendant ~ .d .e)) #descendant";
+testClassChange(addClass, "lightgreen", "has_scope", selector, true, "descendant", lightgreen);
+testClassChange(addClass, "c_descendant", "parent_previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "descendant", true, lightgreen, false, grey);
+testClassChange(removeClass, "c_descendant", "parent_previous", selector, true, "descendant", lightgreen);
+testSiblingInsertionRemoval("c_descendant", "parent_previous", selector, "descendant", false, grey, true, lightgreen);
+testClassChange(addClass, "c_descendant", "previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("invalid", "previous", selector, "descendant", true, lightgreen, false, grey);
+testClassChange(removeClass, "c_descendant", "previous", selector, true, "descendant", lightgreen);
+testSiblingInsertionRemoval("c_descendant", "previous", selector, "descendant", false, grey, true, lightgreen);
+testClassChange(addClass, "c_descendant", "child_previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "descendant", true, lightgreen, false, grey);
+testClassChange(removeClass, "c_descendant", "child_previous", selector, true, "descendant", lightgreen);
+testSiblingInsertionRemoval("c_descendant", "child_previous", selector, "descendant", false, grey, true, lightgreen);
+testClassChange(removeClass, "lightgreen", "has_scope", selector, false, "descendant", grey);
+
+selector = ".darkgreen:has(#descendant:not(.p + .c_indirect_next ~ .d .e)) ~ #indirect_next";
+testClassChange(addClass, "darkgreen", "has_scope", selector, true, "indirect_next", darkgreen);
+testClassChange(addClass, "c_indirect_next", "parent_previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next", true, darkgreen, false, grey);
+testClassChange(removeClass, "c_indirect_next", "parent_previous", selector, true, "indirect_next", darkgreen);
+testSiblingInsertionRemoval("c_indirect_next", "parent_previous", selector, "indirect_next", false, grey, true, darkgreen);
+testClassChange(addClass, "c_indirect_next", "previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next", true, darkgreen, false, grey);
+testClassChange(removeClass, "c_indirect_next", "previous", selector, true, "indirect_next", darkgreen);
+testSiblingInsertionRemoval("c_indirect_next", "previous", selector, "indirect_next", false, grey, true, darkgreen);
+testClassChange(addClass, "c_indirect_next", "child_previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "indirect_next", true, darkgreen, false, grey);
+testClassChange(removeClass, "c_indirect_next", "child_previous", selector, true, "indirect_next", darkgreen);
+testSiblingInsertionRemoval("c_indirect_next", "child_previous", selector, "indirect_next", false, grey, true, darkgreen);
+testClassChange(removeClass, "darkgreen", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".yellowgreen:has(#descendant:not(.p + .c_indirect_next_child ~ .d .e)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "yellowgreen", "has_scope", selector, true, "indirect_next_child", yellowgreen);
+testClassChange(addClass, "c_indirect_next_child", "parent_previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next_child", true, yellowgreen, false, grey);
+testClassChange(removeClass, "c_indirect_next_child", "parent_previous", selector, true, "indirect_next_child", yellowgreen);
+testSiblingInsertionRemoval("c_indirect_next_child", "parent_previous", selector, "indirect_next_child", false, grey, true, yellowgreen);
+testClassChange(addClass, "c_indirect_next_child", "previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next_child", true, yellowgreen, false, grey);
+testClassChange(removeClass, "c_indirect_next_child", "previous", selector, true, "indirect_next_child", yellowgreen);
+testSiblingInsertionRemoval("c_indirect_next_child", "previous", selector, "indirect_next_child", false, grey, true, yellowgreen);
+testClassChange(addClass, "c_indirect_next_child", "child_previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("invalid", "child_previous", selector, "indirect_next_child", true, yellowgreen, false, grey);
+testClassChange(removeClass, "c_indirect_next_child", "child_previous", selector, true, "indirect_next_child", yellowgreen);
+testSiblingInsertionRemoval("c_indirect_next_child", "child_previous", selector, "indirect_next_child", false, grey, true, yellowgreen);
+testClassChange(removeClass, "yellowgreen", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".blue:has(~ #indirect_next:not(.p + .f_has_scope ~ .g))";
+testClassChange(addClass, "blue", "has_scope", selector, true, "has_scope", blue);
+testClassChange(addClass, "f_has_scope", "previous", selector, false, "has_scope", grey);
+testClassChange(removeClass, "f_has_scope", "previous", selector, true, "has_scope", blue);
+testSiblingInsertionRemoval("f_has_scope", "previous", selector, "has_scope", false, grey, true, blue);
+testClassChange(addClass, "f_has_scope", "has_scope", selector, false, "has_scope", grey);
+testClassChange(removeClass, "f_has_scope", "has_scope", selector, true, "has_scope", blue);
+testClassChange(addClass, "f_has_scope", "direct_next", selector, false, "has_scope", grey);
+testClassChange(removeClass, "f_has_scope", "direct_next", selector, true, "has_scope", blue);
+testSiblingInsertionRemoval("f_has_scope", "direct_next", selector, "has_scope", false, grey, true, blue);
+testClassChange(removeClass, "blue", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".skyblue:has(~ #indirect_next:not(.p + .f_descendant ~ .g)) #descendant";
+testClassChange(addClass, "skyblue", "has_scope", selector, true, "descendant", skyblue);
+testClassChange(addClass, "f_descendant", "previous", selector, false, "descendant", grey);
+testClassChange(removeClass, "f_descendant", "previous", selector, true, "descendant", skyblue);
+testSiblingInsertionRemoval("f_descendant", "previous", selector, "descendant", false, grey, true, skyblue);
+testClassChange(addClass, "f_descendant", "has_scope", selector, false, "descendant", grey);
+testClassChange(removeClass, "f_descendant", "has_scope", selector, true, "descendant", skyblue);
+testClassChange(addClass, "f_descendant", "direct_next", selector, false, "descendant", grey);
+testClassChange(removeClass, "f_descendant", "direct_next", selector, true, "descendant", skyblue);
+testSiblingInsertionRemoval("f_descendant", "direct_next", selector, "descendant", false, grey, true, skyblue);
+testClassChange(removeClass, "skyblue", "has_scope", selector, false, "descendant", grey);
+
+selector = ".lightblue:has(~ #indirect_next:not(.p + .f_indirect_next ~ .g)) ~ #indirect_next";
+testClassChange(addClass, "lightblue", "has_scope", selector, true, "indirect_next", lightblue);
+testClassChange(addClass, "f_indirect_next", "previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next", true, lightblue, false, grey);
+testClassChange(removeClass, "f_indirect_next", "previous", selector, true, "indirect_next", lightblue);
+testSiblingInsertionRemoval("f_indirect_next", "previous", selector, "indirect_next", false, grey, true, lightblue);
+testClassChange(addClass, "f_indirect_next", "has_scope", selector, false, "indirect_next", grey);
+testClassChange(removeClass, "f_indirect_next", "has_scope", selector, true, "indirect_next", lightblue);
+testClassChange(addClass, "f_indirect_next", "direct_next", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("invalid", "direct_next", selector, "indirect_next", true, lightblue, false, grey);
+testClassChange(removeClass, "f_indirect_next", "direct_next", selector, true, "indirect_next", lightblue);
+testSiblingInsertionRemoval("f_indirect_next", "direct_next", selector, "indirect_next", false, grey, true, lightblue);
+testClassChange(removeClass, "lightblue", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".darkblue:has(~ #indirect_next:not(.p + .f_indirect_next_child ~ .g)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "darkblue", "has_scope", selector, true, "indirect_next_child", darkblue);
+testClassChange(addClass, "f_indirect_next_child", "previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("invalid", "previous", selector, "indirect_next_child", true, darkblue, false, grey);
+testClassChange(removeClass, "f_indirect_next_child", "previous", selector, true, "indirect_next_child", darkblue);
+testSiblingInsertionRemoval("f_indirect_next_child", "previous", selector, "indirect_next_child", false, grey, true, darkblue);
+testClassChange(addClass, "f_indirect_next_child", "has_scope", selector, false, "indirect_next_child", grey);
+testClassChange(removeClass, "f_indirect_next_child", "has_scope", selector, true, "indirect_next_child", darkblue);
+testClassChange(addClass, "f_indirect_next_child", "direct_next", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("invalid", "direct_next", selector, "indirect_next_child", true, darkblue, false, grey);
+testClassChange(removeClass, "f_indirect_next_child", "direct_next", selector, true, "indirect_next_child", darkblue);
+testSiblingInsertionRemoval("f_indirect_next_child", "direct_next", selector, "indirect_next_child", false, grey, true, darkblue);
+testClassChange(removeClass, "darkblue", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".yellow:has(~ #indirect_next:not(.h_has_scope .i))"
+testClassChange(addClass, "yellow", "has_scope", selector, true, "has_scope", yellow);
+testClassChange(addClass, "h_has_scope", "parent", selector, false, "has_scope", grey);
+testClassChange(removeClass, "h_has_scope", "parent", selector, true, "has_scope", yellow);
+testClassChange(removeClass, "yellow", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".ivory:has(~ #indirect_next:not(.h_descendant .i)) #descendant";
+testClassChange(addClass, "ivory", "has_scope", selector, true, "descendant", ivory);
+testClassChange(addClass, "h_descendant", "parent", selector, false, "descendant", grey);
+testClassChange(removeClass, "h_descendant", "parent", selector, true, "descendant", ivory);
+testClassChange(removeClass, "ivory", "has_scope", selector, false, "descendant", grey);
+
+selector = ".greenyellow:has(~ #indirect_next:not(.h_indirect_next .i)) ~ #indirect_next";
+testClassChange(addClass, "greenyellow", "has_scope", selector, true, "indirect_next", greenyellow);
+testClassChange(addClass, "h_indirect_next", "parent", selector, false, "indirect_next", grey);
+testClassChange(removeClass, "h_indirect_next", "parent", selector, true, "indirect_next", greenyellow);
+testClassChange(removeClass, "greenyellow", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".khaki:has(~ #indirect_next:not(.h_indirect_next_child .i)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "khaki", "has_scope", selector, true, "indirect_next_child", khaki);
+testClassChange(addClass, "h_indirect_next_child", "parent", selector, false, "indirect_next_child", grey);
+testClassChange(removeClass, "h_indirect_next_child", "parent", selector, true, "indirect_next_child", khaki);
+testClassChange(removeClass, "khaki", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".purple:has(~ #indirect_next:not(.p + .j_has_scope ~ .k .l))"
+testClassChange(addClass, "purple", "has_scope", selector, true, "has_scope", purple);
+testClassChange(addClass, "j_has_scope", "parent_previous", selector, false, "has_scope", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "has_scope", true, purple, false, grey);
+testClassChange(removeClass, "j_has_scope", "parent_previous", selector, true, "has_scope", purple);
+testSiblingInsertionRemoval("j_has_scope", "parent_previous", selector, "has_scope", false, grey, true, purple);
+testClassChange(removeClass, "purple", "has_scope", selector, false, "has_scope", grey);
+
+selector = ".violet:has(~ #indirect_next:not(.p + .j_descendant ~ .k .l)) #descendant";
+testClassChange(addClass, "violet", "has_scope", selector, true, "descendant", violet);
+testClassChange(addClass, "j_descendant", "parent_previous", selector, false, "descendant", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "descendant", true, violet, false, grey);
+testClassChange(removeClass, "j_descendant", "parent_previous", selector, true, "descendant", violet);
+testSiblingInsertionRemoval("j_descendant", "parent_previous", selector, "descendant", false, grey, true, violet);
+testClassChange(removeClass, "violet", "has_scope", selector, false, "descendant", grey);
+
+selector = ".orchid:has(~ #indirect_next:not(.p + .j_indirect_next ~ .k .l)) ~ #indirect_next";
+testClassChange(addClass, "orchid", "has_scope", selector, true, "indirect_next", orchid);
+testClassChange(addClass, "j_indirect_next", "parent_previous", selector, false, "indirect_next", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next", true, orchid, false, grey);
+testClassChange(removeClass, "j_indirect_next", "parent_previous", selector, true, "indirect_next", orchid);
+testSiblingInsertionRemoval("j_indirect_next", "parent_previous", selector, "indirect_next", false, grey, true, orchid);
+testClassChange(removeClass, "orchid", "has_scope", selector, false, "indirect_next", grey);
+
+selector = ".plum:has(~ #indirect_next:not(.p + .j_indirect_next_child ~ .k .l)) ~ #indirect_next #indirect_next_child";
+testClassChange(addClass, "plum", "has_scope", selector, true, "indirect_next_child", plum);
+testClassChange(addClass, "j_indirect_next_child", "parent_previous", selector, false, "indirect_next_child", grey);
+testSiblingInsertionRemoval("invalid", "parent_previous", selector, "indirect_next_child", true, plum, false, grey);
+testClassChange(removeClass, "j_indirect_next_child", "parent_previous", selector, true, "indirect_next_child", plum);
+testSiblingInsertionRemoval("j_indirect_next_child", "parent_previous", selector, "indirect_next_child", false, grey, true, plum);
+testClassChange(removeClass, "plum", "has_scope", selector, false, "indirect_next_child", grey);
+
+selector = ".orange:has(#descendant:not(.m:not(.n) .o))";
+testClassChange(addClass, "orange", "has_scope", selector, true, "has_scope", orange);
+testClassChange(addClass, "m", "parent", selector, false, "has_scope", grey);
+testClassChange(addClass, "n", "parent", selector, true, "has_scope", orange);
+testClassChange(removeClass, "n", "parent", selector, false, "has_scope", grey);
+testClassChange(removeClass, "m", "parent", selector, true, "has_scope", orange);
+testClassChange(addClass, "m", "has_scope", selector, false, "has_scope", grey);
+testClassChange(addClass, "n", "has_scope", selector, true, "has_scope", orange);
+testClassChange(removeClass, "n", "has_scope", selector, false, "has_scope", grey);
+testClassChange(removeClass, "m", "has_scope", selector, true, "has_scope", orange);
+testClassChange(addClass, "m", "child", selector, false, "has_scope", grey);
+testClassChange(addClass, "n", "child", selector, true, "has_scope", orange);
+testClassChange(removeClass, "n", "child", selector, false, "has_scope", grey);
+testClassChange(removeClass, "m", "child", selector, true, "has_scope", orange);
+testClassChange(removeClass, "orange", "has_scope", selector, false, "has_scope", grey);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/quirks-mode-stylesheet-dynamic-add-001.html b/testing/web-platform/tests/css/selectors/invalidation/quirks-mode-stylesheet-dynamic-add-001.html
new file mode 100644
index 0000000000..3d7be98237
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/quirks-mode-stylesheet-dynamic-add-001.html
@@ -0,0 +1,27 @@
+<!-- Quirks mode -->
+<meta charset="utf-8">
+<title>Invalidation of style due to a dynamic stylesheet change in quirks mode</title>
+<link rel="help" href="https://html.spec.whatwg.org/#case-sensitivity-of-selectors">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1433589">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #foo {
+ width: 100px;
+ height: 100px;
+ background: red;
+ }
+</style>
+Should see a green square below.
+<div id="foo"></div>
+<script>
+test(function() {
+ let foo = document.getElementById('foo');
+ assert_equals(getComputedStyle(foo).backgroundColor, "rgb(255, 0, 0)");
+ let style = document.createElement('style');
+ style.textContent = "#FoO { background: green; }";
+ document.body.appendChild(style);
+ assert_equals(getComputedStyle(foo).backgroundColor, "rgb(0, 128, 0)");
+}, "Style should've changed to a green background");
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/selectorText-dynamic-001.html b/testing/web-platform/tests/css/selectors/invalidation/selectorText-dynamic-001.html
new file mode 100644
index 0000000000..ac33eca826
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/selectorText-dynamic-001.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: invalidation of class changes when the selector in a rule has changed</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#invalid">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432850">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ body { background: green; }
+ .red { background: red; }
+</style>
+<body class="red">
+Should have a green background.
+<script>
+test(() => {
+ document.body.offsetTop;
+ assert_equals(getComputedStyle(document.body).backgroundColor, "rgb(255, 0, 0)");
+ document.body.className = "";
+ document.styleSheets[0].cssRules[1].selectorText = ".bar";
+ assert_equals(getComputedStyle(document.body).backgroundColor, "rgb(0, 128, 0)");
+}, "Style should be recomputed correctly when the selector it depends on changes");
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-001.html b/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-001.html
new file mode 100644
index 0000000000..44e38ad81e
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-001.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: invalidation of class changes when the sheet the style depends on goes away</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#invalid">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432850">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ body { background: green; }
+</style>
+<style id="style">
+ .red { background: red; }
+</style>
+<body class="red">
+Should have a green background.
+<script>
+test(() => {
+ document.body.offsetTop;
+ assert_equals(getComputedStyle(document.body).backgroundColor, "rgb(255, 0, 0)");
+ document.body.className = "";
+ style.remove();
+ assert_equals(getComputedStyle(document.body).backgroundColor, "rgb(0, 128, 0)");
+}, "Style should be recomputed correctly when the stylesheet it depends on goes away");
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002-ref.html b/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002-ref.html
new file mode 100644
index 0000000000..6784161773
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002-ref.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test Reference</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<p style="color: green">
+ Should be green.
+</p>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002.html b/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002.html
new file mode 100644
index 0000000000..189abbe080
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/sheet-going-away-002.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: invalidation of class changes when the sheet the style depends on goes away</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#invalid">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432850">
+<link rel="match" href="sheet-going-away-002-ref.html">
+<style>
+ p { color: green; }
+</style>
+<style id="style">
+ .red p { color: red; }
+</style>
+<body class="red">
+<p>
+ Should be green.
+</p>
+<script>
+document.body.offsetTop;
+document.body.className = "";
+style.remove();
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/sibling.html b/testing/web-platform/tests/css/selectors/invalidation/sibling.html
new file mode 100644
index 0000000000..c0e04fb8b3
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/sibling.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Selectors Invalidation: sibling</title>
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#adjacent-sibling-combinators">
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#general-sibling-combinators">
+ <meta name="assert" content="This tests that the + next-sibling selector is effective">
+ <meta name="assert" content="This tests that the ~ subsequent-sibling selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+
+* { background-color: inherit; }
+
+body { background-color: rgba(0, 0, 0, 0); }
+
+.t1 .sibling + *,
+.t2 .sibling ~ *,
+.t3 + .sibling + *,
+.t4 + .sibling,
+.t5 + *,
+.t6 ~ .sibling,
+.t7 + * + * .child { background-color: rgb(0, 128, 0); }
+
+ </style>
+ </head>
+ <body>
+
+<div>
+ <div id="t1">
+ <div class="sibling"></div>
+ <div id="r1"></div>
+ <div id="u1"></div>
+ </div>
+</div>
+<div>
+ <div id="t2">
+ <div class="sibling"></div>
+ <div></div>
+ <div id="r2"></div>
+ </div>
+</div>
+<div>
+ <div id="t3"></div>
+ <div class="sibling"></div>
+ <div id="r3"></div>
+</div>
+<div>
+ <div id="t4"></div>
+ <div id="r4" class="sibling"></div>
+ <div id="u4" class="sibling"></div>
+</div>
+<div>
+ <div id="t5"></div>
+ <div id="r5"></div>
+ <div id="u5"></div>
+</div>
+<div>
+ <div id="t6"></div>
+ <div></div>
+ <div id="r6" class="sibling">
+ <div id="r6b"></div>
+ </div>
+ <div id="u6"></div>
+</div>
+<div>
+ <div id="t7">
+ <div class="child"></div>
+ </div>
+ <div></div>
+ <div>
+ <div id="r7" class="child"></div>
+ </div>
+ <div>
+ <div id="u7" class="child"></div>
+ </div>
+</div>
+
+ <script>
+
+test(function() {
+ assert_equals(getComputedStyle(r1).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+ t1.className = "t1";
+ assert_equals(getComputedStyle(r1).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+ assert_equals(getComputedStyle(u1).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Adjacent with universal selector");
+
+test(function() {
+ assert_equals(getComputedStyle(r2).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+ t2.className = "t2";
+ assert_equals(getComputedStyle(r2).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+}, "Indirect adjacent with universal selector");
+
+test(function() {
+ assert_equals(getComputedStyle(r3).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+ t3.className = "t3";
+ assert_equals(getComputedStyle(r3).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+}, "Indirect adjacent with two adjacent selectors");
+
+test(function() {
+ assert_equals(getComputedStyle(r4).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+ t4.className = "t4";
+ assert_equals(getComputedStyle(r4).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+ assert_equals(getComputedStyle(u4).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Adjacent class");
+
+test(function() {
+ assert_equals(getComputedStyle(r5).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+ t5.className = "t5";
+ assert_equals(getComputedStyle(r5).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+ assert_equals(getComputedStyle(u5).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Adjacent universal");
+
+test(function() {
+ assert_equals(getComputedStyle(r6).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+ assert_equals(getComputedStyle(r6b).backgroundColor, "rgba(0, 0, 0, 0)", "Child's background color should initially be transparent");
+
+ t6.className = "t6";
+ assert_equals(getComputedStyle(r6).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+ assert_equals(getComputedStyle(r6b).backgroundColor, "rgb(0, 128, 0)", "Child's background color is green after class change");
+ assert_equals(getComputedStyle(u6).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Sibling subtree through an indirect adjacent combinator");
+
+test(function() {
+ assert_equals(getComputedStyle(r7).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+ t7.className = "t7";
+ assert_equals(getComputedStyle(r7).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+ assert_equals(getComputedStyle(u7).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Sibling descendant through a universal selector");
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/subject-has-invalidation-with-display-none-anchor-element.html b/testing/web-platform/tests/css/selectors/invalidation/subject-has-invalidation-with-display-none-anchor-element.html
new file mode 100644
index 0000000000..6c87560c69
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/subject-has-invalidation-with-display-none-anchor-element.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: subject :has() invalidation with display: none anchor element</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #target:has(~ input:checked) { display: none; }
+</style>
+<p>Click checkbox</p>
+<div id="target">PASS</div>
+<input type="checkbox" id="checkme">
+<label for="checkme">Check me!</label>
+<script>
+ test(function() {
+ checkme.checked = false;
+ assert_equals(getComputedStyle(target).display, "block",
+ "target display should be empty");
+
+ checkme.checked = true;
+ assert_equals(getComputedStyle(target).display, "none",
+ "target display should be none");
+
+ checkme.checked = false;
+ assert_equals(getComputedStyle(target).display, "block",
+ "target display should be empty again");
+ });
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/target-pseudo-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/target-pseudo-in-has.html
new file mode 100644
index 0000000000..629a6a826b
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/target-pseudo-in-has.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: :target pseudo-class in :has() argument</title>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #parent1 { color: grey; }
+ #parent1:has(:target) { color: green; }
+
+ #parent2 { color: blue; }
+ #parent2:has(:not(:target)) { color: grey; }
+ #parent2:has(:target) { color: green; }
+
+ #parent3 { color: green; }
+ #parent3:not(:has(:target)) { color: grey; }
+</style>
+<a href="#fragment">link to #fragment</a>
+<a href="#fragment2">link to #fragment2</a>
+<a href="#fragment3">link to #fragment3</a>
+<a href="#">link to #</a>
+<div id="parent1">
+ 1:
+ <span id="fragment">Must be green when containing :target</span>
+</div>
+<div id="parent2">
+ 2:
+ <span id="fragment2">Must be green when containing :target</span>
+</div>
+<div id="parent3">
+ 3:
+ <span id="fragment3">Must be green when containing :target</span>
+</div>
+<script>
+ const GREEN = "rgb(0, 128, 0)";
+ const GREY = "rgb(128, 128, 128)";
+ const BLUE = "rgb(0, 0, 255)";
+
+ function fragmentLink(fragment) {
+ return document.querySelector(`a[href="#${fragment}"]`);
+ }
+
+ promise_test(async () => {
+ const fragment = document.querySelector("#fragment");
+ const fragment2 = document.querySelector("#fragment2");
+ const fragment3 = document.querySelector("#fragment3");
+
+ location.hash = "";
+
+ assert_equals(getComputedStyle(parent1).color, GREY, "parent1 should be grey without :target");
+ assert_equals(getComputedStyle(parent2).color, GREY, "parent2 should be grey without :target");
+ assert_equals(getComputedStyle(parent3).color, GREY, "parent3 should be grey without :target");
+
+ fragmentLink("fragment").click();
+
+ assert_true(fragment.matches(":target"));
+ assert_equals(getComputedStyle(parent1).color, GREEN, "parent1 should be green on fragment click");
+ assert_equals(getComputedStyle(parent2).color, GREY, "parent2 should be grey without :target");
+ assert_equals(getComputedStyle(parent3).color, GREY, "parent3 should be grey without :target");
+
+ fragmentLink("fragment2").click();
+
+ assert_true(fragment2.matches(":target"));
+ assert_equals(getComputedStyle(parent1).color, GREY, "parent1 should be grey without :target");
+ assert_equals(getComputedStyle(parent2).color, GREEN, "parent2 should be green on fragment click");
+ assert_equals(getComputedStyle(parent3).color, GREY, "parent3 should be grey without :target");
+
+ fragment2.remove();
+ assert_equals(getComputedStyle(parent2).color, BLUE, "parent2 should be blue after removing only child");
+
+ parent2.append(fragment2);
+
+ // Skip to check parent2 color since there is nothing in the spec mentioning DOM mutations affecting the target element.
+ // - https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-fragid
+
+ fragmentLink("fragment3").click();
+
+ assert_true(fragment3.matches(":target"));
+ assert_equals(getComputedStyle(parent1).color, GREY, "parent1 should be grey without :target");
+ assert_equals(getComputedStyle(parent2).color, GREY, "parent2 should be grey without :target");
+ assert_equals(getComputedStyle(parent3).color, GREEN, "parent3 should be green on fragment click");
+
+ fragmentLink("").click();
+
+ assert_equals(location.hash, "");
+ assert_equals(getComputedStyle(parent1).color, GREY, "parent1 should be grey without :target");
+ assert_equals(getComputedStyle(parent2).color, GREY, "parent2 should be grey without :target");
+ assert_equals(getComputedStyle(parent3).color, GREY, "parent3 should be grey without :target");
+ });
+</script>
diff --git a/testing/web-platform/tests/css/selectors/invalidation/typed-child-indexed-pseudo-classes-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/typed-child-indexed-pseudo-classes-in-has.html
new file mode 100644
index 0000000000..b3a91d67cb
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/typed-child-indexed-pseudo-classes-in-has.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: typed child-indexed pseudo classes in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #container ~ div { color: grey }
+ #container:has(div:only-of-type) ~ #only_of_type { color: red }
+ #container:has(div.orange:first-of-type) ~ #first_of_type { color: orange }
+ #container:has(div.yellow:first-of-type) ~ #first_of_type { color: yellow }
+ #container:has(div.green:first-of-type) ~ #first_of_type { color: green }
+ #container:has(div.orange:last-of-type) ~ #last_of_type { color: orange }
+ #container:has(div.yellow:last-of-type) ~ #last_of_type { color: yellow }
+ #container:has(div.green:last-of-type) ~ #last_of_type { color: green }
+ #container:has(div.orange:nth-of-type(3n+1)) ~ #nth_of_type_3n_1 { color: orange }
+ #container:has(div.yellow:nth-of-type(3n+1)) ~ #nth_of_type_3n_1 { color: yellow }
+ #container:has(div.green:nth-of-type(3n+1)) ~ #nth_of_type_3n_1 { color: green }
+ #container:has(div.orange:nth-of-type(3n+2)) ~ #nth_of_type_3n_2 { color: orange }
+ #container:has(div.yellow:nth-of-type(3n+2)) ~ #nth_of_type_3n_2 { color: yellow }
+ #container:has(div.green:nth-of-type(3n+2)) ~ #nth_of_type_3n_2 { color: green }
+ #container:has(div.orange:nth-of-type(3n)) ~ #nth_of_type_3n { color: orange }
+ #container:has(div.yellow:nth-of-type(3n)) ~ #nth_of_type_3n { color: yellow }
+ #container:has(div.green:nth-of-type(3n)) ~ #nth_of_type_3n { color: green }
+</style>
+<div id="container">
+</div>
+<div id="only_of_type"></div>
+<div id="first_of_type"></div>
+<div id="last_of_type"></div>
+<div id="nth_of_type_3n_1"></div>
+<div id="nth_of_type_3n_2"></div>
+<div id="nth_of_type_3n"></div>
+<script>
+const grey = "rgb(128, 128, 128)";
+const red = "rgb(255, 0, 0)";
+const orange = "rgb(255, 165, 0)";
+const yellow = "rgb(255, 255, 0)";
+const green = "rgb(0, 128, 0)";
+
+function testColors(test_name,
+ only_of_type_color,
+ first_of_type_color,
+ last_of_type_color,
+ nth_of_type_3n_1_color,
+ nth_of_type_3n_2_color,
+ nth_of_type_3n_color) {
+ test(function() {
+ assert_equals(getComputedStyle(only_of_type).color, only_of_type_color);
+ }, test_name + ": #only_of_type");
+ test(function() {
+ assert_equals(getComputedStyle(first_of_type).color, first_of_type_color);
+ }, test_name + ": #first_of_type");
+ test(function() {
+ assert_equals(getComputedStyle(last_of_type).color, last_of_type_color);
+ }, test_name + ": #last_of_type");
+ test(function() {
+ assert_equals(getComputedStyle(nth_of_type_3n_1).color, nth_of_type_3n_1_color);
+ }, test_name + ": #nth_of_type_3n_1");
+ test(function() {
+ assert_equals(getComputedStyle(nth_of_type_3n_2).color, nth_of_type_3n_2_color);
+ }, test_name + ": #nth_of_type_3n_2");
+ test(function() {
+ assert_equals(getComputedStyle(nth_of_type_3n).color, nth_of_type_3n_color);
+ }, test_name + ": #nth_of_type_3n");
+}
+
+testColors("Initial colors", grey, grey, grey, grey, grey, grey);
+
+container.insertBefore(document.createElement("span"), container.firstChild);
+testColors("Prepend span (1)", grey, grey, grey, grey, grey, grey);
+
+let div1 = document.createElement("div");
+div1.id = "div1";
+div1.classList.add("green");
+container.insertBefore(div1, container.firstChild);
+testColors("Prepend #div1.green", red, green, green, green, grey, grey);
+
+container.insertBefore(document.createElement("span"), container.firstChild);
+testColors("Prepend span (2)", red, green, green, green, grey, grey);
+
+let div2 = document.createElement("div");
+div2.id = "div2";
+div2.classList.add("yellow");
+container.insertBefore(div2, container.firstChild);
+testColors("Prepend #div2.yellow", grey, yellow, green, yellow, green, grey);
+
+container.insertBefore(document.createElement("span"), container.firstChild);
+testColors("Prepend span (3)", grey, yellow, green, yellow, green, grey);
+
+let div3 = document.createElement("div");
+div3.id = "div3";
+div3.classList.add("orange");
+container.insertBefore(div3, container.firstChild);
+testColors("Prepend #div3.orange", grey, orange, green, orange, yellow, green);
+
+container.insertBefore(document.createElement("span"), container.firstChild);
+testColors("Prepend span (4)", grey, orange, green, orange, yellow, green);
+
+let div4 = document.createElement("div");
+div4.id = "div4";
+container.insertBefore(div4, container.firstChild);
+testColors("Prepend #div4", grey, grey, green, green, orange, yellow);
+
+container.insertBefore(document.createElement("span"), container.firstChild);
+testColors("Prepend span (5)", grey, grey, green, green, orange, yellow);
+
+let div5 = document.createElement("div");
+div5.id = "div5";
+container.insertBefore(div5, container.firstChild);
+testColors("Prepend #div5", grey, grey, green, yellow, green, orange);
+
+container.insertBefore(document.createElement("span"), container.firstChild);
+testColors("Prepend span (6)", grey, grey, green, yellow, green, orange);
+
+div1.remove();
+testColors("Remove #div1", grey, grey, yellow, yellow, grey, orange);
+
+div2.remove();
+testColors("Remove #div2", grey, grey, orange, grey, grey, orange);
+
+div3.remove();
+testColors("Remove #div3", grey, grey, grey, grey, grey, grey);
+
+div4.remove();
+testColors("Remove #div4", red, grey, grey, grey, grey, grey);
+
+div5.remove();
+testColors("Remove #div5", grey, grey, grey, grey, grey, grey);
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/user-action-pseudo-classes-in-has.html b/testing/web-platform/tests/css/selectors/invalidation/user-action-pseudo-classes-in-has.html
new file mode 100644
index 0000000000..466e8610fd
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/user-action-pseudo-classes-in-has.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>CSS Selectors Invalidation: user-action pseudo classes in :has() argument</title>
+<link rel="author" title="Byungwoo Lee" href="blee@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+ .ancestor:has(.descendant1:hover) { color: blue }
+ .ancestor:has(.descendant1:hover) .other-descendant { color: navy }
+ .ancestor:has(.descendant1:hover:active) { color: skyblue }
+ .ancestor:has(.descendant1:hover:active) .other-descendant { color: lightblue }
+ .ancestor:has(:focus) { color: green }
+ .ancestor:has(:focus) .other-descendant { color: darkgreen }
+ .ancestor:has(.descendant2:focus-visible) { color: yellowgreen }
+ .ancestor:has(.descendant2:focus-visible) .other-descendant { color: greenyellow }
+ .ancestor:has(.descendant3:focus-within) { color: lightgreen }
+ .ancestor:has(.descendant3:focus-within) .other-descendant { color: violet }
+</style>
+<div id=subject1 class=ancestor>
+ <div>
+ <div id=hoverme class=descendant1>Hover and click me</div>
+ <div id=focusme1 tabindex=1>Focus me</div>
+ <div id=focusme2 class=descendant2 tabindex=2>Focus me</div>
+ <div class=descendant3>
+ <div><div id=focusme3 tabindex=3>Focus me</div></div>
+ </div>
+ </div>
+ <div><div id=subject3 class=other-descendant>subject</div></div>
+</div>
+<div id=subject2 class=ancestor>
+ <div id=focusme4 tabindex=4>Focus me</div>
+ <div><div id=subject4 class=other-descendant>subject</div></div>
+</div>
+<script>
+ const tab_key = '\ue004';
+
+ async_test(function(t) {
+ hoverme.addEventListener("mouseover", t.step_func(event => {
+ assert_equals(getComputedStyle(subject1).color, "rgb(0, 0, 255)",
+ "subject1 should be blue");
+ assert_equals(getComputedStyle(subject3).color, "rgb(0, 0, 128)",
+ "subject3 should be navy");
+ }));
+ hoverme.addEventListener("mousedown", t.step_func(event => {
+ assert_equals(getComputedStyle(subject1).color, "rgb(135, 206, 235)",
+ "subject1 should be skyblue");
+ assert_equals(getComputedStyle(subject3).color, "rgb(173, 216, 230)",
+ "subject3 should be lightblue");
+ }));
+ hoverme.addEventListener("mouseup", t.step_func(event => {
+ assert_equals(getComputedStyle(subject1).color, "rgb(0, 0, 255)",
+ "subject1 should be blue again");
+ assert_equals(getComputedStyle(subject3).color, "rgb(0, 0, 128)",
+ "subject3 should be navy again");
+ }));
+ focusme1.addEventListener("focus", t.step_func(function() {
+ assert_equals(getComputedStyle(subject1).color, "rgb(0, 128, 0)",
+ "subject1 should be green");
+ assert_equals(getComputedStyle(subject3).color, "rgb(0, 100, 0)",
+ "subject3 should be darkgreen");
+ test_driver.send_keys(document.body, tab_key);
+ }));
+ focusme2.addEventListener("focus", t.step_func(function() {
+ assert_equals(getComputedStyle(subject1).color, "rgb(154, 205, 50)",
+ "subject1 should be yellowgreen");
+ assert_equals(getComputedStyle(subject3).color, "rgb(173, 255, 47)",
+ "subject3 should be greenyellow");
+ test_driver.send_keys(document.body, tab_key);
+ }));
+ focusme3.addEventListener("focus", t.step_func(function() {
+ assert_equals(getComputedStyle(subject1).color, "rgb(144, 238, 144)",
+ "subject1 should be lightgreen");
+ assert_equals(getComputedStyle(subject3).color, "rgb(238, 130, 238)",
+ "subject3 should be violet");
+
+ focusme3.remove();
+ assert_equals(getComputedStyle(subject1).color, "rgb(0, 0, 0)",
+ "subject1 should be black");
+ assert_equals(getComputedStyle(subject3).color, "rgb(0, 0, 0)",
+ "subject3 should be black");
+
+ test_driver.send_keys(document.body, tab_key);
+ }));
+ focusme4.addEventListener("focus", t.step_func_done(function() {
+ assert_equals(getComputedStyle(subject2).color, "rgb(0, 128, 0)",
+ "subject2 should be green");
+ assert_equals(getComputedStyle(subject4).color, "rgb(0, 100, 0)",
+ "subject4 should be darkgreen");
+
+ focusme4.remove();
+ assert_equals(getComputedStyle(subject2).color, "rgb(0, 0, 0)",
+ "subject2 should be black");
+ assert_equals(getComputedStyle(subject4).color, "rgb(0, 0, 0)",
+ "subject4 should be black");
+ }));
+ }, "Invalidation with :focus, :focus-within, :focus-visible in :has()");
+
+ test(() => {
+ assert_equals(getComputedStyle(subject1).color, "rgb(0, 0, 0)");
+ assert_equals(getComputedStyle(subject2).color, "rgb(0, 0, 0)");
+ }, "ancestor should be black");
+
+ new test_driver.Actions()
+ .pointerMove(0, 0, {origin: hoverme})
+ .pointerDown()
+ .pointerUp()
+ .pointerMove(0, 0, {origin: focusme1})
+ .pointerDown()
+ .pointerUp()
+ .send();
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/selectors/invalidation/where.html b/testing/web-platform/tests/css/selectors/invalidation/where.html
new file mode 100644
index 0000000000..170398300f
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/where.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSS Selectors Invalidation: :where()</title>
+ <link rel="author" title="Victoria Su" href="mailto:victoriaytsu@google.com">
+ <link rel="help" href="https://drafts.csswg.org/selectors-4/#zero-matches">
+ <meta name="assert" content="This tests that the :where() selector is effective">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ * {
+ color: black;
+ }
+ .b {
+ color: yellow;
+ }
+ /*Simple selector arguments */
+ :where(.b, .c) {
+ color: red;
+ }
+ /*Compound selector arguments */
+ .a~:where(.c#d, .e) {
+ color: green;
+ }
+ /* Complex selector arguments */
+ .h {
+ color: red;
+ }
+ :where(.a~.h, .a~.h+.f) {
+ color: yellow;
+ }
+ /* Nested */
+ :where(.a>:where(.g+.h, .b)~.i) {
+ color: blue;
+ }
+ :where(:is(.a~.h)) {
+ color: yellow;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="a1">
+ <div class="g">
+ </div>
+ <div class="h">
+ </div>
+ <div class="i" id="i1">
+ Blue
+ </div>
+ </div>
+ <div class="b" id="b1">
+ Yellow
+ </div>
+ <div class="c" id="c1">
+ Red
+ </div>
+ <div class="c" id="d">
+ Green
+ </div>
+ <div class="h" id="h1">
+ Red
+ </div>
+ <div class="f" id="f1">
+ Yellow
+ </div>
+ <script>
+ document.body.offsetTop;
+
+ var black = "rgb(0, 0, 0)";
+ var green = "rgb(0, 128, 0)";
+ var red = "rgb(255, 0, 0)";
+ var yellow = "rgb(255, 255, 0)";
+ var blue = "rgb(0, 0, 255)";
+
+ test(() => {
+ assert_equals(getComputedStyle(b1).color, yellow);
+ assert_equals(getComputedStyle(c1).color, red);
+ assert_equals(getComputedStyle(d).color, red);
+ assert_equals(getComputedStyle(h1).color, red);
+ assert_equals(getComputedStyle(f1).color, black);
+ assert_equals(getComputedStyle(i1).color, black);
+ }, "Preconditions.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(b1).color, yellow);
+ assert_equals(getComputedStyle(c1).color, red);
+ }, "Invalidate :where() for simple selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(d).color, green);
+ }, "Invalidate :where() for compound selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(h1).color, red);
+ assert_equals(getComputedStyle(f1).color, yellow);
+ }, "Invalidate :where() for complex selector arguments.");
+
+ test(() => {
+ a1.className = "a";
+ assert_equals(getComputedStyle(i1).color, blue);
+ }, "Invalidate nested :where().");
+
+ </script>
+ </body>
+</html> \ No newline at end of file