summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/css/css-cascade
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/css/css-cascade')
-rw-r--r--testing/web-platform/tests/css/css-cascade/META.yml4
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-001-ref.html12
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-001.html48
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-002.html42
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-inherit-color.html23
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-initial-color.html23
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-initial-visited-ref.html3
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-initial-visited.html9
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-initial-xml.html42
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-revert-color.html23
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-revert-layer-noop.html70
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html472
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-revert-noop.html68
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-revert-visited-ref.html3
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-revert-visited.html10
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-unset-color.html23
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-unset-visited-ref.html8
-rw-r--r--testing/web-platform/tests/css/css-cascade/all-prop-unset-visited.html10
-rw-r--r--testing/web-platform/tests/css/css-cascade/at-scope-parsing.html78
-rw-r--r--testing/web-platform/tests/css/css-cascade/idlharness.html40
-rw-r--r--testing/web-platform/tests/css/css-cascade/import-conditional-001.html32
-rw-r--r--testing/web-platform/tests/css/css-cascade/import-conditional-002.html30
-rw-r--r--testing/web-platform/tests/css/css-cascade/import-conditions.html132
-rw-r--r--testing/web-platform/tests/css/css-cascade/import-removal.html18
-rw-r--r--testing/web-platform/tests/css/css-cascade/important-prop-ref.html19
-rw-r--r--testing/web-platform/tests/css/css-cascade/important-prop.html39
-rw-r--r--testing/web-platform/tests/css/css-cascade/important-transition-manual.html25
-rw-r--r--testing/web-platform/tests/css/css-cascade/important-vs-inline-001.html40
-rw-r--r--testing/web-platform/tests/css/css-cascade/important-vs-inline-002.html42
-rw-r--r--testing/web-platform/tests/css/css-cascade/important-vs-inline-003.html27
-rw-r--r--testing/web-platform/tests/css/css-cascade/inherit-initial.html36
-rw-r--r--testing/web-platform/tests/css/css-cascade/initial-background-color.html42
-rw-r--r--testing/web-platform/tests/css/css-cascade/initial-color-background-001-ref.html20
-rw-r--r--testing/web-platform/tests/css/css-cascade/initial-color-background-001.html36
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-basic.html524
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-counter-style-override.html150
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse-at-property.html94
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse.html137
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-font-face-override.html141
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-import.html294
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-important.html107
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-keyframes-override.html138
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-media-query.html169
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-media-toggle.html30
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-property-override.html154
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-replaceSync-clears-stale.html56
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-rules-cssom.html113
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-slotted-rule.html33
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-statement-before-import.html157
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-statement-copy-crash.html10
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-important.html18
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-ref.html10
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing.html20
-rw-r--r--testing/web-platform/tests/css/css-cascade/layer-vs-inline-style.html60
-rw-r--r--testing/web-platform/tests/css/css-cascade/parsing/all-invalid.html24
-rw-r--r--testing/web-platform/tests/css/css-cascade/parsing/all-valid.html20
-rw-r--r--testing/web-platform/tests/css/css-cascade/parsing/layer-import-parsing.html79
-rw-r--r--testing/web-platform/tests/css/css-cascade/parsing/layer.html25
-rw-r--r--testing/web-platform/tests/css/css-cascade/parsing/supports-import-parsing.html78
-rw-r--r--testing/web-platform/tests/css/css-cascade/presentational-hints-cascade.html39
-rw-r--r--testing/web-platform/tests/css/css-cascade/presentational-hints-rollback.html125
-rw-r--r--testing/web-platform/tests/css/css-cascade/reference/all-green.html1
-rw-r--r--testing/web-platform/tests/css/css-cascade/reference/ref-filled-green-100px-square.xht19
-rw-r--r--testing/web-platform/tests/css/css-cascade/reference/ref-green-text.html9
-rw-r--r--testing/web-platform/tests/css/css-cascade/resources/scope.css4
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-001.html26
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-002.html24
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-003.html30
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-004.html28
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-005.html34
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-006.html34
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-007.html40
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-008.html43
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-009.html16
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-010.html22
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-011.html29
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-012.html16
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-013.html28
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-014.html29
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-015-ref.html5
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-layer-015.html14
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-001.html41
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-002.html41
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-003.html29
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-004.html22
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-005.html40
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-006.html28
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-007.html34
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-008.html32
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-009.html30
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-010.html36
-rw-r--r--testing/web-platform/tests/css/css-cascade/revert-val-011.html35
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-container.html44
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-cssom.html73
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-declaration-list-crash.html13
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-deep.html52
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-evaluation.html547
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-focus.html106
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-hover.html113
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-implicit-external.html30
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-implicit.html199
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-invalidation.html782
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-layer.html51
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-media.html38
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-name-defining-rules.html112
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-nesting.html545
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-proximity.html123
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-shadow.tentative.html163
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-specificity.html77
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-starting-style.html43
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-supports.html38
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-visited-cssom.html314
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-visited-ref.html96
-rw-r--r--testing/web-platform/tests/css/css-cascade/scope-visited.html215
-rw-r--r--testing/web-platform/tests/css/css-cascade/support/test-green.css4
-rw-r--r--testing/web-platform/tests/css/css-cascade/support/test-red.css4
-rw-r--r--testing/web-platform/tests/css/css-cascade/tons-of-declarations-crash.html6
-rw-r--r--testing/web-platform/tests/css/css-cascade/unset-val-001.html45
-rw-r--r--testing/web-platform/tests/css/css-cascade/unset-val-002.html35
-rw-r--r--testing/web-platform/tests/css/css-cascade/unset-value-storage.html24
120 files changed, 9065 insertions, 0 deletions
diff --git a/testing/web-platform/tests/css/css-cascade/META.yml b/testing/web-platform/tests/css/css-cascade/META.yml
new file mode 100644
index 0000000000..96467729fe
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/META.yml
@@ -0,0 +1,4 @@
+spec: https://drafts.csswg.org/css-cascade/
+suggested_reviewers:
+ - fantasai
+ - tabatkins
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-001-ref.html b/testing/web-platform/tests/css/css-cascade/all-prop-001-ref.html
new file mode 100644
index 0000000000..e255585783
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-001-ref.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: all shorthand Reference File</title>
+</head>
+<body>
+ <p>Test passes if the digits are <strong>in order</strong> and there is <strong>no red</strong>.</p>
+
+ <div class="test">123 456 789</div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-001.html b/testing/web-platform/tests/css/css-cascade/all-prop-001.html
new file mode 100644
index 0000000000..9c07dfe1b8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-001.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: "all" shorthand property excludes "direction" and "unicode-bidi"</title>
+ <link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-3/#all-shorthand">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-4/#all-shorthand">
+ <link rel="match" href="all-prop-001-ref.html">
+ <meta name="assert" content="Test passes if 'all' resets properties other than 'direction' and 'unicode-bidi'.">
+ <style>
+.test {
+ /* these must not be overridden */
+ direction: rtl;
+ unicode-bidi: bidi-override;
+}
+
+.test, bdo {
+ /* all of these must be overridden */
+ border: solid red;
+ background: red;
+ color: red;
+ text-decoration: line-through;
+ font: bold italic small-caps 20px monospace;
+ outline: solid red;
+ float: left;
+ letter-spacing: 1em;
+ display: list-item;
+ text-align: center;
+ width: 0.5em;
+ margin: 10em;
+ overflow: scroll;
+}
+
+.test, bdo {
+ all: initial;
+ /* if incorrectly implemented, this causes direction: initial; unicode-bidi: initial;
+ which is the same as direction:ltr; unicode-bidi: normal */
+}
+ </style>
+</head>
+<body>
+ <p>Test passes if the digits are <strong>in order</strong> and there is <strong>no red</strong>.</p>
+
+ <div class="test"><bdo dir=rtl>987 <span>654</span></bdo> 321</div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-002.html b/testing/web-platform/tests/css/css-cascade/all-prop-002.html
new file mode 100644
index 0000000000..1e5b450ce1
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-002.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: all:inherit includes display:inherit</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-3/#all-shorthand">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-4/#all-shorthand">
+ <link rel="match" href="reference/ref-filled-green-100px-square.xht">
+ <meta name="assert" content="all:inherit should cause display:inherit so the red divs will display inline/horizontal (like their parent span) rather than block/vertical (like the default UA style for a div).">
+ <style>
+.inline {
+ all: inherit;/* inherit display:inline from span */
+}
+.half-red-sq {
+ /* half of a red square */
+ display: inline-block;
+ width: 50px;
+ height: 100px;
+ background-color: red;
+}
+#positioned {
+ position: relative;
+}
+#green-sq {
+ position: absolute;/* put higher on Z axis */
+ width: 100px;
+ height: 100px;
+ background-color: green;
+}
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+ <div id="positioned">
+ <div id="green-sq"></div>
+ <span>
+ <div class="inline"><div class="half-red-sq"></div></div><div class="inline"><div class="half-red-sq"></div></div>
+ </span>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-inherit-color.html b/testing/web-platform/tests/css/css-cascade/all-prop-inherit-color.html
new file mode 100644
index 0000000000..6cd8425cb9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-inherit-color.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: "color" property preceded by "all: initial"</title>
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#all-shorthand">
+ <link rel="match" href="reference/ref-green-text.html">
+ <meta name="assert" content="Own 'color', preceded by 'all: inherit', overrides inherited 'color'.">
+ <style>
+ .outer {
+ color: red;
+ }
+
+ .inner {
+ all: inherit;
+ color: green;
+ }
+ </style>
+</head>
+<body>
+ <p class="outer"><span class="inner">Test passes if this text is green.</span></p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-initial-color.html b/testing/web-platform/tests/css/css-cascade/all-prop-initial-color.html
new file mode 100644
index 0000000000..83a78bd086
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-initial-color.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: "color" property preceded by "all: initial"</title>
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#all-shorthand">
+ <link rel="match" href="reference/ref-green-text.html">
+ <meta name="assert" content="Own 'color', preceded by 'all: initial', overrides inherited 'color'.">
+ <style>
+ .outer {
+ color: red;
+ }
+
+ .inner {
+ all: initial;
+ color: green;
+ }
+ </style>
+</head>
+<body>
+ <p class="outer"><span class="inner">Test passes if this text is green.</span></p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-initial-visited-ref.html b/testing/web-platform/tests/css/css-cascade/all-prop-initial-visited-ref.html
new file mode 100644
index 0000000000..f596b559b0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-initial-visited-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>CSS Test: Reference</title>
+<span style="color:green">This text must be green.</a>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-initial-visited.html b/testing/web-platform/tests/css/css-cascade/all-prop-initial-visited.html
new file mode 100644
index 0000000000..6fb7936652
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-initial-visited.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<title>CSS Cascade: Apply all:initial to a visited link overriding with a color</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#initial">
+<link rel="match" href="all-prop-initial-visited-ref.html">
+<style>
+ a { all: initial }
+ a:visited { color: green }
+</style>
+<a href="">This text must be green.</a>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-initial-xml.html b/testing/web-platform/tests/css/css-cascade/all-prop-initial-xml.html
new file mode 100644
index 0000000000..730ccac25f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-initial-xml.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<title>all: initial on unknown XML tree</title>
+<link rel=help href=https://www.w3.org/TR/css-cascade-3/#all-shorthand>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+const iframe = document.createElement("iframe");
+const setup_test = async_test("setup");
+iframe.onload = setup_test.step_func_done(function() {
+ const root = iframe.contentDocument.documentElement;
+ // we need the empty stylesheet to avoid default XSLT views of the XML
+ const style = iframe.contentDocument.createElementNS("http://www.w3.org/1999/xhtml", "style");
+ root.appendChild(style);
+
+ // Grab initial styles from a random element, as the root can get non-initial UA styling.
+ const div = iframe.contentDocument.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ root.appendChild(div);
+ // the document element should have unicode-bidi 'normal', while <div> has 'isolate':
+ div.style.unicodeBidi = 'normal';
+ const cs = iframe.contentWindow.getComputedStyle(div);
+
+ let actual_initial = Object.create(null);
+ for (let i = 0; i < cs.length; i++) {
+ let prop_name = cs[i];
+ actual_initial[prop_name] = cs[prop_name];
+ }
+ const rootCS = iframe.contentWindow.getComputedStyle(root);
+ test(() => {
+ style.textContent = ":root { color: blue }";
+ assert_equals(rootCS["color"], "rgb(0, 0, 255)");
+ }, "stylesheet takes effect");
+ style.textContent = ":root { all: initial; direction: initial; unicode-bidi: initial; } style { display: none; }";
+ for (let prop_name in actual_initial) {
+ test(() => {
+ assert_equals(rootCS[prop_name], actual_initial[prop_name]);
+ }, prop_name);
+ }
+});
+iframe.src = URL.createObjectURL(new Blob(["<foo/>"], { type: "application/xml" }));
+document.body.appendChild(iframe);
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-revert-color.html b/testing/web-platform/tests/css/css-cascade/all-prop-revert-color.html
new file mode 100644
index 0000000000..786bd08109
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-revert-color.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: "color" property preceded by "all: revert"</title>
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#all-shorthand">
+ <link rel="match" href="reference/ref-green-text.html">
+ <meta name="assert" content="Own 'color', preceded by 'all: revert', overrides inherited 'color'.">
+ <style>
+ .outer {
+ color: red;
+ }
+
+ .inner {
+ all: revert;
+ color: green;
+ }
+ </style>
+</head>
+<body>
+ <p class="outer"><span class="inner">Test passes if this text is green.</span></p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer-noop.html b/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer-noop.html
new file mode 100644
index 0000000000..5c31c5803c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer-noop.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Cascade: "all: revert-layer"</title>
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#revert-layer">
+<meta name="assert" content="Checks that adding 'all: revert-layer' inside @layer has no effect on elements with no other author rules.">
+<!-- Split into chunks to avoid timeouts. -->
+<meta name="variant" content="?include=0">
+<meta name="variant" content="?include=1">
+<meta name="variant" content="?include=2">
+<meta name="variant" content="?include=3">
+<meta name="variant" content="?include=4">
+<meta name="variant" content="?include=5">
+<meta name="variant" content="?include=6">
+<meta name="variant" content="?include=7">
+<script>
+ const CHUNKS = 8;
+</script>
+
+<style>
+@layer {
+ .revert-all {
+ all: revert-layer;
+ }
+}
+</style>
+
+<div id="log"></div>
+<div id="wrapper"></div>
+
+<script src="/common/subset-tests-by-key.js"></script>
+<script src="/html/resources/common.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+function cloneStyle(style) {
+ const clone = Object.create(null);
+ for (let property of style) {
+ clone[property] = style.getPropertyValue(property);
+ }
+ return clone;
+}
+
+function assertSameClones(clone1, clone2, callback) {
+ for (let property in clone1) {
+ const value1 = clone1[property];
+ const value2 = clone2[property];
+ // assert_equals is slow, so only call it if it's going to fail.
+ if (value1 !== value2) {
+ assert_equals(value1, value2, property);
+ }
+ }
+}
+
+const wrapper = document.getElementById("wrapper");
+const elementNames = [...HTML5_ELEMENTS, "math", "svg", "z-custom"].sort();
+for (let i = 0; i < elementNames.length; ++i) {
+ let elementName = elementNames[i];
+ let chunk = i % CHUNKS;
+ subsetTestByKey(chunk.toString(), test, function() {
+ const element = document.createElement(elementName);
+ wrapper.appendChild(element);
+ const style = getComputedStyle(element);
+ const clonedBaseStyle = cloneStyle(style);
+ element.classList.add("revert-all");
+ const clonedRevertedStyle = cloneStyle(style);
+ assertSameClones(clonedRevertedStyle, clonedBaseStyle);
+ }, elementName);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html b/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html
new file mode 100644
index 0000000000..868267b285
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-revert-layer.html
@@ -0,0 +1,472 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Cascade: "all: revert-layer"</title>
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#revert-layer">
+<meta name="assert" content="Checks that adding 'all: revert-layer' on the last layer has no effect.">
+<style>
+/* Set properties to a value different than the initial one. */
+#nothing {
+ accent-color: #123;
+ align-content: baseline;
+ align-items: baseline;
+ align-self: baseline;
+ alignment-baseline: central;
+ alt: "a";
+ animation-composition: add;
+ animation-delay: 123s;
+ animation-delay-start: 123s;
+ animation-delay-end: 456s;
+ animation-direction: reverse;
+ animation-duration: 123s;
+ animation-fill-mode: both;
+ animation-iteration-count: 123;
+ animation-name: \.;
+ animation-play-state: paused;
+ animation-range: 10% 20%;
+ animation-timeline: none;
+ animation-timing-function: linear;
+ app-region: drag;
+ appearance: auto;
+ aspect-ratio: 3 / 4;
+ backdrop-filter: invert(1);
+ backface-visibility: hidden;
+ background-attachment: fixed;
+ background-blend-mode: overlay;
+ background-clip: content-box;
+ background-color: #123;
+ background-image: url("#ref");
+ background-origin: border-box;
+ background-position: 123px;
+ background-repeat: round;
+ background-size: 123px;
+ baseline-shift: 123px;
+ baseline-source: first;
+ block-size: 123px;
+ border-block-end: 123px dashed #123;
+ border-block-start: 123px dashed #123;
+ border-bottom: 123px dashed #123;
+ border-collapse: collapse;
+ border-end-end-radius: 123px;
+ border-end-start-radius: 123px;
+ border-image-outset: 123;
+ border-image-repeat: round;
+ border-image-slice: 123;
+ border-image-source: url("#ref");
+ border-image-width: 123px;
+ border-inline-end: 123px dashed #123;
+ border-inline-start: 123px dashed #123;
+ border-left: 123px dashed #123;
+ border-radius: 123px;
+ border-right: 123px dashed #123;
+ border-start-end-radius: 123px;
+ border-start-start-radius: 123px;
+ border-spacing: 123px;
+ border-top: 123px dashed #123;
+ bottom: 123px;
+ box-decoration-break: clone;
+ box-shadow: #123 123px 123px 123px 123px;
+ box-sizing: border-box;
+ break-after: avoid;
+ break-before: avoid;
+ break-inside: avoid;
+ buffered-rendering: static;
+ caption-side: bottom;
+ caret-color: #123;
+ clear: both;
+ clip: rect(123px, 123px, 123px, 123px);
+ clip-path: url("#ref");
+ clip-rule: evenodd;
+ color: #123;
+ color-interpolation: auto;
+ color-interpolation-filters: auto;
+ color-rendering: optimizespeed;
+ color-scheme: dark;
+ column-count: 123;
+ column-fill: auto;
+ column-gap: 123px;
+ column-rule-color: #123;
+ column-rule-style: dashed;
+ column-rule-width: 123px;
+ column-span: all;
+ column-width: 123px;
+ contain: size;
+ contain-intrinsic-block-size: 123px;
+ contain-intrinsic-inline-size: 123px;
+ contain-intrinsic-size: 123px 123px;
+ container-name: foo;
+ container-type: size;
+ content: "b";
+ content-visibility: auto;
+ counter-increment: add 123;
+ counter-reset: add 123;
+ counter-set: add 123;
+ cursor: none;
+ cx: 123px;
+ cy: 123px;
+ d: path("M 1 1");
+ direction: rtl;
+ display: flow-root;
+ dominant-baseline: middle;
+ empty-cells: hide;
+ fill: #123;
+ fill-opacity: 0.123;
+ fill-rule: evenodd;
+ filter: url("#ref");
+ flex-basis: 123px;
+ flex-direction: column;
+ flex-grow: 123;
+ flex-shrink: 123;
+ flex-wrap: wrap;
+ float: right;
+ flood-color: #123;
+ flood-opacity: 0.123;
+ font-family: "c";
+ font-feature-settings: "smcp";
+ font-kerning: none;
+ font-language-override: "d";
+ font-optical-sizing: none;
+ font-palette: dark;
+ font-size: 123px;
+ font-size-adjust: 123;
+ font-stretch: 123%;
+ font-style: italic;
+ font-synthesis: none;
+ font-variant-alternates: historical-forms;
+ font-variant-caps: small-caps;
+ font-variant-east-asian: full-width;
+ font-variant-emoji: emoji;
+ font-variant-ligatures: none;
+ font-variant-numeric: tabular-nums;
+ font-variant-position: super;
+ font-variation-settings: "smcp" 1;
+ font-weight: 123;
+ forced-color-adjust: none;
+ glyph-orientation-horizontal: 123deg;
+ glyph-orientation-vertical: 123deg;
+ grid-auto-columns: 123px;
+ grid-auto-flow: column;
+ grid-auto-rows: 123px;
+ grid-column-end: 123;
+ grid-column-start: 123;
+ grid-row-end: 123;
+ grid-row-start: 123;
+ grid-template-areas: ".";
+ grid-template-columns: 123fr;
+ grid-template-rows: 123fr;
+ hanging-punctuation: first;
+ height: 123px;
+ hyphenate-character: "e";
+ hyphenate-limit-chars: 5;
+ hyphens: auto;
+ image-orientation: none;
+ image-rendering: pixelated;
+ ime-mode: normal;
+ initial-letter: 123;
+ inline-size: 123px;
+ input-security: none;
+ inset-block-end: 123px;
+ inset-block-start: 123px;
+ inset-inline-end: 123px;
+ inset-inline-start: 123px;
+ isolation: isolate;
+ justify-content: center;
+ justify-items: baseline;
+ justify-self: baseline;
+ kerning: 123px;
+ left: 123px;
+ letter-spacing: 123px;
+ lighting-color: #123;
+ line-break: anywhere;
+ line-height: 123px;
+ line-height-step: 123px;
+ list-style-image: url("#ref");
+ list-style-position: inside;
+ list-style-type: square;
+ margin-block-end: 123px;
+ margin-block-start: 123px;
+ margin-bottom: 123px;
+ margin-inline-end: 123px;
+ margin-inline-start: 123px;
+ margin-left: 123px;
+ margin-right: 123px;
+ margin-top: 123px;
+ margin-trim: block;
+ marker-end: url("#ref");
+ marker-mid: url("#ref");
+ marker-start: url("#ref");
+ mask-clip: content-box;
+ mask-composite: exclude;
+ mask-image: url("#ref");
+ mask-mode: alpha;
+ mask-origin: content-box;
+ mask-position-x: 123px;
+ mask-position-y: 123px;
+ mask-repeat: round;
+ mask-size: 123px;
+ mask-type: alpha;
+ masonry-auto-flow: ordered;
+ math-depth: 123;
+ math-shift: compact;
+ math-style: compact;
+ max-block-size: 123px;
+ max-height: 123px;
+ max-inline-size: 123px;
+ max-width: 123px;
+ min-block-size: 123px;
+ min-height: 123px;
+ min-inline-size: 123px;
+ min-width: 123px;
+ mix-blend-mode: overlay;
+ object-fit: contain;
+ object-overflow: visible;
+ object-position: 123px 123%;
+ object-view-box: inset(123px);
+ offset-anchor: 123px 123%;
+ offset-distance: 123px;
+ offset-path: path("M 1 1");
+ offset-position: 123px;
+ offset-rotate: 123deg;
+ opacity: 0.123;
+ order: 123;
+ orphans: 123;
+ outline-color: #123;
+ outline-offset: 123px;
+ outline-style: auto;
+ outline-width: 123px;
+ overflow-anchor: none;
+ overflow-block: auto;
+ overflow-clip-margin: 123px;
+ overflow-inline: hidden;
+ overflow-wrap: anywhere;
+ overflow-x: auto;
+ overflow-y: hidden;
+ overscroll-behavior-block: contain;
+ overscroll-behavior-inline: contain;
+ overscroll-behavior-x: contain;
+ overscroll-behavior-y: contain;
+ padding-block-end: 123px;
+ padding-block-start: 123px;
+ padding-bottom: 123px;
+ padding-inline-end: 123px;
+ padding-inline-start: 123px;
+ padding-left: 123px;
+ padding-right: 123px;
+ padding-top: 123px;
+ page: page;
+ paint-order: fill;
+ perspective: 123px;
+ perspective-origin: 123px 123%;
+ pointer-events: all;
+ position: relative;
+ print-color-adjust: exact;
+ quotes: none;
+ r: 123px;
+ resize: both;
+ right: 123px;
+ rotate: 123deg;
+ row-gap: 123px;
+ ruby-align: center;
+ ruby-position: under;
+ rx: 123px;
+ ry: 123px;
+ scale: 123;
+ scroll-behavior: smooth;
+ scroll-margin-block-end: 123px;
+ scroll-margin-block-start: 123px;
+ scroll-margin-bottom: 123px;
+ scroll-margin-inline-end: 123px;
+ scroll-margin-inline-start: 123px;
+ scroll-margin-left: 123px;
+ scroll-margin-right: 123px;
+ scroll-margin-top: 123px;
+ scroll-padding-block-end: 123px;
+ scroll-padding-block-start: 123px;
+ scroll-padding-bottom: 123px;
+ scroll-padding-inline-end: 123px;
+ scroll-padding-inline-start: 123px;
+ scroll-padding-left: 123px;
+ scroll-padding-right: 123px;
+ scroll-padding-top: 123px;
+ scroll-snap-align: center;
+ scroll-snap-stop: always;
+ scroll-snap-type: both;
+ scroll-timeline: --foo inline;
+ scrollbar-color: #123 #123;
+ scrollbar-gutter: stable;
+ scrollbar-width: none;
+ shape-image-threshold: 123;
+ shape-margin: 123px;
+ shape-outside: border-box;
+ shape-rendering: optimizespeed;
+ speak: spell-out;
+ speak-as: spell-out;
+ stop-color: #123;
+ stop-opacity: 0.123;
+ stroke: #123;
+ stroke-color: #123;
+ stroke-dasharray: 123px;
+ stroke-dashoffset: 123px;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-miterlimit: 123;
+ stroke-opacity: 0.123;
+ stroke-width: 123px;
+ tab-size: 123;
+ table-layout: fixed;
+ text-align: center;
+ text-align-last: center;
+ text-anchor: middle;
+ text-combine-upright: all;
+ text-decoration-color: #123;
+ text-decoration-line: underline;
+ text-decoration-skip-ink: none;
+ text-decoration-style: dashed;
+ text-decoration-thickness: 123px;
+ text-emphasis-color: #123;
+ text-emphasis-position: under right;
+ text-emphasis-style: dot;
+ text-indent: 123px;
+ text-justify: none;
+ text-orientation: sideways;
+ text-overflow: ellipsis;
+ text-rendering: optimizespeed;
+ text-shadow: #123 123px 123px 123px;
+ text-size-adjust: none;
+ text-transform: lowercase;
+ text-underline-offset: 123px;
+ text-underline-position: under;
+ text-wrap-style: balance;
+ timeline-scope: --foo;
+ top: 123px;
+ touch-action: none;
+ transform: scale(-1);
+ transform-box: fill-box;
+ transform-origin: 123px 123px 123px;
+ transform-style: preserve-3d;
+ transition-behavior: allow-discrete;
+ transition-delay: 123s;
+ transition-duration: 123s;
+ transition-property: add;
+ transition-timing-function: linear;
+ translate: 123px;
+ unicode-bidi: plaintext;
+ user-select: all;
+ vector-effect: non-scaling-stroke;
+ vertical-align: 123px;
+ view-timeline: --foo inline 10px;
+ view-transition-name: --foo;
+ visibility: collapse;
+ white-space: pre;
+ white-space-trim: discard-inner;
+ widows: 123;
+ width: 123px;
+ will-change: height;
+ word-break: break-word;
+ word-spacing: 123px;
+ word-wrap: break-word;
+ writing-mode: vertical-lr;
+ x: 123px;
+ y: 123px;
+ z-index: 123;
+ zoom: 123;
+}
+
+@layer layer1 {
+ /* Reset properties to their initial value */
+ #target {
+ all: initial;
+ }
+}
+
+@layer layer2 {
+ /* This will be populated with properties set to a non-initial value */
+ #target {}
+}
+
+@layer layer3 {
+ /* This should roll back to the values from the previous layer */
+ #target.rollback {
+ all: revert-layer;
+ }
+}
+</style>
+
+<div id="log"></div>
+
+<!-- This custom element is unlikely to get important UA styles -->
+<foo-bar id="target"></foo-bar>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const { sheet } = document.querySelector("style");
+const nonInitialStyle = sheet.cssRules[0].style;
+const layer2Style = sheet.cssRules[2].cssRules[0].style;
+
+const target = document.getElementById("target");
+const cs = getComputedStyle(target);
+
+// Some properties can be forced to compute to their initial value
+// unless another property is set to a certain value.
+function prerequisites(property) {
+ switch (property) {
+ case "border-block-end-width":
+ case "border-block-start-width":
+ case "border-bottom-width":
+ case "border-inline-end-width":
+ case "border-inline-start-width":
+ case "border-left-width":
+ case "border-right-width":
+ case "border-top-width":
+ return "border-style: solid";
+ case "column-rule-width":
+ return "column-rule-style: solid";
+ case "outline-width":
+ return "outline-style: solid";
+ case "rotate":
+ case "scale":
+ case "transform":
+ case "transform-style":
+ case "translate":
+ return "display: block";
+ default:
+ return "";
+ }
+}
+
+const initialValues = Object.create(null);
+for (let property of cs) {
+ if (!property.startsWith("-")) {
+ initialValues[property] = cs.getPropertyValue(property);
+ }
+}
+
+for (let property in initialValues) {
+ // Skip property if the stylesheet above doesn't provide a non-initial value.
+ // This is to avoid having to update the test every time a new CSS property is added.
+ const nonInitialValue = nonInitialStyle.getPropertyValue(property);
+ if (nonInitialValue === "") {
+ continue;
+ }
+
+ test(function() {
+ const initialValue = initialValues[property];
+ assert_not_equals(initialValue, "", "Should have the initial value.");
+
+ this.add_cleanup(() => {
+ layer2Style.cssText = "";
+ target.classList.remove("rollback");
+ });
+
+ layer2Style.cssText = prerequisites(property);
+ layer2Style.setProperty(property, nonInitialValue);
+ const changedValue = cs.getPropertyValue(property);
+ assert_not_equals(changedValue, initialValue, "Should get a different computed value.");
+
+ target.classList.add("rollback");
+ const revertedValue = cs.getPropertyValue(property);
+ assert_equals(revertedValue, changedValue, "Layer 3 should rollback to layer 2.");
+ }, property);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-revert-noop.html b/testing/web-platform/tests/css/css-cascade/all-prop-revert-noop.html
new file mode 100644
index 0000000000..e8f560d685
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-revert-noop.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Cascade: "all: revert"</title>
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-4/#default">
+<meta name="assert" content="Checks that adding 'all: revert' has no effect on elements with no other author rules.">
+<!-- Split into chunks to avoid timeouts. -->
+<meta name="variant" content="?include=0">
+<meta name="variant" content="?include=1">
+<meta name="variant" content="?include=2">
+<meta name="variant" content="?include=3">
+<meta name="variant" content="?include=4">
+<meta name="variant" content="?include=5">
+<meta name="variant" content="?include=6">
+<meta name="variant" content="?include=7">
+<script>
+ const CHUNKS = 8;
+</script>
+
+<style>
+.revert-all {
+ all: revert;
+}
+</style>
+
+<div id="log"></div>
+<div id="wrapper"></div>
+
+<script src="/common/subset-tests-by-key.js"></script>
+<script src="/html/resources/common.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+function cloneStyle(style) {
+ const clone = Object.create(null);
+ for (let property of style) {
+ clone[property] = style.getPropertyValue(property);
+ }
+ return clone;
+}
+
+function assertSameClones(clone1, clone2, callback) {
+ for (let property in clone1) {
+ const value1 = clone1[property];
+ const value2 = clone2[property];
+ // assert_equals is slow, so only call it if it's going to fail.
+ if (value1 !== value2) {
+ assert_equals(value1, value2, property);
+ }
+ }
+}
+
+const wrapper = document.getElementById("wrapper");
+const elementNames = [...HTML5_ELEMENTS, "math", "svg", "z-custom"].sort();
+for (let i = 0; i < elementNames.length; ++i) {
+ let elementName = elementNames[i];
+ let chunk = i % CHUNKS;
+ subsetTestByKey(chunk.toString(), test, function() {
+ const element = document.createElement(elementName);
+ wrapper.appendChild(element);
+ const style = getComputedStyle(element);
+ const clonedBaseStyle = cloneStyle(style);
+ element.classList.add("revert-all");
+ const clonedRevertedStyle = cloneStyle(style);
+ assertSameClones(clonedRevertedStyle, clonedBaseStyle);
+ }, elementName);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-revert-visited-ref.html b/testing/web-platform/tests/css/css-cascade/all-prop-revert-visited-ref.html
new file mode 100644
index 0000000000..0ef326c272
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-revert-visited-ref.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>CSS Cascade: all:revert in :visited</title>
+<a href="">Test passes if this text has UA style for visited links</a> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-revert-visited.html b/testing/web-platform/tests/css/css-cascade/all-prop-revert-visited.html
new file mode 100644
index 0000000000..9df1277aca
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-revert-visited.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>CSS Cascade: all:revert in :visited</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#all-shorthand">
+<link rel="match" href="all-prop-revert-visited-ref.html">
+<style>
+ :root { color: red; }
+ a:visited { color: red; }
+ a:visited { all: revert; }
+</style>
+<a href="">Test passes if this text has UA style for visited links</a>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-unset-color.html b/testing/web-platform/tests/css/css-cascade/all-prop-unset-color.html
new file mode 100644
index 0000000000..457901f841
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-unset-color.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: "color" property preceded by "all: unset"</title>
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#all-shorthand">
+ <link rel="match" href="reference/ref-green-text.html">
+ <meta name="assert" content="Own 'color', preceded by 'all: unset', overrides inherited 'color'.">
+ <style>
+ .outer {
+ color: red;
+ }
+
+ .inner {
+ all: unset;
+ color: green;
+ }
+ </style>
+</head>
+<body>
+ <p class="outer"><span class="inner">Test passes if this text is green.</span></p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-unset-visited-ref.html b/testing/web-platform/tests/css/css-cascade/all-prop-unset-visited-ref.html
new file mode 100644
index 0000000000..e67b972768
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-unset-visited-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<title>CSS Cascade: all:unset in :visited</title>
+<style>
+ :root { color: green; }
+ a:visited { color: red; }
+ a:visited { color: unset; }
+</style>
+<a href="">Test passes if this text is green.</a>
diff --git a/testing/web-platform/tests/css/css-cascade/all-prop-unset-visited.html b/testing/web-platform/tests/css/css-cascade/all-prop-unset-visited.html
new file mode 100644
index 0000000000..598d3f5edc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/all-prop-unset-visited.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>CSS Cascade: all:unset in :visited</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#all-shorthand">
+<link rel="match" href="all-prop-unset-visited-ref.html">
+<style>
+ :root { color: green; }
+ a:visited { color: red; }
+ a:visited { all: unset; }
+</style>
+<a href="">Test passes if this text is green.</a>
diff --git a/testing/web-platform/tests/css/css-cascade/at-scope-parsing.html b/testing/web-platform/tests/css/css-cascade/at-scope-parsing.html
new file mode 100644
index 0000000000..88e28fe4ff
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/at-scope-parsing.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<title>@scope: parsing</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<main id=main></main>
+<script>
+ function test_valid(actual, expected) {
+ if (expected === undefined)
+ expected = actual;
+ test(t => {
+ t.add_cleanup(() => main.replaceChildren());
+ let style = document.createElement('style');
+ style.textContent = `${actual}{}`;
+ main.append(style);
+ assert_equals(style.sheet.rules.length, 1);
+ let rule = style.sheet.rules[0];
+ assert_equals(rule.cssText, `${expected} {\n}`);
+ }, `${actual} is valid`);
+ }
+
+ function test_invalid(actual) {
+ test(t => {
+ t.add_cleanup(() => main.replaceChildren());
+ let style = document.createElement('style');
+ style.textContent = `${actual}{}`;
+ main.append(style);
+ assert_equals(style.sheet.rules.length, 0);
+ }, `${actual} is not valid`);
+ }
+
+ test_valid('@scope (.a)');
+ test_valid('@scope (.a + .b)');
+ test_valid('@scope (.a:hover)');
+ test_valid('@scope (.a:hover, #b, div)');
+ test_valid('@scope (:is(div, span))');
+
+ test_valid('@scope (.a) to (.b)');
+ test_valid('@scope (.a)to (.b)', '@scope (.a) to (.b)');
+ test_valid('@scope (.a) to (.b:hover, #c, div)');
+ test_valid('@scope');
+ test_valid('@scope to (.a)');
+ test_valid('@scope (.a) to (&)');
+ test_valid('@scope (.a) to (& > &)');
+ test_valid('@scope (.a) to (> .b)');
+ test_valid('@scope (.a) to (+ .b)');
+ test_valid('@scope (.a) to (~ .b)');
+ test_valid('@scope ()', '@scope');
+ test_valid('@scope to ()', '@scope');
+ test_valid('@scope () to ()', '@scope');
+
+ // Forgiving behavior (keep invalid selector as-is for the serialization):
+ test_valid('@scope (.c <> .d)');
+ test_valid('@scope (.a, .c <> .d)');
+ test_valid('@scope (.a <> .b, .c)');
+ test_valid('@scope (div::before)');
+ test_valid('@scope (div::after)');
+ test_valid('@scope (slotted(div))');
+ test_valid('@scope (.a) to (div::before)');
+ test_valid('@scope (> &) to (>>)');
+
+ test_invalid('@scope div');
+ test_invalid('@scope (.a) unknown (.c)');
+ test_invalid('@scope (.a) to unknown (.c)');
+ test_invalid('@scope (.a) 1px (.c)');
+ test_invalid('@scope (.a) to unknown(c)');
+ test_invalid('@scope unknown(.a)');
+ test_invalid('@scope 1px');
+ test_invalid('@scope creep');
+ test_invalid('@scope )))');
+ test_invalid('@scope (');
+ test_invalid('@scope ( {}');
+ test_invalid('@scope to');
+ test_invalid('@scope }');
+ test_invalid('@scope (.a');
+ test_invalid('@scope (.a to (.b)');
+ test_invalid('@scope ( to (.b)');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/idlharness.html b/testing/web-platform/tests/css/css-cascade/idlharness.html
new file mode 100644
index 0000000000..4f89f73aa7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/idlharness.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<title>CSS Cascade IDL tests</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layer-apis">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+
+<style>
+@layer bar, baz;
+@import url('data:text/css,') layer(qux);
+@layer foo { }
+@scope (div) to (span) { }
+</style>
+
+<script>
+ 'use strict';
+ idl_test(
+ ['css-cascade', 'css-cascade-6'],
+ ['cssom'],
+ idl_array => {
+ try {
+ self.statement = document.styleSheets[0].cssRules.item(0);
+ self.layeredImport = document.styleSheets[0].cssRules.item(1);
+ self.block = document.styleSheets[0].cssRules.item(2);
+ self.scope = document.styleSheets[0].cssRules.item(3);
+ } catch (e) {
+ // Will be surfaced when any rule is undefined below.
+ }
+
+ idl_array.add_objects({
+ CSSLayerBlockRule: ['block'],
+ CSSLayerStatementRule: ['statement'],
+ CSSImportRule: ['layeredImport'],
+ CSSScopeRule: ['scope'],
+ });
+ }
+ );
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/import-conditional-001.html b/testing/web-platform/tests/css/css-cascade/import-conditional-001.html
new file mode 100644
index 0000000000..a841f26545
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/import-conditional-001.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: @import with basic media query</title>
+ <link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-3/#conditional-import">
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#conditional-import">
+ <link rel="help" href="https://www.w3.org/TR/css3-mediaqueries/#syntax">
+ <link rel="match" href="reference/ref-filled-green-100px-square.xht">
+ <meta name="assert" content="Test passes on visual UAs if @import can be combined with a media query.">
+ <style>
+ @import "support/test-red.css";
+ @import "support/test-green.css"
+ (min-width: 1px) and /* assuming screen < 1km */ (max-width: 40000in), nonsense;
+ @import "support/test-red.css"
+ (max-width: 1px), nonsense;
+ div {
+ box-sizing: border-box;
+ width: 100px;
+ height: 100px;
+ padding: 5px; /* Avoids text antialiasing issues */
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+
+ <div class="test">FAIL</div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/import-conditional-002.html b/testing/web-platform/tests/css/css-cascade/import-conditional-002.html
new file mode 100644
index 0000000000..79e850a742
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/import-conditional-002.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: @import with basic supports condition</title>
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#conditional-import">
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-5/#conditional-import">
+ <link rel="match" href="reference/ref-filled-green-100px-square.xht">
+ <meta name="assert" content="Test passes on visual UAs if @import can be combined with a supports condition.">
+ <style>
+ @import "support/test-red.css";
+ @import "support/test-green.css"
+ supports(display: block);
+ @import "support/test-red.css"
+ supports(foo: bar);
+ div {
+ box-sizing: border-box;
+ width: 100px;
+ height: 100px;
+ padding: 5px; /* Avoids text antialiasing issues */
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+
+ <div class="test"></div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/import-conditions.html b/testing/web-platform/tests/css/css-cascade/import-conditions.html
new file mode 100644
index 0000000000..9c1e5c6e87
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/import-conditions.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<title>CSS Cascade Test: import conditions</title>
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#import-conditions">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @layer {
+ .target { color: red; }
+ }
+</style>
+<div id="target" class="target"></div>
+<script>
+ const testCases = [
+ {
+ importCondition: "supports(display:block)",
+ matches: true
+ },
+ {
+ importCondition: "supports((display:flex))",
+ matches: true
+ },
+ {
+ importCondition: "supports((display:block) and (display:flex))",
+ matches: true
+ },
+ {
+ importCondition: "supports((display:block) and (foo:bar))",
+ matches: false
+ },
+ {
+ importCondition: "supports((display:block) or (display:flex))",
+ matches: true
+ },
+ {
+ importCondition: "supports((display:block) or (foo:bar))",
+ matches: true
+ },
+ {
+ importCondition: "supports(not (display: flex))",
+ matches: false
+ },
+ {
+ importCondition: "supports(display: block !important)",
+ matches: true
+ },
+ {
+ importCondition: "supports(foo:bar)",
+ matches: false
+ },
+ {
+ importCondition: "supports(display:block) (width >= 0px)",
+ matches: true
+ },
+ {
+ importCondition: "(width >= 0px) supports(foo:bar)",
+ matches: false
+ },
+ {
+ importCondition: "(width >= 0px) supports(display:block)",
+ matches: false
+ },
+
+ // selector()
+ {
+ importCondition: "supports(selector(a))",
+ matches: true
+ },
+ {
+ importCondition: "supports(selector(p a))",
+ matches: true
+ },
+ {
+ importCondition: "supports(selector(p > a))",
+ matches: true
+ },
+ {
+ importCondition: "supports(selector(p + a))",
+ matches: true
+ },
+
+ // font-tech()
+ {
+ importCondition: "supports(font-tech(color-COLRv1))",
+ matches: true
+ },
+ {
+ importCondition: "supports(font-tech(invalid))",
+ matches: false
+ },
+
+ // font-format()
+ {
+ importCondition: "supports(font-format(opentype))",
+ matches: true
+ },
+ {
+ importCondition: "supports(font-format(woff))",
+ matches: true
+ },
+ {
+ importCondition: "supports(font-format(invalid))",
+ matches: false
+ },
+ {
+ importCondition: "layer(A.B) supports(font-format(opentype))",
+ matches: true
+ },
+ {
+ importCondition: "layer supports(selector(a))",
+ matches: true
+ },
+ ];
+ let target = document.getElementById("target");
+ for (let testCase of testCases) {
+ promise_test(async t => {
+ let styleElement = document.createElement("style");
+ styleElement.innerText = "@import url(data:text/css,.target{color:green}) " + testCase.importCondition + ";";
+
+ await new Promise(resolve => {
+ styleElement.onload = resolve;
+ styleElement.onerror = resolve;
+ document.head.appendChild(styleElement);
+ });
+
+ try {
+ assert_equals(getComputedStyle(target).color, testCase.matches ? "rgb(0, 128, 0)" : "rgb(255, 0, 0)");
+ } finally {
+ styleElement.remove();
+ }
+ }, testCase.importCondition + " is " + (testCase.matches ? "" : "not ") + "a valid import condition");
+ }
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/import-removal.html b/testing/web-platform/tests/css/css-cascade/import-removal.html
new file mode 100644
index 0000000000..6fb1ea3458
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/import-removal.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title></title>
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#at-import">
+<link rel="help" href="https://bugs.webkit.org/show_bug.cgi?id=235930">
+<link rel="match" href="../reference/ref-filled-green-100px-square-only.html">
+<meta name="assert" content="Checks that the page is rendered correctly when @import rule is removed with JS.">
+<p>Test passes if there is a filled green square.</p>
+<div style="width:100px; height:100px;"></div>
+<script>
+const style = document.createElement("style");
+document.head.append(style);
+const {sheet} = style;
+sheet.insertRule("@import url('data:text/css,div { background: red !important }');");
+sheet.insertRule("div { background: green }", 1);
+sheet.deleteRule(0);
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/important-prop-ref.html b/testing/web-platform/tests/css/css-cascade/important-prop-ref.html
new file mode 100644
index 0000000000..004679da73
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/important-prop-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Cascade Green Right Square Reference File</title>
+<link rel="author" title="David Burns" href="http://www.theautomatedtester.co.uk">
+<style>
+#success {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+}
+</style>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+
+ <div>
+ <div id="success"></div>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/important-prop.html b/testing/web-platform/tests/css/css-cascade/important-prop.html
new file mode 100644
index 0000000000..e8abffdf88
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/important-prop.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: Important vs. Animations</title>
+ <link rel="author" title="David Burns" href="http://www.theautomatedtester.co.uk">
+ <link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
+ <link rel="help" href="https://drafts.csswg.org/css-cascade/#importance">
+ <link rel="match" href="important-prop-ref.html">
+ <meta name="assert" content="Test passes if normal rules are overridden by animations, important rules override animations, and !important declarations are ignored in animations.">
+ <style>
+ @keyframes override {
+ from, to {
+ background: #f00; color: green;
+ border-color: green; border-color: red !important;
+ }
+ }
+
+ .square {
+ color:#00f;
+ animation: override 1s infinite;
+ width: 80px;
+ height: 80px;
+ border: 10px solid red;
+ text-align: center;
+ }
+ div {
+ background-color:green !important;
+ color: red;
+ }
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+
+ <div class="square green">FAIL</div>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/important-transition-manual.html b/testing/web-platform/tests/css/css-cascade/important-transition-manual.html
new file mode 100644
index 0000000000..b9abed469f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/important-transition-manual.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: Author !important vs. Transitions</title>
+ <link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-3/#cascade-sort">
+ <style>
+ .container { padding: 1em 0; border-style: dotted none; border-width: 1px; }
+ .container > div { width: 5em; text-align: center; border: solid; transition: all 3s; }
+
+ .container > .ref { border-color: blue ; color: navy ; background: aqua ; margin: 0.25em ; }
+ :hover > .ref { border-color: aqua ; color: orange ; background: teal ; margin-left: 40% ; }
+
+ :not(:hover) > .test { border-color: blue !important; color: navy !important; background: aqua ; }
+ div > .test { margin: 0.25em !important; }
+ :hover > .test { border-color: aqua !important; color: orange ; background: teal !important; margin-left: 40% !important; }
+ </style>
+
+<p>Test passes if the two boxes transition identically when hovering with a mouse below.
+
+<div class=container>
+ <div class=test>Box 1</div>
+ <div class=ref>Box 2</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-cascade/important-vs-inline-001.html b/testing/web-platform/tests/css/css-cascade/important-vs-inline-001.html
new file mode 100644
index 0000000000..33b33bf943
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/important-vs-inline-001.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: inline style loses to !important</title>
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#cascade-sort">
+ <link rel="author" href="mailto:sesse@chromium.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .outer {
+ opacity: 0.5 !important;
+ }
+ </style>
+</head>
+<body>
+ <p class="outer" id="el">Test passes if this text is semi-transparent.</p>
+</body>
+<script>
+test(() => {
+ el.offsetTop;
+ assert_equals(getComputedStyle(el).opacity, "0.5", "style is set correctly");
+});
+test(() => {
+ el.offsetTop;
+ el.style.opacity = 0.75;
+ assert_equals(getComputedStyle(el).opacity, "0.5", "!important has higher priority than adding inline style");
+});
+test(() => {
+ el.offsetTop;
+ el.style.opacity = 1.0;
+ assert_equals(getComputedStyle(el).opacity, "0.5", "!important has higher priority than modifying inline style");
+});
+test(() => {
+ el.offsetTop;
+ el.style.opacity = null;
+ assert_equals(getComputedStyle(el).opacity, "0.5", "!important has higher priority than removing inline style");
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/important-vs-inline-002.html b/testing/web-platform/tests/css/css-cascade/important-vs-inline-002.html
new file mode 100644
index 0000000000..e16aedc5bb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/important-vs-inline-002.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: inline style loses to !important</title>
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#cascade-sort">
+ <link rel="author" href="mailto:sesse@chromium.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .outer {
+ font-size: 18px !important;
+ line-height: 2em;
+ border: 1px solid black;
+ }
+ </style>
+</head>
+<body>
+ <p class="outer" id="el">Test passes if the line-height is twice the font size.</p>
+</body>
+<script>
+test(() => {
+ el.offsetTop;
+ assert_equals(getComputedStyle(el).lineHeight, "36px", "style is set correctly");
+});
+test(() => {
+ el.offsetTop;
+ el.style.fontSize = "24px";
+ assert_equals(getComputedStyle(el).lineHeight, "36px", "!important has higher priority than adding inline style");
+});
+test(() => {
+ el.offsetTop;
+ el.style.fontSize = "36px";
+ assert_equals(getComputedStyle(el).lineHeight, "36px", "!important has higher priority than modifying inline style");
+});
+test(() => {
+ el.offsetTop;
+ el.style.fontSize = null;
+ assert_equals(getComputedStyle(el).lineHeight, "36px", "!important has higher priority than removing inline style");
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/important-vs-inline-003.html b/testing/web-platform/tests/css/css-cascade/important-vs-inline-003.html
new file mode 100644
index 0000000000..b1103abbf2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/important-vs-inline-003.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: inline style loses to !important</title>
+ <link rel="help" href="https://crbug.com/1332956">
+ <link rel="author" href="mailto:sesse@chromium.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .cls {
+ visibility: inherit !important;
+ }
+ </style>
+</head>
+<body>
+ <div class="cls" id="el" style="visibility: hidden; height: 200px;"><iframe></iframe></div>
+</body>
+<script>
+test(() => {
+ el.setAttribute('disabled', 'disabled');
+ el.offsetTop;
+ el.style.height = '400px';
+ assert_equals(getComputedStyle(el).visibility, "visible", "!important has higher priority than inline style");
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/inherit-initial.html b/testing/web-platform/tests/css/css-cascade/inherit-initial.html
new file mode 100644
index 0000000000..8d8dfef38d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/inherit-initial.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>CSS Cascading and Inheritance test: Root element inherits from initial values</title>
+<link rel="author" title="Rune Lillesveen" href="mailto:rune@opera.com">
+<link rel="help" href="https://www.w3.org/TR/css3-cascade/#inheriting">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+html {
+ z-index: inherit;
+ position: inherit;
+ overflow: inherit;
+ background-color: inherit;
+}
+body {
+ overflow: scroll;
+ background-color: pink;
+}
+</style>
+<script>
+ test(function() {
+ assert_equals(getComputedStyle(document.documentElement).zIndex, "auto");
+ }, "z-index:inherit on root element should compute to 'auto'.");
+
+ test(function() {
+ assert_equals(getComputedStyle(document.documentElement).position, "static");
+ }, "position:inherit on root element should compute to 'static'.");
+
+ test(function() {
+ assert_equals(getComputedStyle(document.documentElement).overflow, "visible");
+ }, "overflow:inherit on root element should compute to 'visible'.");
+
+ test(function() {
+ assert_equals(getComputedStyle(document.documentElement).backgroundColor, "rgba(0, 0, 0, 0)");
+ }, "background-color:inherit on root element should compute to 'rgba(0, 0, 0, 0)'.");
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/initial-background-color.html b/testing/web-platform/tests/css/css-cascade/initial-background-color.html
new file mode 100644
index 0000000000..80897e0ef2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/initial-background-color.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>
+ CSS Cascading and Inheritance Test:
+ Initial property and background-color
+ </title>
+ <meta name="assert" content="
+ The initial keyword is supported on background-color.
+ " />
+
+ <link
+ rel="author"
+ title="François REMY"
+ href="mailto:fremycompany.developer@yahoo.fr"
+ / >
+
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-3/#initial"/>
+
+ <link
+ rel="match"
+ href="reference/all-green.html"
+ />
+
+ <style type="text/css">
+
+ html, body { margin: 0px; padding: 0px; }
+
+ html { background: green; overflow: hidden; }
+ #outer { position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; }
+ #outer { background: red; background-color: initial; }
+
+ </style>
+
+</head>
+<body>
+
+ <div id="outer"></div>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/initial-color-background-001-ref.html b/testing/web-platform/tests/css/css-cascade/initial-color-background-001-ref.html
new file mode 100644
index 0000000000..ffac42763f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/initial-color-background-001-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade "W" Reference File</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <style>
+body {
+ background-color: white;
+}
+div {
+ font-size: 100px;
+}
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a "W" and <strong>no red</strong>.</p>
+ <div>W</div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/initial-color-background-001.html b/testing/web-platform/tests/css/css-cascade/initial-color-background-001.html
new file mode 100644
index 0000000000..50e1384b0f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/initial-color-background-001.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: the "initial" value</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-3/#initial">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-4/#initial">
+ <link rel="match" href="initial-color-background-001-ref.html">
+ <meta name="assert" content="initial is not the same as inherit. color:initial results in non-red. background-color:initial results in transparent.">
+ <style>
+body {
+ background-color: white;
+}
+div {
+ font-size: 100px;
+}
+.outer {
+ color: red;
+}
+.inner {
+ background-color: red;
+}
+.inner {
+ color: initial;/* normally black, almost certainly not red */
+ background-color: initial;/* transparent, so the body's white will show thru */
+}
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a "W" and <strong>no red</strong>.</p>
+ <div class="outer">
+ <div class="inner">W</div>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-basic.html b/testing/web-platform/tests/css/css-cascade/layer-basic.html
new file mode 100644
index 0000000000..e214bffc25
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-basic.html
@@ -0,0 +1,524 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>CSS Cascade Layers: Basic functionality</title>
+<meta name="assert" content="Basic functionality of CSS Cascade Layers">
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#layering">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<target class="first"></target>
+<target class="second"></target>
+<div id="log"></div>
+<script>
+
+// In all test cases, the rule specified as "color: green" should win.
+var testCases = [
+ {
+ title: 'A1 Anonymous layers',
+ style: `
+ @layer { }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'A2 Anonymous layers',
+ style: `
+ target { color: green; }
+ @layer {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'A3 Anonymous layers',
+ style: `
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'A4 Anonymous layers',
+ style: `
+ @layer {
+ target { color: red; }
+ }
+ @layer {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A5 Anonymous layers',
+ style: `
+ @layer {
+ target { color: green; }
+ @layer {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'A6 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A7 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: red; }
+ }
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A8 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ }
+ target { color: red; }
+ }
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A9 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: red; }
+ }
+ @layer {
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'B1 Named layers',
+ style: `
+ @layer A {
+ }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'B2 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'B3 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ @layer A {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'B4 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ @layer A {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'B5 Named layers',
+ style: `
+ @layer A {
+ target { color: green; }
+ @layer A {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B6 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer A {
+ @layer A {
+ target { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B7 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B8 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ @layer A {
+ target { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B9 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ @layer A {
+ target.first { color: green; }
+ }
+ }
+ @layer A {
+ @layer A {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B10 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ @layer A {
+ target.first { color: green; }
+ }
+ }
+ @layer A {
+ @layer B {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'C1 Named layers shorthand',
+ style: `
+ @layer A.A {
+ target { color: red; }
+ }
+ @layer B.A {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'C2 Named layers shorthand',
+ style: `
+ @layer A.A {
+ target { color: red; }
+ }
+ @layer B.A {
+ target.first { color: green; }
+ }
+ @layer A.A {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'C3 Named layers shorthand',
+ style: `
+ @layer A.A {
+ target { color: red; }
+ }
+ @layer B.A {
+ target.first { color: green; }
+ }
+ @layer A.B {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'C4 Named layers shorthand',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B.A {
+ target { color: green; }
+ }
+ @layer A.A
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'C5 Named layers shorthand',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B.A {
+ target { color: green; }
+ }
+ @layer A.B {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'D1 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ @layer {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'D2 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ @layer A {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'D3 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ @layer A {
+ @layer {
+ target { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'D4 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ @layer {
+ target { color: green; }
+ }
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'D5 Mixed named and anonymous layers',
+ style: `
+ @layer {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'E1 Statement syntax',
+ style: `
+ @layer A, B, C;
+ @layer A {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ @layer B {
+ target.first { color: red; }
+ }
+ @layer C {
+ target.first { color: green; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'E2 Statement syntax',
+ style: `
+ @layer A, C, B;
+ @layer A {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ @layer B {
+ target.first { color: green; }
+ }
+ @layer C {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'E3 Statement syntax',
+ style: `
+ @layer C, B, A;
+ @layer A {
+ target.first { color: green; }
+ target.second { color: green; }
+ }
+ @layer B {
+ target.first { color: red; }
+ }
+ @layer C {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'E4 Statement syntax',
+ style: `
+ @layer B, A.B, A.A;
+ @layer A {
+ @layer A {
+ target.first { color: green; }
+ }
+ @layer B {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ }
+ @layer B {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'E5 Statement syntax',
+ style: `
+ @layer A.B, B, A.A;
+ @layer A {
+ @layer A {
+ target.first { color: red; }
+ }
+ @layer B {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ `,
+ },
+];
+
+for (let testCase of testCases) {
+ const styleElement = document.createElement('style');
+ styleElement.textContent = testCase['style'];
+ document.head.append(styleElement);
+
+ test(function () {
+ var targets = document.querySelectorAll('target');
+ for (target of targets)
+ assert_equals(window.getComputedStyle(target).color, 'rgb(0, 128, 0)',
+ testCase['title'] + ", target '" + target.classList[0] + "'");
+ }, testCase['title']);
+
+ styleElement.remove();
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-counter-style-override.html b/testing/web-platform/tests/css/css-cascade/layer-counter-style-override.html
new file mode 100644
index 0000000000..1720898457
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-counter-style-override.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<title>Resolving @counter-style name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#target, #reference {
+ font-family: monospace;
+ width: min-content;
+}
+
+#reference::before {
+ content: '0000';
+}
+
+@counter-style three {
+ system: cyclic;
+ symbols: '000';
+}
+
+@counter-style four {
+ system: cyclic;
+ symbols: '0000';
+}
+</style>
+
+<ul>
+ <li id="target"></li>
+ <li id="reference"></li>
+</ul>
+
+<script>
+// In all tests, #target::before should have 4 characters, same as #reference.
+
+const testCases = [
+ {
+ title: '@counter-style unlayered overrides layered',
+ style: `
+ #target::before {
+ content: counter(dont-care, custom-counter-style);
+ }
+
+ @counter-style custom-counter-style {
+ system: extends four;
+ }
+
+ @layer {
+ @counter-style custom-counter-style {
+ system: extends three;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@counter-style override between layers',
+ style: `
+ @layer base, override;
+
+ #target::before {
+ content: counter(dont-care, custom-counter-style);
+ }
+
+ @layer override {
+ @counter-style custom-counter-style {
+ system: extends four;
+ }
+ }
+
+ @layer base {
+ @counter-style custom-counter-style {
+ system: extends three;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@counter-style override update with appended sheet 1',
+ style: `
+ @layer base, override;
+
+ #target::before {
+ content: counter(dont-care, custom-counter-style);
+ }
+
+ @layer override {
+ @counter-style custom-counter-style {
+ system: extends four;
+ }
+ }
+ `,
+ append: `
+ @layer base {
+ @counter-style custom-counter-style {
+ system: extends three;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@counter-style override update with appended sheet 2',
+ style: `
+ @layer base, override;
+
+ #target::before {
+ content: counter(dont-care, custom-counter-style);
+ }
+
+ @layer base {
+ @counter-style custom-counter-style {
+ system: extends three;
+ }
+ }
+ `,
+ append: `
+ @layer override {
+ @counter-style custom-counter-style {
+ system: extends four;
+ }
+ }
+ `
+ },
+];
+
+for (let testCase of testCases) {
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ var appendedStyle;
+ if (testCase['append']) {
+ document.body.offsetLeft; // Force style update
+ appendedStyle = document.createElement('style');
+ appendedStyle.appendChild(document.createTextNode(testCase['append']));
+ document.head.appendChild(appendedStyle);
+ }
+
+ test(function () {
+ assert_equals(getComputedStyle(target).width,
+ getComputedStyle(reference).width);
+ }, testCase['title']);
+
+ if (appendedStyle)
+ appendedStyle.remove();
+ documentStyle.remove();
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse-at-property.html b/testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse-at-property.html
new file mode 100644
index 0000000000..dfa6bbfcfb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse-at-property.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: @property rule invalidation on layer order changes</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="stylesheet" href="/fonts/ahem.css">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#reference {
+ color: green;
+ --foo: green;
+}
+</style>
+
+<div id=target>Lorem ipsum</div>
+<div id=reference>Lorem ipsum</div>
+
+<script>
+const testCases = [
+ {
+ title: 'Insert layer invalidates @property',
+ sheets: [
+ '',
+ `
+ @layer first {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+ @layer second {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `,
+ ],
+ update: function(sheets) {
+ sheets[0].insertRule('@layer second {}', 0);
+ },
+ property: '--foo',
+ },
+ {
+ title: 'Delete layer invalidates @property',
+ sheets: [
+ '@layer second {}',
+ `
+ @layer first {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ @layer second {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+ `,
+ ],
+ update: function(sheets) {
+ sheets[0].deleteRule(0);
+ },
+ property: '--foo',
+ },
+];
+
+for (let testCase of testCases) {
+ test(testObj => {
+ const styleElements = testCase.sheets.map(sheet => {
+ const element = document.createElement('style');
+ element.appendChild(document.createTextNode(sheet));
+ document.head.appendChild(element);
+ return element;
+ });
+ testObj.add_cleanup(() => {
+ for (let element of styleElements)
+ element.remove();
+ });
+
+ const sheets = styleElements.map(element => element.sheet);
+ testCase.update(sheets);
+ const actual = getComputedStyle(target).getPropertyValue(testCase.property);
+ const expected = getComputedStyle(reference).getPropertyValue(testCase.property);
+ assert_equals(actual, expected);
+ }, testCase.title);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse.html b/testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse.html
new file mode 100644
index 0000000000..ddc5977d42
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-cssom-order-reverse.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: at-rule and style invalidation on layer order changes</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="stylesheet" href="/fonts/ahem.css">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#reference {
+ color: green;
+ font: 20px/1 ahem;
+ width: max-content;
+}
+</style>
+
+<div id=target>Lorem ipsum</div>
+<div id=reference>Lorem ipsum</div>
+
+<script>
+const testCases = [
+ {
+ title: 'Insert layer invalidates style',
+ sheets: [
+ '',
+ `
+ @layer first {
+ #target { color: green; }
+ }
+ @layer second {
+ #target { color: red; }
+ }
+ `,
+ ],
+ update: function(sheets) {
+ sheets[0].insertRule('@layer second {}', 0);
+ },
+ property: 'color',
+ },
+ {
+ title: 'Delete layer invalidates style',
+ sheets: [
+ '@layer second {}',
+ `
+ @layer first {
+ #target { color: red; }
+ }
+ @layer second {
+ #target { color: green; }
+ }
+ `,
+ ],
+ update: function(sheets) {
+ sheets[0].deleteRule(0);
+ },
+ property: 'color',
+ },
+ {
+ title: 'Insert layer invalidates @font-face',
+ sheets: [
+ '',
+ `
+ @layer first {
+ @font-face {
+ font-family: custom;
+ src: local('Ahem'), url('/fonts/Ahem.ttf');
+ }
+ }
+ @layer second {
+ @font-face {
+ font-family: custom;
+ src: url('/fonts/noto/noto-sans-v8-latin-regular.woff') format('woff');
+ }
+ }
+ #target { font: 20px/1 custom; width: max-content; }
+ `,
+ ],
+ update: async function(sheets) {
+ await document.fonts.load('20px/1 ahem');
+ await document.fonts.load('20px/1 custom');
+ document.body.offsetLeft; // Force style recalc
+ sheets[0].insertRule('@layer second {}', 0);
+ await document.fonts.load('20px/1 custom');
+ },
+ property: 'width',
+ },
+ {
+ title: 'Delete layer invalidates @font-face',
+ sheets: [
+ '@layer second {}',
+ `
+ @layer first {
+ @font-face {
+ font-family: custom;
+ src: url('/fonts/noto/noto-sans-v8-latin-regular.woff') format('woff');
+ }
+ }
+ @layer second {
+ @font-face {
+ font-family: custom;
+ src: local('Ahem'), url('/fonts/Ahem.ttf');
+ }
+ }
+ #target { font: 20px/1 custom; width: max-content; }
+ `,
+ ],
+ update: async function(sheets) {
+ await document.fonts.load('20px/1 ahem');
+ await document.fonts.load('20px/1 custom');
+ document.body.offsetLeft; // Force style recalc
+ sheets[0].deleteRule(0);
+ await document.fonts.load('20px/1 custom');
+ },
+ property: 'width',
+ },
+];
+
+for (let testCase of testCases) {
+ promise_test(async test => {
+ const styleElements = testCase.sheets.map(sheet => {
+ const element = document.createElement('style');
+ element.appendChild(document.createTextNode(sheet));
+ document.head.appendChild(element);
+ return element;
+ });
+ test.add_cleanup(() => {
+ for (let element of styleElements)
+ element.remove();
+ });
+
+ const sheets = styleElements.map(element => element.sheet);
+ await testCase.update(sheets);
+ const actual = getComputedStyle(target).getPropertyValue(testCase.property);
+ const expected = getComputedStyle(reference).getPropertyValue(testCase.property);
+ assert_equals(actual, expected);
+}, testCase.title);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-font-face-override.html b/testing/web-platform/tests/css/css-cascade/layer-font-face-override.html
new file mode 100644
index 0000000000..d35caca012
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-font-face-override.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<title>Resolving @keyframe name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#target {
+ font-size: 20px;
+ width: min-content;
+}
+</style>
+
+<div id="target">Test</div>
+
+<script>
+// In all tests, width of #target should be 80px.
+
+const testCases = [
+ {
+ title: '@font-face unlayered overrides layered',
+ style: `
+ #target {
+ font-family: custom-font;
+ }
+
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/Ahem.ttf');
+ }
+
+ @layer {
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/noto/noto-sans-v8-latin-regular.woff') format('woff');
+ }
+ }
+ `
+ },
+
+ {
+ title: '@font-face override between layers',
+ style: `
+ @layer base, override;
+
+ #target {
+ font-family: custom-font;
+ }
+
+ @layer override {
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/Ahem.ttf');
+ }
+ }
+
+ @layer base {
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/noto/noto-sans-v8-latin-regular.woff') format('woff');
+ }
+ }
+ `
+ },
+
+ {
+ title: '@font-face override update with appended sheet 1',
+ style: `
+ @layer base, override;
+
+ #target {
+ font-family: custom-font;
+ }
+
+ @layer override {
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/Ahem.ttf');
+ }
+ }
+ `,
+ append: `
+ @layer base {
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/noto/noto-sans-v8-latin-regular.woff') format('woff');
+ }
+ }
+ `
+ },
+
+ {
+ title: '@font-face override update with appended sheet 2',
+ style: `
+ @layer base, override;
+
+ #target {
+ font-family: custom-font;
+ }
+
+ @layer base {
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/noto/noto-sans-v8-latin-regular.woff') format('woff');
+ }
+ }
+ `,
+ append: `
+ @layer override {
+ @font-face {
+ font-family: custom-font;
+ src: url('/fonts/Ahem.ttf');
+ }
+ }
+ `
+ },
+];
+
+for (let testCase of testCases) {
+ promise_test(async () => {
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ var appendedStyle;
+ if (testCase['append']) {
+ document.body.offsetLeft; // Force style update
+ appendedStyle = document.createElement('style');
+ appendedStyle.appendChild(document.createTextNode(testCase['append']));
+ document.head.appendChild(appendedStyle);
+ }
+
+ await document.fonts.load('20px/1 custom-font');
+ assert_equals(getComputedStyle(target).width, '80px');
+
+ if (appendedStyle)
+ appendedStyle.remove();
+ documentStyle.remove();
+ }, testCase['title']);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-import.html b/testing/web-platform/tests/css/css-cascade/layer-import.html
new file mode 100644
index 0000000000..821bc7d72f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-import.html
@@ -0,0 +1,294 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>CSS Cascade Layers: Imports</title>
+<meta name="assert" content="Import functionality of CSS Cascade Layers">
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#layering">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<target class="first"></target>
+<div id="log"></div>
+<script>
+
+// In all test cases, the rule specified as "color: green" should win.
+const imports = {
+ "basic-green.css": `
+ target { color: green; }
+ `,
+ "basic-red.css": `
+ target { color: red; }
+ `,
+ "layer-green.css": `
+ @layer {
+ target { color: green; }
+ }
+ `,
+ "layer-red.css": `
+ @layer {
+ target { color: red; }
+ }
+ `,
+ "layer-A-green.css": `
+ @layer A {
+ target { color: green; }
+ }
+ `,
+ "layer-A-red.css": `
+ @layer A {
+ target { color: red; }
+ }
+ `,
+ "layer-B-green.css": `
+ @layer B {
+ target { color: green; }
+ }
+ `,
+ "layer-B-red.css": `
+ @layer B {
+ target { color: red; }
+ }
+ `,
+};
+
+const testCases = [
+ {
+ title: 'A1 Layer rules with import',
+ style: `
+ @import url(basic-green.css);
+ @layer {
+ target { color: red; }
+ }
+ `
+ },
+ {
+ title: 'A2 Layer rules with import',
+ style: `
+ @import url(layer-red.css);
+ target { color: green; }
+ `
+ },
+ {
+ title: 'A3 Layer rules with import',
+ style: `
+ @import url(basic-green.css);
+ @import url(layer-red.css);
+ `
+ },
+ {
+ title: 'A4 Layer rules with import',
+ style: `
+ @import url(layer-A-red.css);
+ @layer B {
+ target { color: green; }
+ }
+ @layer A {
+ target { color: red; }
+ }
+ `
+ },
+ {
+ title: 'B1 Anonymous imports',
+ style: `
+ @import url(basic-red.css) layer;
+ target { color: green; }
+ `
+ },
+ {
+ title: 'B2 Anonymous imports',
+ style: `
+ @import url(basic-red.css) layer;
+ @import url(basic-green.css) layer;
+ `
+ },
+ {
+ title: 'B3 Anonymous imports',
+ style: `
+ @import url(basic-red.css) layer;
+ @layer {
+ target { color: green; }
+ }
+ `
+ },
+ {
+ title: 'B4 Anonymous imports',
+ style: `
+ @import url(layer-red.css);
+ @import url(basic-green.css) layer;
+ `
+ },
+ {
+ title: 'C1 Named imports',
+ style: `
+ @import url(basic-red.css) layer(A);
+ target { color: green; }
+ `
+ },
+ {
+ title: 'C2 Named imports',
+ style: `
+ @import url(basic-red.css) layer(A);
+ @import url(basic-green.css) layer(A);
+ `
+ },
+ {
+ title: 'C3 Named imports',
+ style: `
+ @import url(basic-red.css) layer(A);
+ @layer A {
+ target { color: green; }
+ }
+ `
+ },
+ {
+ title: 'C4 Named imports',
+ style: `
+ @import url(layer-red.css) layer(A);
+ @layer A {
+ target { color: green; }
+ }
+ `
+ },
+ {
+ title: 'C5 Named imports',
+ style: `
+ @import url(layer-A-red.css) layer(A);
+ @layer A.A {
+ target { color: green; }
+ }
+ `
+ },
+ {
+ title: 'C6 Named imports',
+ style: `
+ @import url(layer-A-red.css) layer(A);
+ @layer B {
+ target { color: green; }
+ }
+ @layer A.B {
+ target { color: red; }
+ }
+ `
+ },
+ {
+ title: 'C7 Named imports',
+ style: `
+ @import url(basic-green.css) layer(A);
+ @import url(basic-red.css) layer(B);
+ @import url(basic-green.css) layer(C);
+ `
+ },
+ {
+ title: 'C8 Named imports',
+ style: `
+ @import url(basic-red.css) layer(A);
+ @import url(basic-green.css) layer(B);
+ @import url(basic-red.css) layer(A);
+ `
+ },
+ {
+ title: 'C9 Named imports',
+ style: `
+ @import url(basic-red.css) layer(A);
+ @import url(basic-red.css) layer(B.A);
+ @import url(basic-green.css) layer(B);
+ `
+ },
+ {
+ title: 'D1 Layer statement with imports',
+ style: `
+ @import url(basic-red.css) layer(A);
+ @import url(basic-green.css) layer(B);
+ @layer B, A;
+ `
+ },
+ {
+ title: 'D2 Layer statement with imports',
+ style: `
+ @layer B;
+ @import url(basic-green.css) layer(A);
+ @layer B {
+ target { color: red; }
+ }
+ `
+ },
+ {
+ title: 'D3 Layer statement with imports',
+ style: `
+ @layer B;
+ @import url(basic-green.css) layer(A);
+ @import url(basic-red.css) layer(B);
+ `
+ },
+ {
+ title: 'D4 Layer statement with imports',
+ style: `
+ @layer C, B, A;
+ @import url(basic-green.css) layer(A);
+ @import url(basic-red.css) layer(B);
+ @layer C {
+ target { color: red; }
+ }
+ `
+ },
+ {
+ title: 'D5 Layer statement with imports',
+ style: `
+ @layer A.B, A.A;
+ @import url(basic-green.css) layer(A.A);
+ @import url(layer-B-red.css) layer(A);
+ `
+ },
+ {
+ title: 'D6 Layer statement with imports',
+ style: `
+ @layer B, A;
+ @import url(layer-A-red.css) layer(A);
+ @import url(layer-A-red.css) layer(B);
+ @layer A.B {
+ target { color: green; }
+ }
+ `
+ },
+ {
+ title: 'E1 Named imports establish layer even with network errors',
+ style: `
+ @import "nonexist.css" layer(A);
+ @layer B {
+ target { color: green; }
+ }
+ @layer A {
+ target { color: red; }
+ }
+ `,
+ },
+];
+
+for (let testCase of testCases) {
+ promise_test(async t => {
+ const styleElement = document.createElement('style');
+ const styleText = testCase['style'].replaceAll(/url\((.+?)\)/g, (match, p1) => {
+ return `url(data:text/css,${ encodeURI(imports[p1]) })`;
+ });
+ styleElement.textContent = styleText;
+
+ await new Promise(resolve => {
+ styleElement.onload = resolve;
+ styleElement.onerror = resolve;
+ document.head.append(styleElement);
+ });
+
+ try {
+ const targets = document.querySelectorAll('target');
+ for (target of targets)
+ assert_equals(window.getComputedStyle(target).color, 'rgb(0, 128, 0)', testCase['title'] + ", target '" + target.classList[0] + "'");
+ } finally {
+ styleElement.remove();
+ }
+ }, testCase['title']);
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-important.html b/testing/web-platform/tests/css/css-cascade/layer-important.html
new file mode 100644
index 0000000000..23bfd167fd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-important.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>CSS Cascade Layers: !important </title>
+<meta name="assert" content="!important functionality of CSS Cascade Layers">
+<link rel="author" title="Romain Menke" href="mailto:romainmenke@gmail.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#cascade-layering">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<target class="first"></target>
+<target class="second"></target>
+<div id="log"></div>
+<script>
+
+// In all test cases, the rule specified as "color: green" should win.
+var testCases = [
+ {
+ title: 'A1 Unlayered !important style',
+ style: `
+ target { color: green !important; }
+ target { color: red; }
+ `
+ },
+ {
+ title: 'B1 Same specificity, layered !important first',
+ style: `
+ @layer { target { color: green !important; } }
+ target { color: red; }
+ `,
+ },
+ {
+ title: 'C1 Same specificity, layered !important second',
+ style: `
+ target { color: red; }
+ @layer { target { color: green !important; } }
+ `,
+ },
+ {
+ title: 'D1 Same specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ @layer { target { color: red !important; } }
+ target { color: red !important; }
+ `,
+ },
+ {
+ title: 'D2 Same specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ target { color: red !important; }
+ @layer { target { color: red !important; } }
+ `,
+ },
+ {
+ title: 'D3 Same specificity, all !important',
+ style: `
+ target { color: red !important; }
+ @layer { target { color: green !important; } }
+ @layer { target { color: red !important; } }
+ `,
+ },
+ {
+ title: 'D4 Same specificity, all !important',
+ style: `
+ @layer A, B;
+ @layer B { target { color: red !important; } }
+ @layer A { target { color: green !important; } }
+ target { color: red !important; }
+ `,
+ },
+ {
+ title: 'E1 Different specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ @layer { target { color: red !important; } }
+ target.first { color: red !important; }
+ `,
+ },
+ {
+ title: 'E2 Different specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ @layer { target.first { color: red !important; } }
+ target { color: red !important; }
+ `,
+ },
+];
+
+for (let testCase of testCases) {
+ const styleElement = document.createElement('style');
+ styleElement.textContent = testCase['style'];
+ document.head.append(styleElement);
+
+ test(function () {
+ var targets = document.querySelectorAll('target');
+ for (target of targets)
+ assert_equals(window.getComputedStyle(target).color, 'rgb(0, 128, 0)',
+ testCase['title'] + ", target '" + target.classList[0] + "'");
+ }, testCase['title']);
+
+ styleElement.remove();
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-keyframes-override.html b/testing/web-platform/tests/css/css-cascade/layer-keyframes-override.html
new file mode 100644
index 0000000000..d0f4044f1e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-keyframes-override.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<title>Resolving @keyframe name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#target, #reference {
+ width: 100px;
+ height: 100px;
+}
+
+#reference {
+ background-color: green;
+}
+</style>
+
+<div id="target"></div>
+<div id="reference"></div>
+
+<script>
+// In all tests, background color of #target should be green, same as #reference
+
+const testCases = [
+ {
+ title: '@keyframes unlayered overrides layered',
+ style: `
+ #target {
+ animation: anim 1s paused;
+ }
+
+ @keyframes anim {
+ from { background-color: green; }
+ }
+
+ @layer {
+ @keyframes anim {
+ from { background-color: red; }
+ }
+ }
+ `
+ },
+
+ {
+ title: '@keyframes override between layers',
+ style: `
+ @layer base, override;
+
+ #target {
+ animation: anim 1s paused;
+ }
+
+ @layer override {
+ @keyframes anim {
+ from { background-color: green; }
+ }
+ }
+
+ @layer base {
+ @keyframes anim {
+ from { background-color: red; }
+ }
+ }
+ `
+ },
+
+ {
+ title: '@keyframes override update with appended sheet 1',
+ style: `
+ @layer base, override;
+
+ #target {
+ animation: anim 1s paused;
+ }
+
+ @layer override {
+ @keyframes anim {
+ from { background-color: green; }
+ }
+ }
+ `,
+ append: `
+ @layer base {
+ @keyframes anim {
+ from { background-color: red; }
+ }
+ }
+ `
+ },
+
+ {
+ title: '@keyframes override update with appended sheet 2',
+ style: `
+ @layer base, override;
+
+ #target {
+ animation: anim 1s paused;
+ }
+
+ @layer base {
+ @keyframes anim {
+ from { background-color: red; }
+ }
+ }
+ `,
+ append: `
+ @layer override {
+ @keyframes anim {
+ from { background-color: green; }
+ }
+ }
+ `
+ },
+];
+
+for (let testCase of testCases) {
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ var appendedStyle;
+ if (testCase['append']) {
+ document.body.offsetLeft; // Force style update
+ appendedStyle = document.createElement('style');
+ appendedStyle.appendChild(document.createTextNode(testCase['append']));
+ document.head.appendChild(appendedStyle);
+ }
+
+ test(function () {
+ assert_equals(getComputedStyle(target).backgroundColor,
+ getComputedStyle(reference).backgroundColor);
+ }, testCase['title']);
+
+ if (appendedStyle)
+ appendedStyle.remove();
+ documentStyle.remove();
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-media-query.html b/testing/web-platform/tests/css/css-cascade/layer-media-query.html
new file mode 100644
index 0000000000..92a0f55a6a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-media-query.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>CSS Cascade Layers: Media queries</title>
+<meta name="assert" content="CSS Cascade Layers nested in Media Queries">
+<link rel="author" title="Antti Koivisto" href="mailto:antti@apple.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#layering">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<iframe width=300 height=300 frameborder=0></iframe>
+<div id="log"></div>
+<script>
+
+const imports = {
+ "basic-green.css": `
+ target { color: green; }
+ `,
+ "basic-red.css": `
+ target { color: red; }
+ `,
+ "empty.css": "",
+};
+
+// For 300px wide iframe the target should be red and for 500px green.
+const testCases = [
+ {
+ title: 'A1 Basic',
+ style: `
+ @layer { target { color: red } }
+ @media (min-width: 500px) {
+ @layer {
+ target { color: green; }
+ }
+ }
+ `
+ },
+ {
+ title: 'A2 Basic',
+ style: `
+ @media (min-width: 500px) {
+ @layer {
+ target { color: green; }
+ }
+ }
+ @media (max-width: 300px) {
+ @layer {
+ target { color: red; }
+ }
+ }
+ `
+ },
+ {
+ title: 'B1 Basic import',
+ style: `
+ @import url(basic-red.css) layer;
+ @import url(basic-green.css) layer (min-width: 500px);
+ `
+ },
+ {
+ title: 'B2 Basic import',
+ style: `
+ @import url(basic-green.css) layer (min-width: 500px);
+ @import url(basic-red.css) layer (max-width: 300px);
+ `
+ },
+ {
+ title: 'C1 Reordering',
+ style: `
+ @media (max-width: 300px) {
+ @layer B {
+ target { color: green; }
+ }
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @media (min-width: 500px) {
+ @layer A {
+ target { color: red; }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ }
+ `
+ },
+ {
+ title: 'C2 Reordering',
+ style: `
+ @media (max-width: 300px) {
+ @layer B { }
+ @layer A { target { color: red; } }
+ }
+ @media (min-width: 500px) {
+ @layer A { target { color: red; } }
+ @layer B { }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ `
+ },
+ {
+ title: 'C3 Reordering',
+ style: `
+ @media (max-width: 300px) {
+ @layer B, A;
+ }
+ @media (min-width: 500px) {
+ @layer A, B;
+ }
+ @layer A {
+ target { color: red; }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ `
+ },
+ {
+ title: 'C4 Reordering',
+ style: `
+ @import url(empty.css) layer(B) (max-width: 300px);
+ @import url(empty.css) layer(A) (max-width: 300px);
+ @import url(empty.css) layer(A) (min-width: 500px);
+ @import url(empty.css) layer(B) (min-width: 500px);
+ @layer A {
+ target { color: red; }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ `
+ },
+];
+
+let iframe = document.querySelector("iframe");
+
+for (let testCase of testCases) {
+ promise_test(async t => {
+ const styleText = testCase['style'].replaceAll(/url\((.+?)\)/g, (match, p1) => {
+ return `url(data:text/css,${ encodeURI(imports[p1]) })`;
+ });
+
+ iframe.width = 300;
+
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ iframe.srcdoc = `
+ <style>
+ ${styleText}
+ </style>
+ <target></target>
+ `;
+ });
+
+ const target = iframe.contentDocument.querySelector('target');
+ assert_equals(getComputedStyle(target).color, 'rgb(255, 0, 0)', testCase['title']);
+
+ iframe.width = 500;
+
+ assert_equals(getComputedStyle(target).color, 'rgb(0, 128, 0)', testCase['title']);
+ }, testCase['title']);
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-media-toggle.html b/testing/web-platform/tests/css/css-cascade/layer-media-toggle.html
new file mode 100644
index 0000000000..83a037a2bd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-media-toggle.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: Tests against a Chrome bug that modifying a sheet affects existing layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1313357">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+@layer foo, bar;
+@layer bar {
+ #target { background-color: green; }
+}
+@layer foo {
+ #target { background-color: red; }
+}
+</style>
+<style media="print" id="toggle">
+#target {
+ width: 100px;
+ height: 100px;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
+
+<script>
+document.body.offsetWidth; // Force style calculation
+document.getElementById('toggle').media = 'all';
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-property-override.html b/testing/web-platform/tests/css/css-cascade/layer-property-override.html
new file mode 100644
index 0000000000..9d3f9cb926
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-property-override.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>Resolving @property name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#target, #reference {
+ width: 100px;
+ height: 100px;
+}
+
+#reference {
+ background-color: green;
+}
+</style>
+
+<div id="target"></div>
+<div id="reference"></div>
+
+<script>
+// In all tests, background color of #target should be green, same as #reference
+
+const testCases = [
+ {
+ title: '@property unlayered overrides layered',
+ style: `
+ #target {
+ background-color: var(--foo);
+ }
+
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+
+ @layer {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@property override between layers',
+ style: `
+ @layer base, override;
+
+ #target {
+ background-color: var(--foo);
+ }
+
+ @layer override {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+
+ @layer base {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@property override update with appended sheet 1',
+ style: `
+ @layer base, override;
+
+ #target {
+ background-color: var(--foo);
+ }
+
+ @layer override {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+ `,
+ append: `
+ @layer base {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@property override update with appended sheet 2',
+ style: `
+ @layer base, override;
+
+ #target {
+ background-color: var(--foo);
+ }
+
+ @layer base {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `,
+ append: `
+ @layer override {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+ `
+ },
+];
+
+for (let testCase of testCases) {
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ var appendedStyle;
+ if (testCase['append']) {
+ document.body.offsetLeft; // Force style update
+ appendedStyle = document.createElement('style');
+ appendedStyle.appendChild(document.createTextNode(testCase['append']));
+ document.head.appendChild(appendedStyle);
+ }
+
+ test(function () {
+ assert_equals(getComputedStyle(target).backgroundColor,
+ getComputedStyle(reference).backgroundColor);
+ }, testCase['title']);
+
+ if (appendedStyle)
+ appendedStyle.remove();
+ documentStyle.remove();
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-replaceSync-clears-stale.html b/testing/web-platform/tests/css/css-cascade/layer-replaceSync-clears-stale.html
new file mode 100644
index 0000000000..c9d88681bd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-replaceSync-clears-stale.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: CSSStyleSheet.replaceSync clears stale statements</title>
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#layering">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="target"></div>
+<div id="reference" style="color: green"></div>
+
+<script>
+// In all test cases, the 'color' property value of #target should be green.
+
+const testCases = [
+ {
+ title: 'replaceSync clears stale layer statements',
+ style: `
+ @layer second, first;
+ @layer second {
+ #target { color: green; }
+ }
+ @layer first {
+ #target { color: red; }
+ }
+ `,
+ operations: function(sheet) {
+ sheet.replaceSync(`
+ @layer first {
+ #target { color: red; }
+ }
+ @layer second {
+ #target { color: green; }
+ }
+ `);
+ }
+ },
+];
+
+const target = document.getElementById('target');
+const reference = document.getElementById('reference');
+
+for (let testCase of testCases) {
+ test(t => {
+ let sheet = new CSSStyleSheet();
+ sheet.replaceSync(testCase.style);
+ document.adoptedStyleSheets = [sheet];
+
+ try {
+ testCase.operations(sheet);
+ assert_equals(getComputedStyle(target).color, getComputedStyle(reference).color);
+ } finally {
+ document.adoptedStyleSheets = [];
+ }
+ }, testCase.title);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-rules-cssom.html b/testing/web-platform/tests/css/css-cascade/layer-rules-cssom.html
new file mode 100644
index 0000000000..b81960df6f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-rules-cssom.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<title>The CSSOM API for Cascade Layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layer-apis">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const testCases = [
+ {
+ style: `@layer foo { }`,
+ expectedNames: ['foo'],
+ title: 'Basic layer block name',
+ },
+ {
+ style: `@layer { }`,
+ expectedNames: [''],
+ title: 'Anonymous layer block name',
+ },
+ {
+ style: `
+ @layer foo;
+ `,
+ expectedNames: [['foo']],
+ title: 'Basic layer statement name',
+ },
+ {
+ style: `
+ @layer foo, bar;
+ `,
+ expectedNames: [['foo', 'bar']],
+ title: 'Layer statement with multiple names',
+ },
+ {
+ style: `
+ @layer outer {
+ @layer foo.bar { }
+ }
+ @layer outer.foo.bar { }
+ `,
+ expectedNames: ['outer', 'foo.bar', 'outer.foo.bar'],
+ title: 'Nested layer block names',
+ },
+ {
+ style: `
+ @layer outer {
+ @layer foo.bar, baz;
+ }
+ @layer outer.foo.bar, outer.baz;
+ `,
+ expectedNames: ['outer', ['foo.bar', 'baz'], ['outer.foo.bar', 'outer.baz']],
+ title: 'Nested layer statement name lists',
+ },
+ {
+ style: `
+ @import url('data:text/css,') layer;
+ `,
+ expectedNames: [''],
+ title: 'Import into anonymous layer',
+ },
+ {
+ style: `
+ @import url('data:text/css,') layer(foo);
+ `,
+ expectedNames: ['foo'],
+ title: 'Import into named layer',
+ },
+ {
+ style: `
+ @import url('data:text/css,');
+ `,
+ expectedNames: [null],
+ title: 'Import without layer',
+ },
+];
+
+for (let testCase of testCases) {
+ promise_test(async function (t) {
+ assert_implements(window.CSSLayerBlockRule);
+ assert_implements(window.CSSLayerStatementRule);
+
+ const style = document.createElement('style');
+ t.add_cleanup(() => style.remove());
+
+ const isLoadAsync = testCase.style.includes("@import");
+ const load = new Promise(resolve => {
+ style.addEventListener("load", resolve, { once: true });
+ });
+
+ style.appendChild(document.createTextNode(testCase.style));
+ document.head.appendChild(style);
+
+ if (isLoadAsync) {
+ await load;
+ }
+
+ let index = 0;
+ function compareNames(ruleOrSheet) {
+ if (ruleOrSheet instanceof CSSLayerBlockRule)
+ assert_equals(ruleOrSheet.name, testCase.expectedNames[index++]);
+ else if (ruleOrSheet instanceof CSSImportRule)
+ assert_equals(ruleOrSheet.layerName, testCase.expectedNames[index++]);
+ else if (ruleOrSheet instanceof CSSLayerStatementRule)
+ assert_array_equals(ruleOrSheet.nameList, testCase.expectedNames[index++]);
+ if (ruleOrSheet.cssRules) {
+ for (let i = 0; i < ruleOrSheet.cssRules.length; ++i)
+ compareNames(ruleOrSheet.cssRules.item(i));
+ }
+ }
+ compareNames(style.sheet);
+ assert_equals(index, testCase.expectedNames.length);
+ }, testCase.title);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-slotted-rule.html b/testing/web-platform/tests/css/css-cascade/layer-slotted-rule.html
new file mode 100644
index 0000000000..a33a5a6787
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-slotted-rule.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>::slotted rules should be associated with the correct cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="host">
+ <div id="target"></target>
+</div>
+
+<script>
+const host = document.getElementById('host');
+host.attachShadow({mode: 'open'}).innerHTML = `
+<style>
+@layer {
+ ::slotted(*) {
+ background-color: green !important;
+ }
+}
+::slotted(*) {
+ background-color: red !important;
+}
+</style>
+<slot></slot>
+`;
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-statement-before-import.html b/testing/web-platform/tests/css/css-cascade/layer-statement-before-import.html
new file mode 100644
index 0000000000..fcde960532
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-statement-before-import.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: Empty layer statements before import rules</title>
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#layering">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="target"></div>
+<div id="reference" style="color: green"></div>
+
+<script>
+// In all test cases, the 'color' property value of #target should be green.
+
+const testCases = [
+ {
+ title: 'length and item',
+ style: `
+ @layer first, second;
+ @import url(data:text/css,);
+ @layer second {
+ #target { color: green; }
+ }
+ @layer first {
+ #target { color: red; }
+ }
+ `,
+ operations: function(sheet) {
+ assert_equals(sheet.cssRules.length, 4);
+ assert_equals(sheet.cssRules.item(0).cssText, '@layer first, second;');
+ assert_equals(sheet.cssRules.item(1).cssText, `@import url("data:text/css,");`);
+ assert_equals(sheet.cssRules.item(2).cssText,
+ '@layer second {\n #target { color: green; }\n}');
+ assert_equals(sheet.cssRules.item(3).cssText,
+ '@layer first {\n #target { color: red; }\n}');
+ }
+ },
+ {
+ title: 'insertRule before imports',
+ style: `
+ @import url(data:text/css,);
+ @layer second {
+ #target { color: green; }
+ }
+ @layer first {
+ #target { color: red; }
+ }
+ `,
+ operations: function(sheet) {
+ sheet.insertRule('@layer first, second', 0);
+ }
+ },
+ {
+ title: 'insertRule after imports',
+ style: `
+ @layer first, second;
+ @import url(data:text/css,);
+ @layer first {
+ #target { color: red; }
+ }
+ `,
+ operations: function(sheet) {
+ sheet.insertRule('@layer second { #target { color: green; } }', 2);
+ }
+ },
+ {
+ title: 'insert other rules to pre-import layer statements fails',
+ style: `
+ @layer first, second;
+ @import url(data:text/css,);
+ @layer second {
+ #target { color: green; }
+ }
+ @layer first {
+ #target { color: red; }
+ }
+ `,
+ operations: function(sheet) {
+ assert_throws_dom('HierarchyRequestError',
+ () => sheet.insertRule('#target { color: red !important; }', 0));
+ assert_throws_dom('HierarchyRequestError',
+ () => sheet.insertRule('#target { color: red !important; }', 1));
+ }
+ },
+ {
+ title: 'insert other rules before the first layer statement without imports',
+ style: `
+ @layer first, second;
+ @layer second {
+ #target { color: red !important; }
+ }
+ `,
+ operations: function(sheet) {
+ sheet.insertRule(`@layer first {
+ #target { color: green !important; }
+ }`, 0);
+ }
+ },
+ {
+ title: 'deleteRule before imports',
+ style: `
+ @layer second, first;
+ @import url(data:text/css,);
+ @layer first {
+ #target { color: red; }
+ }
+ @layer second {
+ #target { color: green; }
+ }
+ `,
+ operations: function(sheet) {
+ sheet.deleteRule(0);
+ }
+ },
+ {
+ title: 'deleteRule after imports',
+ style: `
+ @layer first, second;
+ @import url(data:text/css,);
+ @layer second {
+ #target { color: green; }
+ }
+ @layer first {
+ #target { color: red; }
+ }
+ #target {
+ color: red;
+ }
+ `,
+ operations: function(sheet) {
+ sheet.deleteRule(4);
+ }
+ },
+];
+
+const target = document.getElementById('target');
+const reference = document.getElementById('reference');
+
+for (let testCase of testCases) {
+ promise_test(async t => {
+ let styleElement = document.createElement('style');
+ styleElement.textContent = testCase.style;
+ await new Promise(resolve => {
+ styleElement.onload = resolve;
+ styleElement.onerror = resolve;
+ document.head.append(styleElement);
+ });
+ let sheet = styleElement.sheet;
+
+ try {
+ testCase.operations(sheet);
+ assert_equals(getComputedStyle(target).color, getComputedStyle(reference).color);
+ } finally {
+ styleElement.remove();
+ }
+ }, testCase.title);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-statement-copy-crash.html b/testing/web-platform/tests/css/css-cascade/layer-statement-copy-crash.html
new file mode 100644
index 0000000000..f183ab30ab
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-statement-copy-crash.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Chromium bug: Crash when copying layer statement rule from memory cache</title>
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#layer-empty">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1345181">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="stylesheet" href="data:text/css,@layer foo;@media(all){}">
+<link rel="stylesheet" href="data:text/css,@layer foo;@media(all){}">
+<body>
+ <p style="color: green">Test passes if it does not crash.</p>
+</body>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-important.html b/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-important.html
new file mode 100644
index 0000000000..7b3ff4abc6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-important.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>Test important style in anonymous layers with stylesheet sharing</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="layer-stylesheet-sharing-ref.html">
+<style>
+ target {
+ display: block;
+ width: 100px;
+ height: 100px;
+ }
+</style>
+<link rel=stylesheet href="data:text/css,@layer{target{background-color:green !important}}">
+<style>
+@layer A { target { background-color: red !important} }
+</style>
+<link rel=stylesheet href="data:text/css,@layer{target{background-color:green !important}}">
+<target></target> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-ref.html b/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-ref.html
new file mode 100644
index 0000000000..fe004e5bda
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing-ref.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<style>
+ target {
+ display: block;
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ }
+</style>
+<target></target>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing.html b/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing.html
new file mode 100644
index 0000000000..c172baaf81
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-stylesheet-sharing.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Test anonymous layers with stylesheet sharing</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1730123">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="match" href="layer-stylesheet-sharing-ref.html">
+<style>
+ target {
+ display: block;
+ width: 100px;
+ height: 100px;
+ }
+</style>
+<link rel=stylesheet href="data:text/css,@layer{target{background-color:green}}">
+<style>
+@layer A { target { background-color: red } }
+</style>
+<link rel=stylesheet href="data:text/css,@layer{target{background-color:green}}">
+<target></target>
diff --git a/testing/web-platform/tests/css/css-cascade/layer-vs-inline-style.html b/testing/web-platform/tests/css/css-cascade/layer-vs-inline-style.html
new file mode 100644
index 0000000000..9ddfbc3907
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/layer-vs-inline-style.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#cascade-sort">
+<link rel="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#target, #reference {
+ width: 100px;
+ height: 100px;
+}
+
+#reference {
+ background-color: green;
+}
+</style>
+
+<div id="target"></div>
+<div id="reference"></div>
+
+<script>
+// In all tests, #target should have green background color, same as #reference
+
+const testCases = [
+ {
+ title: 'Normal inline style > normal layered style',
+ style: '@layer { #target { background-color: red; }}',
+ inlineStyle: 'background-color: green'
+ },
+ {
+ title: 'Normal inline style < important layered style',
+ style: '@layer { #target { background-color: green !important; }}',
+ inlineStyle: 'background-color: red'
+ },
+ {
+ title: 'Important inline style > normal layered style',
+ style: '@layer { #target { background-color: red; }}',
+ inlineStyle: 'background-color: green !important'
+ },
+ {
+ title: 'Important inline style > important layered style',
+ style: '@layer { #target { background-color: red !important; }}',
+ inlineStyle: 'background-color: green !important'
+ },
+];
+
+for (let testCase of testCases) {
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ target.style = testCase['inlineStyle'];
+
+ test(function () {
+ assert_equals(getComputedStyle(target).backgroundColor,
+ getComputedStyle(reference).backgroundColor);
+ }, testCase['title']);
+
+ documentStyle.remove();
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/parsing/all-invalid.html b/testing/web-platform/tests/css/css-cascade/parsing/all-invalid.html
new file mode 100644
index 0000000000..4a1d045ecc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/parsing/all-invalid.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Cascading and Inheritance Level 3: parsing all with invalid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-3/#propdef-all">
+<meta name="assert" content="all supports only the grammar 'initial | inherit | unset'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("all", "auto");
+test_invalid_value("all", "none");
+test_invalid_value("all", "filter");
+test_invalid_value("all", "unset inherit");
+test_invalid_value("all", "inherit initial");
+test_invalid_value("all", "initial unset");
+test_invalid_value("all", "opacity transform");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/parsing/all-valid.html b/testing/web-platform/tests/css/css-cascade/parsing/all-valid.html
new file mode 100644
index 0000000000..3a9e5922de
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/parsing/all-valid.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Cascading and Inheritance Level 3: parsing all with valid values</title>
+<link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-3/#propdef-all">
+<meta name="assert" content="all supports the full grammar 'initial | inherit | unset'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("all", "initial");
+test_valid_value("all", "inherit");
+test_valid_value("all", "unset");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/parsing/layer-import-parsing.html b/testing/web-platform/tests/css/css-cascade/parsing/layer-import-parsing.html
new file mode 100644
index 0000000000..f879ba8897
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/parsing/layer-import-parsing.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>@import rule with layer parsing / serialization</title>
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#at-import">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ function setupSheet(rule) {
+ const style = document.createElement("style");
+ document.head.append(style);
+ const {sheet} = style;
+ const {cssRules} = sheet;
+
+ assert_equals(cssRules.length, 0, "Sheet should have no rules");
+ sheet.insertRule(rule);
+ assert_equals(cssRules.length, 1, "Sheet should have 1 rule");
+
+ return {sheet, cssRules};
+ }
+
+ function test_valid_layer_import(rule, serialized) {
+ if (serialized === undefined)
+ serialized = rule;
+
+ test(function() {
+ const {sheet, cssRules} = setupSheet(rule);
+
+ const serialization = cssRules[0].cssText;
+ assert_equals(serialization, serialized, 'serialization should be canonical');
+
+ const media = cssRules[0].media;
+ assert_equals(media.length, 0, 'layer() should be valid');
+
+ sheet.deleteRule(0);
+ assert_equals(cssRules.length, 0, 'Sheet should have no rule');
+ sheet.insertRule(serialization);
+ assert_equals(cssRules.length, 1, 'Sheet should have 1 rule');
+
+ assert_equals(cssRules[0].cssText, serialization, 'serialization should round-trip');
+ }, rule + ' should be a valid layered import rule');
+ }
+
+ function test_invalid_layer_import(rule) {
+ test(function() {
+ const {sheet, cssRules} = setupSheet(rule);
+
+ const media = cssRules[0].media;
+ assert_not_equals(media.length, 0,
+ 'invalid layer declaration should be parsed as <general-enclosed> media query');
+
+ sheet.deleteRule(0);
+ assert_equals(cssRules.length, 0, 'Sheet should have no rule');
+ }, rule + ' should still be a valid import rule with an invalid layer declaration');
+ }
+
+ test_valid_layer_import('@import url("nonexist.css") layer;');
+ test_valid_layer_import('@import url("nonexist.css") layer(A);');
+ test_valid_layer_import('@import url("nonexist.css") layer(A.B);');
+
+ test_valid_layer_import('@import url(nonexist.css) layer;',
+ '@import url("nonexist.css") layer;');
+ test_valid_layer_import('@import url(nonexist.css) layer(A);',
+ '@import url("nonexist.css") layer(A);');
+ test_valid_layer_import('@import url(nonexist.css) layer(A.B);',
+ '@import url("nonexist.css") layer(A.B);');
+
+ test_valid_layer_import('@import "nonexist.css" layer;',
+ '@import url("nonexist.css") layer;');
+ test_valid_layer_import('@import "nonexist.css" layer(A);',
+ '@import url("nonexist.css") layer(A);');
+ test_valid_layer_import('@import "nonexist.css" layer(A.B);',
+ '@import url("nonexist.css") layer(A.B);');
+
+ test_invalid_layer_import('@import url("nonexist.css") layer();');
+ test_invalid_layer_import('@import url("nonexist.css") layer(A B);');
+ test_invalid_layer_import('@import url("nonexist.css") layer(A . B);');
+ test_invalid_layer_import('@import url("nonexist.css") layer(A, B, C);');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/parsing/layer.html b/testing/web-platform/tests/css/css-cascade/parsing/layer.html
new file mode 100644
index 0000000000..3bfc863ede
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/parsing/layer.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>@layer rule parsing / serialization</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+<script>
+ test_valid_rule("@layer A;");
+ test_valid_rule("@layer A, B, C;");
+ test_valid_rule("@layer A.A;");
+ test_valid_rule("@layer A, B.C.D, C;");
+
+ test_invalid_rule("@layer;");
+ test_invalid_rule("@layer A . A;");
+
+ test_valid_rule("@layer {\n}");
+ test_valid_rule("@layer A {\n}");
+ test_valid_rule("@layer A.B {\n}");
+ test_invalid_rule("@layer A . B {\n}");
+
+ test_invalid_rule("@layer A, B, C {\n}");
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/parsing/supports-import-parsing.html b/testing/web-platform/tests/css/css-cascade/parsing/supports-import-parsing.html
new file mode 100644
index 0000000000..64cf930e6c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/parsing/supports-import-parsing.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>@import rule with supports parsing / serialization</title>
+<link rel="author" href="mailto:oj@oojmed.com">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-4/#at-import">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ function setupSheet(rule) {
+ const style = document.createElement("style");
+ document.head.append(style);
+ const {sheet} = style;
+ const {cssRules} = sheet;
+
+ assert_equals(cssRules.length, 0, "Sheet should have no rules");
+ sheet.insertRule(rule);
+ assert_equals(cssRules.length, 1, "Sheet should have 1 rule");
+
+ return {sheet, cssRules};
+ }
+
+ function test_valid_supports_import(rule, serialized) {
+ if (serialized === undefined)
+ serialized = rule;
+
+ test(function() {
+ const {sheet, cssRules} = setupSheet(rule);
+
+ const serialization = cssRules[0].cssText;
+ assert_equals(serialization, serialized, 'serialization should be canonical');
+
+ sheet.deleteRule(0);
+ assert_equals(cssRules.length, 0, 'Sheet should have no rule');
+ sheet.insertRule(serialization);
+ assert_equals(cssRules.length, 1, 'Sheet should have 1 rule');
+
+ assert_equals(cssRules[0].cssText, serialization, 'serialization should round-trip');
+ }, rule + ' should be a valid supports() import rule');
+ }
+
+ function test_invalid_supports_import(rule) {
+ test(function() {
+ const {sheet, cssRules} = setupSheet(rule);
+
+ sheet.deleteRule(0);
+ assert_equals(cssRules.length, 0, 'Sheet should have no rule');
+ }, rule + ' should still be a valid import rule with an invalid supports() declaration');
+ }
+
+ test_valid_supports_import('@import url("nonexist.css") supports();');
+ test_valid_supports_import('@import url("nonexist.css") supports(display:block);');
+ test_valid_supports_import('@import url("nonexist.css") supports((display:flex));');
+ test_valid_supports_import('@import url("nonexist.css") supports(not (display: flex));');
+ test_valid_supports_import('@import url("nonexist.css") supports((display: flex) and (display: block));');
+ test_valid_supports_import('@import url("nonexist.css") supports((display: flex) or (display: block));');
+ test_valid_supports_import('@import url("nonexist.css") supports((display: flex) or (foo: bar));');
+ test_valid_supports_import('@import url("nonexist.css") supports(display: block !important);');
+
+ test_valid_supports_import('@import url("nonexist.css") layer supports();');
+ test_valid_supports_import('@import url("nonexist.css") layer(A) supports((display: flex) or (foo: bar));');
+ test_valid_supports_import('@import url("nonexist.css") layer(A.B) supports((display: flex) and (foo: bar));');
+
+ test_valid_supports_import('@import url("nonexist.css") supports(selector(a));');
+ test_valid_supports_import('@import url("nonexist.css") supports(selector(p a));');
+ test_valid_supports_import('@import url("nonexist.css") supports(selector(p > a));');
+ test_valid_supports_import('@import url("nonexist.css") supports(selector(p + a));');
+
+ test_valid_supports_import('@import url("nonexist.css") supports(font-tech(color-colrv1));');
+ test_valid_supports_import('@import url("nonexist.css") supports(font-format(opentype));');
+
+ test_valid_supports_import('@import url(nonexist.css) supports(display:block);',
+ '@import url("nonexist.css") supports(display:block);');
+
+ test_valid_supports_import('@import "nonexist.css" supports(display:block);',
+ '@import url("nonexist.css") supports(display:block);');
+
+ test_invalid_supports_import('@import url("nonexist.css") supports;');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/presentational-hints-cascade.html b/testing/web-platform/tests/css/css-cascade/presentational-hints-cascade.html
new file mode 100644
index 0000000000..c3188fd0d7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/presentational-hints-cascade.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#preshint">
+<link rel="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+.test {
+ height: 100px;
+ background-color: green;
+}
+
+#target1 {
+ width: 100px;
+}
+
+@layer {
+ #target3 {
+ width: 100px;
+ }
+}
+</style>
+
+<img class=test id=target1 width=200>
+<img class=test id=target2 width=200 style="width: 100px">
+<img class=test id=target3 width=200>
+
+<script>
+test(() => {
+ assert_equals(getComputedStyle(target1).width, '100px');
+}, 'Presentational hints have lower precedence than regular author style sheets');
+
+test(() => {
+ assert_equals(getComputedStyle(target2).width, '100px');
+}, 'Presentational hints have lower precedence than the style attribute');
+
+test(() => {
+ assert_equals(getComputedStyle(target3).width, '100px');
+}, 'Presentational hints have lower precedence than layered style');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/presentational-hints-rollback.html b/testing/web-platform/tests/css/css-cascade/presentational-hints-rollback.html
new file mode 100644
index 0000000000..8178daf60c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/presentational-hints-rollback.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Cascade: rolling back the cascade with presentation hints</title>
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#preshint">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#default">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-5/#revert-layer">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes">
+<meta name="assert" content="Checks that 'revert' considers presentational hints as part of the author origin,
+ and 'revert-layer' considers them an independent origin between the user origin and the author origin.">
+
+<style>
+@layer {
+ .revert-1 {
+ width: revert;
+ height: revert;
+ }
+ .revert-layer-1 {
+ width: revert-layer;
+ height: revert-layer;
+ }
+}
+
+.revert-2 {
+ width: revert;
+ height: revert;
+}
+.revert-layer-2 {
+ width: revert-layer;
+ height: revert-layer;
+}
+
+.revert-3 {
+ animation: revert-3 paused 2s -1s;
+}
+.revert-layer-3 {
+ animation: revert-layer-3 paused 2s -1s;
+}
+@keyframes revert-3 {
+ from, to {
+ width: revert;
+ height: revert;
+ }
+}
+@keyframes revert-layer-3 {
+ from, to {
+ width: revert-layer;
+ height: revert-layer;
+ }
+}
+</style>
+
+<div id="log"></div>
+
+<div id="tests">
+ <!-- 'revert' considers presentational hints as part of the author origin, so it rolls back to user origin.
+ The images should then get an 'auto' size, which will use the natural size of the resource. -->
+ <img class="revert-1" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="60"
+ height="33" data-expected-client-height="60">
+ <img class="revert-2" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="60"
+ height="33" data-expected-client-height="60">
+ <img class="revert-3" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="60"
+ height="33" data-expected-client-height="60">
+ <img style="width: revert; height: revert" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="60"
+ height="33" data-expected-client-height="60">
+
+ <!-- 'revert-layer' considers presentational hints as an independent origin, so it rolls back to them.
+ The images should then get size specified in the attributes. -->
+ <img class="revert-layer-1" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33">
+ <img class="revert-layer-2" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33">
+ <img class="revert-layer-3" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33">
+ <img style="width: revert-layer; height: revert-layer" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33">
+
+ <!-- 'revert' considers presentational hints as part of the author origin, so it rolls back to user origin.
+ The iframes should then get an 'auto' size, which will default to 300x150. -->
+ <iframe class="revert-1" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="300"
+ height="33" data-expected-client-height="150"></iframe>
+ <iframe class="revert-2" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="300"
+ height="33" data-expected-client-height="150"></iframe>
+ <iframe class="revert-3" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="300"
+ height="33" data-expected-client-height="150"></iframe>
+ <iframe style="width: revert; height: revert" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="300"
+ height="33" data-expected-client-height="150"></iframe>
+
+ <!-- 'revert-layer' considers presentational hints as an independent origin, so it rolls back to them.
+ The iframes should then get size specified in the attributes. -->
+ <iframe class="revert-layer-1" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33"></iframe>
+ <iframe class="revert-layer-2" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33"></iframe>
+ <iframe class="revert-layer-3" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33"></iframe>
+ <iframe style="width: revert-layer; height: revert-layer" src="/css/support/60x60-green.png"
+ width="44" data-expected-client-width="44"
+ height="33" data-expected-client-height="33"></iframe>
+</div>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<script>
+addEventListener("load", function() {
+ checkLayout("#tests > *", false);
+ done();
+}, {once: true});
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/reference/all-green.html b/testing/web-platform/tests/css/css-cascade/reference/all-green.html
new file mode 100644
index 0000000000..c70532129a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/reference/all-green.html
@@ -0,0 +1 @@
+<html style="background: green"></html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-cascade/reference/ref-filled-green-100px-square.xht b/testing/web-platform/tests/css/css-cascade/reference/ref-filled-green-100px-square.xht
new file mode 100644
index 0000000000..05a1379448
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/reference/ref-filled-green-100px-square.xht
@@ -0,0 +1,19 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>CSS Reftest Reference</title>
+ <link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/" />
+ <style type="text/css"><![CDATA[
+ div
+ {
+ background-color: green;
+ height: 100px;
+ width: 100px;
+ }
+ ]]></style>
+ </head>
+ <body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+ <div></div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/reference/ref-green-text.html b/testing/web-platform/tests/css/css-cascade/reference/ref-green-text.html
new file mode 100644
index 0000000000..8183c04087
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/reference/ref-green-text.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Green text reference</title>
+<style>
+ .test { color: green; }
+</style>
+<body>
+ <p class="test">Test passes if this text is green.</p>
+</body>
diff --git a/testing/web-platform/tests/css/css-cascade/resources/scope.css b/testing/web-platform/tests/css/css-cascade/resources/scope.css
new file mode 100644
index 0000000000..780e90fb67
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/resources/scope.css
@@ -0,0 +1,4 @@
+@scope {
+ :scope { z-index:1; }
+ .a { z-index:2; }
+}
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-001.html b/testing/web-platform/tests/css/css-cascade/revert-layer-001.html
new file mode 100644
index 0000000000..009867e751
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-001.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' from one explicit layer to another</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+}
+
+@layer {
+ #target { background-color: green; }
+}
+
+@layer {
+ #target {
+ background-color: red;
+ background-color: revert-layer;
+ }
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-002.html b/testing/web-platform/tests/css/css-cascade/revert-layer-002.html
new file mode 100644
index 0000000000..38d3d33d93
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-002.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' from the implicit outer layer to explicit</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+}
+
+@layer {
+ #target { background-color: green; }
+}
+
+#target {
+ background-color: red;
+ background-color: revert-layer;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-003.html b/testing/web-platform/tests/css/css-cascade/revert-layer-003.html
new file mode 100644
index 0000000000..e4e331c82d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-003.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'all: revert-layer'</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+@layer {
+ #target {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ }
+}
+
+@layer {
+ #target {
+ width: 200px;
+ height: 200px;
+ background-color: red;
+ }
+
+ #target {
+ all: revert-layer;
+ }
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-004.html b/testing/web-platform/tests/css/css-cascade/revert-layer-004.html
new file mode 100644
index 0000000000..b751359857
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-004.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' to previous context</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+ background-color: revert-layer;
+}
+
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
+
+<script>
+target.attachShadow({mode: 'open'}).innerHTML = `
+<style>
+:host {
+ background-color: green;
+}
+</style>
+`;
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-005.html b/testing/web-platform/tests/css/css-cascade/revert-layer-005.html
new file mode 100644
index 0000000000..6cd4030727
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-005.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: important 'revert-layer'</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+}
+
+@layer {
+ #target { background-color: green; }
+}
+
+@layer {
+ #target {
+ background-color: red;
+ background-color: red !important;
+ background-color: revert-layer !important;
+ }
+}
+
+@layer {
+ #target {
+ background-color: red;
+ background-color: red !important;
+ }
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-006.html b/testing/web-platform/tests/css/css-cascade/revert-layer-006.html
new file mode 100644
index 0000000000..678c3e1e9b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-006.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' reverts origin when no lower priority declarations in the same origin</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#outer {
+ background-color: red;
+ width: 100px;
+ height: 100px;
+ overflow: hidden;
+}
+#inner {
+ color: green;
+ background-color: green;
+ display: inline;
+ display: revert-layer; /* This should behave as 'revert', setting 'display' to 'block' */
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="outer">
+ <div id="inner">
+ This<br>
+ is<br>
+ filler<br>
+ text.<br>
+ This<br>
+ is<br>
+ filler<br>
+ text.
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-007.html b/testing/web-platform/tests/css/css-cascade/revert-layer-007.html
new file mode 100644
index 0000000000..7915beeafa
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-007.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' chain</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+}
+
+@layer {
+ #target { background-color: green; }
+}
+
+@layer {
+ #target {
+ background-color: red;
+ background-color: revert-layer;
+ }
+}
+
+@layer {
+ #target {
+ background-color: red;
+ background-color: revert-layer;
+ }
+}
+
+@layer {
+ #target {
+ background-color: red;
+ background-color: revert-layer;
+ }
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-008.html b/testing/web-platform/tests/css/css-cascade/revert-layer-008.html
new file mode 100644
index 0000000000..cafb17dee1
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-008.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' triggers a smooth transition</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+@layer revert-to, revert-from;
+
+@layer revert-from {
+ #target {
+ font-size: 10px;
+ transition: font-size 2s linear -1s;
+ }
+
+ #target.reverted {
+ font-size: revert-layer;
+ }
+}
+
+@layer revert-to {
+ #target { font-size: 20px; }
+}
+</style>
+
+<div id="target"></div>
+
+<script>
+function raf() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
+promise_test(async () => {
+ const target = document.getElementById('target');
+ getComputedStyle(target).getPropertyValue('font-size');
+
+ await raf();
+ target.classList.toggle('reverted');
+
+ const result = getComputedStyle(target).getPropertyValue('font-size');
+ assert_equals(result, '15px');
+}, "'revert-layer' should revert font-size to 20px and trigger a smooth transition");
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-009.html b/testing/web-platform/tests/css/css-cascade/revert-layer-009.html
new file mode 100644
index 0000000000..e5c8e62ae0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-009.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' from the style attribute to other style sheets</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target" style="background-color: red; background-color: revert-layer"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-010.html b/testing/web-platform/tests/css/css-cascade/revert-layer-010.html
new file mode 100644
index 0000000000..278905c6cf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-010.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' from animation origin to author origin</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 150px;
+ height: 100px;
+ background-color: green;
+ animation: anim linear 2s -1s paused;
+}
+
+@keyframes anim {
+ from { width: 50px; }
+ to { width: revert-layer; }
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-011.html b/testing/web-platform/tests/css/css-cascade/revert-layer-011.html
new file mode 100644
index 0000000000..73a3772f80
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-011.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' from animation origin to author origin on custom property</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: var(--x);
+ --x: 150px;
+ height: 100px;
+ background-color: green;
+ animation: anim linear 2s -1s paused;
+}
+
+@property --x {
+ syntax: '<length>';
+ initial-value: 0px;
+ inherits: false;
+}
+
+@keyframes anim {
+ from { --x: 50px; }
+ to { --x: revert-layer; }
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-012.html b/testing/web-platform/tests/css/css-cascade/revert-layer-012.html
new file mode 100644
index 0000000000..e065defb53
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-012.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: important 'revert-layer' from the style attribute to other style sheets</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: green !important;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target" style="background-color: red !important; background-color: revert-layer !important"></div>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-013.html b/testing/web-platform/tests/css/css-cascade/revert-layer-013.html
new file mode 100644
index 0000000000..862ee72746
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-013.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' to host context</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: revert-layer;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="target"></div>
+
+<script>
+target.attachShadow({mode: 'open'}).innerHTML = `
+<style>
+@layer first {
+ :host { background-color: green; }
+}
+@layer second {
+ :host { background-color: revert-layer; }
+}
+</style>
+`;
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-014.html b/testing/web-platform/tests/css/css-cascade/revert-layer-014.html
new file mode 100644
index 0000000000..6b96862562
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-014.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' in slotted context</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: revert-layer;
+}
+</style>
+
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="host"><div id="target"></div></div>
+
+<script>
+host.attachShadow({mode: 'open'}).innerHTML = `
+<style>
+@layer first {
+ ::slotted(*) { background-color: green; }
+}
+@layer second {
+ ::slotted(*) { background-color: revert-layer; }
+}
+</style>
+<slot></slot>
+`;
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-015-ref.html b/testing/web-platform/tests/css/css-cascade/revert-layer-015-ref.html
new file mode 100644
index 0000000000..661016619c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-015-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<style>
+input::placeholder { background-color: green; }
+</style>
+<input placeholder="placeholder">
diff --git a/testing/web-platform/tests/css/css-cascade/revert-layer-015.html b/testing/web-platform/tests/css/css-cascade/revert-layer-015.html
new file mode 100644
index 0000000000..a60f5d78f5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-layer-015.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>CSS Cascade Layers: 'revert-layer' with shadow pseudo-element</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#revert-layer">
+<link rel="match" href="revert-layer-015-ref.html">
+
+<style>
+@layer first {
+ input::placeholder { background-color: green; }
+}
+@layer second {
+ input::placeholder { background-color: revert-layer; }
+}
+</style>
+<input placeholder="placeholder">
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-001.html b/testing/web-platform/tests/css/css-cascade/revert-val-001.html
new file mode 100644
index 0000000000..b3d79d9e69
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-001.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: 'revert' keyword for 'display' property of div element</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="help" href="https://www.w3.org/TR/css-cascade-4/#default">
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/rendering.html#flow-content-3">
+ <link rel="match" href="reference/ref-filled-green-100px-square.xht">
+ <meta name="assert" content="On a <div>, display:revert should compute to display:block per the default styles for <div>s in the UA stylesheet.">
+ <style>
+#outer {
+ background-color: red;
+ width: 100px;
+ height: 100px;
+ overflow: hidden;
+}
+#inner {
+ color: green;
+ background-color: green;
+ display: inline;
+ display: revert;/* since #inner is a <div>, this should compute to 'block' */
+}
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+ <div id="outer">
+ <div id="inner">
+ This<br>
+ is<br>
+ filler<br>
+ text.<br>
+ This<br>
+ is<br>
+ filler<br>
+ text.
+ </div>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-002.html b/testing/web-platform/tests/css/css-cascade/revert-val-002.html
new file mode 100644
index 0000000000..d145ea42b2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-002.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Cascade: 'revert' keyword interaction with !important</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<link rel="match" href="reference/ref-filled-green-100px-square.xht">
+<style>
+#outer {
+ background-color: red;
+ width: 100px;
+ height: 100px;
+ overflow: hidden;
+}
+#inner {
+ /* This should win over `revert` */
+ display: block !important;
+}
+#inner {
+ color: green;
+ background-color: green;
+ display: revert;
+}
+</style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+ <div id="outer">
+ <span id="inner">
+ This<br>
+ is<br>
+ filler<br>
+ text.<br>
+ This<br>
+ is<br>
+ filler<br>
+ text.
+ </span>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-003.html b/testing/web-platform/tests/css/css-cascade/revert-val-003.html
new file mode 100644
index 0000000000..b819eb0b2d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-003.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Cascade: 'revert' keyword in transition</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+html, body { margin: 0 }
+h1 {
+ margin: 0;
+ transition: margin 10s;
+ transition-delay: -5s; /* So we can expect it to be half-way the transition when toggling the property */
+}
+</style>
+<h1>This is a header that should get some margin</h1>
+<script>
+test(function() {
+ const el = document.querySelector("h1");
+ const cs = getComputedStyle(el);
+ assert_equals(cs.marginTop, "0px", "Margin before transition");
+ el.style.margin = "revert";
+ const midTransition = cs.marginTop;
+ assert_not_equals(midTransition, "0px", "Margin mid transition");
+ el.style.transition = "none";
+ assert_not_equals(cs.marginTop, midTransition, "Default margin");
+}, "revert works with transitions");
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-004.html b/testing/web-platform/tests/css/css-cascade/revert-val-004.html
new file mode 100644
index 0000000000..6a7046c0e4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-004.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>CSS Cascade: using 'revert' with the 'all' property</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ div {
+ display: inline;
+ }
+ .revert {
+ all: revert;
+ }
+</style>
+<div id=div></div>
+<script>
+ test(function() {
+ let cs = getComputedStyle(div);
+ assert_equals(cs.display, 'inline');
+ div.className = 'revert';
+ assert_equals(cs.display, 'block');
+ }, 'The revert keyword works with the all property');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-005.html b/testing/web-platform/tests/css/css-cascade/revert-val-005.html
new file mode 100644
index 0000000000..7295605d4b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-005.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>CSS Cascade: 'revert' in css-logical properties</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #h1_physical {
+ margin: 0px;
+ margin: revert;
+ }
+ #h1_logical {
+ margin: 0px;
+ margin-inline-start: revert;
+ margin-inline-end: revert;
+ margin-block-start: revert;
+ margin-block-end: revert;
+ }
+</style>
+<h1 id=h1_physical></h1>
+<h1 id=h1_logical></h1>
+<h1 id=ref></h1>
+<script>
+ test(function() {
+ let actual = getComputedStyle(h1_physical).marginTop;
+ let expected = getComputedStyle(ref).marginTop;
+ // This test assumes that the UA style sheet sets a non-0px value on
+ // <h1> elements:
+ assert_not_equals(expected, '0px');
+ assert_equals(actual, expected);
+ }, 'The revert keyword works with physical properties');
+
+ test(function() {
+ let actual = getComputedStyle(h1_logical).marginTop;
+ let expected = getComputedStyle(ref).marginTop;
+ // This test assumes that the UA style sheet sets a non-0px value on
+ // <h1> elements:
+ assert_not_equals(expected, '0px');
+ assert_equals(actual, expected);
+ }, 'The revert keyword works with logical properties');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-006.html b/testing/web-platform/tests/css/css-cascade/revert-val-006.html
new file mode 100644
index 0000000000..2b238f6d7e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-006.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>CSS Cascade: 'revert' keyword in keyframe animations</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @keyframes test {
+ from { margin-top: 0px; }
+ 50% { margin-top: revert; }
+ to { margin-top: 0px; }
+ }
+ #h1 {
+ margin-top: 0px;
+ animation: test linear 1000s -500s;
+ }
+</style>
+<h1 id=h1></h1>
+<h1 id=ref></h1>
+<script>
+ test(function() {
+ let actual = getComputedStyle(h1).marginTop;
+ let expected = getComputedStyle(ref).marginTop;
+ // This test assumes that the UA style sheet sets a non-0px value on
+ // <h1> elements:
+ assert_not_equals(expected, '0px');
+ assert_equals(actual, expected);
+ }, 'The revert keyword works with @keyframes');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-007.html b/testing/web-platform/tests/css/css-cascade/revert-val-007.html
new file mode 100644
index 0000000000..38078fcfa9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-007.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<title>CSS Cascade: 'revert' in keyframe animations on identical elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<link rel="help" href="https://crbug.com/1065387">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @keyframes test {
+ from { margin-top: revert; }
+ to { margin-top: 100px; }
+ }
+ .anim {
+ margin-top: 0px;
+ animation: test linear 1s paused;
+ }
+</style>
+<h1 class="anim"></h1>
+<h1 class="anim"></h1>
+<h1 class="anim"></h1>
+<h1 id=ref></h1>
+<script>
+ test(function() {
+ // This querySelectorAll includes #ref, but that's OK.
+ let targets = document.querySelectorAll('h1');
+ for (let t of targets) {
+ let actual = getComputedStyle(t).marginTop;
+ let expected = getComputedStyle(ref).marginTop;
+ // This test assumes that the UA style sheet sets a non-0px value on
+ // <h1> elements:
+ assert_not_equals(expected, '0px');
+ assert_equals(actual, expected);
+ }
+ }, 'A @keyframe animation with revert works when applied to multiple identical elements');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-008.html b/testing/web-platform/tests/css/css-cascade/revert-val-008.html
new file mode 100644
index 0000000000..77cceae7a3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-008.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>CSS Cascade: 'revert' in final keyframe of web animation</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<link rel="help" href="https://crbug.com/1065387">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<h1 id=h1></h1>
+<h1 id=ref></h1>
+<script>
+ test(function() {
+ let expected_lower = parseInt(getComputedStyle(ref).marginTop);
+ let expected_upper = expected_lower * 2;
+
+ h1.animate([
+ { marginTop: `${expected_lower * 4}px` },
+ { marginTop: `${expected_lower * 3}px` },
+ { marginTop: `${expected_lower * 2}px` },
+ { marginTop: 'revert' },
+ ], {
+ duration: 4000,
+ delay: -3500,
+ }).pause();
+
+ let actual = parseInt(getComputedStyle(h1).marginTop);
+
+ // This test assumes that the UA style sheet sets a non-0px value on
+ // <h1> elements:
+ assert_not_equals(expected_lower, 0);
+ assert_not_equals(expected_upper, 0);
+ assert_between_exclusive(actual, expected_lower, expected_upper);
+ }, 'The revert keyword works in the final frame of a web animation');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-009.html b/testing/web-platform/tests/css/css-cascade/revert-val-009.html
new file mode 100644
index 0000000000..e9683e90f4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-009.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>CSS Cascade: 'revert' in implicit keyframes</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<link rel="help" href="https://crbug.com/1065387">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<h1 id=h1></h1>
+<h1 id=ref></h1>
+<script>
+ test(function() {
+ let expected_lower = parseInt(getComputedStyle(ref).marginTop);
+ let expected_upper = expected_lower * 2;
+ h1.style = `margin-top: ${expected_lower * 1000}px; margin-top: revert;`;
+
+ h1.animate([
+ { marginTop: `${expected_upper}px` },
+ ], {
+ duration: 1000,
+ delay: -500,
+ }).pause();
+
+ let actual = parseInt(getComputedStyle(h1).marginTop);
+
+ // This test assumes that the UA style sheet sets a non-0px value on
+ // <h1> elements:
+ assert_not_equals(expected_lower, 0);
+ assert_not_equals(expected_upper, 0);
+ assert_between_exclusive(actual, expected_lower, expected_upper);
+ }, 'The revert keyword works in implicit keyframes');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-010.html b/testing/web-platform/tests/css/css-cascade/revert-val-010.html
new file mode 100644
index 0000000000..58449e9132
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-010.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>CSS Cascade: 'revert' appearing in setKeyframes</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-setkeyframes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<h1 id=h1></h1>
+<script>
+ test(function() {
+ let original = parseInt(getComputedStyle(h1).marginTop);
+
+ // This test assumes that the UA style sheet sets a non-0px value on
+ // <h1> elements:
+ assert_not_equals(original, 0);
+
+ let animation = h1.animate([
+ { marginTop: `${original*4}px` },
+ { marginTop: `${original*8}px` },
+ ], {
+ duration: 1000000,
+ delay: -500000,
+ easing: 'steps(2, end)'
+ });
+
+ let animated = parseInt(getComputedStyle(h1).marginTop);
+ assert_equals(animated, original*6);
+
+ animation.effect.setKeyframes([
+ { marginTop: 'revert' },
+ { marginTop: `${original*3}px` },
+ ]);
+
+ let animated_revert = parseInt(getComputedStyle(h1).marginTop);
+ assert_equals(animated_revert, original*2);
+ }, 'The revert works when appearing in setKeyframes');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/revert-val-011.html b/testing/web-platform/tests/css/css-cascade/revert-val-011.html
new file mode 100644
index 0000000000..9c034084c5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/revert-val-011.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: 'revert' from mutating inline style</title>
+ <link rel="help" href="https://drafts.csswg.org/css-cascade/#default">
+ <link rel="author" href="mailto:sesse@chromium.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ .outer {
+ left: 1px;
+ }
+ </style>
+</head>
+<body>
+ <p class="outer" id="el">Test passes if the text is black (not red).</p>
+</body>
+<script>
+test(() => {
+ el.offsetTop;
+ assert_equals(getComputedStyle(el).left, "1px", "style is set correctly");
+});
+test(() => {
+ el.offsetTop;
+ el.style.left = "2px";
+ assert_equals(getComputedStyle(el).left, "2px", "style is modified correctly");
+});
+test(() => {
+ el.offsetTop;
+ el.style.left = "revert";
+ assert_equals(getComputedStyle(el).left, "auto", "style is reverted correctly");
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-container.html b/testing/web-platform/tests/css/css-cascade/scope-container.html
new file mode 100644
index 0000000000..3e976088e2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-container.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>@scope - inner @container</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<link rel="help" href="https://drafts.csswg.org/css-contain-3/#container-rule">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#scope-scope">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ main {
+ width: 100px;
+ height: 100px;
+ container-type: size;
+ }
+
+ @scope (.a) {
+ @container (width > 0px) {
+ :scope {
+ z-index: 1;
+ }
+
+ .b {
+ background-color: green;
+ }
+ }
+ }
+</style>
+<main>
+ <div class=a>
+ <div class=b>
+ </div>
+ </div>
+ <div class=b></div>
+</main>
+<script>
+ test(() => {
+ let a = document.querySelector('main > .a');
+ let b = document.querySelector('main > .a > .b');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ assert_equals(getComputedStyle(b).backgroundColor, 'rgb(0, 128, 0)');
+
+ let out_of_scope_b = document.querySelector('main > .b');
+ assert_equals(getComputedStyle(out_of_scope_b).backgroundColor, 'rgba(0, 0, 0, 0)');
+ }, 'Style rules within @container are scoped');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-cssom.html b/testing/web-platform/tests/css/css-cascade/scope-cssom.html
new file mode 100644
index 0000000000..3603fc0e83
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-cssom.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>@scope - CSSOM</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#the-cssscoperule-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id=style>
+ @scope {}
+ @scope (.a) {}
+ @scope (.a) to (.b) {
+ div {
+ display: block;
+ }
+ }
+ @scope to (.b) {}
+</style>
+<script>
+
+// CSSScopeRule.cssText
+test(() => {
+ assert_equals(style.sheet.rules[0].cssText, '@scope {\n}');
+}, 'CSSScopeRule.cssText, implicit scope');
+
+test(() => {
+ assert_equals(style.sheet.rules[1].cssText, '@scope (.a) {\n}');
+}, 'CSSScopeRule.cssText, root only');
+
+test(() => {
+ assert_equals(style.sheet.rules[2].cssText, '@scope (.a) to (.b) {\n div { display: block; }\n}');
+}, 'CSSScopeRule.cssText, root and limit');
+
+test(() => {
+ assert_equals(style.sheet.rules[3].cssText, '@scope to (.b) {\n}');
+}, 'CSSScopeRule.cssText, limit only');
+
+// start
+test(() => {
+ assert_equals(style.sheet.rules[0].start, null);
+}, 'CSSScopeRule.start, implicit scope');
+
+test(() => {
+ assert_equals(style.sheet.rules[1].start, '.a');
+}, 'CSSScopeRule.start, root only');
+
+test(() => {
+ assert_equals(style.sheet.rules[2].start, '.a');
+}, 'CSSScopeRule.start, root and limit');
+
+test(() => {
+ assert_equals(style.sheet.rules[3].start, null);
+}, 'CSSScopeRule.start, limit only');
+
+// end
+test(() => {
+ assert_equals(style.sheet.rules[0].end, null);
+}, 'CSSScopeRule.end, implicit scope');
+
+test(() => {
+ assert_equals(style.sheet.rules[1].end, null);
+}, 'CSSScopeRule.end, root only');
+
+test(() => {
+ assert_equals(style.sheet.rules[2].end, '.b');
+}, 'CSSScopeRule.end, root and limit');
+
+test(() => {
+ assert_equals(style.sheet.rules[3].end, '.b');
+}, 'CSSScopeRule.end, limit only');
+
+test(() => {
+ assert_true(style.sheet.rules[0] instanceof CSSGroupingRule);
+ assert_false(style.sheet.rules[0] instanceof CSSConditionRule);
+}, 'CSSScopeRule is a CSSGroupingRule');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-declaration-list-crash.html b/testing/web-platform/tests/css/css-cascade/scope-declaration-list-crash.html
new file mode 100644
index 0000000000..c459da053a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-declaration-list-crash.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>@scope crash with invalid selectors</title>
+<style>
+ @scope (div) {
+ z-index: 1;
+ }
+ @scope (div) {
+ .a;
+ }
+</style>
+<p>
+ PASS if no crash.
+</p>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-deep.html b/testing/web-platform/tests/css/css-cascade/scope-deep.html
new file mode 100644
index 0000000000..0e88778202
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-deep.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<title>@scope - deeply nested</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ main * { background-color: black; }
+</style>
+<main id=main></main>
+<script>
+
+// @scope (.s0) { @scope (.s1) { ... span {} ... } }
+function createStyleSheet(length, i) {
+ if (length == 0)
+ return 'span { background-color: green; }';
+ if (i === undefined)
+ i = 0;
+ return `
+ @scope (.s${i}) {
+ ${createStyleSheet(length - 1, i + 1)}
+ }
+ `.trim();
+}
+
+// <div class=s0><div class=s1>...<span></span>...</div></div>
+function createElementChain(length, i) {
+ if (length < 1)
+ throw 'Invalid length';
+ if (i === undefined)
+ i = 0;
+ let e = document.createElement('div');
+ e.classList.add(`s${i}`);
+ if (length > 1)
+ e.append(createElementChain(length - 1, i + 1));
+ else
+ e.append(document.createElement('span'));
+ return e;
+}
+
+const COUNT = 90;
+
+let style_node = document.createElement('style');
+style_node.textContent = createStyleSheet(COUNT);
+main.append(style_node);
+
+main.append(createElementChain(COUNT));
+
+test(() => {
+ for (let span of main.querySelectorAll('span'))
+ assert_equals(getComputedStyle(span).backgroundColor, 'rgb(0, 128, 0)');
+}, 'Deep @scope nesting');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-evaluation.html b/testing/web-platform/tests/css/css-cascade/scope-evaluation.html
new file mode 100644
index 0000000000..f181048115
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-evaluation.html
@@ -0,0 +1,547 @@
+<!DOCTYPE html>
+<title>@scope - evaluation</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function test_scope(script_element, callback_fn, description) {
+ test((t) => {
+ // The provided <script> element must be an immedate subsequent sibling of
+ // a <template> element.
+ let template_element = script_element.previousElementSibling;
+ assert_equals(template_element.tagName, 'TEMPLATE');
+
+ t.add_cleanup(() => main.replaceChildren());
+
+ main.append(template_element.content.cloneNode(true));
+
+ callback_fn();
+ }, description);
+}
+
+function assert_green(selector) {
+ assert_equals(getComputedStyle(main.querySelector(selector)).backgroundColor, 'rgb(0, 128, 0)');
+}
+function assert_not_green(selector) {
+ assert_equals(getComputedStyle(main.querySelector(selector)).backgroundColor, 'rgb(0, 0, 0)');
+}
+</script>
+<style>
+ :where(main *) {
+ background-color: black;
+ }
+</style>
+<main id=main>
+</main>
+
+<!-- Tests follow -->
+
+<template>
+ <style>
+ @scope (.a) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <span>green</span>
+ </div>
+ <div class=b>
+ <span>not green</span>
+ </div>
+ <span>not green</span>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a > span');
+ assert_not_green('.b > span');
+ assert_not_green(':scope > span');
+}, 'Single scope');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ .a { background-color: green; }
+ }
+ </style>
+ <div class=a> <!-- green -->
+ <span>not green</span>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('.a');
+ assert_not_green('.a > span');
+}, 'Scope can not match its own root without :scope');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <div class=a> <!-- green -->
+ <span>not green</span>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a');
+ assert_not_green('.a > span');
+}, 'Selecting self with :scope');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.c) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span>green</span>
+ </div>
+ <div class=c>
+ <span>not green</span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.b > span');
+ assert_not_green('.c > span');
+}, 'Single scope with limit');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ :scope > span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <span>green</span>
+ <div class=b>
+ <span>not green</span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a > span');
+ assert_not_green('.b > span');
+}, 'Single scope, :scope pseudo in main selector');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (:scope > .b) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span>not green</span>
+ </div>
+ <div class=c>
+ <div class=b>
+ <span>green</span>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('.a > .b > span');
+ assert_green('.a > .c > .b > span');
+}, 'Single scope, :scope pseudo in to-selector');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (:scope > .b) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span>not green</span>
+ </div>
+ <div class=a>
+ <div class=b>
+ <span>green</span>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('.a > .b > span');
+ // Note that this span is in the outer .a-scope, but not in the inner scope.
+ assert_green('.a > .a > .b > span');
+}, 'Multiple scopes, :scope pseudo in to-selector');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ @scope (:scope > .b) {
+ span { background-color: green; }
+ }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span>green</span>
+ </div>
+ <div>
+ <div class=b>
+ <span>not green</span>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a > .b > span');
+ assert_not_green('.a > div > .b > span');
+}, 'Inner @scope with :scope in from-selector');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (:scope > .b) {
+ .c { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div>
+ <div class=a>
+ <div class=b>
+ <div class=c></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ // Not in the inner scope, but is in the outer scope.
+ assert_green('.c');
+}, 'Multiple scopes from same @scope-rule, only one limited');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b) {
+ .c { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div>
+ <div class=a>
+ <div class=b>
+ <div class=c></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('.c');
+}, 'Multiple scopes from same @scope-rule, both limited');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ @scope (.b) {
+ span { background-color: green; }
+ }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span>green</span>
+ </div>
+ <span>not green</span>
+ </div>
+ <div class=b>
+ <span>not green</span>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a > .b > span');
+ assert_not_green('.a > span');
+ assert_not_green(':scope > .b > span');
+}, 'Nested scopes');
+</script>
+
+<template>
+ <style>
+ @scope (.b) {
+ @scope (.a) {
+ span { background-color: green; }
+ }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span>not green</span>
+ </div>
+ <span>not green</span>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('.a > .b > span');
+ assert_not_green('.a > span');
+}, 'Nested scopes, reverse');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) {
+ @scope (.b) to (.c) {
+ span { background-color: green; }
+ }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span>green</span>
+ </div>
+ <div class=b>
+ <div class=c>
+ <span>not green</span>
+ </div>
+ </div>
+ <span>not green</span>
+ </div>
+ <div class=b>
+ <span>not green</span>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a > .b > span');
+ assert_not_green('.a > span');
+ assert_not_green('.a > .b > .c > span');
+ assert_not_green(':scope > .b > span');
+}, 'Nested scopes, with to-selector');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <div class=a></div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a');
+}, ':scope selecting itself');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b) {
+ * { background-color: green; }
+ }
+ </style>
+ <div id=above>
+ <div class=a>
+ <div>
+ <div class=b>
+ <div id=below></div>
+ </div>
+ </div>
+ </div>
+ <div id=adjacent></div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('#above');
+ assert_not_green('#adjacent');
+ assert_not_green('.a');
+ assert_green('.a > div');
+ assert_not_green('.b');
+ assert_not_green('#below');
+}, 'The scoping limit is not in scope');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b > *) {
+ * { background-color: green; }
+ }
+ </style>
+ <div id=above>
+ <div class=a>
+ <div>
+ <div class=b>
+ <div id=limit></div>
+ </div>
+ </div>
+ </div>
+ <div id=adjacent></div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('#above');
+ assert_not_green('#adjacent');
+ assert_not_green('.a');
+ assert_green('.a > div');
+ assert_green('.b');
+ assert_not_green('#limit');
+}, 'Simulated inclusive scoping limit');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (:scope) {
+ * { background-color: green; }
+ }
+ </style>
+ <div id=above>
+ <div class=a>
+ <div>
+ <div class=b>
+ <div id=inner></div>
+ </div>
+ </div>
+ </div>
+ <div id=adjacent></div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('#above');
+ assert_not_green('#adjacent');
+ assert_not_green('.a');
+ assert_not_green('.a > div');
+ assert_not_green('.b');
+ assert_not_green('#inner');
+}, 'Scope with no elements');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) {
+ :scope + .c { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div class=a></div>
+ <div class=c></div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ // A :scope sibling can never match, as the scoping element must
+ // be on the ancestor chain.
+ assert_not_green('.c');
+}, ':scope direct adjacent sibling');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) {
+ :scope + .c { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div class=a></div>
+ <div></div>
+ <div class=c></div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ // A :scope sibling can never match, as the scoping element must
+ // be on the ancestor chain.
+ assert_not_green('.c');
+}, ':scope indirect adjacent sibling');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) {
+ > span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <span>green</span>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_green('.a > span');
+}, 'Relative selector inside @scope');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) {
+ /* Can never match anything. */
+ :scope > :scope { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div id=inner class=a>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('.a');
+ assert_not_green('#inner');
+}, ':scope in two different compounds');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a:has(.c)) {
+ .b { background-color:green; }
+ }
+ </style>
+ <div class=first>
+ <div class=a>
+ <div class=b>
+ <div class=c></div>
+ </div>
+ </div>
+ </div>
+ <div class=second>
+ <div class=a>
+ <div class=b>
+ <div class=d></div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_not_green('.first .a');
+ assert_green('.first .b');
+ assert_not_green('.first .c');
+
+ assert_not_green('.second .a');
+ assert_not_green('.second .b');
+ assert_not_green('.second .d');
+}, 'Scope root with :has()');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-focus.html b/testing/web-platform/tests/css/css-cascade/scope-focus.html
new file mode 100644
index 0000000000..578a7702c1
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-focus.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<title>@scope and :focus</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<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>
+<main id=main></main>
+
+<template id=test_subject>
+ <div>
+ <style>
+ @scope (.a:focus) {
+ :scope { z-index: 1; }
+ }
+ </style>
+ <div class=a tabindex=0>1</div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_subject.content.cloneNode(true));
+
+ let a = main.querySelector('.a');
+
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+ await test_driver.bless('focus', () => a.focus());
+ assert_equals(getComputedStyle(a).zIndex, '1');
+}, ':focus via :scope in subject');
+</script>
+
+<template id=test_non_subject>
+ <div>
+ <style>
+ @scope (.a:focus) {
+ :scope .b { z-index: 1; }
+ }
+ </style>
+ <div class=a tabindex=0>
+ <div class=b>2</div>
+ </div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_non_subject.content.cloneNode(true));
+
+ let a = main.querySelector('.a');
+ let b = main.querySelector('.b');
+
+ assert_equals(getComputedStyle(b).zIndex, 'auto');
+ await test_driver.bless('focus', () => a.focus());
+ assert_equals(getComputedStyle(b).zIndex, '1');
+}, ':focus via :scope in non-subject');
+</script>
+
+<template id=test_subject_limit>
+ <div>
+ <style>
+ @scope (.a) to (:scope:focus) {
+ :scope { z-index: 1; }
+ }
+ </style>
+ <div class=a tabindex=0>3</div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_subject_limit.content.cloneNode(true));
+
+ let a = main.querySelector('.a');
+
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ await test_driver.bless('focus', () => a.focus());
+ // After focus(), we're no longer in scope because the limit (to-selector)
+ // kicks in.
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+}, ':focus in limit, :scope in subject');
+</script>
+
+<template id=test_non_subject_limit>
+ <div>
+ <style>
+ @scope (.a) to (.b:focus) {
+ .b { z-index: 1; }
+ }
+ </style>
+ <div class=a tabindex=0>
+ <div class=b tabindex=1>4</div>
+ </div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_non_subject_limit.content.cloneNode(true));
+
+ let a = main.querySelector('.a');
+ let b = main.querySelector('.b');
+
+ assert_equals(getComputedStyle(b).zIndex, '1');
+ await test_driver.bless('focus', () => b.focus());
+ // After focus(), we're no longer in scope because the limit (to-selector)
+ // kicks in.
+ assert_equals(getComputedStyle(b).zIndex, 'auto');
+}, ':focus in intermediate limit, :scope in subject');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-hover.html b/testing/web-platform/tests/css/css-cascade/scope-hover.html
new file mode 100644
index 0000000000..648fb361d6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-hover.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<title>@scope and :hover</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<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>
+
+<main id=main></main>
+
+<script>
+ async function hover(element) {
+ let actions = new test_driver.Actions().pointerMove(0, 0, {origin: element});
+ await actions.send();
+ }
+</script>
+
+<template id=test_subject>
+ <div>
+ <style>
+ @scope (.a:hover) {
+ :scope { z-index: 1; }
+ }
+ </style>
+ <div class=a>1</div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_subject.content.cloneNode(true));
+ let a = main.querySelector('.a');
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+ await hover(a);
+ assert_equals(getComputedStyle(a).zIndex, '1');
+}, ':hover via :scope in subject');
+</script>
+
+<template id=test_non_subject>
+ <div>
+ <style>
+ @scope (.a:hover) {
+ :scope .b { z-index: 1; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>2</div>
+ </div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_non_subject.content.cloneNode(true));
+
+ let a = main.querySelector('.a');
+ let b = main.querySelector('.b');
+
+ assert_equals(getComputedStyle(b).zIndex, 'auto');
+ await hover(a);
+ assert_equals(getComputedStyle(b).zIndex, '1');
+}, ':hover via :scope in non-subject');
+</script>
+
+<template id=test_subject_limit>
+ <div>
+ <style>
+ @scope (.a) to (:scope:hover) {
+ :scope { z-index: 1; }
+ }
+ </style>
+ <div class=a tabindex=0>3</div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_subject_limit.content.cloneNode(true));
+
+ let a = main.querySelector('.a');
+
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ await hover(a);
+ // After hover, we're no longer in scope because the limit (to-selector)
+ // kicks in.
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+}, ':hover in limit, :scope in subject');
+</script>
+
+<template id=test_non_subject_limit>
+ <div>
+ <style>
+ @scope (.a) to (.b:hover) {
+ .b { z-index: 1; }
+ }
+ </style>
+ <div class=a tabindex=0>
+ <div class=b tabindex=1>4</div>
+ </div>
+</template>
+<script>
+promise_test(async (t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_non_subject_limit.content.cloneNode(true));
+
+ let a = main.querySelector('.a');
+ let b = main.querySelector('.b');
+
+ assert_equals(getComputedStyle(b).zIndex, '1');
+ await hover(b);
+ // After hover, we're no longer in scope because the limit (to-selector)
+ // kicks in.
+ assert_equals(getComputedStyle(b).zIndex, 'auto');
+}, ':hover in intermediate limit, :scope in subject');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-implicit-external.html b/testing/web-platform/tests/css/css-cascade/scope-implicit-external.html
new file mode 100644
index 0000000000..d1ac738b77
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-implicit-external.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>@scope - implicit scope root (external sheet)</title>
+ <link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div class="a outside"><div>
+ <div id=root>
+ <link rel="stylesheet" href="resources/scope.css">
+ <div class=a></div>
+ </div>
+ <div class="a outside"><div>
+
+ <script>
+ test((t) => {
+ assert_equals(getComputedStyle(root).zIndex, '1');
+ assert_equals(getComputedStyle(document.querySelector('#root > .a')).zIndex, '2');
+
+ let outside = document.querySelectorAll('.outside');
+ assert_equals(outside.length, 2);
+ for (let div of outside) {
+ assert_equals(getComputedStyle(div).zIndex, 'auto');
+ }
+ }, '@scope with external stylesheet');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-implicit.html b/testing/web-platform/tests/css/css-cascade/scope-implicit.html
new file mode 100644
index 0000000000..9add25fc9a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-implicit.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<title>@scope - implicit scope root</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<main id=main></main>
+
+<template id=test_basic>
+ <div>
+ <style>
+ @scope {
+ .a { z-index:1; }
+ }
+ </style>
+ <div id=inner class=a></div>
+ </div>
+ <div id=outer class=a></div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_basic.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(inner).zIndex, '1');
+ assert_equals(getComputedStyle(outer).zIndex, 'auto');
+}, '@scope without prelude implicitly scopes to parent of owner node');
+</script>
+
+<template id=test_scope_pseudo>
+ <div>
+ <div></div>
+ </div>
+ <div>
+ <div id=root>
+ <style>
+ @scope {
+ :scope { z-index:1; }
+ }
+ </style>
+ <div>
+ <div></div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div></div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_scope_pseudo.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(root).zIndex, '1');
+
+ // Only #root should be affected.
+ for (let div of main.querySelectorAll('div:not(#root)')) {
+ assert_equals(getComputedStyle(div).zIndex, 'auto');
+ }
+}, ':scope can style implicit root');
+</script>
+
+<template id=test_duplicate>
+ <div>
+ <style>
+ @scope {
+ .a { z-index:1; }
+ }
+ </style>
+ <div id=first class=a></div>
+ </div>
+ <div>
+ <style>
+ @scope {
+ .a { z-index:1; }
+ }
+ </style>
+ <div id=second class=a></div>
+ </div>
+ <div id=outer class=a></div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_duplicate.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(first).zIndex, '1');
+ assert_equals(getComputedStyle(second).zIndex, '1');
+ assert_equals(getComputedStyle(outer).zIndex, 'auto');
+}, '@scope works with two identical stylesheets');
+</script>
+
+
+<template id=test_forgiving>
+ <div>
+ <style>
+ @scope ($invalid) {
+ #a { z-index:1; }
+ }
+ </style>
+ <div id=a></div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_forgiving.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+}, '@scope with effectively empty :is() must not match anything');
+</script>
+
+<template id=test_implicit_descendant>
+ <div id=div>
+ <style>
+ @scope {
+ #div { z-index:1; }
+ }
+ </style>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_implicit_descendant.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(div).zIndex, 'auto');
+}, 'Implicit @scope has implicitly added :scope descendant combinator');
+</script>
+
+<template id=test_implicit_relative>
+ <div id=outer>
+ <style>
+ @scope {
+ > div { z-index:1; }
+ }
+ </style>
+ <div id=child>
+ <div id=inner></div>
+ </div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_implicit_relative.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(outer).zIndex, 'auto');
+ assert_equals(getComputedStyle(child).zIndex, '1');
+ assert_equals(getComputedStyle(inner).zIndex, 'auto');
+}, 'Implicit @scope with inner relative selector');
+</script>
+
+<template id=test_implicit_descendant_nesting_selector>
+ <div id=div>
+ <style>
+ @scope {
+ /* Behaves like :scope */
+ & { z-index:1; }
+ }
+ </style>
+ <div id=inner></div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_implicit_descendant_nesting_selector.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(div).zIndex, '1');
+ assert_equals(getComputedStyle(inner).zIndex, 'auto');
+}, 'Implicit @scope with inner nesting selector');
+</script>
+
+<template id=test_limit>
+ <div>
+ <style>
+ @scope to (.b) {
+ .a { z-index:1; }
+ }
+ </style>
+ <div id=inner class=a>
+ <div class=b>
+ <div id=outside_limit class=a></div>
+ </div>
+ </div>
+ </div>
+ <div id=outer class=a></div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_limit.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(inner).zIndex, '1');
+ assert_equals(getComputedStyle(outer).zIndex, 'auto');
+ assert_equals(getComputedStyle(outside_limit).zIndex, 'auto');
+}, 'Implicit @scope with limit');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-cascade/scope-invalidation.html b/testing/web-platform/tests/css/css-cascade/scope-invalidation.html
new file mode 100644
index 0000000000..d53257e894
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-invalidation.html
@@ -0,0 +1,782 @@
+<!DOCTYPE html>
+<title>@scope - invalidation</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function test_scope_invalidation(script_element, callback_fn, description) {
+ test((t) => {
+ // The provided <script> element must be an immedate subsequent sibling of
+ // a <template> element.
+ let template_element = script_element.previousElementSibling;
+ assert_equals(template_element.tagName, 'TEMPLATE');
+
+ t.add_cleanup(() => {
+ while (main.firstChild)
+ main.firstChild.remove()
+ });
+
+ main.append(template_element.content.cloneNode(true));
+
+ callback_fn();
+ }, description);
+}
+
+function assert_green(element) {
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(0, 128, 0)');
+}
+function assert_not_green(element) {
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(0, 0, 0)');
+}
+</script>
+<style>
+ main * {
+ background-color: black;
+ }
+</style>
+<main id=main>
+</main>
+
+<!-- Tests follow -->
+
+<template>
+ <style>
+ @scope (.a) {
+ span { background-color: green; }
+ }
+ </style>
+ <div>
+ <span></span>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let div = main.querySelector('div');
+ let span = main.querySelector('div > span');
+ assert_not_green(span);
+ div.classList.add('a');
+ assert_green(span);
+ div.classList.remove('a');
+ assert_not_green(span);
+}, 'Element becoming scope root');
+</script>
+
+<template>
+ <style>
+ @scope (.a, .b) {
+ span { background-color: green; }
+ }
+ </style>
+ <div>
+ <span></span>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let div = main.querySelector('div');
+ let span = main.querySelector('div > span');
+
+ // .a
+ assert_not_green(span);
+ div.classList.add('a');
+ assert_green(span);
+ div.classList.remove('a');
+ assert_not_green(span);
+
+ // .b
+ assert_not_green(span);
+ div.classList.add('b');
+ assert_green(span);
+ div.classList.remove('b');
+ assert_not_green(span);
+}, 'Element becoming scope root (selector list)');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <div class=b></div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let b = main.querySelector('.b');
+ assert_not_green(b);
+ b.classList.add('a');
+ assert_green(b);
+ b.classList.remove('a');
+ assert_not_green(b);
+}, 'Element becoming scope root, with inner :scope rule');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div>
+ <span></span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let inner_div = main.querySelector('.a > div');
+ let span = main.querySelector('.a > div > span');
+ assert_green(span);
+ inner_div.classList.add('b');
+ assert_not_green(span);
+ inner_div.classList.remove('b');
+ assert_green(span);
+}, 'Parent element becoming scope limit');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b, .c) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div>
+ <span></span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let inner_div = main.querySelector('.a > div');
+ let span = main.querySelector('.a > div > span');
+
+ // .b
+ assert_green(span);
+ inner_div.classList.add('b');
+ assert_not_green(span);
+ inner_div.classList.remove('b');
+ assert_green(span);
+
+ // .c
+ assert_green(span);
+ inner_div.classList.add('c');
+ assert_not_green(span);
+ inner_div.classList.remove('c');
+ assert_green(span);
+}, 'Parent element becoming scope limit (selector list)');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div>
+ <span></span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let span = main.querySelector('.a > div > span');
+ assert_green(span);
+ span.classList.add('b');
+ assert_not_green(span);
+ span.classList.remove('b');
+ assert_green(span);
+}, 'Subject element becoming scope limit');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b .c) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div>
+ <div class=c>
+ <span></span>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let intermediate_div = main.querySelector('.a > div');
+ let span = main.querySelector('span');
+ assert_green(span);
+ intermediate_div.classList.add('b');
+ assert_not_green(span);
+ intermediate_div.classList.remove('b');
+ assert_green(span);
+}, 'Parent element affecting scope limit');
+</script>
+
+<template>
+ <style>
+ @scope (.a) to (.b ~ .c) {
+ span { background-color: green; }
+ }
+ </style>
+ <div class=a>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class=c>
+ <span></span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let sibling_div = main.querySelector('.a > div');
+ let span = main.querySelector('span');
+ assert_green(span);
+ sibling_div.classList.add('b');
+ assert_not_green(span);
+ sibling_div.classList.remove('b');
+ assert_green(span);
+}, 'Sibling element affecting scope limit');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ @scope (.b) {
+ span { background-color: green; }
+ }
+ }
+ </style>
+ <div>
+ <div>
+ <span></span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let outer_div = main.querySelector(':scope > div');
+ let inner_div = main.querySelector(':scope > div > div');
+ let span = main.querySelector('div > div > span');
+
+ assert_not_green(span);
+
+ outer_div.classList.add('a');
+ assert_not_green(span);
+
+ inner_div.classList.add('b');
+ assert_green(span);
+
+ // Toggle .b while .a remains.
+ inner_div.classList.remove('b');
+ assert_not_green(span);
+ inner_div.classList.add('b');
+ assert_green(span);
+
+ // Toggle .a while .b remains.
+ outer_div.classList.remove('a');
+ assert_not_green(span);
+ outer_div.classList.add('a');
+ assert_green(span);
+}, 'Toggling inner/outer scope roots');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) {
+ :scope { background-color:green; }
+ }
+ </style>
+ <div></div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let div = main.querySelector('main > div');
+ assert_not_green(div);
+ div.classList.add('a');
+ assert_green(div);
+ div.classList.remove('a');
+ assert_not_green(div);
+}, 'Element becoming root, with :scope in subject');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a:has(.c)) {
+ .b { background-color:green; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <div></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let b = main.querySelector('.b');
+ let innermost = main.querySelector('.b > div');
+ assert_not_green(b);
+ innermost.classList.add('c');
+ assert_green(b);
+ innermost.classList.remove('c');
+ assert_not_green(b);
+}, 'Scope root with :has()');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a:has(.c)) {
+ :scope { background-color:green; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <div></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let a = main.querySelector('.a');
+ let innermost = main.querySelector('.b > div');
+ assert_not_green(a);
+ innermost.classList.add('c');
+ assert_green(a);
+ innermost.classList.remove('c');
+ assert_not_green(a);
+}, 'Scope root with :has(), :scope subject');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a:has(.c)) {
+ :scope { background-color:green; }
+ :scope .b { background-color:green; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <div></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let a = main.querySelector('.a');
+ let b = main.querySelector('.b');
+ let innermost = main.querySelector('.b > div');
+ assert_not_green(a);
+ assert_not_green(b);
+ innermost.classList.add('c');
+ assert_green(a);
+ assert_green(b);
+ innermost.classList.remove('c');
+ assert_not_green(a);
+ assert_not_green(b);
+}, 'Scope root with :has(), :scope both subject and non-subject');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) to (.b:has(.c)) {
+ .b { background-color:green; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <div></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let b = main.querySelector('.b');
+ let innermost = main.querySelector('.b > div');
+ assert_green(b);
+ innermost.classList.add('c');
+ assert_not_green(b);
+ innermost.classList.remove('c');
+ assert_green(b);
+}, 'Scope limit with :has()');
+</script>
+
+<template>
+ <style>
+ @scope (.a) {
+ .b ~ :scope { background-color:green; }
+ }
+ </style>
+ <div></div>
+ <div></div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let div1 = main.querySelector('main > div:nth-of-type(1)');
+ let div2 = main.querySelector('main > div:nth-of-type(2)');
+
+ assert_not_green(div2);
+ div1.classList.add('b');
+ assert_not_green(div2);
+ div2.classList.add('a');
+ assert_green(div2);
+ div1.classList.remove('b');
+ assert_not_green(div2);
+}, 'Element becoming root, with :scope selected by ~ combinator');
+</script>
+
+<template>
+ <style>
+ @scope (.a ~ .b) {
+ .c { background-color:green; }
+ }
+ </style>
+ <div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class=b>
+ <div class=c></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let root = main.querySelector('div > div:first-child');
+ let c = main.querySelector('.c');
+ assert_not_green(c);
+ root.classList.add('a');
+ assert_green(c);
+ root.classList.remove('a');
+ assert_not_green(c);
+}, 'Element becoming root via ~ combinator');
+</script>
+
+<template>
+ <style>
+ @scope (.a + .b) {
+ .c { background-color:green; }
+ }
+ </style>
+ <div>
+ <div></div>
+ <div class=b>
+ <div class=c></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let root = main.querySelector('div > div:first-child');
+ let c = main.querySelector('.c');
+ assert_not_green(c);
+ root.classList.add('a');
+ assert_green(c);
+ root.classList.remove('a');
+ assert_not_green(c);
+}, 'Element becoming root via + combinator');
+</script>
+
+<template>
+ <style>
+ @scope (.root) {
+ :not(:scope) { background-color:green; }
+ }
+ </style>
+ <div class=root>
+ <div class=a></div>
+ <div class=b></div>
+ <div class=c></div>
+ </div>
+ <div class=a></div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let root = main.querySelector('.root');
+ let a1 = main.querySelector('.root > .a');
+ let b = main.querySelector('.root > .b');
+ let c = main.querySelector('.root > .c');
+ let a2 = main.querySelector('main > .a');
+
+ assert_not_green(root);
+ assert_green(a1);
+ assert_green(b);
+ assert_green(c);
+ assert_not_green(a2);
+
+ root.classList.remove('root');
+ assert_not_green(root);
+ assert_not_green(a1);
+ assert_not_green(b);
+ assert_not_green(c);
+ assert_not_green(a2);
+
+ root.classList.add('root');
+ assert_not_green(root);
+ assert_green(a1);
+ assert_green(b);
+ assert_green(c);
+ assert_not_green(a2);
+}, ':not(scope) in subject');
+</script>
+
+<template>
+ <style>
+ @scope (.root) {
+ :not(:scope) > .a { background-color:green; }
+ }
+ </style>
+ <div class=root>
+ <div class=a></div>
+ <div>
+ <div class=a></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let root = main.querySelector('.root');
+ let outer_a = main.querySelector('.root > .a');
+ let inner_a = main.querySelector('.root > div > .a');
+
+ assert_not_green(outer_a);
+ assert_green(inner_a);
+
+ root.classList.remove('root');
+ assert_not_green(outer_a);
+ assert_not_green(inner_a);
+
+ root.classList.add('root');
+ assert_not_green(outer_a);
+ assert_green(inner_a);
+}, ':not(scope) in ancestor');
+</script>
+
+<template>
+ <style>
+ @scope (.root) to (:not(:scope)) {
+ :is(div, :scope) { background-color: green; }
+ }
+ </style>
+ <div class=root>
+ <div class=a></div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let root = main.querySelector('.root');
+ let a = main.querySelector('.root > .a');
+
+ assert_green(root);
+ assert_not_green(a);
+
+ root.classList.remove('root');
+ assert_not_green(root);
+ assert_not_green(a);
+
+ root.classList.add('root');
+ assert_green(root);
+ assert_not_green(a);
+}, ':not(scope) in limit subject');
+</script>
+
+<template>
+ <style>
+ @scope (.root) to (:not(:scope) > .a) {
+ :is(div, :scope) { background-color: green; }
+ }
+ </style>
+ <div class=root>
+ <div class=a>
+ <div class=a></div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let root = main.querySelector('.root');
+ let outer_a = main.querySelector('.root > .a');
+ let inner_a = main.querySelector('.root > .a > .a');
+
+ assert_green(root);
+ assert_green(outer_a);
+ assert_not_green(inner_a);
+
+ root.classList.remove('root');
+ assert_not_green(root);
+ assert_not_green(outer_a);
+ assert_not_green(inner_a);
+
+ root.classList.add('root');
+ assert_green(root);
+ assert_green(outer_a);
+ assert_not_green(inner_a);
+}, ':not(scope) in limit ancestor');
+</script>
+
+<template>
+ <style>
+ @scope (:nth-child(2n of .a)) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <div id=wrapper>
+ <div class=a></div>
+ <div></div>
+ <div class=a></div>
+ <div></div>
+ <div class=a></div>
+ <div></div>
+ <div class=a></div>
+ <div></div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let e = main.querySelectorAll('#wrapper > div');
+ assert_equals(e.length, 8);
+
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ assert_not_green(e[0]);
+ assert_not_green(e[1]);
+ assert_green(e[2]);
+ assert_not_green(e[3]);
+ assert_not_green(e[4]);
+ assert_not_green(e[5]);
+ assert_green(e[6]);
+ assert_not_green(e[7]);
+
+ e[1].classList.add('a');
+ // <div class=a></div>
+ // <div class=a></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ assert_not_green(e[0]);
+ assert_green(e[1]);
+ assert_not_green(e[2]);
+ assert_not_green(e[3]);
+ assert_green(e[4]);
+ assert_not_green(e[5]);
+ assert_not_green(e[6]);
+ assert_not_green(e[7]);
+
+ e[1].classList.remove('a');
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ assert_not_green(e[0]);
+ assert_not_green(e[1]);
+ assert_green(e[2]);
+ assert_not_green(e[3]);
+ assert_not_green(e[4]);
+ assert_not_green(e[5]);
+ assert_green(e[6]);
+ assert_not_green(e[7]);
+}, ':nth-child() in scope root');
+</script>
+
+<template>
+ <style>
+ @scope (#wrapper) to (:nth-child(4n of .a)) {
+ div { background-color: green; }
+ }
+ </style>
+ <div id=wrapper>
+ <div class=a></div>
+ <div></div>
+ <div class=a></div>
+ <div></div>
+ <div class=a></div>
+ <div></div>
+ <div class=a></div>
+ <div></div>
+ </div>
+</template>
+<script>
+test_scope_invalidation(document.currentScript, () => {
+ let e = main.querySelectorAll('#wrapper > div');
+ assert_equals(e.length, 8);
+
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div> <= limit
+ // <div></div>
+ assert_green(e[0]);
+ assert_green(e[1]);
+ assert_green(e[2]);
+ assert_green(e[3]);
+ assert_green(e[4]);
+ assert_green(e[5]);
+ assert_not_green(e[6]);
+ assert_green(e[7]);
+
+ e[1].classList.add('a');
+ // <div class=a></div>
+ // <div class=a></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div> <= limit
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ assert_green(e[0]);
+ assert_green(e[1]);
+ assert_green(e[2]);
+ assert_green(e[3]);
+ assert_not_green(e[4]);
+ assert_green(e[5]);
+ assert_green(e[6]);
+ assert_green(e[7]);
+
+ e[1].classList.remove('a');
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div>
+ // <div></div>
+ // <div class=a></div> <= limit
+ // <div></div>
+ assert_green(e[0]);
+ assert_green(e[1]);
+ assert_green(e[2]);
+ assert_green(e[3]);
+ assert_green(e[4]);
+ assert_green(e[5]);
+ assert_not_green(e[6]);
+ assert_green(e[7]);
+}, ':nth-child() in scope limit');
+
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-layer.html b/testing/web-platform/tests/css/css-cascade/scope-layer.html
new file mode 100644
index 0000000000..e8a89ba68c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-layer.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>@scope - inner @layer</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#scope-scope">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @scope (.a) {
+ /* The theme layer wins over the base layer. Note that @layer statements
+ are allowed here, but aren't affected by the enclosing @scope. */
+ @layer base, theme;
+
+ @layer theme {
+ :scope {
+ z-index: 1;
+ }
+
+ .b {
+ background-color: green;
+ }
+ }
+ }
+
+ @layer base {
+ .a {
+ z-index: 0;
+ }
+ .a .b {
+ background-color: red;
+ }
+ }
+</style>
+<main>
+ <div class=a>
+ <div class=b>
+ </div>
+ </div>
+ <div class=b></div>
+</main>
+<script>
+ test(() => {
+ let a = document.querySelector('main > .a');
+ let b = document.querySelector('main > .a > .b');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ assert_equals(getComputedStyle(b).backgroundColor, 'rgb(0, 128, 0)');
+
+ let out_of_scope_b = document.querySelector('main > .b');
+ assert_equals(getComputedStyle(out_of_scope_b).backgroundColor, 'rgba(0, 0, 0, 0)');
+ }, 'Style rules within @layer are scoped');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-media.html b/testing/web-platform/tests/css/css-cascade/scope-media.html
new file mode 100644
index 0000000000..ae2e7694f7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-media.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>@scope - inner @media</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<link rel="help" href="https://drafts.csswg.org/css-conditional-3/#at-ruledef-media">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#scope-scope">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @scope (.a) {
+ @media (width > 0px) {
+ :scope {
+ z-index: 1;
+ }
+
+ .b {
+ background-color: green;
+ }
+ }
+ }
+</style>
+<main>
+ <div class=a>
+ <div class=b>
+ </div>
+ </div>
+ <div class=b></div>
+</main>
+<script>
+ test(() => {
+ let a = document.querySelector('main > .a');
+ let b = document.querySelector('main > .a > .b');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ assert_equals(getComputedStyle(b).backgroundColor, 'rgb(0, 128, 0)');
+
+ let out_of_scope_b = document.querySelector('main > .b');
+ assert_equals(getComputedStyle(out_of_scope_b).backgroundColor, 'rgba(0, 0, 0, 0)');
+ }, 'Style rules within @media are scoped');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-name-defining-rules.html b/testing/web-platform/tests/css/css-cascade/scope-name-defining-rules.html
new file mode 100644
index 0000000000..9f4b342887
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-name-defining-rules.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<title>@scope - name-defining at-rules</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#scope-scope">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<main id=main></main>
+
+<template id=test_keyframes>
+ <style>
+ @scope (#inner) {
+ @keyframes --my-anim {
+ from { background-color: rgb(0, 0, 255); }
+ to { background-color: rgb(0, 0, 255); }
+ }
+ }
+ @scope (#outer) {
+ @keyframes --my-anim {
+ from { background-color: rgb(0, 128, 0); }
+ to { background-color: rgb(0, 128, 0); }
+ }
+ }
+ #animating {
+ animation: --my-anim 1000s linear;
+ }
+ </style>
+ <div id=outer>
+ <div id=inner>
+ <div id=animating></div>
+ <div>
+ </div>
+</template>
+<script>
+ test((t) => {
+ main.append(test_keyframes.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(animating).backgroundColor, 'rgb(0, 128, 0)');
+ }, '@keyframes is unaffected by @scope');
+</script>
+
+<template id=test_keyframes_non_matching>
+ <style>
+ @scope (#nomatch) {
+ @keyframes --my-anim {
+ from { background-color: rgb(0, 128, 0); }
+ to { background-color: rgb(0, 128, 0); }
+ }
+ }
+ #animating {
+ animation: --my-anim 1000s linear;
+ }
+ </style>
+ <div id=animating></div>
+</template>
+<script>
+ test((t) => {
+ main.append(test_keyframes_non_matching.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(animating).backgroundColor, 'rgb(0, 128, 0)');
+ }, '@keyframes is unaffected by non-matching @scope');
+</script>
+
+<template id=test_property>
+ <style>
+ @scope (#inner) {
+ @property --my-prop {
+ syntax: "<length>";
+ initial-value: 1px;
+ inherits: false;
+ }
+ }
+ @scope (#outer) {
+ @property --my-prop {
+ syntax: "<length>";
+ initial-value: 2px;
+ inherits: false;
+ }
+ }
+ </style>
+ <div id=outer>
+ <div id=inner>
+ <div id=subject></div>
+ <div>
+ </div>
+</template>
+<script>
+ test((t) => {
+ main.append(test_property.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(subject).getPropertyValue('--my-prop'), '2px');
+ }, '@property is unaffected by @scope');
+</script>
+
+<template id=test_property_non_matching>
+ <style>
+ @scope (#nomatch) {
+ @property --my-prop {
+ syntax: "<length>";
+ initial-value: 2px;
+ inherits: false;
+ }
+ }
+ </style>
+ <div id=subject></div>
+</template>
+<script>
+ test((t) => {
+ main.append(test_property_non_matching.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(subject).getPropertyValue('--my-prop'), '2px');
+ }, '@property is unaffected by non-matching @scope');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-nesting.html b/testing/web-platform/tests/css/css-cascade/scope-nesting.html
new file mode 100644
index 0000000000..d299ba3037
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-nesting.html
@@ -0,0 +1,545 @@
+<!DOCTYPE html>
+<title>@scope - nesting (&)</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<link rel="help" href="https://drafts.csswg.org/css-nesting-1/#nest-selector">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<main id=main></main>
+
+<template id=test_nest_scope_end>
+ <div>
+ <style>
+ @scope (.a) to (& > &) {
+ * { z-index:1; }
+ }
+ </style>
+ <div class=a> <!-- This scope is limited by the element below. -->
+ <div class=a> <!-- This scope is limited by its own root. -->
+ <div id=below></div>
+ </div>
+ </div>
+ </div>
+ <div id=outside></div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_nest_scope_end.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(below).zIndex, 'auto');
+ assert_equals(getComputedStyle(outside).zIndex, 'auto');
+}, 'Nesting-selector in <scope-end>');
+</script>
+
+<template id=test_nest_scope_end_implicit_scope>
+ <div>
+ <style>
+ /* (.b) behaves like (:scope .b), due :scope being prepended
+ implicitly. */
+ @scope (.a) to (.b) {
+ :scope { z-index:1; }
+ }
+
+ /* Should not match, since <scope-end> refers to the scope itself. */
+ @scope (.a) to (.b:scope) {
+ :scope { z-index:42; }
+ }
+ </style>
+ <div class="a b">
+ <div class=b>
+ <div id=below></div>
+ </div>
+ </div>
+ </div>
+ <div id=outside></div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_nest_scope_end_implicit_scope.content.cloneNode(true));
+ let a = document.querySelector('.a');
+ let b = document.querySelector('.a > .b');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ assert_equals(getComputedStyle(b).zIndex, 'auto');
+ assert_equals(getComputedStyle(below).zIndex, 'auto');
+ assert_equals(getComputedStyle(outside).zIndex, 'auto');
+}, 'Implicit :scope in <scope-end>');
+</script>
+
+<template id=test_relative_selector_scope_end>
+ <div>
+ <style>
+ @scope (.a) to (> .b) {
+ *, :scope { z-index:1; }
+ }
+ </style>
+ <div class="a b">
+ <div class=b>
+ <div id=below></div>
+ </div>
+ </div>
+ </div>
+ <div id=outside></div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_relative_selector_scope_end.content.cloneNode(true));
+ let a = document.querySelector('.a');
+ let b = document.querySelector('.a > .b');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ assert_equals(getComputedStyle(b).zIndex, 'auto');
+ assert_equals(getComputedStyle(below).zIndex, 'auto');
+ assert_equals(getComputedStyle(outside).zIndex, 'auto');
+}, 'Relative selectors in <scope-end>');
+</script>
+
+<template id=test_inner_nest>
+ <div>
+ <style>
+ @scope (.a) {
+ & + & {
+ z-index:1;
+ }
+ }
+ </style>
+ <div class=a>
+ <div id=inner1 class=a></div>
+ <div id=inner2 class=a></div>
+ </div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_inner_nest.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(inner1).zIndex, 'auto');
+ assert_equals(getComputedStyle(inner2).zIndex, '1');
+}, 'Nesting-selector in the scope\'s <stylesheet>');
+</script>
+
+<template id=test_parent_in_pseudo_scope>
+ <div>
+ <style>
+ @scope (#div) {
+ :scope {
+ z-index: 1;
+ & {
+ z-index: 2;
+ }
+ }
+ }
+ </style>
+ <div id=div></div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_parent_in_pseudo_scope.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(div).zIndex, '2');
+}, 'Nesting-selector within :scope rule');
+</script>
+
+<template id=test_parent_in_pseudo_scope_double>
+ <div>
+ <style>
+ @scope (#div) {
+ :scope {
+ z-index: 1;
+ & {
+ & {
+ z-index: 2;
+ }
+ }
+ }
+ }
+ </style>
+ <div id=div></div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_parent_in_pseudo_scope_double.content.cloneNode(true));
+
+ assert_equals(getComputedStyle(div).zIndex, '2');
+}, 'Nesting-selector within :scope rule (double nested)');
+</script>
+
+<template id=test_scope_within_style_rule>
+ <div>
+ <style>
+ .a {
+ @scope (.b) {
+ .c { z-index: 1; }
+ }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <div class=c>
+ </div>
+ </div>
+ <div id=out_of_scope class=c>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_scope_within_style_rule.content.cloneNode(true));
+
+ let c = document.querySelector('.c');
+ assert_equals(getComputedStyle(c).zIndex, '1');
+ assert_equals(getComputedStyle(out_of_scope).zIndex, 'auto');
+}, '@scope nested within style rule');
+</script>
+
+<template id=test_parent_pseudo_in_nested_scope_start>
+ <div>
+ <style>
+ .a {
+ @scope (&.b) {
+ :scope { z-index: 1; }
+ }
+ }
+ </style>
+ <div class=a></div>
+ <div class=b></div>
+ <div class="a b"></div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_parent_pseudo_in_nested_scope_start.content.cloneNode(true));
+
+ let a = document.querySelector('.a:not(.b)');
+ let b = document.querySelector('.b:not(.a)');
+ let ab = document.querySelector('.a.b');
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+ assert_equals(getComputedStyle(b).zIndex, 'auto');
+ assert_equals(getComputedStyle(ab).zIndex, '1');
+}, 'Parent pseudo class within scope-start');
+</script>
+
+<template id=test_parent_pseudo_in_nested_scope_end>
+ <div>
+ <style>
+ .a {
+ /* Note that & in <scope-end> refers to <scope-start>,
+ not the outer style rule. */
+ @scope (&.b) to (&.c) {
+ :scope, * { z-index: 1; }
+ }
+ }
+ </style>
+ <div class="a b">
+ <div class="a c">
+ <div class="a b c">
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_parent_pseudo_in_nested_scope_end.content.cloneNode(true));
+
+ let ab = document.querySelector('.a.b:not(.c)');
+ let ac = document.querySelector('.a.c:not(.b)');
+ let abc = document.querySelector('.a.b.c');
+ assert_equals(getComputedStyle(ab).zIndex, '1');
+ assert_equals(getComputedStyle(ac).zIndex, '1');
+ assert_equals(getComputedStyle(abc).zIndex, 'auto', 'limit element is not in scope');
+}, 'Parent pseudo class within scope-end');
+</script>
+
+<template id=test_parent_pseudo_in_nested_scope_body>
+ <div>
+ <style>
+ .a {
+ @scope (.b) {
+ /* The & points to <scope-start>, which contains an implicit &
+ which points to .a. */
+ &.c { z-index: 1; }
+ }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <div class="c"></div>
+ <div class="a c"></div>
+ <div class="a b c" matching></div>
+ </div>
+ </div>
+ <div>
+ <div class=a></div>
+ <div class=b></div>
+ <div class=c></div>
+ <div class="a b"></div>
+ <div class="a c"></div>
+ <div class="b c"></div>
+ </div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_parent_pseudo_in_nested_scope_body.content.cloneNode(true));
+
+ let matching = main.querySelectorAll("div[matching]");
+ let non_matching = main.querySelectorAll("div:not([matching])");
+
+ for (let m of matching) {
+ assert_equals(getComputedStyle(m).zIndex, '1', `matching: ${m.nodeName}${m.className}`);
+ }
+ for (let m of non_matching) {
+ assert_equals(getComputedStyle(m).zIndex, 'auto', `non-matching: ${m.nodeName}${m.className}`);
+ }
+}, 'Parent pseudo class within body of nested @scope');
+</script>
+
+<template id=test_direct_declarations_in_nested_scope>
+ <div>
+ <style>
+ .a {
+ @scope (.b) {
+ z-index: 1;
+ }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <div class="c"></div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_direct_declarations_in_nested_scope.content.cloneNode(true));
+
+ let a = document.querySelector('.a');
+ let b = document.querySelector('.b');
+ let c = document.querySelector('.c');
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+ assert_equals(getComputedStyle(b).zIndex, '1');
+ assert_equals(getComputedStyle(c).zIndex, 'auto');
+}, 'Implicit rule within nested @scope ');
+</script>
+
+<template id=test_direct_declarations_in_nested_scope_proximity>
+ <div>
+ <style>
+ .a {
+ /* We're supposed to prepend :scope to this declaration. If we do that,
+ then :where() does not matter, since :scope does not gain any
+ specificity from the enclosing @scope rule. However, if an
+ implementation incorrectly prepends & instead, then :where() is
+ needed to avoid the test incorrectly passing due to specificity. */
+ @scope (:where(&) .b) {
+ z-index: 1; /* Should win due to proximity */
+ }
+ }
+ .b { z-index: 2; }
+ </style>
+ <div class=a>
+ <div class="b x">
+ <div class=c>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_direct_declarations_in_nested_scope_proximity.content.cloneNode(true));
+
+ let a = document.querySelector('.a');
+ let b = document.querySelector('.b');
+ let c = document.querySelector('.c');
+ assert_equals(getComputedStyle(a).zIndex, 'auto');
+ assert_equals(getComputedStyle(b).zIndex, '1');
+ assert_equals(getComputedStyle(c).zIndex, 'auto');
+}, 'Implicit rule within nested @scope (proximity)');
+</script>
+
+<template id=test_nested_scope_inside_an_is>
+ <div>
+ <style>
+ @scope (.a) {
+ .b {
+ /* When nesting, because we’re inside a defined scope,
+ the `:scope` should reference the scoping root node properly, and
+ check for the presence of an extra class on it, essentially
+ being equal to `:scope.x .b { z-index: 1 }`. */
+ &:is(:scope.x *) {
+ z-index: 1;
+ }
+ /* This should not match, as we have a defined scope, and should
+ not skip to the root. */
+ &:is(:root:scope *) {
+ z-index: 2;
+ }
+ }
+ /* The nested case can be though of the following when expanded: */
+ .c:is(:scope.x *) {
+ z-index: 3;
+ }
+ }
+ </style>
+ <div class="b">
+ </div>
+ <div class="a x">
+ <div class="b">
+ </div>
+ <div class="c">
+ </div>
+ </div>
+</div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_nested_scope_inside_an_is.content.cloneNode(true));
+
+ let b_outside = document.querySelector('.b');
+ let b_inside = document.querySelector('.a .b');
+ let c = document.querySelector('.c');
+ assert_equals(getComputedStyle(b_outside).zIndex, 'auto');
+ assert_equals(getComputedStyle(b_inside).zIndex, '1');
+ assert_equals(getComputedStyle(c).zIndex, '3');
+}, 'Nested :scope inside an :is');
+</script>
+
+<template id=test_nested_scope_pseudo>
+ <div>
+ <style>
+ @scope (.b) {
+ .a:not(:scope) {
+ & :scope {
+ z-index: 1;
+ }
+ }
+ }
+ </style>
+ <div class="b">
+ </div>
+ <div class="a">
+ <div class="b">
+ </div>
+ </div>
+</div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_nested_scope_pseudo.content.cloneNode(true));
+
+ let b_outside = document.querySelector('.b');
+ let b_inside = document.querySelector('.a .b');
+ assert_equals(getComputedStyle(b_outside).zIndex, 'auto');
+ assert_equals(getComputedStyle(b_inside).zIndex, '1');
+}, ':scope within nested and scoped rule');
+</script>
+
+<template id=test_nested_scope_pseudo_implied>
+ <div>
+ <style>
+ @scope (.b) {
+ .a:not(:scope) {
+ :scope { /* & implied */
+ z-index: 1;
+ }
+ }
+ }
+ </style>
+ <div class="b">
+ </div>
+ <div class="a">
+ <div class="b">
+ </div>
+ </div>
+</div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_nested_scope_pseudo_implied.content.cloneNode(true));
+
+ let b_outside = document.querySelector('.b');
+ let b_inside = document.querySelector('.a .b');
+ assert_equals(getComputedStyle(b_outside).zIndex, 'auto');
+ assert_equals(getComputedStyle(b_inside).zIndex, '1');
+}, ':scope within nested and scoped rule (implied &)');
+</script>
+
+<template id=test_nested_scope_pseudo_relative>
+ <div>
+ <style>
+ @scope (.b) {
+ .a:not(:scope) {
+ > :scope { /* & implied */
+ z-index: 1;
+ }
+ }
+ }
+ </style>
+ <div class="b">
+ </div>
+ <div class="a">
+ <div class="b">
+ </div>
+ </div>
+</div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_nested_scope_pseudo_relative.content.cloneNode(true));
+
+ let b_outside = document.querySelector('.b');
+ let b_inside = document.querySelector('.a .b');
+ assert_equals(getComputedStyle(b_outside).zIndex, 'auto');
+ assert_equals(getComputedStyle(b_inside).zIndex, '1');
+}, ':scope within nested and scoped rule (relative)');
+</script>
+
+<template id=test_scoped_nested_group_rule>
+ <div>
+ <style>
+ @scope (.a) {
+ .b:not(:scope) {
+ @media (width) {
+ z-index: 1;
+ }
+ }
+ }
+ </style>
+ <div class="b">
+ </div>
+ <div class="a">
+ <div class="b">
+ </div>
+ </div>
+</div>
+</template>
+<script>
+test((t) => {
+ t.add_cleanup(() => main.replaceChildren());
+ main.append(test_scoped_nested_group_rule.content.cloneNode(true));
+
+ let b_outside = document.querySelector('.b');
+ let b_inside = document.querySelector('.a .b');
+ assert_equals(getComputedStyle(b_outside).zIndex, 'auto');
+ assert_equals(getComputedStyle(b_inside).zIndex, '1');
+}, 'Scoped nested group rule');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-proximity.html b/testing/web-platform/tests/css/css-cascade/scope-proximity.html
new file mode 100644
index 0000000000..c133a71e9a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-proximity.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<title>@scope - proximity to root</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-proximity">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function test_scope(script_element, callback_fn, description) {
+ test((t) => {
+ // The provided <script> element must be an immedate subsequent sibling of
+ // a <template> element.
+ let template_element = script_element.previousElementSibling;
+ assert_equals(template_element.tagName, 'TEMPLATE');
+
+ t.add_cleanup(() => {
+ while (main.firstChild)
+ main.firstChild.remove()
+ });
+
+ main.append(template_element.content.cloneNode(true));
+
+ callback_fn();
+ }, description);
+}
+
+function assert_green(selector) {
+ assert_equals(getComputedStyle(main.querySelector(selector)).backgroundColor, 'rgb(0, 128, 0)');
+}
+function assert_not_green(selector) {
+ assert_equals(getComputedStyle(main.querySelector(selector)).backgroundColor, 'rgb(0, 0, 0)');
+}
+</script>
+<style>
+ main * {
+ background-color: black;
+ }
+</style>
+<main id=main>
+</main>
+
+<template>
+ <style>
+ .item {
+ padding: 0px;
+ border: 5px solid red;
+ }
+
+ @scope (.light) {
+ [id] { border-color: rgb(100, 100, 100); }
+ }
+
+ @scope (.dark) {
+ [id] { border-color: rgb(200, 200, 200); }
+ }
+ </style>
+ <div class=light>
+ <div id=item1>
+ <div class=dark>
+ <div id=item2>
+ <div class=light>
+ <div id=item3>
+ <div class=dark>
+ <div id=item4></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_equals(getComputedStyle(item1).borderColor, 'rgb(100, 100, 100)');
+ assert_equals(getComputedStyle(item2).borderColor, 'rgb(200, 200, 200)');
+ assert_equals(getComputedStyle(item3).borderColor, 'rgb(100, 100, 100)');
+ assert_equals(getComputedStyle(item4).borderColor, 'rgb(200, 200, 200)');
+}, 'Alternating light/dark');
+</script>
+
+
+<template>
+ <style>
+ @scope (.b) {
+ [id] { border-color:green; }
+ }
+ @scope (.a) {
+ [id] { border-color:red; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span id=item></span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_equals(getComputedStyle(item).borderColor, 'rgb(0, 128, 0)');
+}, 'Proximity wins over order of appearance');
+</script>
+
+
+<template>
+ <style>
+ @scope (.a) {
+ span[id] { border-color:green; }
+ }
+ @scope (.b) {
+ [id] { border-color:red; }
+ }
+ </style>
+ <div class=a>
+ <div class=b>
+ <span id=item></span>
+ </div>
+ </div>
+</template>
+<script>
+test_scope(document.currentScript, () => {
+ assert_equals(getComputedStyle(item).borderColor, 'rgb(0, 128, 0)');
+}, 'Specificity wins over proximity');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-shadow.tentative.html b/testing/web-platform/tests/css/css-cascade/scope-shadow.tentative.html
new file mode 100644
index 0000000000..83a468bd07
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-shadow.tentative.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<title>@scope - ShadowDOM</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/9025">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id=host_plain>
+ <template shadowrootmode=open>
+ <style>
+ @scope (:host) {
+ .a {
+ z-index: 1;
+ }
+ }
+ </style>
+ <div class=a>
+ </div>
+ </template>
+</div>
+<script>
+ test(() => {
+ let a = host_plain.shadowRoot.querySelector('.a');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ }, '@scope can match :host');
+</script>
+
+<div id=host_functional>
+ <template shadowrootmode=open>
+ <style>
+ @scope (:host(div)) {
+ .a {
+ z-index: 1;
+ }
+ }
+ /* Should not match: */
+ @scope (:host(span)) {
+ .a {
+ z-index: 42;
+ }
+ }
+ </style>
+ <div class=a>
+ </div>
+ </template>
+</div>
+<script>
+ test(() => {
+ let a = host_functional.shadowRoot.querySelector('.a');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ }, '@scope can match :host(...)');
+</script>
+
+<div id=host_scope_subject>
+ <template shadowrootmode=open>
+ <style>
+ @scope (:host) {
+ :scope {
+ z-index: 1;
+ }
+ }
+ </style>
+ <div class=a>
+ </div>
+ </template>
+</div>
+<script>
+ test(() => {
+ assert_equals(getComputedStyle(host_scope_subject).zIndex, '1');
+ }, ':scope matches host via the scoping root');
+</script>
+
+<div id=host_scope_subject_is>
+ <div class=host>
+ <template shadowrootmode=open>
+ <style>
+ /* Should not match host, nor outside shadow. */
+ :is(:scope, .a, .host) {
+ z-index: 2;
+ }
+
+ @scope (:host) {
+ :is(:scope, .a) {
+ z-index: 1;
+ }
+ }
+ </style>
+ <div class=a>
+ </div>
+ </template>
+ </div>
+ <div class=a>
+ </div>
+</div>
+<script>
+ test(() => {
+ let host = host_scope_subject_is.querySelector('.host');
+ assert_equals(getComputedStyle(host).zIndex, '1');
+ let a = host.shadowRoot.querySelector('.a');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+
+ let a_outside = host_scope_subject_is.querySelector('.a');
+ assert_equals(getComputedStyle(a_outside).zIndex, 'auto');
+ }, ':scope within :is() matches host via the scoping root');
+</script>
+
+<!-- Tentative. https://github.com/w3c/csswg-drafts/issues/9178 -->
+<div id=implicit_scope_shadow_parent>
+ <div class=host>
+ <template shadowrootmode=open>
+ <style>
+ @scope {
+ /* Matches host */
+ :scope {
+ z-index: 1;
+ }
+ :scope > .a {
+ z-index: 2;
+ }
+ }
+ </style>
+ <div class=a>
+ </div>
+ </template>
+ </div>
+</div>
+<script>
+ test(() => {
+ let host = implicit_scope_shadow_parent.querySelector('.host');
+ let a = host.shadowRoot.querySelector('.a');
+ assert_equals(getComputedStyle(host).zIndex, '1');
+ assert_equals(getComputedStyle(a).zIndex, '2');
+ }, 'Implicit @scope as direct child of shadow root');
+</script>
+
+<!-- Tentative. https://github.com/w3c/csswg-drafts/issues/9178 -->
+<div id=implicit_scope_constructed>
+ <div class=host>
+ <template shadowrootmode=open>
+ <div class=a>
+ </div>
+ </template>
+ </div>
+</div>
+<script>
+ test(() => {
+ let host = implicit_scope_constructed.querySelector('.host');
+ let sheet = new CSSStyleSheet();
+ sheet.replaceSync(`
+ @scope {
+ :scope {
+ z-index: 1;
+ }
+ :scope .a {
+ z-index: 2;
+ }
+ `);
+ host.shadowRoot.adoptedStyleSheets = [sheet];
+ let a = host.shadowRoot.querySelector('.a');
+ assert_equals(getComputedStyle(host).zIndex, '1');
+ assert_equals(getComputedStyle(a).zIndex, '2');
+ }, 'Implicit @scope in construted stylesheet');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-specificity.html b/testing/web-platform/tests/css/css-cascade/scope-specificity.html
new file mode 100644
index 0000000000..0f48c605a8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-specificity.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<title>@scope - specificty</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id=style>
+</style>
+<main id=main>
+ <div id=a class=a>
+ <div id=b class=b>
+ </div>
+ </div>
+</main>
+<script>
+
+// Format a scoped style rule using the selector at scoped_selector's last element,
+// with each preceding array item representing an enclosing @scope rule.
+//
+// Example:
+//
+// scoped_selector=['@scope (foo)', '@scope (bar)', 'div']
+// declarations='z-index:42'
+// => '@scope (foo) { @scope (bar) { div { z-index:42 } } }'
+function format_scoped_rule(scoped_selector, declarations) {
+ if (scoped_selector.length < 2) {
+ throw "Fail";
+ }
+ let scope_prelude = scoped_selector[0];
+ let remainder = scoped_selector.slice(1);
+ let content = remainder.length == 1
+ ? `${remainder[0]} { ${declarations} }`
+ : format_scoped_rule(remainder, declarations);
+ return `${scope_prelude} { ${content} }`;
+}
+
+// Verify that the specificity of 'scoped_selector' is the same
+// as the specificity of 'ref_selector'. Both selectors must select
+// an element within #main.
+function test_scope_specificity(scoped_selector, ref_selector) {
+ test(() => {
+ let element = main.querySelector(ref_selector);
+ assert_not_equals(element, null);
+
+ let scoped_rule = format_scoped_rule(scoped_selector, 'z-index:1');
+ let ref_rule = `:is(${ref_selector}) { z-index:2 }`;
+
+ style.textContent = `${scoped_rule}`;
+ assert_equals(getComputedStyle(element).zIndex, '1', 'scoped rule');
+
+ style.textContent = `${ref_rule}`;
+ assert_equals(getComputedStyle(element).zIndex, '2', 'unscoped rule');
+
+ // The scoped rule should win due to proximity.
+ style.textContent = `${scoped_rule} ${ref_rule}`;
+ assert_equals(getComputedStyle(element).zIndex, '1', 'scoped + unscoped');
+
+ // The scoped rule should win due to proximity (reverse).
+ style.textContent = `${ref_rule} ${scoped_rule}`;
+ assert_equals(getComputedStyle(element).zIndex, '1', 'unscoped + scoped');
+
+ // Add one (1) to the specificty of the unscoped rule. This should
+ // cause the unscoped rule to win instead.
+ style.textContent = `div${ref_rule} ${scoped_rule}`;
+ assert_equals(getComputedStyle(element).zIndex, '2', 'unscoped + scoped');
+ }, format_scoped_rule(scoped_selector, ''));
+}
+
+test_scope_specificity(['@scope (#main)', '.b'], '.b');
+test_scope_specificity(['@scope (#main) to (.b)', '.a'], '.a');
+test_scope_specificity(['@scope (#main, .foo, .bar)', '#a'], '#a');
+test_scope_specificity(['@scope (#main)', 'div.b'], 'div.b');
+test_scope_specificity(['@scope (#main)', ':scope .b'], '.a .b');
+test_scope_specificity(['@scope (#main)', '& .b'], '#main .b');
+test_scope_specificity(['@scope (#main)', 'div .b'], 'div .b');
+test_scope_specificity(['@scope (#main)', '@scope (.a)', '.b'], '.b');
+
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-starting-style.html b/testing/web-platform/tests/css/css-cascade/scope-starting-style.html
new file mode 100644
index 0000000000..b9b0580b38
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-starting-style.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>@scope - inner @starting-style</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/pull/8876">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#scope-scope">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @scope (.a) {
+ @starting-style {
+ :scope {
+ width: 100px;
+ }
+
+ .b {
+ width: 100px;
+ }
+ }
+ }
+
+ .a, .b {
+ transition: width 100s steps(2, start); /* 50% progress */
+ width: 200px;
+ }
+</style>
+<main>
+ <div class=a>
+ <div class=b>
+ </div>
+ </div>
+ <div class=b></div>
+</main>
+<script>
+ test(() => {
+ let a = document.querySelector('main > .a');
+ let b = document.querySelector('main > .a > .b');
+ assert_equals(getComputedStyle(a).width, '150px');
+ assert_equals(getComputedStyle(b).width, '150px');
+
+ let out_of_scope_b = document.querySelector('main > .b');
+ assert_equals(getComputedStyle(out_of_scope_b).width, '200px');
+ }, 'Style rules within @starting-style are scoped');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-supports.html b/testing/web-platform/tests/css/css-cascade/scope-supports.html
new file mode 100644
index 0000000000..9be41de0e9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-supports.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>@scope - inner @supports</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
+<link rel="help" href="https://drafts.csswg.org/css-conditional-3/#at-supports">
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#scope-scope">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ @scope (.a) {
+ @supports (width:0px) {
+ :scope {
+ z-index: 1;
+ }
+
+ .b {
+ background-color: green;
+ }
+ }
+ }
+</style>
+<main>
+ <div class=a>
+ <div class=b>
+ </div>
+ </div>
+ <div class=b></div>
+</main>
+<script>
+ test(() => {
+ let a = document.querySelector('main > .a');
+ let b = document.querySelector('main > .a > .b');
+ assert_equals(getComputedStyle(a).zIndex, '1');
+ assert_equals(getComputedStyle(b).backgroundColor, 'rgb(0, 128, 0)');
+
+ let out_of_scope_b = document.querySelector('main > .b');
+ assert_equals(getComputedStyle(out_of_scope_b).backgroundColor, 'rgba(0, 0, 0, 0)');
+ }, 'Style rules within @supports are scoped');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-visited-cssom.html b/testing/web-platform/tests/css/css-cascade/scope-visited-cssom.html
new file mode 100644
index 0000000000..aba4b752b2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-visited-cssom.html
@@ -0,0 +1,314 @@
+<!DOCTYPE html>
+<title>@scope - :visited and CSSOM</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<main id=main></main>
+
+<style>
+ :where(:visited, :link), :where(div) {
+ background-color: white;
+ }
+</style>
+
+<!--
+ Both #visited and #unvisited should appear to be in an unvisited state
+ through getComputedStyle.
+-->
+
+<!-- :visited/:link in scoped selector -->
+
+<template id=test_link>
+ <style>
+ @scope (main) {
+ :link { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_link.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(0, 128, 0)');
+}, ':link as scoped selector');
+</script>
+
+<template id=test_visited>
+ <style>
+ @scope (main) {
+ :visited { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_visited.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(255, 255, 255)');
+}, ':visited as scoped selector');
+</script>
+
+<template id=test_not_link>
+ <style>
+ @scope (main) {
+ :not(:link) { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_link.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(255, 255, 255)');
+}, ':not(:link) as scoped selector');
+</script>
+
+<template id=test_not_visited>
+ <style>
+ @scope (main) {
+ :not(:visited) { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_visited.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(0, 128, 0)');
+}, ':not(:visited) as scoped selector');
+</script>
+
+<!-- :visited/:link in root -->
+
+<template id=test_link_root>
+ <style>
+ @scope (main :link) {
+ div { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""><div></div></a>
+ <a id=unvisited href="x"><div></div></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_link_root.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited.firstElementChild).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited.firstElementChild).backgroundColor, 'rgb(0, 128, 0)');
+}, ':link as scoping root');
+</script>
+
+<template id=test_visited_root>
+ <style>
+ @scope (main :visited) {
+ div { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""><div></div></a>
+ <a id=unvisited href="x"><div></div></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_visited_root.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited.firstElementChild).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited.firstElementChild).backgroundColor, 'rgb(255, 255, 255)');
+}, ':visited as scoping root');
+</script>
+
+<template id=test_not_visited_root>
+ <style>
+ @scope (main :not(:visited)) {
+ div { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""><div></div></a>
+ <a id=unvisited href="x"><div></div></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_visited_root.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited.firstElementChild).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited.firstElementChild).backgroundColor, 'rgb(0, 128, 0)');
+}, ':not(:visited) as scoping root');
+</script>
+
+<template id=test_not_link_root>
+ <style>
+ @scope (main :not(:link)) {
+ div { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""><div></div></a>
+ <a id=unvisited href="x"><div></div></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_link_root.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited.firstElementChild).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited.firstElementChild).backgroundColor, 'rgb(255, 255, 255)');
+}, ':not(:link) as scoping root');
+</script>
+
+<!-- :visited/:link in scoping root, with inner :scope -->
+
+<template id=test_link_root_scope>
+ <style>
+ @scope (main :link) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_link_root_scope.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(0, 128, 0)');
+}, ':link as scoping root, :scope');
+</script>
+
+<template id=test_visited_root_scope>
+ <style>
+ @scope (main :visited) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_visited_root_scope.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(255, 255, 255)');
+}, ':visited as scoping root, :scope');
+</script>
+
+<template id=test_not_visited_root_scope>
+ <style>
+ @scope (main :not(:visited)) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_visited_root_scope.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(0, 128, 0)');
+}, ':not(:visited) as scoping root, :scope');
+</script>
+
+<template id=test_not_link_root_scope>
+ <style>
+ @scope (main :not(:link)) {
+ :scope { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_link_root_scope.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(255, 255, 255)');
+}, ':not(:link) as scoping root, :scope');
+</script>
+
+<!-- :visited/:link in scoping limit -->
+
+<template id=test_link_scoping_limit>
+ <style>
+ @scope (main) to (:link) {
+ * { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_link_scoping_limit.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(255, 255, 255)');
+}, ':link as scoping limit');
+</script>
+
+<template id=test_visited_scoping_limit>
+ <style>
+ @scope (main) to (:visited) {
+ * { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_visited_scoping_limit.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(0, 128, 0)');
+}, ':visited as scoping limit');
+</script>
+
+<template id=test_not_link_scoping_limit>
+ <style>
+ @scope (main) to (:not(:link)) {
+ * { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_link_scoping_limit.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(0, 128, 0)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(0, 128, 0)');
+}, ':not(:link) as scoping limit');
+</script>
+
+<template id=test_not_visited_scoping_limit>
+ <style>
+ @scope (main) to (:not(:visited)) {
+ * { background-color: green; }
+ }
+ </style>
+ <a id=visited href=""></a>
+ <a id=unvisited href="x"></a>
+</template>
+<script>
+test((t) => {
+ main.append(test_not_visited_scoping_limit.content.cloneNode(true));
+ t.add_cleanup(() => main.replaceChildren());
+ assert_equals(getComputedStyle(visited).backgroundColor, 'rgb(255, 255, 255)');
+ assert_equals(getComputedStyle(unvisited).backgroundColor, 'rgb(255, 255, 255)');
+}, ':not(:visited) as scoping limit');
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-visited-ref.html b/testing/web-platform/tests/css/css-cascade/scope-visited-ref.html
new file mode 100644
index 0000000000..91efd65921
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-visited-ref.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<title>@scope - :visited</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+
+<style>
+ /* The visited background-color magically gets the alpha of the
+ unvisited color, which by default is rgba(0, 0, 0, 0). Set alpha to
+ 255 so that visited colors also gets this alpha. */
+ * { background-color: rgba(255, 255, 255, 255); }
+</style>
+
+<!-- :visited via :scope in subject -->
+<div>
+ <a id=visited href="" style="background-color:coral">
+ Visited <span>Span</span>
+ </a>
+</div>
+
+<!-- :link via :scope in subject -->
+<div>
+ <a id=unvisited href="x" style="background-color:skyblue">
+ Unvisited <span>Span</span>
+ </a>
+</div>
+
+<!-- :visited via :scope in non-subject -->
+<div>
+ <a id=visited href="">
+ Visited <span style="background-color:coral">Span</span>
+ </a>
+</div>
+
+<!-- :link via :scope in non-subject -->
+<div>
+ <a id=unvisited href="x">
+ Unvisited <span style="background-color:skyblue">Span</span>
+ </a>
+</div>
+
+<!-- :visited in scope-end -->
+<div>
+ <main>
+ Main
+ <a id=visited href="">
+ Visited <span>Span</span>
+ </a>
+ </main>
+</div>
+
+<!-- :visited in scope-end, unvisited link -->
+<div>
+ <main>
+ Main
+ <a id=unvisited href="x" style="background-color:skyblue">
+ Unvisited <span>Span</span>
+ </a>
+ </main>
+</div>
+
+<!-- :link in scope-end -->
+<div>
+ <main>
+ Main
+ <a id=unvisited href="x">
+ Unvisited <span>Span</span>
+ </a>
+ </main>
+</div>
+
+<!-- :link in scope-end, visited link -->
+<div>
+ <main>
+ Main
+ <a id=visited href="" style="background-color:coral">
+ Visited <span>Span</span>
+ </a>
+ </main>
+</div>
+
+<!-- :link in scope-end, visited link -->
+<div>
+ <main>
+ Main
+ <a id=outer_visited href="">
+ Visited1
+ </a>
+ </main>
+</div>
+<script>
+ // Insert inner <a> with JS, since the parser can't produce this result.
+ let inner_a = document.createElement('a');
+ inner_a.setAttribute('href', '');
+ inner_a.textContent = 'Visited2';
+ outer_visited.append(inner_a);
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/scope-visited.html b/testing/web-platform/tests/css/css-cascade/scope-visited.html
new file mode 100644
index 0000000000..392aeb667b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/scope-visited.html
@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<title>@scope - :visited</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="match" href="scope-visited-ref.html">
+
+<!-- Sub-tests use ShadowDOM to stay isolated from eachother. -->
+
+<!-- :visited via :scope in subject -->
+<div>
+ <template shadowrootmode=open>
+ <a id=visited href="">
+ Visited <span>Span</span>
+ </a>
+ <style>
+ /* The visited background-color magically gets the alpha of the
+ unvisited color, which by default is rgba(0, 0, 0, 0). Set alpha to
+ 255 so that visited colors also gets this alpha. */
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (:visited) {
+ :scope { background-color: coral; }
+ }
+ @scope (:link) {
+ :scope { background-color: skyblue; } /* Does not match. */
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :link via :scope in subject -->
+<div>
+ <template shadowrootmode=open>
+ <a id=unvisited href="x">
+ Unvisited <span>Span</span>
+ </a>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (:link) {
+ :scope { background-color: skyblue; }
+ }
+ @scope (:visited) {
+ :scope { background-color: coral; } /* Does not match. */
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :visited via :scope in non-subject -->
+<div>
+ <template shadowrootmode=open>
+ <a id=visited href="">
+ Visited <span>Span</span>
+ </a>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (:visited) {
+ :scope span { background-color: coral; }
+ }
+ @scope (:link) {
+ :scope span { background-color: skyblue; } /* Does not match. */
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :link via :scope in non-subject -->
+<div>
+ <template shadowrootmode=open>
+ <a id=unvisited href="x">
+ Unvisited <span>Span</span>
+ </a>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (:link) {
+ :scope span { background-color: skyblue; }
+ }
+ @scope (:visited) {
+ :scope span { background-color: coral; } /* Does not match. */
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :visited in scope-end -->
+<div>
+ <template shadowrootmode=open>
+ <main>
+ Main
+ <a id=visited href="">
+ Visited <span>Span</span>
+ </a>
+ </main>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (main) to (:visited) {
+ /* Does not match, because #visited is not in scope. */
+ :scope :visited { background-color: coral; }
+ }
+ @scope (main) {
+ :scope :link { background-color: skyblue; } /* Also doesn't match. */
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :visited in scope-end, unvisited link -->
+<div>
+ <template shadowrootmode=open>
+ <main>
+ Main
+ <a id=unvisited href="x">
+ Unvisited <span>Span</span>
+ </a>
+ </main>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (main) to (:visited) {
+ /* Does not match, because #unvisited does not match it. */
+ :scope :visited { background-color: coral; }
+ }
+ @scope (main) {
+ /* Should match, because the scope-end selector (:visited) does not
+ match anything, hence we are in-scope. */
+ :scope :link { background-color: skyblue; }
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :link in scope-end -->
+<div>
+ <template shadowrootmode=open>
+ <main>
+ Main
+ <a id=unvisited href="x">
+ Unvisited <span>Span</span>
+ </a>
+ </main>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (main) to (:link) {
+ /* Does not match, because #unvisited is not in scope. */
+ :scope :link { background-color: skyblue; }
+ }
+ @scope (main) {
+ :scope :visited { background-color: coral; } /* Also doesn't match. */
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :link in scope-end, visited link -->
+<div>
+ <template shadowrootmode=open>
+ <main>
+ Main
+ <a id=visited href="">
+ Visited <span>Span</span>
+ </a>
+ </main>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ @scope (main) to (:link) {
+ /* Does not match, because #visited does not match it. */
+ :scope :link { background-color: skyblue; }
+ }
+ @scope (main) {
+ /* Should match, because the scope-end selector (:visited) does not
+ match anything, hence we are in-scope. */
+ :scope :visited { background-color: coral; }
+ }
+ </style>
+ </template>
+</div>
+
+<!-- :visited within :visited -->
+<div id=visited_in_visited>
+ <template shadowrootmode=open>
+ <main>
+ Main
+ <a href="">
+ Visited1
+ </a>
+ </main>
+ <style>
+ * { background-color: rgba(255, 255, 255, 255); }
+
+ /* Should not match since visited-link matching stops applying
+ once a link is seen. */
+ @scope (:visited) {
+ :scope > :visited { background-color: coral; }
+ }
+ </style>
+ </template>
+</div>
+
+<script>
+ window.onload = () => {
+ // Insert the inner :visited link with JS, since the parser
+ // can't produce this.
+ let outer_a = visited_in_visited.shadowRoot.querySelector('main > a');
+ let inner_a = document.createElement('a');
+ inner_a.setAttribute('href', '');
+ inner_a.textContent = 'Visited2';
+ outer_a.append(inner_a);
+ }
+</script>
diff --git a/testing/web-platform/tests/css/css-cascade/support/test-green.css b/testing/web-platform/tests/css/css-cascade/support/test-green.css
new file mode 100644
index 0000000000..da8e1014d2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/support/test-green.css
@@ -0,0 +1,4 @@
+.test {
+ background: green;
+ color: green;
+}
diff --git a/testing/web-platform/tests/css/css-cascade/support/test-red.css b/testing/web-platform/tests/css/css-cascade/support/test-red.css
new file mode 100644
index 0000000000..bb309fcd57
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/support/test-red.css
@@ -0,0 +1,4 @@
+.test {
+ background: red;
+ color: red;
+}
diff --git a/testing/web-platform/tests/css/css-cascade/tons-of-declarations-crash.html b/testing/web-platform/tests/css/css-cascade/tons-of-declarations-crash.html
new file mode 100644
index 0000000000..50a32c1938
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/tons-of-declarations-crash.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<style></style>
+<script>
+document.querySelector('style').innerText = `input { border: 1px solid }`.repeat(2 << 16);
+</script>
+<input>
diff --git a/testing/web-platform/tests/css/css-cascade/unset-val-001.html b/testing/web-platform/tests/css/css-cascade/unset-val-001.html
new file mode 100644
index 0000000000..857cb5d40d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/unset-val-001.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: the "unset" value</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-3/#inherit-initial">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-4/#inherit-initial">
+ <link rel="match" href="reference/ref-filled-green-100px-square.xht">
+ <meta name="assert" content="color:unset is the same as color:inherit since color is an inherited property. background-color:unset is the same as background-color:initial since background-color is a non-inherited property.">
+ <style>
+.square {
+ width: 100px;
+ height: 100px;
+}
+.under {
+ background-color: green;
+ margin-bottom: -100px;
+}
+.outer {
+ color: green;
+ background: red;
+}
+.inner {
+ color: red;
+ background-color: red;
+ font-size: 40px;
+ text-align: center;
+}
+.inner {
+ color: unset;/* inherit from .outer */
+ background-color: unset;/* initial, transparent, .under shows thru */
+}
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+
+ <div class="outer square">
+ <div class="under square"></div>
+ <div class="inner square">XX</div>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/unset-val-002.html b/testing/web-platform/tests/css/css-cascade/unset-val-002.html
new file mode 100644
index 0000000000..61f941a038
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/unset-val-002.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>CSS Cascade: the "unset" value</title>
+ <link rel="author" title="Chris Rebert" href="http://chrisrebert.com">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-3/#inherit-initial">
+ <link rel="help" href="http://www.w3.org/TR/css-cascade-4/#inherit-initial">
+ <link rel="match" href="reference/ref-filled-green-100px-square.xht">
+ <meta name="assert" content="display:unset should be the same as display:initial since 'display' is not an inherited property. display:unset should be the same as display:inline since 'inline' is the initial value of 'display'.">
+ <style>
+.square {
+ width: 100px;
+ height: 100px;
+}
+.green {
+ background-color: green;
+}
+.top {
+ position: absolute;
+}
+.fail {
+ background-color: red;
+ display: inline-block;
+ display: unset;/* initial, inline */
+}
+ </style>
+</head>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+
+ <div class="top"><span class="square fail"></span></div>
+ <div class="green square"></div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-cascade/unset-value-storage.html b/testing/web-platform/tests/css/css-cascade/unset-value-storage.html
new file mode 100644
index 0000000000..97a27ff67b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-cascade/unset-value-storage.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Storage of "unset" value</title>
+<meta name="author" title="Xidorn Quan" href="https://www.upsuper.org">
+<link rel="help" href="https://www.w3.org/TR/css-cascade-3/#inherit-initial"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ div {
+ color: unset;
+ border: unset;
+ }
+</style>
+<body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ let properties = ["color", "border", "border-left", "border-color", "border-right-style"];
+ let style = document.styleSheets[0].cssRules[0].style;
+ for (let prop of properties) {
+ assert_equals(style.getPropertyValue(prop), "unset", `${prop} is expected to be "unset"`);
+ }
+ });
+ </script>