diff --git a/cli/engine/browser/injected/index.mjs b/cli/engine/browser/injected/index.mjs index 77e805e90..76fc558e5 100644 --- a/cli/engine/browser/injected/index.mjs +++ b/cli/engine/browser/injected/index.mjs @@ -1547,6 +1547,10 @@ if (IS_BROWSER) { const declarations = Object.entries(frame) .filter(([property, value]) => !metadata.has(property) && value != null && value !== '') .map(([property, value]) => `${cssPropertyName(property)}: ${value};`); + const easing = String(frame.easing || '').trim(); + if (easing && easing.toLowerCase() !== 'linear') { + declarations.push(`animation-timing-function: ${easing};`); + } if (declarations.length === 0) continue; blocks.push(`${offset}% { ${declarations.join(' ')} }`); } diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index 068dab3a5..460ffb45b 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -8451,6 +8451,10 @@ if (IS_BROWSER) { const declarations = Object.entries(frame) .filter(([property, value]) => !metadata.has(property) && value != null && value !== '') .map(([property, value]) => `${cssPropertyName(property)}: ${value};`); + const easing = String(frame.easing || '').trim(); + if (easing && easing.toLowerCase() !== 'linear') { + declarations.push(`animation-timing-function: ${easing};`); + } if (declarations.length === 0) continue; blocks.push(`${offset}% { ${declarations.join(' ')} }`); } diff --git a/tests/detect-antipatterns-browser.test.mjs b/tests/detect-antipatterns-browser.test.mjs index 8b05bbbcf..cccf12400 100644 --- a/tests/detect-antipatterns-browser.test.mjs +++ b/tests/detect-antipatterns-browser.test.mjs @@ -1469,7 +1469,8 @@ describe('detectUrl — browser-only fixtures', () => { assert.equal(grids[0].severity, 'advisory'); assert.equal(grids[0].advisory, true); assert.equal( - linkedFindings.some(finding => finding.type === 'bounce-easing'), + linkedFindings.some(finding => finding.type === 'bounce-easing' + && finding.detail === 'cubic-bezier(0.34, 1.56, 0.64, 1)'), true, JSON.stringify({ linkedFindings, linkedCssom }), ); diff --git a/tests/fixtures/antipatterns/linked-url-patterns.css b/tests/fixtures/antipatterns/linked-url-patterns.css index c09f9f49e..c0f750408 100644 --- a/tests/fixtures/antipatterns/linked-url-patterns.css +++ b/tests/fixtures/antipatterns/linked-url-patterns.css @@ -47,6 +47,18 @@ main > ::before { to { transform: translateX(-50%); } } +.linked-keyframe-overshoot { + animation: linked-keyframe-curve 2s linear infinite; +} + +@keyframes linked-keyframe-curve { + from { + transform: translateY(0); + animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); + } + to { transform: translateY(2px); } +} + .overridden-keyframes-animation { animation: overridden-horizontal-loop 2s linear infinite; } diff --git a/tests/fixtures/antipatterns/linked-url-patterns.html b/tests/fixtures/antipatterns/linked-url-patterns.html index c8408fc04..eebb3acc5 100644 --- a/tests/fixtures/antipatterns/linked-url-patterns.html +++ b/tests/fixtures/antipatterns/linked-url-patterns.html @@ -12,6 +12,7 @@
Empty attribute-value decoy
Escaped identifier selector
Rendered linked marquee animation
+
Rendered linked keyframe easing
Overridden linked keyframes
Layer-priority linked keyframes