Files
sousa-gecko/testing/web-platform/tests/css/css-cascade/scope-nesting.html
T
Anders Hartvoll Ruud 81bf0dcc28 Bug 1993475 [wpt PR 55297] - Include child rules when adding rules to RuleSetDiff, a=testonly
Automatic update from web-platform-tests
Include child rules when adding rules to RuleSetDiff

When some style rule changes (e.g. its selector text), any inner
style rules may also effectively change due to the &/:scope selector.
We already accounted for this during RuleSet::AddFilteredRulesFrom-
OtherBucket by traversing the chain of parent rules to see if any
were marked for inclusion. However, we did this by searching for
the '&' selector specifically, which means :scope cases did not
invalidate properly.

Unlike '&', :scope does not capture its parent style rule during
parsing, and a StyleRule has no general way of reaching its parent
(we don't store that pointer). Therefore, this CL traverses the child
rules top-down instead, during RuleSetDiff::AddDiff. The end result
should be the same, except that we now also include child rules
of @scope rules.

Note: There is little point in checking if inner style rules actually
contain &/:scope in their selector or not; if omitted by the author,
we insert them implicitly.

Bug: 449542602
Change-Id: I9a42e61ede422f41e83200064f74bff6c164047a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7013292
Reviewed-by: Steinar H Gunderson <sesse@chromium.org>
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1526740}

--

wpt-commits: 2bba74c15b2b421eee0de9c55f8777db39439848
wpt-pr: 55297
2025-10-20 16:38:29 +00:00

