Files
ladybird/Tests/LibWeb/Text/input/css/style-invalidation/structural-matrix.js
Andreas Kling c9cac66677 Tests: Expand style invalidation coverage
Add broader style-invalidation regression coverage for selector shapes
that are easy to mis-model when invalidation data is flattened into
feature sets. Cover selector-list alternatives, nested pseudo-class
arguments, pseudo-element arguments, nth-child selector lists,
quirks-mode class matching, mixed-case ancestor filters, duplicate
invalidation rule merging, and concrete :has() feature filtering.

Keep the expectations on the unoptimized baseline so follow-up
optimization commits can show the counter progressions separately.
These tests dump the normal invalidation counters instead of asserting
specific counter values in script.
2026-04-29 15:47:23 +02:00

2469 lines
90 KiB
JavaScript

const BASE = "base";
const MATCH = "match";
const PROBE_PROPERTY = "--style-invalidation-probe";
const MATCH_DECLARATION = `${PROBE_PROPERTY}: ${MATCH}; content: "";`;
function assertEqual(testName, actual, expected) {
if (actual !== expected) throw new Error(`${testName}: expected ${expected}, got ${actual}`);
}
function resetStyleCounters() {
internals.resetStyleInvalidationCounters();
}
function styleCounterSummary() {
const counters = internals.getStyleInvalidationCounters();
return (
`styleInvalidations=${counters.styleInvalidations}, fullStyleInvalidations=${counters.fullStyleInvalidations}, ` +
`elementStyleRecomputations=${counters.elementStyleRecomputations}, ` +
`elementStyleNoopRecomputations=${counters.elementStyleNoopRecomputations}, ` +
`elementInheritedStyleRecomputations=${counters.elementInheritedStyleRecomputations}, ` +
`elementInheritedStyleNoopRecomputations=${counters.elementInheritedStyleNoopRecomputations}, ` +
`hasAncestorWalkInvocations=${counters.hasAncestorWalkInvocations}, ` +
`hasInvalidationMetadataCandidates=${counters.hasInvalidationMetadataCandidates}, ` +
`hasMatchInvocations=${counters.hasMatchInvocations}, ` +
`hasResultCacheHits=${counters.hasResultCacheHits}, hasResultCacheMisses=${counters.hasResultCacheMisses}`
);
}
function styleCounters() {
return internals.getStyleInvalidationCounters();
}
function printPassWithCounters(testName) {
println(`PASS: ${testName} | ${styleCounterSummary()}`);
}
function makeElement(tagName, options = {}) {
const element = document.createElement(tagName);
if (options.id) element.id = options.id;
if (options.className) element.className = options.className;
if (options.text !== undefined) element.textContent = options.text;
if (options.attributes) {
for (const [name, value] of Object.entries(options.attributes)) element.setAttribute(name, value);
}
return element;
}
function makeSvgElement(tagName, options = {}) {
const element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
if (options.id) element.id = options.id;
if (options.className) element.setAttribute("class", options.className);
if (options.attributes) {
for (const [name, value] of Object.entries(options.attributes)) element.setAttribute(name, value);
}
return element;
}
function addStyle(root, cssText) {
const style = document.createElement("style");
style.textContent = cssText;
root.appendChild(style);
return style;
}
function addProbeStyle(root, selectorText, duplicateRuleCount = 1) {
const rules = [];
for (let i = 0; i < duplicateRuleCount; ++i) rules.push(`${selectorText} { ${MATCH_DECLARATION} }`);
return addStyle(root, rules.join("\n"));
}
function makeSubject(options = {}) {
const tagName = options.svgTagName || options.tagName || (options.shadowHost ? "x-subject" : "div");
const subject = options.svgTagName
? makeSvgElement(tagName, { className: "subject", attributes: options.attributes })
: makeElement(tagName, { className: "subject", attributes: options.attributes });
if (options.shadowHost) subject.attachShadow({ mode: "open" }).appendChild(document.createElement("slot"));
return subject;
}
function appendShadowPartHost(fixture, options = {}) {
const host = makeElement("x-host");
fixture.appendChild(host);
const shadowRoot = host.attachShadow({ mode: "open" });
const subject = makeSubject({
...options.subjectOptions,
shadowHost: options.subjectIsShadowHost,
attributes: { ...(options.subjectOptions?.attributes || {}), part: "foo" },
});
shadowRoot.appendChild(subject);
return { host, shadowRoot, subject };
}
function appendSvgUseDocumentStyledSubject(fixture) {
const sourceId = `source-${appendSvgUseDocumentStyledSubject.nextId++}`;
const useId = `use-${appendSvgUseDocumentStyledSubject.nextId++}`;
fixture.innerHTML = `<svg><defs><g id="${sourceId}" class="subject"></g></defs><use id="${useId}" href="#${sourceId}"></use></svg>`;
document.body.offsetWidth;
const use = document.getElementById(useId);
return internals.getShadowRoot(use).firstChild;
}
appendSvgUseDocumentStyledSubject.nextId = 0;
function buildFixture(scope, selector, pseudoElement, options = {}) {
const fixture = makeElement("section");
document.body.appendChild(fixture);
const duplicateRuleCount = options.duplicateRuleCount ?? 1;
let subject = makeSubject(options.subjectOptions);
let target = subject;
let ruleStyle = null;
let cleanup = () => {};
if (scope === "document") {
fixture.appendChild(subject);
ruleStyle = addProbeStyle(document.head, `.subject${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "shadow-internal") {
const host = makeElement("div");
fixture.appendChild(host);
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.appendChild(subject);
addProbeStyle(shadowRoot, `.subject${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "shadow-host") {
target = makeElement("div");
fixture.appendChild(target);
const shadowRoot = target.attachShadow({ mode: "open" });
shadowRoot.appendChild(document.createElement("slot"));
target.className = "subject";
subject = target;
addProbeStyle(shadowRoot, `:host(.subject${selector})${pseudoElement}`, duplicateRuleCount);
} else if (scope === "slotted-default") {
const host = makeElement("div");
fixture.appendChild(host);
host.appendChild(subject);
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.appendChild(document.createElement("slot"));
addProbeStyle(shadowRoot, `::slotted(.subject${selector})`, duplicateRuleCount);
} else if (scope === "slotted-named") {
const host = makeElement("div");
fixture.appendChild(host);
subject.slot = "named";
host.appendChild(subject);
const shadowRoot = host.attachShadow({ mode: "open" });
const slot = makeElement("slot", { attributes: { name: "named" } });
shadowRoot.appendChild(slot);
addProbeStyle(shadowRoot, `::slotted(.subject${selector})`, duplicateRuleCount);
} else if (scope === "slotted-shadow-host-default") {
subject = makeSubject({ shadowHost: true });
target = subject;
const host = makeElement("div");
fixture.appendChild(host);
host.appendChild(subject);
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.appendChild(document.createElement("slot"));
addProbeStyle(shadowRoot, `::slotted(.subject${selector})`, duplicateRuleCount);
} else if (scope === "slotted-shadow-host-named") {
subject = makeSubject({ shadowHost: true });
target = subject;
const host = makeElement("div");
fixture.appendChild(host);
subject.slot = "named";
host.appendChild(subject);
const shadowRoot = host.attachShadow({ mode: "open" });
const slot = makeElement("slot", { attributes: { name: "named" } });
shadowRoot.appendChild(slot);
addProbeStyle(shadowRoot, `::slotted(.subject${selector})`, duplicateRuleCount);
} else if (scope === "part-document") {
const partFixture = appendShadowPartHost(fixture, options);
subject = partFixture.subject;
target = subject;
ruleStyle = addProbeStyle(document.head, `x-host::part(foo)${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "part-shadow-host-document") {
const partFixture = appendShadowPartHost(fixture, { subjectIsShadowHost: true });
subject = partFixture.subject;
target = subject;
ruleStyle = addProbeStyle(document.head, `x-host::part(foo)${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "part-shadow-internal") {
const partFixture = appendShadowPartHost(fixture, options);
subject = partFixture.subject;
target = subject;
addProbeStyle(partFixture.shadowRoot, `:host::part(foo)${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "part-shadow-host-internal") {
const partFixture = appendShadowPartHost(fixture, { subjectIsShadowHost: true });
subject = partFixture.subject;
target = subject;
addProbeStyle(partFixture.shadowRoot, `:host::part(foo)${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "part-shadow-ancestor") {
const outerHost = makeElement("x-outer-host");
fixture.appendChild(outerHost);
const outerShadowRoot = outerHost.attachShadow({ mode: "open" });
const innerHost = makeElement("x-host");
outerShadowRoot.appendChild(innerHost);
const innerShadowRoot = innerHost.attachShadow({ mode: "open" });
subject = makeElement("div", { className: "subject", attributes: { part: "foo" } });
target = subject;
innerShadowRoot.appendChild(subject);
addProbeStyle(outerShadowRoot, `x-host::part(foo)${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "part-shadow-host-ancestor") {
const outerHost = makeElement("x-outer-host");
fixture.appendChild(outerHost);
const outerShadowRoot = outerHost.attachShadow({ mode: "open" });
const innerHost = makeElement("x-host");
outerShadowRoot.appendChild(innerHost);
const innerShadowRoot = innerHost.attachShadow({ mode: "open" });
subject = makeSubject({ shadowHost: true, attributes: { part: "foo" } });
target = subject;
innerShadowRoot.appendChild(subject);
addProbeStyle(outerShadowRoot, `x-host::part(foo)${selector}${pseudoElement}`, duplicateRuleCount);
} else if (scope === "document-styled-use-shadow") {
ruleStyle = addProbeStyle(document.head, `.subject${selector}${pseudoElement}`, duplicateRuleCount);
subject = appendSvgUseDocumentStyledSubject(fixture);
target = subject;
} else {
throw new Error(`Unknown scope ${scope}`);
}
cleanup = () => {
ruleStyle?.remove();
fixture.remove();
};
return { cleanup, fixture, subject, target };
}
function readProbe(target, pseudoElement) {
return (
getComputedStyle(target, pseudoElement || null)
.getPropertyValue(PROBE_PROPERTY)
.trim() || BASE
);
}
function caseSupportedInScope(scope, selectorCase, pseudoElement = "") {
if (selectorCase.requiresSvgSubject) {
if (pseudoElement) return false;
return ![
"document-styled-use-shadow",
"shadow-host",
"slotted-shadow-host-default",
"slotted-shadow-host-named",
"part-shadow-host-document",
"part-shadow-host-internal",
"part-shadow-ancestor",
"part-shadow-host-ancestor",
].includes(scope);
}
return true;
}
function runCase({
suite,
scope,
selector,
name,
setup,
mutate,
initial,
after,
pseudoElement = "",
skipInitialRead = false,
fixtureOptions = {},
}) {
if (
!caseSupportedInScope(
scope,
{ requiresSvgSubject: fixtureOptions.subjectOptions?.svgTagName !== undefined },
pseudoElement
)
)
return;
const testName = `${suite}: ${scope}: ${name}`;
const { cleanup, fixture, subject, target } = buildFixture(scope, selector, pseudoElement, fixtureOptions);
try {
setup?.(subject, fixture);
if (!skipInitialRead)
assertEqual(`${testName} initial`, readProbe(target, pseudoElement), initial ? MATCH : BASE);
resetStyleCounters();
mutate(subject, fixture);
assertEqual(`${testName} after mutation`, readProbe(target, pseudoElement), after ? MATCH : BASE);
printPassWithCounters(testName);
} finally {
cleanup();
}
}
function appendScopedDuplicateRuleFixture(scope, ruleText) {
const fixture = makeElement("section");
document.body.appendChild(fixture);
if (scope === "document") {
const style = addStyle(document.head, ruleText);
return {
fixture,
cleanup: () => {
style.remove();
fixture.remove();
},
};
}
if (scope === "shadow-internal") {
const host = makeElement("div");
fixture.appendChild(host);
const shadowRoot = host.attachShadow({ mode: "open" });
addStyle(shadowRoot, ruleText);
return { fixture: shadowRoot, cleanup: () => fixture.remove() };
}
throw new Error(`Unsupported duplicate rule scope ${scope}`);
}
function runDuplicateDescendantInvalidationRuleCase(scope) {
const ruleText = `
.active .target { ${MATCH_DECLARATION} }
.active .target { ${MATCH_DECLARATION} }
`;
const scoped = appendScopedDuplicateRuleFixture(scope, ruleText);
const ancestor = makeElement("div");
const target = makeElement("div", { className: "target" });
ancestor.appendChild(target);
scoped.fixture.appendChild(ancestor);
try {
assertEqual(`duplicate descendant invalidation rule ${scope} initial`, readProbe(target), BASE);
resetStyleCounters();
ancestor.classList.add("active");
assertEqual(`duplicate descendant invalidation rule ${scope} after mutation`, readProbe(target), MATCH);
printPassWithCounters(`duplicate descendant invalidation rules merge: ${scope}`);
} finally {
scoped.cleanup();
}
}
function runDuplicateSiblingInvalidationRuleCase(scope) {
const ruleText = `
.trigger.active + .target { ${MATCH_DECLARATION} }
.trigger.active + .target { ${MATCH_DECLARATION} }
`;
const scoped = appendScopedDuplicateRuleFixture(scope, ruleText);
const trigger = makeElement("div", { className: "trigger" });
const target = makeElement("div", { className: "target" });
scoped.fixture.append(trigger, target);
try {
assertEqual(`duplicate sibling invalidation rule ${scope} initial`, readProbe(target), BASE);
resetStyleCounters();
trigger.classList.add("active");
assertEqual(`duplicate sibling invalidation rule ${scope} after mutation`, readProbe(target), MATCH);
printPassWithCounters(`duplicate sibling invalidation rules merge: ${scope}`);
} finally {
scoped.cleanup();
}
}
function appendHitDescendant(subject) {
const outer = makeElement("span", { className: "outer" });
const middle = makeElement("span", { className: "middle" });
middle.appendChild(makeElement("span", { className: "hit" }));
outer.appendChild(middle);
subject.appendChild(outer);
}
function appendSvgHit(subject) {
subject.appendChild(makeSvgElement("rect", { className: "hit" }));
}
function appendSiblingPair(subject, firstClass, secondClass) {
subject.appendChild(makeElement("span", { className: firstClass }));
subject.appendChild(makeElement("span", { className: secondClass }));
}
const childMutationCases = [
{
name: ":empty becomes false after element append",
selector: ":empty",
initial: true,
after: false,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":empty becomes false after textContent",
selector: ":empty",
initial: true,
after: false,
mutate: subject => (subject.textContent = "text"),
},
{
name: "mixed-case SVG type selector with :empty becomes false after element append",
selector: ":is(linearGradient.subject):empty",
initial: true,
after: false,
requiresSvgSubject: true,
fixtureOptions: { subjectOptions: { svgTagName: "linearGradient" } },
mutate: subject => subject.appendChild(makeSvgElement("stop")),
},
{
name: "mixed-case SVG type selector with :not(:empty) becomes true after element append",
selector: ":is(linearGradient.subject):not(:empty)",
initial: false,
after: true,
requiresSvgSubject: true,
fixtureOptions: { subjectOptions: { svgTagName: "linearGradient" } },
mutate: subject => subject.appendChild(makeSvgElement("stop")),
},
{
name: "mixed-case SVG foreignObject type selector with :empty becomes false after element append",
selector: ":is(foreignObject.subject):empty",
initial: true,
after: false,
requiresSvgSubject: true,
fixtureOptions: { subjectOptions: { svgTagName: "foreignObject" } },
mutate: subject => subject.appendChild(makeSvgElement("g")),
},
{
name: ":has(.hit) after direct child append",
selector: ":has(.hit)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit) after DocumentFragment append",
selector: ":has(.hit)",
initial: false,
after: true,
mutate: subject => {
const fragment = document.createDocumentFragment();
fragment.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(fragment);
},
},
{
name: ":has(.hit) after existing child move into parent",
selector: ":has(.hit)",
initial: false,
after: true,
mutate: (subject, fixture) => {
const oldParent = makeElement("div");
const child = makeElement("span", { className: "hit" });
oldParent.appendChild(child);
fixture.appendChild(oldParent);
subject.appendChild(child);
},
},
{
name: ":has(.hit) after nested child append",
selector: ":has(.hit)",
initial: false,
after: true,
mutate: subject => {
const wrapper = makeElement("span");
wrapper.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(wrapper);
},
},
{
name: ":has(> .hit) after direct child append",
selector: ":has(> .hit)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(> .hit:first-child) after prepend",
selector: ":has(> .hit:first-child)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.prepend(makeElement("span", { className: "hit" })),
},
{
name: ":has(> .hit:first-child) after insertBefore",
selector: ":has(> .hit:first-child)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.insertBefore(makeElement("span", { className: "hit" }), subject.firstChild),
},
{
name: ":has(> .hit) ignores nested child append",
selector: ":has(> .hit)",
initial: false,
after: false,
mutate: subject => {
const wrapper = makeElement("span");
wrapper.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(wrapper);
},
},
{
name: ":has(span.hit) after typed child append",
selector: ":has(span.hit)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has([data-hit]) after attribute-bearing child append",
selector: ":has([data-hit])",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { attributes: { "data-hit": "" } })),
},
{
name: ":has(#hit-id) after identified child append",
selector: ":has(#hit-id)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { id: "hit-id" })),
},
{
name: ":has(:is(.hit)) after direct child append",
selector: ":has(:is(.hit))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(:where(.hit)) after direct child append",
selector: ":has(:where(.hit))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:not(.miss)) after direct child append",
selector: ":has(.hit:not(.miss))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:first-child) after first child append",
selector: ":has(.hit:first-child)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:last-child) after last child append",
selector: ":has(.hit:last-child)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:nth-child(2)) after second child append",
selector: ":has(.hit:nth-child(2))",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit + .after) after adjacent sibling append",
selector: ":has(.hit + .after)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.appendChild(makeElement("span", { className: "after" })),
},
{
name: ":has(.hit ~ .after) after indirect sibling append",
selector: ":has(.hit ~ .after)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => {
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "after" }));
},
},
{
name: ":has(> .wrapper > .hit) after nested structure append",
selector: ":has(> .wrapper > .hit)",
initial: false,
after: true,
mutate: subject => {
const wrapper = makeElement("span", { className: "wrapper" });
wrapper.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(wrapper);
},
},
{
name: ":not(:has(.hit)) becomes false after child append",
selector: ":not(:has(.hit))",
initial: true,
after: false,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit, .fallback) after first-list child append",
selector: ":has(.hit, .fallback)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(:empty) after empty child append",
selector: ":has(:empty)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(.hit) after replaceChild",
selector: ":has(.hit)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.replaceChild(makeElement("span", { className: "hit" }), subject.firstChild),
},
{
name: ":has(.hit) after replaceChildren",
selector: ":has(.hit)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.replaceChildren(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit) becomes false after child removal",
selector: ":has(.hit)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.firstChild.remove(),
},
{
name: ":has(.hit) becomes false after removeChild",
selector: ":has(.hit)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.removeChild(subject.firstChild),
},
{
name: ":has(.hit) becomes false after child moves out",
selector: ":has(.hit)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: (subject, fixture) => makeElement("div").appendChild(subject.firstChild),
},
{
name: ":empty becomes true after replaceChildren",
selector: ":empty",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.replaceChildren(),
},
{
name: ":empty becomes true after textContent clear",
selector: ":empty",
initial: false,
after: true,
setup: subject => (subject.textContent = "text"),
mutate: subject => (subject.textContent = ""),
},
{
name: ":empty stays true after comment append",
selector: ":empty",
initial: true,
after: true,
mutate: subject => subject.appendChild(document.createComment("ignored")),
},
{
name: ":empty becomes false after whitespace text append",
selector: ":empty",
initial: true,
after: false,
mutate: subject => subject.appendChild(document.createTextNode(" ")),
},
{
name: ":dir(rtl) after dir=auto text mutation",
selector: ":dir(rtl)",
initial: false,
after: true,
setup: subject => {
subject.dir = "auto";
subject.textContent = "a";
},
mutate: subject => (subject.firstChild.data = "א"),
},
];
childMutationCases.push(
{
name: ":has(*) after child append",
selector: ":has(*)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(> *) after child append",
selector: ":has(> *)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(:checked) after checked checkbox append",
selector: ":has(:checked)",
initial: false,
after: true,
mutate: subject => {
const input = makeElement("input", { attributes: { type: "checkbox" } });
input.checked = true;
subject.appendChild(input);
},
},
{
name: ":has(:checked) becomes false after checked checkbox removal",
selector: ":has(:checked)",
initial: true,
after: false,
setup: subject => {
const input = makeElement("input", { attributes: { type: "checkbox" } });
input.checked = true;
subject.appendChild(input);
},
mutate: subject => subject.firstChild.remove(),
},
{
name: ":has(:not(:checked)) after unchecked checkbox append",
selector: ":has(:not(:checked))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } })),
},
{
name: ":has(:enabled) after enabled input append",
selector: ":has(:enabled)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("input")),
},
{
name: ":has(:disabled) after disabled input append",
selector: ":has(:disabled)",
initial: false,
after: true,
mutate: subject => {
const input = makeElement("input");
input.disabled = true;
subject.appendChild(input);
},
},
{
name: ":has(:required) after required input append",
selector: ":has(:required)",
initial: false,
after: true,
mutate: subject => {
const input = makeElement("input");
input.required = true;
subject.appendChild(input);
},
},
{
name: ":has(:optional) after optional input append",
selector: ":has(:optional)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("input")),
},
{
name: ":has(:empty) becomes false after text inserted into only child",
selector: ":has(:empty)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.firstChild.appendChild(document.createTextNode("x")),
},
{
name: ":has(:not(:empty)) after non-empty child append",
selector: ":has(:not(:empty))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { text: "x" })),
},
{
name: ":has(:dir(rtl)) after dir=auto child append",
selector: ":has(:dir(rtl))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { text: "א", attributes: { dir: "auto" } })),
},
{
name: ":has(:lang(he)) after Hebrew language child append",
selector: ":has(:lang(he))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { attributes: { lang: "he" } })),
},
{
name: ":has(:link) after linked anchor append",
selector: ":has(:link)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("a", { attributes: { href: "https://example.com/" } })),
},
{
name: ":has(:any-link) after linked anchor append",
selector: ":has(:any-link)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("a", { attributes: { href: "https://example.com/" } })),
},
{
name: ":has(:only-child) after only child append",
selector: ":has(:only-child)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(:only-child) becomes false after sibling append",
selector: ":has(:only-child)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(:nth-child(2)) after second child append",
selector: ":has(:nth-child(2))",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(:nth-last-child(2)) after trailing child append",
selector: ":has(:nth-last-child(2))",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(:first-of-type) after typed child append",
selector: ":has(:first-of-type)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(:is(:checked, :disabled)) after disabled input append",
selector: ":has(:is(:checked, :disabled))",
initial: false,
after: true,
mutate: subject => {
const input = makeElement("input");
input.disabled = true;
subject.appendChild(input);
},
},
{
name: ":has(:where(:empty, :checked)) after empty child append",
selector: ":has(:where(:empty, :checked))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(rect.hit) after SVG child append",
selector: ":has(rect.hit)",
initial: false,
after: true,
mutate: appendSvgHit,
},
{
name: ":has(> rect.hit) after direct SVG child append",
selector: ":has(> rect.hit)",
initial: false,
after: true,
mutate: appendSvgHit,
},
{
name: ":has(.outer .middle .hit) after deep descendant append",
selector: ":has(.outer .middle .hit)",
initial: false,
after: true,
mutate: appendHitDescendant,
},
{
name: ":has(> .outer .hit) after child subtree append",
selector: ":has(> .outer .hit)",
initial: false,
after: true,
mutate: appendHitDescendant,
},
{
name: ":has(> .outer > .middle > .hit) after exact deep append",
selector: ":has(> .outer > .middle > .hit)",
initial: false,
after: true,
mutate: appendHitDescendant,
},
{
name: ":has(.miss .hit) ignores hit without required ancestor",
selector: ":has(.miss .hit)",
initial: false,
after: false,
mutate: appendHitDescendant,
},
{
name: ":has(.before + .hit) after adjacent pair append",
selector: ":has(.before + .hit)",
initial: false,
after: true,
mutate: subject => appendSiblingPair(subject, "before", "hit"),
},
{
name: ":has(.before ~ .hit) after separated sibling append",
selector: ":has(.before ~ .hit)",
initial: false,
after: true,
mutate: subject => {
subject.appendChild(makeElement("span", { className: "before" }));
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "hit" }));
},
},
{
name: ":has(.hit + .after + .tail) after sibling chain append",
selector: ":has(.hit + .after + .tail)",
initial: false,
after: true,
mutate: subject => {
subject.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(makeElement("span", { className: "after" }));
subject.appendChild(makeElement("span", { className: "tail" }));
},
},
{
name: ":has(.hit ~ .after ~ .tail) after indirect chain append",
selector: ":has(.hit ~ .after ~ .tail)",
initial: false,
after: true,
mutate: subject => {
subject.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "after" }));
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "tail" }));
},
},
{
name: ":has(.hit:only-child) after only child append",
selector: ":has(.hit:only-child)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:only-child) becomes false after sibling append",
selector: ":has(.hit:only-child)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(.hit:nth-last-child(1)) after last child append",
selector: ":has(.hit:nth-last-child(1))",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:nth-last-child(2)) after trailing child append",
selector: ":has(.hit:nth-last-child(2))",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => {
subject.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(makeElement("span"));
},
},
{
name: ":has(.hit:nth-of-type(2)) after second typed child append",
selector: ":has(.hit:nth-of-type(2))",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:only-of-type) after only typed child append",
selector: ":has(.hit:only-of-type)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:only-of-type) becomes false after same type sibling append",
selector: ":has(.hit:only-of-type)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(:is(.hit, .fallback)) after second-list child append",
selector: ":has(:is(.hit, .fallback))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "fallback" })),
},
{
name: ":has(:where(.hit, .fallback)) after second-list child append",
selector: ":has(:where(.hit, .fallback))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "fallback" })),
},
{
name: ":has(.hit):has(.also) after independent children append",
selector: ":has(.hit):has(.also)",
initial: false,
after: true,
mutate: subject => {
subject.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(makeElement("span", { className: "also" }));
},
},
{
name: ":is(:has(.hit)) after direct child append",
selector: ":is(:has(.hit))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":where(:has(.hit)) after direct child append",
selector: ":where(:has(.hit))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":not(:has(.miss)):has(.hit) after direct child append",
selector: ":not(:has(.miss)):has(.hit)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:not(:empty)) after non-empty child append",
selector: ":has(.hit:not(:empty))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit", text: "text" })),
},
{
name: ":has(.hit:empty) becomes false after nested text append",
selector: ":has(.hit:empty)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.firstChild.appendChild(document.createTextNode("text")),
},
{
name: ":has(.hit) after append with multiple arguments",
selector: ":has(.hit)",
initial: false,
after: true,
mutate: subject => subject.append(makeElement("span"), makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:first-child + .after) after prepend before existing child",
selector: ":has(.hit:first-child + .after)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span", { className: "after" })),
mutate: subject => subject.prepend(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit:last-child) becomes false after trailing sibling append",
selector: ":has(.hit:last-child)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: ":has(.hit + .after) becomes false after middle child removal",
selector: ":has(.hit + .after)",
initial: true,
after: false,
setup: subject => {
subject.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(makeElement("span", { className: "after" }));
},
mutate: subject => subject.firstChild.remove(),
},
{
name: ":has(.hit ~ .after) becomes false after following child removal",
selector: ":has(.hit ~ .after)",
initial: true,
after: false,
setup: subject => {
subject.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "after" }));
},
mutate: subject => subject.lastChild.remove(),
},
{
name: ":has(> .wrapper > .hit) becomes false after nested child removal",
selector: ":has(> .wrapper > .hit)",
initial: true,
after: false,
setup: subject => {
const wrapper = makeElement("span", { className: "wrapper" });
wrapper.appendChild(makeElement("span", { className: "hit" }));
subject.appendChild(wrapper);
},
mutate: subject => subject.firstChild.firstChild.remove(),
},
{
name: ":has(.hit) after replaceWith matching child",
selector: ":has(.hit)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.firstChild.replaceWith(makeElement("span", { className: "hit" })),
},
{
name: ":has(.hit) becomes false after replaceWith nonmatching child",
selector: ":has(.hit)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.firstChild.replaceWith(makeElement("span")),
},
{
name: ":has(.hit) after cloned child append",
selector: ":has(.hit)",
initial: false,
after: true,
mutate: subject => {
const template = makeElement("span", { className: "hit" });
subject.appendChild(template.cloneNode(true));
},
},
{
name: ":has(.hit) after fragment replaceChildren",
selector: ":has(.hit)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => {
const fragment = document.createDocumentFragment();
fragment.appendChild(makeElement("span", { className: "hit" }));
subject.replaceChildren(fragment);
},
},
{
name: ":has(:not(.miss)) after non-miss child append",
selector: ":has(:not(.miss))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(:not(:empty)) after text child append",
selector: ":has(:not(:empty))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { text: "text" })),
},
{
name: ":has(:nth-child(even).hit) after even child append",
selector: ":has(:nth-child(even).hit)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: ":has(:nth-child(odd).hit) after prepend shifts child index",
selector: ":has(:nth-child(odd).hit)",
initial: false,
after: true,
setup: subject => {
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "hit" }));
},
mutate: subject => subject.prepend(makeElement("span")),
}
);
const attributeMutationCases = [
{
name: ":has(.hit) after descendant class change",
selector: ":has(.hit)",
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.HIT) after matching uppercase descendant class change",
selector: ":has(.HIT)",
mutate: child => child.classList.add("HIT"),
},
{
name: ":has(.HIT) ignores lowercase descendant class change in standards mode",
selector: ":has(.HIT)",
after: false,
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit) ignores uppercase descendant class change in standards mode",
selector: ":has(.hit)",
after: false,
mutate: child => child.classList.add("HIT"),
},
{
name: ":has([data-hit]) after descendant attribute change",
selector: ":has([data-hit])",
mutate: child => child.setAttribute("data-hit", ""),
},
{
name: ":has(#hit-id) after descendant id change",
selector: ":has(#hit-id)",
mutate: child => (child.id = "hit-id"),
},
{
name: ":has(span.hit:not(.miss)) after descendant compound change",
selector: ":has(span.hit:not(.miss))",
mutate: child => child.classList.add("hit"),
},
{
name: ":not(:has(.hit)) becomes false after descendant class change",
selector: ":not(:has(.hit))",
initial: true,
after: false,
mutate: child => child.classList.add("hit"),
},
{
name: ":has(:is(.hit)) after descendant class change",
selector: ":has(:is(.hit))",
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit) becomes false after descendant class removal",
selector: ":has(.hit)",
initial: true,
after: false,
setup: child => child.classList.add("hit"),
mutate: child => child.classList.remove("hit"),
},
{
name: ":has([data-hit]) becomes false after descendant attribute removal",
selector: ":has([data-hit])",
initial: true,
after: false,
setup: child => child.setAttribute("data-hit", ""),
mutate: child => child.removeAttribute("data-hit"),
},
{
name: ":not(:has(.hit)) becomes true after descendant class removal",
selector: ":not(:has(.hit))",
initial: false,
after: true,
setup: child => child.classList.add("hit"),
mutate: child => child.classList.remove("hit"),
},
];
attributeMutationCases.push(
{
name: ":has(.hit.extra) after descendant second class change",
selector: ":has(.hit.extra)",
setup: child => child.classList.add("hit"),
mutate: child => child.classList.add("extra"),
},
{
name: ":has(.hit) becomes false after descendant className clear",
selector: ":has(.hit)",
initial: true,
after: false,
setup: child => child.classList.add("hit"),
mutate: child => (child.className = ""),
},
{
name: ":has([data-hit='yes']) after descendant attribute value change",
selector: ":has([data-hit='yes'])",
mutate: child => child.setAttribute("data-hit", "yes"),
},
{
name: ":has([data-hit^='ye']) after descendant prefix attribute change",
selector: ":has([data-hit^='ye'])",
mutate: child => child.setAttribute("data-hit", "yes"),
},
{
name: ":has([data-hit$='es']) after descendant suffix attribute change",
selector: ":has([data-hit$='es'])",
mutate: child => child.setAttribute("data-hit", "yes"),
},
{
name: ":has([data-hit*='e']) after descendant substring attribute change",
selector: ":has([data-hit*='e'])",
mutate: child => child.setAttribute("data-hit", "yes"),
},
{
name: ":has([data-hit~='yes']) after descendant word attribute change",
selector: ":has([data-hit~='yes'])",
mutate: child => child.setAttribute("data-hit", "no yes maybe"),
},
{
name: ":has([data-hit|='yes']) after descendant dash attribute change",
selector: ":has([data-hit|='yes'])",
mutate: child => child.setAttribute("data-hit", "yes-now"),
},
{
name: ":has([data-hit='yes']) becomes false after descendant value change",
selector: ":has([data-hit='yes'])",
initial: true,
after: false,
setup: child => child.setAttribute("data-hit", "yes"),
mutate: child => child.setAttribute("data-hit", "no"),
},
{
name: ":has([hidden]) after descendant hidden property change",
selector: ":has([hidden])",
mutate: child => (child.hidden = true),
},
{
name: ":has(:is(.hit, [data-hit])) after descendant attribute change",
selector: ":has(:is(.hit, [data-hit]))",
mutate: child => child.setAttribute("data-hit", ""),
},
{
name: ":has(:where(.hit, [data-hit])) after descendant class change",
selector: ":has(:where(.hit, [data-hit]))",
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit:not([data-miss])) after descendant class change",
selector: ":has(.hit:not([data-miss]))",
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit:not([data-miss])) becomes false after descendant miss attribute change",
selector: ":has(.hit:not([data-miss]))",
initial: true,
after: false,
setup: child => child.classList.add("hit"),
mutate: child => child.setAttribute("data-miss", ""),
},
{
name: ":has(.hit + .after) after descendant sibling class change",
selector: ":has(.hit + .after)",
setup: child => {
child.parentNode.appendChild(makeElement("span", { className: "after" }));
},
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit ~ .after) after indirect sibling class change",
selector: ":has(.hit ~ .after)",
setup: child => {
child.parentNode.appendChild(makeElement("span"));
child.parentNode.appendChild(makeElement("span", { className: "after" }));
},
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit:first-child) after first descendant class change",
selector: ":has(.hit:first-child)",
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit:last-child) after last descendant class change",
selector: ":has(.hit:last-child)",
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit:only-child) after only descendant class change",
selector: ":has(.hit:only-child)",
mutate: child => child.classList.add("hit"),
},
{
name: ":has(.hit:nth-child(2)) after second descendant class change",
selector: ":has(.hit:nth-child(2))",
setup: child => {
child.parentNode.appendChild(makeElement("span"));
},
mutate: child => child.nextSibling.classList.add("hit"),
},
{
name: ":not(:has([data-hit])) becomes false after descendant attribute change",
selector: ":not(:has([data-hit]))",
initial: true,
after: false,
mutate: child => child.setAttribute("data-hit", ""),
},
{
name: ":has([data-hit]) becomes true after descendant toggleAttribute",
selector: ":has([data-hit])",
mutate: child => child.toggleAttribute("data-hit", true),
},
{
name: ":has([data-hit]) becomes false after descendant toggleAttribute",
selector: ":has([data-hit])",
initial: true,
after: false,
setup: child => child.setAttribute("data-hit", ""),
mutate: child => child.toggleAttribute("data-hit", false),
},
{
name: ":has(#hit-id.extra) after descendant id and class change",
selector: ":has(#hit-id.extra)",
mutate: child => {
child.id = "hit-id";
child.classList.add("extra");
},
}
);
const descendantTextMutationCases = [
{
name: ":has(:empty) becomes false after descendant text insert",
selector: ":has(:empty)",
initial: true,
after: false,
setup: child => {},
mutate: child => (child.textContent = "text"),
},
{
name: ":has(:not(:empty)) becomes true after descendant text insert",
selector: ":has(:not(:empty))",
initial: false,
after: true,
setup: child => {},
mutate: child => (child.textContent = "text"),
},
];
descendantTextMutationCases.push(
{
name: ":has(.hit:empty) becomes false after descendant textContent insert",
selector: ":has(.hit:empty)",
initial: true,
after: false,
setup: child => child.classList.add("hit"),
mutate: child => (child.textContent = "text"),
},
{
name: ":has(.hit:not(:empty)) becomes true after descendant textContent insert",
selector: ":has(.hit:not(:empty))",
initial: false,
after: true,
setup: child => child.classList.add("hit"),
mutate: child => (child.textContent = "text"),
},
{
name: ":has(:empty:first-child) becomes false after descendant text insert",
selector: ":has(:empty:first-child)",
initial: true,
after: false,
setup: child => {},
mutate: child => (child.textContent = "text"),
},
{
name: ":has(:not(:empty):first-child) becomes true after descendant text insert",
selector: ":has(:not(:empty):first-child)",
initial: false,
after: true,
setup: child => {},
mutate: child => (child.textContent = "text"),
},
{
name: ":not(:has(:not(:empty))) becomes false after descendant text insert",
selector: ":not(:has(:not(:empty)))",
initial: true,
after: false,
setup: child => {},
mutate: child => (child.textContent = "text"),
}
);
const descendantDirectionMutationCases = [
{
name: ":has(:dir(rtl)) after descendant dir=auto text mutation",
selector: ":has(:dir(rtl))",
initial: false,
after: true,
setup: child => {
child.dir = "auto";
child.textContent = "a";
},
mutate: child => (child.firstChild.data = "א"),
},
{
name: ":has(:dir(ltr)) becomes false after descendant dir=auto text mutation",
selector: ":has(:dir(ltr))",
initial: true,
after: false,
setup: child => {
child.dir = "auto";
child.textContent = "a";
},
mutate: child => (child.firstChild.data = "א"),
},
{
name: ":has(:dir(rtl):first-child) after first descendant dir=auto text mutation",
selector: ":has(:dir(rtl):first-child)",
initial: false,
after: true,
setup: child => {
child.dir = "auto";
child.textContent = "a";
},
mutate: child => (child.firstChild.data = "א"),
},
{
name: ":not(:has(:dir(rtl))) becomes false after descendant dir=auto text mutation",
selector: ":not(:has(:dir(rtl)))",
initial: true,
after: false,
setup: child => {
child.dir = "auto";
child.textContent = "a";
},
mutate: child => (child.firstChild.data = "א"),
},
];
const descendantLanguageMutationCases = [
{
name: ":has(:lang(he)) after descendant lang attribute change",
selector: ":has(:lang(he))",
initial: false,
after: true,
setup: child => {},
mutate: child => (child.lang = "he"),
},
{
name: ":has(:lang(en)) becomes false after descendant lang attribute change",
selector: ":has(:lang(en))",
initial: true,
after: false,
setup: child => (child.lang = "en"),
mutate: child => (child.lang = "he"),
},
{
name: ":has(:lang(he):first-child) after first descendant lang attribute change",
selector: ":has(:lang(he):first-child)",
initial: false,
after: true,
setup: child => {},
mutate: child => (child.lang = "he"),
},
{
name: ":not(:has(:lang(he))) becomes false after descendant lang attribute change",
selector: ":not(:has(:lang(he)))",
initial: true,
after: false,
setup: child => {},
mutate: child => (child.lang = "he"),
},
];
const partChildMutationCases = [
{
name: "::part():empty becomes false after element append",
selector: ":empty",
initial: true,
after: false,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: "::part():empty becomes false after textContent",
selector: ":empty",
initial: true,
after: false,
mutate: subject => (subject.textContent = "text"),
},
{
name: "::part():is(:empty) becomes false after element append",
selector: ":is(:empty)",
initial: true,
after: false,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: "::part():where(:empty) becomes false after element append",
selector: ":where(:empty)",
initial: true,
after: false,
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: "::part():not(:empty) becomes true after textContent",
selector: ":not(:empty)",
initial: false,
after: true,
mutate: subject => (subject.textContent = "text"),
},
{
name: "::part():empty becomes true after child removal",
selector: ":empty",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.firstChild.remove(),
},
{
name: "::part():empty becomes true after replaceChildren",
selector: ":empty",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.replaceChildren(),
},
{
name: "::part():dir(rtl) after dir=auto text mutation",
selector: ":dir(rtl)",
initial: false,
after: true,
setup: subject => {
subject.dir = "auto";
subject.textContent = "a";
},
mutate: subject => (subject.firstChild.data = "א"),
},
];
partChildMutationCases.push(
{
name: "::part():has(.hit) after direct child append",
selector: ":has(.hit)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: "::part():has(> .hit) after direct child append",
selector: ":has(> .hit)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: "::part():has(.outer .middle .hit) after deep child append",
selector: ":has(.outer .middle .hit)",
initial: false,
after: true,
mutate: appendHitDescendant,
},
{
name: "::part():has(.hit + .after) after adjacent sibling append",
selector: ":has(.hit + .after)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.appendChild(makeElement("span", { className: "after" })),
},
{
name: "::part():has(.hit ~ .after) after indirect sibling append",
selector: ":has(.hit ~ .after)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => {
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "after" }));
},
},
{
name: "::part():has(rect.hit) after SVG child append",
selector: ":has(rect.hit)",
initial: false,
after: true,
mutate: appendSvgHit,
},
{
name: "::part():has(.hit:first-child) after prepend",
selector: ":has(.hit:first-child)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.prepend(makeElement("span", { className: "hit" })),
},
{
name: "::part():has(.hit:last-child) becomes false after trailing sibling append",
selector: ":has(.hit:last-child)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.appendChild(makeElement("span")),
},
{
name: "::part():not(:has(.hit)) becomes false after child append",
selector: ":not(:has(.hit))",
initial: true,
after: false,
mutate: subject => subject.appendChild(makeElement("span", { className: "hit" })),
},
{
name: "::part():has(:is(.hit, [data-hit])) after child append",
selector: ":has(:is(.hit, [data-hit]))",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("span", { attributes: { "data-hit": "" } })),
},
{
name: "::part():has(.hit) becomes false after child removal",
selector: ":has(.hit)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("span", { className: "hit" })),
mutate: subject => subject.firstChild.remove(),
},
{
name: "::part():has(.hit) after replaceChildren",
selector: ":has(.hit)",
initial: false,
after: true,
setup: subject => subject.appendChild(makeElement("span")),
mutate: subject => subject.replaceChildren(makeElement("span", { className: "hit" })),
}
);
const formChildMutationCases = [
{
name: ":invalid after required input append",
selector: ":invalid",
initial: false,
after: true,
fixtureOptions: { subjectOptions: { tagName: "form" } },
mutate: subject => {
const input = makeElement("input");
input.required = true;
subject.appendChild(input);
},
},
{
name: ":valid after required input removal",
selector: ":valid",
initial: false,
after: true,
fixtureOptions: { subjectOptions: { tagName: "form" } },
setup: subject => {
const input = makeElement("input");
input.required = true;
subject.appendChild(input);
},
mutate: subject => subject.firstChild.remove(),
},
];
formChildMutationCases.push(
{
name: ":invalid after required fieldset input append",
selector: ":invalid",
initial: false,
after: true,
fixtureOptions: { subjectOptions: { tagName: "form" } },
mutate: subject => {
const fieldset = makeElement("fieldset");
const input = makeElement("input");
input.required = true;
fieldset.appendChild(input);
subject.appendChild(fieldset);
},
},
{
name: ":has(:invalid) after required input append",
selector: ":has(:invalid)",
initial: false,
after: true,
fixtureOptions: { subjectOptions: { tagName: "form" } },
mutate: subject => {
const input = makeElement("input");
input.required = true;
subject.appendChild(input);
},
},
{
name: ":has(:valid) after optional input append",
selector: ":has(:valid)",
initial: false,
after: true,
fixtureOptions: { subjectOptions: { tagName: "form" } },
mutate: subject => subject.appendChild(makeElement("input")),
},
{
name: ":has(:invalid) becomes false after required input removal",
selector: ":has(:invalid)",
initial: true,
after: false,
fixtureOptions: { subjectOptions: { tagName: "form" } },
setup: subject => {
const input = makeElement("input");
input.required = true;
subject.appendChild(input);
},
mutate: subject => subject.firstChild.remove(),
}
);
const placeholderChildMutationCases = [
{
name: ":placeholder-shown becomes false after text insert",
selector: ":placeholder-shown",
initial: true,
after: false,
fixtureOptions: { subjectOptions: { tagName: "textarea", attributes: { placeholder: "placeholder" } } },
mutate: subject => subject.appendChild(document.createTextNode("value")),
},
{
name: ":not(:placeholder-shown) becomes true after text insert",
selector: ":not(:placeholder-shown)",
initial: false,
after: true,
fixtureOptions: { subjectOptions: { tagName: "textarea", attributes: { placeholder: "placeholder" } } },
mutate: subject => subject.appendChild(document.createTextNode("value")),
},
];
placeholderChildMutationCases.push(
{
name: ":placeholder-shown becomes true after text removal",
selector: ":placeholder-shown",
initial: false,
after: true,
fixtureOptions: { subjectOptions: { tagName: "textarea", attributes: { placeholder: "placeholder" } } },
setup: subject => subject.appendChild(document.createTextNode("value")),
mutate: subject => subject.firstChild.remove(),
},
{
name: ":has(:placeholder-shown) after textarea child append",
selector: ":has(:placeholder-shown)",
initial: false,
after: true,
mutate: subject => subject.appendChild(makeElement("textarea", { attributes: { placeholder: "placeholder" } })),
},
{
name: ":has(:not(:placeholder-shown)) after filled textarea child append",
selector: ":has(:not(:placeholder-shown))",
initial: false,
after: true,
mutate: subject => {
const textarea = makeElement("textarea", { attributes: { placeholder: "placeholder" } });
textarea.appendChild(document.createTextNode("value"));
subject.appendChild(textarea);
},
}
);
const descendantStateMutationCases = [
{
name: ":has(:checked) after checkbox checked",
selector: ":has(:checked)",
setup: subject => subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } })),
mutate: subject => (subject.firstChild.checked = true),
},
{
name: ":has(:checked) becomes false after checkbox unchecked",
selector: ":has(:checked)",
initial: true,
after: false,
setup: subject => {
const input = makeElement("input", { attributes: { type: "checkbox" } });
input.checked = true;
subject.appendChild(input);
},
mutate: subject => (subject.firstChild.checked = false),
},
{
name: ":has(:required) after input required",
selector: ":has(:required)",
setup: subject => subject.appendChild(makeElement("input")),
mutate: subject => (subject.firstChild.required = true),
},
{
name: ":has(:optional) becomes false after input required",
selector: ":has(:optional)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("input")),
mutate: subject => (subject.firstChild.required = true),
},
{
name: ":has(:disabled) after input disabled",
selector: ":has(:disabled)",
setup: subject => subject.appendChild(makeElement("input")),
mutate: subject => (subject.firstChild.disabled = true),
},
{
name: ":has(:enabled) becomes false after input disabled",
selector: ":has(:enabled)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("input")),
mutate: subject => (subject.firstChild.disabled = true),
},
{
name: ":has(:placeholder-shown) after textarea placeholder added",
selector: ":has(:placeholder-shown)",
setup: subject => subject.appendChild(makeElement("textarea")),
mutate: subject => (subject.firstChild.placeholder = "placeholder"),
},
{
name: ":has(:placeholder-shown) becomes false after textarea value append",
selector: ":has(:placeholder-shown)",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("textarea", { attributes: { placeholder: "placeholder" } })),
mutate: subject => subject.firstChild.appendChild(document.createTextNode("value")),
},
{
name: ":has(:checked:first-child) after first checkbox checked",
selector: ":has(:checked:first-child)",
setup: subject => subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } })),
mutate: subject => (subject.firstChild.checked = true),
},
{
name: ":has(:checked:nth-child(2)) after second checkbox checked",
selector: ":has(:checked:nth-child(2))",
setup: subject => {
subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } }));
subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } }));
},
mutate: subject => (subject.children[1].checked = true),
},
{
name: ":has(:checked + .after) after checkbox checked",
selector: ":has(:checked + .after)",
setup: subject => {
subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } }));
subject.appendChild(makeElement("span", { className: "after" }));
},
mutate: subject => (subject.firstChild.checked = true),
},
{
name: ":has(:checked ~ .after) after checkbox checked",
selector: ":has(:checked ~ .after)",
setup: subject => {
subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } }));
subject.appendChild(makeElement("span"));
subject.appendChild(makeElement("span", { className: "after" }));
},
mutate: subject => (subject.firstChild.checked = true),
},
{
name: ":not(:has(:checked)) becomes false after checkbox checked",
selector: ":not(:has(:checked))",
initial: true,
after: false,
setup: subject => subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } })),
mutate: subject => (subject.firstChild.checked = true),
},
{
name: ":has(:is(:checked, .fallback)) after checkbox checked",
selector: ":has(:is(:checked, .fallback))",
setup: subject => subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } })),
mutate: subject => (subject.firstChild.checked = true),
},
{
name: ":has(:checked):has(:disabled) after independent state changes",
selector: ":has(:checked):has(:disabled)",
setup: subject => {
subject.appendChild(makeElement("input", { attributes: { type: "checkbox" } }));
subject.appendChild(makeElement("input"));
},
mutate: subject => {
subject.firstChild.checked = true;
subject.children[1].disabled = true;
},
},
];
function runDetachedReconnectCase(scope, selectorCase, pseudoElement = "") {
runCase({
suite: pseudoElement ? "detached pseudo-element child mutation" : "detached child mutation",
scope,
initial: selectorCase.initial,
after: selectorCase.after,
pseudoElement,
...selectorCase,
mutate: (subject, fixture) => {
fixture.remove();
selectorCase.mutate(subject, fixture);
document.body.appendChild(fixture);
},
});
}
function runInheritedLanguageCase(scope, selectorCase, pseudoElement = "") {
const suite = pseudoElement ? "inherited language pseudo-element mutation" : "inherited language mutation";
const testName = `${suite}: ${scope}: ${selectorCase.name}`;
const { cleanup, fixture, subject, target } = buildFixture(scope, selectorCase.selector, pseudoElement);
try {
subject.appendChild(makeElement("span"));
selectorCase.setup?.(subject, fixture);
assertEqual(`${testName} initial`, readProbe(target, pseudoElement), selectorCase.initial ? MATCH : BASE);
resetStyleCounters();
selectorCase.mutate(subject, fixture);
assertEqual(`${testName} after mutation`, readProbe(target, pseudoElement), selectorCase.after ? MATCH : BASE);
printPassWithCounters(testName);
} finally {
cleanup();
}
}
const inheritedLanguageCases = [
{
name: ":has(:lang(he)) after ancestor lang change",
selector: ":has(:lang(he))",
initial: false,
after: true,
mutate: (subject, fixture) => (fixture.lang = "he"),
},
{
name: ":has(:lang(en)) becomes false after ancestor lang change",
selector: ":has(:lang(en))",
initial: true,
after: false,
setup: (subject, fixture) => (fixture.lang = "en"),
mutate: (subject, fixture) => (fixture.lang = "he"),
},
{
name: ":not(:has(:lang(he))) becomes false after ancestor lang change",
selector: ":not(:has(:lang(he)))",
initial: true,
after: false,
mutate: (subject, fixture) => (fixture.lang = "he"),
},
{
name: ":has(:lang(he):first-child) after ancestor lang change",
selector: ":has(:lang(he):first-child)",
initial: false,
after: true,
mutate: (subject, fixture) => (fixture.lang = "he"),
},
{
name: ":has(:is(:lang(he), .fallback)) after ancestor lang change",
selector: ":has(:is(:lang(he), .fallback))",
initial: false,
after: true,
mutate: (subject, fixture) => (fixture.lang = "he"),
},
{
name: ":has(:where(:lang(he), .fallback)) after ancestor lang change",
selector: ":has(:where(:lang(he), .fallback))",
initial: false,
after: true,
mutate: (subject, fixture) => (fixture.lang = "he"),
},
];
function runPrecomputedDetachedInsertionCase(selectorCase) {
const fixture = makeElement("section");
document.body.appendChild(fixture);
const style = addStyle(document.head, selectorCase.cssText);
try {
const holdingParent = makeElement("section");
const container = makeElement("section", { className: "container" });
fixture.append(holdingParent, container);
const subtree = selectorCase.makeSubtree();
const target = selectorCase.target(subtree);
holdingParent.appendChild(subtree);
assertEqual(`${selectorCase.name} precomputed initial`, readProbe(target), BASE);
subtree.remove();
resetStyleCounters();
container.appendChild(subtree);
assertEqual(`${selectorCase.name} after insertion`, readProbe(target), selectorCase.after ?? MATCH);
printPassWithCounters(`precomputed detached insertion: ${selectorCase.name}`);
} finally {
style.remove();
fixture.remove();
}
}
const precomputedDetachedInsertionCases = [
{
name: "descendant class selector",
cssText: `.container .leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "direct child chain selector",
cssText: `.container > .detached-root > .leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div", { className: "detached-root" });
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "id selector",
cssText: `.container #detached-leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { id: "detached-leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "mixed-case ancestor id selector",
cssText: `.container #Foo .leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div", { id: "Foo" });
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "mixed-case ancestor id selector rejects lowercase id",
cssText: `.container #Foo .leaf { ${MATCH_DECLARATION} }`,
after: BASE,
makeSubtree: () => {
const root = makeElement("div", { id: "foo" });
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "mixed-case ancestor id selector inside selector list",
cssText: `.container :is(#Foo) .leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div", { id: "Foo" });
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "attribute selector",
cssText: `.container [data-detached-leaf] { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { attributes: { "data-detached-leaf": "" } }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "adjacent sibling selector",
cssText: `.container .before + .leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { className: "before" }));
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.lastChild,
},
{
name: "subsequent sibling selector",
cssText: `.container .before ~ .leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { className: "before" }));
root.appendChild(makeElement("span"));
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.lastChild,
},
{
name: "positional selector",
cssText: `.container .leaf:nth-child(2) { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span"));
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.lastChild,
},
{
name: "inherited custom-property probe",
cssText: `.container { ${MATCH_DECLARATION} } .leaf { --style-invalidation-probe: inherit; }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "custom property inheritance",
cssText: `.container { --detached-probe: ${MATCH}; } .leaf { --style-invalidation-probe: var(--detached-probe, ${BASE}); }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
{
name: "selector list",
cssText: `.container .miss, .container .leaf { ${MATCH_DECLARATION} }`,
makeSubtree: () => {
const root = makeElement("div");
root.appendChild(makeElement("span", { className: "leaf" }));
return root;
},
target: subtree => subtree.firstChild,
},
];
function stressCaseSupportedInScope(scope, selectorCase) {
if (!caseSupportedInScope(scope, selectorCase)) return false;
return !(scope === "document-styled-use-shadow" && selectorCase.selector.startsWith(":dir("));
}
function runStressCase(scope, selectorCase, mode) {
if (!stressCaseSupportedInScope(scope, selectorCase)) return;
if (mode === "warm") {
runCase({
suite: "stress warm child mutation",
scope,
initial: selectorCase.initial,
after: selectorCase.after,
...selectorCase,
});
} else if (mode === "cold") {
runCase({
suite: "stress cold child mutation",
scope,
initial: selectorCase.initial,
after: selectorCase.after,
skipInitialRead: true,
...selectorCase,
});
} else if (mode === "pseudo-warm") {
runCase({
suite: "stress warm pseudo-element child mutation",
scope,
initial: selectorCase.initial,
after: selectorCase.after,
pseudoElement: "::before",
...selectorCase,
});
} else if (mode === "pseudo-cold") {
runCase({
suite: "stress cold pseudo-element child mutation",
scope,
initial: selectorCase.initial,
after: selectorCase.after,
pseudoElement: "::before",
skipInitialRead: true,
...selectorCase,
});
} else if (mode === "detached") {
runDetachedReconnectCase(scope, selectorCase);
} else if (mode === "detached-pseudo") {
runDetachedReconnectCase(scope, selectorCase, "::before");
} else {
throw new Error(`Unknown stress mode ${mode}`);
}
}
function runDescendantStressCase(scope, selectorCase, mode, options = {}) {
if (options.skipDocumentStyledUseShadow && scope === "document-styled-use-shadow") return;
const pseudoElement = mode.includes("pseudo") ? "::before" : "";
const skipInitialRead = mode.includes("cold");
const isDetached = mode.includes("detached");
runCase({
suite: `${options.suite} stress ${mode}`,
scope,
...selectorCase,
initial: selectorCase.initial ?? false,
after: selectorCase.after ?? true,
pseudoElement,
skipInitialRead,
setup: subject => {
const child = makeElement(options.childTagName || "span", options.childOptions || {});
subject.appendChild(child);
selectorCase.setup?.(child);
},
mutate: (subject, fixture) => {
if (isDetached) fixture.remove();
selectorCase.mutate(subject.firstChild);
if (isDetached) document.body.appendChild(fixture);
},
});
}
function runStateStressCase(scope, selectorCase, mode) {
const pseudoElement = mode.includes("pseudo") ? "::before" : "";
const skipInitialRead = mode.includes("cold");
const isDetached = mode.includes("detached");
runCase({
suite: `descendant state stress ${mode}`,
scope,
initial: selectorCase.initial ?? false,
after: selectorCase.after ?? true,
pseudoElement,
skipInitialRead,
...selectorCase,
mutate: (subject, fixture) => {
if (isDetached) fixture.remove();
selectorCase.mutate(subject, fixture);
if (isDetached) document.body.appendChild(fixture);
},
});
}
function runInheritedLanguageStressCase(scope, selectorCase, mode) {
const pseudoElement = mode.includes("pseudo") ? "::before" : "";
const suite = `inherited language stress ${mode}`;
const testName = `${suite}: ${scope}: ${selectorCase.name}`;
const { cleanup, fixture, subject, target } = buildFixture(scope, selectorCase.selector, pseudoElement);
try {
subject.appendChild(makeElement("span"));
selectorCase.setup?.(subject, fixture);
if (!mode.includes("cold"))
assertEqual(`${testName} initial`, readProbe(target, pseudoElement), selectorCase.initial ? MATCH : BASE);
resetStyleCounters();
if (mode.includes("detached")) fixture.remove();
selectorCase.mutate(subject, fixture);
if (mode.includes("detached")) document.body.appendChild(fixture);
assertEqual(`${testName} after mutation`, readProbe(target, pseudoElement), selectorCase.after ? MATCH : BASE);
printPassWithCounters(testName);
} finally {
cleanup();
}
}
function cloneSubtreeForPrecomputedStress(fixture, selectorCase) {
const holdingParent = makeElement("section");
fixture.appendChild(holdingParent);
const subtree = selectorCase.makeSubtree();
const target = selectorCase.target(subtree);
holdingParent.appendChild(subtree);
assertEqual(`${selectorCase.name} precomputed stress initial`, readProbe(target), BASE);
subtree.remove();
holdingParent.remove();
return { subtree, target };
}
function runPrecomputedDetachedInsertionStressCase(selectorCase, mode) {
const fixture = makeElement("section");
document.body.appendChild(fixture);
const style = addStyle(document.head, selectorCase.cssText);
try {
const container = makeElement("section", { className: "container" });
fixture.appendChild(container);
if (mode === "append") {
const { subtree, target } = cloneSubtreeForPrecomputedStress(fixture, selectorCase);
resetStyleCounters();
container.appendChild(subtree);
assertEqual(
`${selectorCase.name} append stress after insertion`,
readProbe(target),
selectorCase.after ?? MATCH
);
} else if (mode === "prepend") {
container.appendChild(makeElement("span"));
const { subtree, target } = cloneSubtreeForPrecomputedStress(fixture, selectorCase);
resetStyleCounters();
container.prepend(subtree);
assertEqual(
`${selectorCase.name} prepend stress after insertion`,
readProbe(target),
selectorCase.after ?? MATCH
);
} else if (mode === "replaceChildren") {
container.appendChild(makeElement("span"));
const { subtree, target } = cloneSubtreeForPrecomputedStress(fixture, selectorCase);
resetStyleCounters();
container.replaceChildren(subtree);
assertEqual(
`${selectorCase.name} replaceChildren stress after insertion`,
readProbe(target),
selectorCase.after ?? MATCH
);
} else if (mode === "fragment") {
const { subtree, target } = cloneSubtreeForPrecomputedStress(fixture, selectorCase);
const fragment = document.createDocumentFragment();
fragment.appendChild(subtree);
resetStyleCounters();
container.appendChild(fragment);
assertEqual(
`${selectorCase.name} fragment stress after insertion`,
readProbe(target),
selectorCase.after ?? MATCH
);
} else {
throw new Error(`Unknown precomputed detached insertion stress mode ${mode}`);
}
printPassWithCounters(`precomputed detached insertion stress ${mode}: ${selectorCase.name}`);
} finally {
style.remove();
fixture.remove();
}
}
function runSlotMoveCase() {
const fixture = makeElement("section");
document.body.appendChild(fixture);
try {
const styledHost = makeElement("x-styled-host");
const plainHost = makeElement("x-plain-host");
const subject = makeSubject({ shadowHost: true });
subject.appendChild(makeElement("span"));
fixture.append(styledHost, plainHost);
styledHost.appendChild(subject);
const styledShadowRoot = styledHost.attachShadow({ mode: "open" });
styledShadowRoot.appendChild(document.createElement("slot"));
addStyle(styledShadowRoot, `::slotted(.subject:not(:empty)) { ${MATCH_DECLARATION} }`);
const plainShadowRoot = plainHost.attachShadow({ mode: "open" });
plainShadowRoot.appendChild(document.createElement("slot"));
assertEqual("slot move out initial", readProbe(subject), MATCH);
resetStyleCounters();
plainHost.appendChild(subject);
assertEqual("slot move out after mutation", readProbe(subject), BASE);
styledHost.appendChild(subject);
assertEqual("slot move in after mutation", readProbe(subject), MATCH);
printPassWithCounters("scope move: slotted shadow-host target moves between slot scopes");
} finally {
fixture.remove();
}
}
function runNestedSlotPartCase() {
const fixture = makeElement("section");
document.body.appendChild(fixture);
try {
const outerHost = makeElement("x-outer-host");
const innerHost = makeElement("x-host");
fixture.appendChild(outerHost);
const outerShadowRoot = outerHost.attachShadow({ mode: "open" });
outerShadowRoot.appendChild(innerHost);
addStyle(outerShadowRoot, `x-host::part(foo):not(:empty)::before { ${MATCH_DECLARATION} }`);
const innerShadowRoot = innerHost.attachShadow({ mode: "open" });
const partSlot = makeElement("slot", { className: "subject", attributes: { part: "foo" } });
innerShadowRoot.appendChild(partSlot);
assertEqual("nested slot part initial", readProbe(partSlot, "::before"), BASE);
resetStyleCounters();
partSlot.appendChild(makeElement("span"));
assertEqual("nested slot part after mutation", readProbe(partSlot, "::before"), MATCH);
printPassWithCounters("combined topology: ancestor shadow exposes part slot with generated pseudo-element");
} finally {
fixture.remove();
}
}