| <!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> |