769 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/9740">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<main id=main></main>
<template id=test_nest_scope_end>
<div>
<style>
/* (& > .b) behaves like (:where(:scope) > .b), due & mapping to :where(:scope).*/
@scope (.a) to (& > .b) {
:scope { z-index:1; }
}
/* Should not match, since <scope-end> refers to the scope itself. */
@scope (.a) to (.b&) {
: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.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');
}, '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 (#div) {
& {
z-index: 1;
& {
z-index: 2;
}
}
}
</style>
<div id=div></div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_inner_nest.content.cloneNode(true));
assert_equals(getComputedStyle(div).zIndex, '2');
}, 'Nested nesting-selectors within scope\'s <stylesheet> select inclusive descendants of the scope root');
</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_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 {
/* The '& .b' selector is wrapped in :where() to prevent a false
positive when the implementation incorrectly wraps
the z-index declaration in a rule with &-behavior
rather than :where(:scope)-behavior. */
@scope (:where(& .b)) {
z-index: 1; /* Should win due to proximity */
}
}
:where(.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>
<template id=test_scoped_within_scoped>
<div>
<style>
@scope (.a) {
@scope(#descendant) {
:scope {
z-index: 1;
}
}
@scope (> #child) {
:scope {
z-index: 1;
}
}
}
</style>
<div class="a">
<div id="descendant">
</div>
<div id="child">
</div>
</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_scoped_within_scoped.content.cloneNode(true));
assert_equals(getComputedStyle(descendant).zIndex, '1');
assert_equals(getComputedStyle(child).zIndex, '1');
}, 'Scoped nested within another scope');
</script>
<template id=test_implicit_scope_nested_group_rule>
<div class=nest>
<style>
.nest {
@scope {
#child {
color: green;
}
}
}
</style>
<div id=child>Foo</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_implicit_scope_nested_group_rule.content.cloneNode(true));
assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)');
}, 'Implicit (prelude-less) @scope as a nested group rule');
</script>
<template id=test_insert_ampersand_rule_within_scope>
<style>
.a {
@scope (.b) {
#child {
color: red;
}
}
}
</style>
<div class=a>
<div class=b>
<div id=child>Foo</div>
</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_insert_ampersand_rule_within_scope.content.cloneNode(true));
assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)');
let scope_rule = main.querySelector('style').sheet.cssRules[0].cssRules[0];
// & does not add specificity - inserting it up front does nothing...
scope_rule.insertRule('& #child { color: green; }', 0);
assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)');
scope_rule.deleteRule(0);
// ... But inserting it at the end makes it win by order of appearance.
scope_rule.insertRule('& #child { color: green; }', scope_rule.cssRules.length);
assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)');
}, 'Insert a nested style rule within @scope, &');
</script>
<template id=test_insert_pseudo_scope_rule_within_scope>
<style>
.a {
@scope (.b) {
#child {
color: red;
}
}
}
</style>
<div class=a>
<div class=b>
<div id=child>Foo</div>
</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_insert_pseudo_scope_rule_within_scope.content.cloneNode(true));
assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)');
let scope_rule = main.querySelector('style').sheet.cssRules[0].cssRules[0];
scope_rule.insertRule(':scope #child { color: green; }');
assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)');
}, 'Insert a nested style rule within @scope, :scope');
</script>
<template id=test_insert_nested_declarations_directly>
<style>
:where(.a) {
color: red;
}
@scope (.a) {
}
</style>
<div class=a>
<div id=child>Foo</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_insert_nested_declarations_directly.content.cloneNode(true));
assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)');
let scope_rule = main.querySelector('style').sheet.cssRules[1];
assert_true(scope_rule instanceof CSSScopeRule);
scope_rule.insertRule('color: green');
assert_true(scope_rule.cssRules[0] instanceof CSSNestedDeclarations);
assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)');
}, 'Insert a CSSNestedDeclarations rule directly in top-level @scope');
</script>
<template id=test_mutate_outer_selector_text_nested_declaration>
<style>
#child {
color: green; /* Specificity: (1, 0, 0) */
}
.b {
#child {
@scope (&) {
--x: 1;
color: red; /* Specificity: (0, 0, 0), effectively :where(:scope) */
}
}
}
</style>
<div class=a>
<div id=child>Foo</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_mutate_outer_selector_text_nested_declaration.content.cloneNode(true));
assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)');
assert_equals(getComputedStyle(child).getPropertyValue('--x'), '');
let outer_rule = main.querySelector('style').sheet.cssRules[1];
assert_equals(outer_rule.selectorText, '.b');
outer_rule.selectorText = '.a';
assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)'); // Unchanged.
assert_equals(getComputedStyle(child).getPropertyValue('--x'), '1'); // Changed.
}, 'Mutating selectorText on outer style rule causes correct inner specificity');
</script>
<template id=test_set_selector_text>
<style id=style>
:where(.x) {
background-color: black;
}
.a {
@scope (&) {
:scope .x { background-color: green; }
}
}
</style>
<div class=a>
<div class=x>
</div>
</div>
<div class=b>
<div class=x>
</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_set_selector_text.content.cloneNode(true));
let ax = main.querySelector('.a > .x');
let bx = main.querySelector('.b > .x');
assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 128, 0)');
assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 0, 0)');
style.sheet.cssRules[1].selectorText = '.b'; // .a -> .
assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 0, 0)');
assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 128, 0)');
}, 'Modifying selectorText invalidates inner scoped rules');
</script>
<template id=test_set_selector_text_nested_declarations>
<style id=style>
:where(.x) {
background-color: black;
}
.a {
@scope (&) {
:scope .x {
.unused {}
/* CSSNestedDeclarations { */
background-color: green;
/* } { */
}
}
}
</style>
<div class=a>
<div class=x>
</div>
</div>
<div class=b>
<div class=x>
</div>
</div>
</template>
<script>
test((t) => {
t.add_cleanup(() => main.replaceChildren());
main.append(test_set_selector_text_nested_declarations.content.cloneNode(true));
let ax = main.querySelector('.a > .x');
let bx = main.querySelector('.b > .x');
assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 128, 0)');
assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 0, 0)');
style.sheet.cssRules[1].selectorText = '.b'; // .a -> .
assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 0, 0)');
assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 128, 0)');
}, 'Modifying selectorText invalidates inner scoped rules (nested declarations)');
</script>