mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 09:06:53 +03:00
Handle grouped linked keyframes
AI assistance disclosure: Codex helped implement and verify this fix under maintainer direction.
This commit is contained in:
@@ -1532,7 +1532,7 @@ if (IS_BROWSER) {
|
||||
const seen = new Set();
|
||||
const animationNames = new Set();
|
||||
const keyframeCandidates = [];
|
||||
const appendRules = (rules, containerStates = []) => {
|
||||
const appendRules = (rules, requiresAppliedMatch = false) => {
|
||||
for (const rule of rules) {
|
||||
if (rule.styleSheet) {
|
||||
appendSheet(rule.styleSheet);
|
||||
@@ -1546,11 +1546,10 @@ if (IS_BROWSER) {
|
||||
// rendered, and retaining them would leak unused CSS into findings.
|
||||
if (
|
||||
matches?.length > 0
|
||||
&& (containerStates.length === 0 || styleRuleAppliesToLiveMatches(rule, matches))
|
||||
&& (!requiresAppliedMatch || styleRuleAppliesToLiveMatches(rule, matches))
|
||||
) {
|
||||
parts.push(cssText);
|
||||
for (const name of animationNamesDeclaredByRule(rule)) animationNames.add(name);
|
||||
for (const state of containerStates) state.active = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1568,16 +1567,12 @@ if (IS_BROWSER) {
|
||||
keyframeCandidates.push({
|
||||
name: keyframesName,
|
||||
cssText,
|
||||
containerStates: [...containerStates],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (hasNestedRules) {
|
||||
if (!conditionalCssRuleIsActive(rule)) continue;
|
||||
const nextContainerStates = isContainerCssRule(rule)
|
||||
? [...containerStates, { active: false }]
|
||||
: containerStates;
|
||||
appendRules(nested, nextContainerStates);
|
||||
appendRules(nested, requiresAppliedMatch || isContainerCssRule(rule));
|
||||
continue;
|
||||
}
|
||||
// Other selector-less leaf at-rules cannot be tied to a rendered node.
|
||||
@@ -1601,13 +1596,14 @@ if (IS_BROWSER) {
|
||||
appendSheet(sheet);
|
||||
}
|
||||
// Motion checks need the body of a live animation's keyframes. Retain only
|
||||
// definitions referenced by a retained selector rule, and only when every
|
||||
// enclosing container query was proven active by a declaration applying
|
||||
// to a live match. This preserves linked marquee/pulse detection without
|
||||
// letting unused or inactive keyframe bodies feed page-level checks.
|
||||
// definitions referenced by a retained selector rule. Browsers make nested
|
||||
// keyframes globally available even when a surrounding container condition
|
||||
// is currently false, so lexical grouping cannot decide whether the named
|
||||
// animation renders. The live reference is the useful gate: it preserves
|
||||
// linked marquee/pulse detection without letting unreferenced keyframe
|
||||
// bodies feed page-level checks.
|
||||
for (const candidate of keyframeCandidates) {
|
||||
if (!animationNames.has(candidate.name)) continue;
|
||||
if (candidate.containerStates.some(state => !state.active)) continue;
|
||||
parts.push(candidate.cssText);
|
||||
}
|
||||
return parts.join('\n');
|
||||
|
||||
@@ -8436,7 +8436,7 @@ if (IS_BROWSER) {
|
||||
const seen = new Set();
|
||||
const animationNames = new Set();
|
||||
const keyframeCandidates = [];
|
||||
const appendRules = (rules, containerStates = []) => {
|
||||
const appendRules = (rules, requiresAppliedMatch = false) => {
|
||||
for (const rule of rules) {
|
||||
if (rule.styleSheet) {
|
||||
appendSheet(rule.styleSheet);
|
||||
@@ -8450,11 +8450,10 @@ if (IS_BROWSER) {
|
||||
// rendered, and retaining them would leak unused CSS into findings.
|
||||
if (
|
||||
matches?.length > 0
|
||||
&& (containerStates.length === 0 || styleRuleAppliesToLiveMatches(rule, matches))
|
||||
&& (!requiresAppliedMatch || styleRuleAppliesToLiveMatches(rule, matches))
|
||||
) {
|
||||
parts.push(cssText);
|
||||
for (const name of animationNamesDeclaredByRule(rule)) animationNames.add(name);
|
||||
for (const state of containerStates) state.active = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -8472,16 +8471,12 @@ if (IS_BROWSER) {
|
||||
keyframeCandidates.push({
|
||||
name: keyframesName,
|
||||
cssText,
|
||||
containerStates: [...containerStates],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (hasNestedRules) {
|
||||
if (!conditionalCssRuleIsActive(rule)) continue;
|
||||
const nextContainerStates = isContainerCssRule(rule)
|
||||
? [...containerStates, { active: false }]
|
||||
: containerStates;
|
||||
appendRules(nested, nextContainerStates);
|
||||
appendRules(nested, requiresAppliedMatch || isContainerCssRule(rule));
|
||||
continue;
|
||||
}
|
||||
// Other selector-less leaf at-rules cannot be tied to a rendered node.
|
||||
@@ -8505,13 +8500,14 @@ if (IS_BROWSER) {
|
||||
appendSheet(sheet);
|
||||
}
|
||||
// Motion checks need the body of a live animation's keyframes. Retain only
|
||||
// definitions referenced by a retained selector rule, and only when every
|
||||
// enclosing container query was proven active by a declaration applying
|
||||
// to a live match. This preserves linked marquee/pulse detection without
|
||||
// letting unused or inactive keyframe bodies feed page-level checks.
|
||||
// definitions referenced by a retained selector rule. Browsers make nested
|
||||
// keyframes globally available even when a surrounding container condition
|
||||
// is currently false, so lexical grouping cannot decide whether the named
|
||||
// animation renders. The live reference is the useful gate: it preserves
|
||||
// linked marquee/pulse detection without letting unreferenced keyframe
|
||||
// bodies feed page-level checks.
|
||||
for (const candidate of keyframeCandidates) {
|
||||
if (!animationNames.has(candidate.name)) continue;
|
||||
if (candidate.containerStates.some(state => !state.active)) continue;
|
||||
parts.push(candidate.cssText);
|
||||
}
|
||||
return parts.join('\n');
|
||||
|
||||
@@ -1405,14 +1405,31 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
})));
|
||||
const linkedFindings = await linkedPage.evaluate(() => window.impeccableDetect({ serialize: true })
|
||||
.flatMap(group => group.findings || []));
|
||||
const containerBackgrounds = await linkedPage.evaluate(() => ({
|
||||
inactive: getComputedStyle(document.querySelector('.inactive-container-stripes')).backgroundImage,
|
||||
active: getComputedStyle(document.querySelector('.active-container-halo')).backgroundImage,
|
||||
pseudoClass: getComputedStyle(document.querySelector('.inactive-pseudo-stripes')).backgroundImage,
|
||||
}));
|
||||
const containerBackgrounds = await linkedPage.evaluate(async () => {
|
||||
const activeAnimation = document.querySelector('.active-container-animation-reference');
|
||||
const inactiveAnimation = document.querySelector('.inactive-container-animation-reference');
|
||||
const before = {
|
||||
active: getComputedStyle(activeAnimation).transform,
|
||||
inactive: getComputedStyle(inactiveAnimation).transform,
|
||||
};
|
||||
await new Promise(resolve => setTimeout(resolve, 120));
|
||||
return {
|
||||
inactive: getComputedStyle(document.querySelector('.inactive-container-stripes')).backgroundImage,
|
||||
active: getComputedStyle(document.querySelector('.active-container-halo')).backgroundImage,
|
||||
pseudoClass: getComputedStyle(document.querySelector('.inactive-pseudo-stripes')).backgroundImage,
|
||||
activeTransforms: [before.active, getComputedStyle(activeAnimation).transform],
|
||||
inactiveTransforms: [before.inactive, getComputedStyle(inactiveAnimation).transform],
|
||||
};
|
||||
});
|
||||
assert.equal(containerBackgrounds.inactive, 'none');
|
||||
assert.equal(containerBackgrounds.pseudoClass, 'none');
|
||||
assert.match(containerBackgrounds.active, /radial-gradient/i);
|
||||
assert.notEqual(containerBackgrounds.activeTransforms[0], containerBackgrounds.activeTransforms[1]);
|
||||
assert.notEqual(
|
||||
containerBackgrounds.inactiveTransforms[0],
|
||||
containerBackgrounds.inactiveTransforms[1],
|
||||
JSON.stringify(containerBackgrounds),
|
||||
);
|
||||
const grids = linkedFindings.filter(finding => finding.type === 'codex-grid-background');
|
||||
assert.equal(grids.length, 1, JSON.stringify({ linkedFindings, linkedCssom }));
|
||||
assert.equal(grids[0].severity, 'advisory');
|
||||
@@ -1423,8 +1440,15 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
JSON.stringify({ linkedFindings, linkedCssom }),
|
||||
);
|
||||
const marquees = linkedFindings.filter(finding => finding.type === 'marquee');
|
||||
assert.equal(marquees.length, 1, JSON.stringify({ linkedFindings, linkedCssom }));
|
||||
assert.match(marquees[0].detail, /\.linked-marquee/);
|
||||
assert.equal(marquees.length, 3, JSON.stringify({ linkedFindings, linkedCssom }));
|
||||
assert.deepEqual(
|
||||
new Set(marquees.map(finding => finding.detail.match(/^\S+/)?.[0])),
|
||||
new Set([
|
||||
'.linked-marquee',
|
||||
'.active-container-animation-reference',
|
||||
'.inactive-container-animation-reference',
|
||||
]),
|
||||
);
|
||||
const pulsingDots = linkedFindings.filter(finding => finding.type === 'pulsing-dot');
|
||||
assert.equal(pulsingDots.length, 1, JSON.stringify({ linkedFindings, linkedCssom }));
|
||||
assert.match(pulsingDots[0].detail, /\.linked-pulse-dot/);
|
||||
|
||||
+14
-2
@@ -58,8 +58,8 @@ main > ::before {
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
/* A live selector can name keyframes that only exist under an inactive
|
||||
container query. The inaccessible body must not create a marquee hit. */
|
||||
/* Chromium makes nested keyframes globally available even while the enclosing
|
||||
container condition is false, so this live reference must still scan. */
|
||||
.inactive-container-animation-reference {
|
||||
animation: inactive-container-horizontal-loop 8s linear infinite;
|
||||
}
|
||||
@@ -71,6 +71,18 @@ main > ::before {
|
||||
}
|
||||
}
|
||||
|
||||
.active-container-animation-reference {
|
||||
animation: active-container-horizontal-loop 8s linear infinite;
|
||||
}
|
||||
|
||||
/* The declaration is intentionally outside the container group. */
|
||||
@container (width > 900px) {
|
||||
@keyframes active-container-horizontal-loop {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
}
|
||||
|
||||
/* A pseudo-element whose originating element is absent must remain outside
|
||||
live URL findings instead of being retained as an unresolvable selector. */
|
||||
.absent > ::before {
|
||||
|
||||
+2
-1
@@ -13,15 +13,16 @@
|
||||
<div class="::before">Escaped identifier selector</div>
|
||||
<div class="linked-marquee">Rendered linked marquee animation</div>
|
||||
<div class="linked-pulse-dot"></div>
|
||||
<div class="inactive-container-animation-reference">Inactive container keyframes reference</div>
|
||||
<div class="inactive-media-stripes">Inactive media stripes</div>
|
||||
<div class="inactive-supports-stripes">Inactive supports stripes</div>
|
||||
<div class="inactive-pseudo-stripes active">Inactive pseudo-class stripes</div>
|
||||
<div class="container-query-host">
|
||||
<div class="inactive-container-stripes">Inactive container-query stripes</div>
|
||||
<div class="inactive-container-animation-reference">False-container keyframes reference</div>
|
||||
</div>
|
||||
<div class="container-query-host container-query-host-active">
|
||||
<div class="active-container-halo">Active container-query halo</div>
|
||||
<div class="active-container-animation-reference">Active container keyframes reference</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user