/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {shim} from './utils'; describe('ShadowCss, keyframes and animations', () => { it('should scope keyframes rules', () => { const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}'; const expected = '@keyframes host-a_foo {0% {transform:translate(-50%) scaleX(0);}}'; expect(shim(css, 'host-a')).toEqual(expected); }); it('should scope -webkit-keyframes rules', () => { const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}} '; const expected = '@-webkit-keyframes host-a_foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}'; expect(shim(css, 'host-a')).toEqual(expected); }); it('should scope animations using local keyframes identifiers', () => { const css = ` button { animation: foo 10s ease; } @keyframes foo { 0% { transform: translate(-50%) scaleX(0); } } `; const result = shim(css, 'host-a'); expect(result).toContain('animation: host-a_foo 10s ease;'); }); it('should not scope animations using non-local keyframes identifiers', () => { const css = ` button { animation: foo 10s ease; } `; const result = shim(css, 'host-a'); expect(result).toContain('animation: foo 10s ease;'); }); it('should scope animation-names using local keyframes identifiers', () => { const css = ` button { animation-name: foo; } @keyframes foo { 0% { transform: translate(-50%) scaleX(0); } } `; const result = shim(css, 'host-a'); expect(result).toContain('animation-name: host-a_foo;'); }); it('should not scope animation-names using non-local keyframes identifiers', () => { const css = ` button { animation-name: foo; } `; const result = shim(css, 'host-a'); expect(result).toContain('animation-name: foo;'); }); it('should handle (scope or not) multiple animation-names', () => { const css = ` button { animation-name: foo, bar,baz, qux , quux ,corge ,grault ,garply, waldo; } @keyframes foo {} @keyframes baz {} @keyframes quux {} @keyframes grault {} @keyframes waldo {}`; const result = shim(css, 'host-a'); const animationNames = [ 'host-a_foo', ' bar', 'host-a_baz', ' qux ', ' host-a_quux ', 'corge ', 'host-a_grault ', 'garply', ' host-a_waldo', ]; const expected = `animation-name: ${animationNames.join(',')};`; expect(result).toContain(expected); }); it('should handle (scope or not) multiple animation-names defined over multiple lines', () => { const css = ` button { animation-name: foo, bar,baz, qux , quux , grault, garply, waldo; } @keyframes foo {} @keyframes baz {} @keyframes quux {} @keyframes grault {}`; const result = shim(css, 'host-a'); ['foo', 'baz', 'quux', 'grault'].forEach((scoped) => expect(result).toContain(`host-a_${scoped}`), ); ['bar', 'qux', 'garply', 'waldo'].forEach((nonScoped) => { expect(result).toContain(nonScoped); expect(result).not.toContain(`host-a_${nonScoped}`); }); }); it('should handle (scope or not) animation definition containing some names which do not have a preceding space', () => { const COMPONENT_VARIABLE = '%COMP%'; const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; const css = `.test { animation:my-anim 1s,my-anim2 2s, my-anim3 3s,my-anim4 4s; } @keyframes my-anim { 0% {color: red} 100% {color: blue} } @keyframes my-anim2 { 0% {font-size: 1em} 100% {font-size: 1.2em} } `; const result = shim(css, CONTENT_ATTR, HOST_ATTR); const animationLineMatch = result.match(/animation:[^;]+;/); let animationLine = ''; if (animationLineMatch) { animationLine = animationLineMatch[0]; } ['my-anim', 'my-anim2'].forEach((scoped) => expect(animationLine).toContain(`_ngcontent-%COMP%_${scoped}`), ); ['my-anim3', 'my-anim4'].forEach((nonScoped) => { expect(animationLine).toContain(nonScoped); expect(animationLine).not.toContain(`_ngcontent-%COMP%_${nonScoped}`); }); }); it('should handle (scope or not) animation definitions preceded by an erroneous comma', () => { const COMPONENT_VARIABLE = '%COMP%'; const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; const css = `.test { animation:, my-anim 1s,my-anim2 2s, my-anim3 3s,my-anim4 4s; } @keyframes my-anim { 0% {color: red} 100% {color: blue} } @keyframes my-anim2 { 0% {font-size: 1em} 100% {font-size: 1.2em} } `; const result = shim(css, CONTENT_ATTR, HOST_ATTR); const animationLineMatch = result.match(/animation:[^;]+;/); let animationLine = ''; if (animationLineMatch) { animationLine = animationLineMatch[0]; } expect(result).not.toContain('animation:,'); ['my-anim', 'my-anim2'].forEach((scoped) => expect(animationLine).toContain(`_ngcontent-%COMP%_${scoped}`), ); ['my-anim3', 'my-anim4'].forEach((nonScoped) => { expect(animationLine).toContain(nonScoped); expect(animationLine).not.toContain(`_ngcontent-%COMP%_${nonScoped}`); }); }); it('should handle (scope or not) multiple animation definitions in a single declaration', () => { const css = ` div { animation: 1s ease foo, 2s bar infinite, forwards baz 3s; } p { animation: 1s "foo", 2s "bar"; } span { animation: .5s ease 'quux', 1s foo infinite, forwards "baz'" 1.5s, 2s bar; } button { animation: .5s bar, 1s foo 0.3s, 2s quux; } @keyframes bar {} @keyframes quux {} @keyframes "baz'" {}`; const result = shim(css, 'host-a'); expect(result).toContain('animation: 1s ease foo, 2s host-a_bar infinite, forwards baz 3s;'); expect(result).toContain('animation: 1s "foo", 2s "host-a_bar";'); expect(result).toContain(` animation: .5s host-a_bar, 1s foo 0.3s, 2s host-a_quux;`); expect(result).toContain(` animation: .5s ease 'host-a_quux', 1s foo infinite, forwards "host-a_baz'" 1.5s, 2s host-a_bar;`); }); it(`should not modify css variables ending with 'animation' even if they reference a local keyframes identifier`, () => { const css = ` button { --variable-animation: foo; } @keyframes foo {}`; const result = shim(css, 'host-a'); expect(result).toContain('--variable-animation: foo;'); }); it(`should not modify css variables ending with 'animation-name' even if they reference a local keyframes identifier`, () => { const css = ` button { --variable-animation-name: foo; } @keyframes foo {}`; const result = shim(css, 'host-a'); expect(result).toContain('--variable-animation-name: foo;'); }); it('should maintain the spacing when handling (scoping or not) keyframes and animations', () => { const css = ` div { animation-name : foo; animation: 5s bar 1s backwards; animation : 3s baz ; animation-name:foobar ; animation:1s "foo" , 2s "bar",3s "quux"; } @-webkit-keyframes bar {} @keyframes foobar {} @keyframes quux {} `; const result = shim(css, 'host-a'); expect(result).toContain('animation-name : foo;'); expect(result).toContain('animation: 5s host-a_bar 1s backwards;'); expect(result).toContain('animation : 3s baz ;'); expect(result).toContain('animation-name:host-a_foobar ;'); expect(result).toContain('@-webkit-keyframes host-a_bar {}'); expect(result).toContain('@keyframes host-a_foobar {}'); expect(result).toContain('animation:1s "foo" , 2s "host-a_bar",3s "host-a_quux"'); }); it('should correctly process animations defined without any prefixed space', () => { let css = '.test{display: flex;animation:foo 1s forwards;} @keyframes foo {}'; let expected = '.test[host-a]{display: flex;animation:host-a_foo 1s forwards;} @keyframes host-a_foo {}'; expect(shim(css, 'host-a')).toEqual(expected); css = '.test{animation:foo 2s forwards;} @keyframes foo {}'; expected = '.test[host-a]{animation:host-a_foo 2s forwards;} @keyframes host-a_foo {}'; expect(shim(css, 'host-a')).toEqual(expected); css = 'button {display: block;animation-name: foobar;} @keyframes foobar {}'; expected = 'button[host-a] {display: block;animation-name: host-a_foobar;} @keyframes host-a_foobar {}'; expect(shim(css, 'host-a')).toEqual(expected); }); it('should correctly process keyframes defined without any prefixed space', () => { let css = '.test{display: flex;animation:bar 1s forwards;}@keyframes bar {}'; let expected = '.test[host-a]{display: flex;animation:host-a_bar 1s forwards;}@keyframes host-a_bar {}'; expect(shim(css, 'host-a')).toEqual(expected); css = '.test{animation:bar 2s forwards;}@-webkit-keyframes bar {}'; expected = '.test[host-a]{animation:host-a_bar 2s forwards;}@-webkit-keyframes host-a_bar {}'; expect(shim(css, 'host-a')).toEqual(expected); }); it('should ignore keywords values when scoping local animations', () => { const css = ` div { animation: inherit; animation: unset; animation: 3s ease reverse foo; animation: 5s foo 1s backwards; animation: none 1s foo; animation: .5s foo paused; animation: 1s running foo; animation: 3s linear 1s infinite running foo; animation: 5s foo ease; animation: 3s .5s infinite steps(3,end) foo; animation: 5s steps(9, jump-start) jump .5s; animation: 1s step-end steps; } @keyframes foo {} @keyframes inherit {} @keyframes unset {} @keyframes ease {} @keyframes reverse {} @keyframes backwards {} @keyframes none {} @keyframes paused {} @keyframes linear {} @keyframes running {} @keyframes end {} @keyframes jump {} @keyframes start {} @keyframes steps {} `; const result = shim(css, 'host-a'); expect(result).toContain('animation: inherit;'); expect(result).toContain('animation: unset;'); expect(result).toContain('animation: 3s ease reverse host-a_foo;'); expect(result).toContain('animation: 5s host-a_foo 1s backwards;'); expect(result).toContain('animation: none 1s host-a_foo;'); expect(result).toContain('animation: .5s host-a_foo paused;'); expect(result).toContain('animation: 1s running host-a_foo;'); expect(result).toContain('animation: 3s linear 1s infinite running host-a_foo;'); expect(result).toContain('animation: 5s host-a_foo ease;'); expect(result).toContain('animation: 3s .5s infinite steps(3,end) host-a_foo;'); expect(result).toContain('animation: 5s steps(9, jump-start) host-a_jump .5s;'); expect(result).toContain('animation: 1s step-end host-a_steps;'); }); it('should handle the usage of quotes', () => { const css = ` div { animation: 1.5s foo; } p { animation: 1s 'foz bar'; } @keyframes 'foo' {} @keyframes "foz bar" {} @keyframes bar {} @keyframes baz {} @keyframes qux {} @keyframes quux {} `; const result = shim(css, 'host-a'); expect(result).toContain("@keyframes 'host-a_foo' {}"); expect(result).toContain('@keyframes "host-a_foz bar" {}'); expect(result).toContain('animation: 1.5s host-a_foo;'); expect(result).toContain("animation: 1s 'host-a_foz bar';"); }); it('should handle the usage of quotes containing escaped quotes', () => { const css = ` div { animation: 1.5s "foo\\"bar"; } p { animation: 1s 'bar\\' \\'baz'; } button { animation-name: 'foz " baz'; } @keyframes "foo\\"bar" {} @keyframes "bar' 'baz" {} @keyframes "foz \\" baz" {} `; const result = shim(css, 'host-a'); expect(result).toContain('@keyframes "host-a_foo\\"bar" {}'); expect(result).toContain(`@keyframes "host-a_bar' 'baz" {}`); expect(result).toContain('@keyframes "host-a_foz \\" baz" {}'); expect(result).toContain('animation: 1.5s "host-a_foo\\"bar";'); expect(result).toContain("animation: 1s 'host-a_bar\\' \\'baz';"); expect(result).toContain(`animation-name: 'host-a_foz " baz';`); }); it('should handle the usage of commas in multiple animation definitions in a single declaration', () => { const css = ` button { animation: 1s "foo bar, baz", 2s 'qux quux'; } div { animation: 500ms foo, 1s 'bar, baz', 1500ms bar; } p { animation: 3s "bar, baz", 3s 'foo, bar' 1s, 3s "qux quux"; } @keyframes "qux quux" {} @keyframes "bar, baz" {} `; const result = shim(css, 'host-a'); expect(result).toContain('@keyframes "host-a_qux quux" {}'); expect(result).toContain('@keyframes "host-a_bar, baz" {}'); expect(result).toContain(`animation: 1s "foo bar, baz", 2s 'host-a_qux quux';`); expect(result).toContain("animation: 500ms foo, 1s 'host-a_bar, baz', 1500ms bar;"); expect(result).toContain( `animation: 3s "host-a_bar, baz", 3s 'foo, bar' 1s, 3s "host-a_qux quux";`, ); }); it('should handle the usage of double quotes escaping in multiple animation definitions in a single declaration', () => { const css = ` div { animation: 1s "foo", 1.5s "bar"; animation: 2s "fo\\"o", 2.5s "bar"; animation: 3s "foo\\"", 3.5s "bar", 3.7s "ba\\"r"; animation: 4s "foo\\\\", 4.5s "bar", 4.7s "baz\\""; animation: 5s "fo\\\\\\"o", 5.5s "bar", 5.7s "baz\\""; } @keyframes "foo" {} @keyframes "fo\\"o" {} @keyframes 'foo"' {} @keyframes 'foo\\\\' {} @keyframes bar {} @keyframes "ba\\"r" {} @keyframes "fo\\\\\\"o" {} `; const result = shim(css, 'host-a'); expect(result).toContain('@keyframes "host-a_foo" {}'); expect(result).toContain('@keyframes "host-a_fo\\"o" {}'); expect(result).toContain(`@keyframes 'host-a_foo"' {}`); expect(result).toContain("@keyframes 'host-a_foo\\\\' {}"); expect(result).toContain('@keyframes host-a_bar {}'); expect(result).toContain('@keyframes "host-a_ba\\"r" {}'); expect(result).toContain('@keyframes "host-a_fo\\\\\\"o"'); expect(result).toContain('animation: 1s "host-a_foo", 1.5s "host-a_bar";'); expect(result).toContain('animation: 2s "host-a_fo\\"o", 2.5s "host-a_bar";'); expect(result).toContain( 'animation: 3s "host-a_foo\\"", 3.5s "host-a_bar", 3.7s "host-a_ba\\"r";', ); expect(result).toContain('animation: 4s "host-a_foo\\\\", 4.5s "host-a_bar", 4.7s "baz\\"";'); expect(result).toContain( 'animation: 5s "host-a_fo\\\\\\"o", 5.5s "host-a_bar", 5.7s "baz\\"";', ); }); it('should handle the usage of single quotes escaping in multiple animation definitions in a single declaration', () => { const css = ` div { animation: 1s 'foo', 1.5s 'bar'; animation: 2s 'fo\\'o', 2.5s 'bar'; animation: 3s 'foo\\'', 3.5s 'bar', 3.7s 'ba\\'r'; animation: 4s 'foo\\\\', 4.5s 'bar', 4.7s 'baz\\''; animation: 5s 'fo\\\\\\'o', 5.5s 'bar', 5.7s 'baz\\''; } @keyframes foo {} @keyframes 'fo\\'o' {} @keyframes 'foo'' {} @keyframes 'foo\\\\' {} @keyframes "bar" {} @keyframes 'ba\\'r' {} @keyframes "fo\\\\\\'o" {} `; const result = shim(css, 'host-a'); expect(result).toContain('@keyframes host-a_foo {}'); expect(result).toContain("@keyframes 'host-a_fo\\'o' {}"); expect(result).toContain("@keyframes 'host-a_foo'' {}"); expect(result).toContain("@keyframes 'host-a_foo\\\\' {}"); expect(result).toContain('@keyframes "host-a_bar" {}'); expect(result).toContain("@keyframes 'host-a_ba\\'r' {}"); expect(result).toContain(`@keyframes "host-a_fo\\\\\\'o" {}`); expect(result).toContain("animation: 1s 'host-a_foo', 1.5s 'host-a_bar';"); expect(result).toContain("animation: 2s 'host-a_fo\\'o', 2.5s 'host-a_bar';"); expect(result).toContain( "animation: 3s 'host-a_foo\\'', 3.5s 'host-a_bar', 3.7s 'host-a_ba\\'r';", ); expect(result).toContain("animation: 4s 'host-a_foo\\\\', 4.5s 'host-a_bar', 4.7s 'baz\\'';"); expect(result).toContain("animation: 5s 'host-a_fo\\\\\\'o', 5.5s 'host-a_bar', 5.7s 'baz\\''"); }); it('should handle the usage of mixed single and double quotes escaping in multiple animation definitions in a single declaration', () => { const css = ` div { animation: 1s 'f\\"oo', 1.5s "ba\\'r"; animation: 2s "fo\\"\\"o", 2.5s 'b\\\\"ar'; animation: 3s 'foo\\\\', 3.5s "b\\\\\\"ar", 3.7s 'ba\\'\\"\\'r'; animation: 4s 'fo\\'o', 4.5s 'b\\"ar\\"', 4.7s "baz\\'"; } @keyframes 'f"oo' {} @keyframes 'fo""o' {} @keyframes 'foo\\\\' {} @keyframes 'fo\\'o' {} @keyframes 'ba\\'r' {} @keyframes 'b\\\\"ar' {} @keyframes 'b\\\\\\"ar' {} @keyframes 'b"ar"' {} @keyframes 'ba\\'\\"\\'r' {} `; const result = shim(css, 'host-a'); expect(result).toContain(`@keyframes 'host-a_f"oo' {}`); expect(result).toContain(`@keyframes 'host-a_fo""o' {}`); expect(result).toContain("@keyframes 'host-a_foo\\\\' {}"); expect(result).toContain("@keyframes 'host-a_fo\\'o' {}"); expect(result).toContain("@keyframes 'host-a_ba\\'r' {}"); expect(result).toContain(`@keyframes 'host-a_b\\\\"ar' {}`); expect(result).toContain(`@keyframes 'host-a_b\\\\\\"ar' {}`); expect(result).toContain(`@keyframes 'host-a_b"ar"' {}`); expect(result).toContain(`@keyframes 'host-a_ba\\'\\"\\'r' {}`); expect(result).toContain(`animation: 1s 'host-a_f\\"oo', 1.5s "host-a_ba\\'r";`); expect(result).toContain(`animation: 2s "host-a_fo\\"\\"o", 2.5s 'host-a_b\\\\"ar';`); expect(result).toContain( `animation: 3s 'host-a_foo\\\\', 3.5s "host-a_b\\\\\\"ar", 3.7s 'host-a_ba\\'\\"\\'r';`, ); expect(result).toContain( `animation: 4s 'host-a_fo\\'o', 4.5s 'host-a_b\\"ar\\"', 4.7s "baz\\'";`, ); }); it('should handle the usage of commas inside quotes', () => { const css = ` div { animation: 3s 'bar,, baz'; } p { animation-name: "bar,, baz", foo,'ease, linear , inherit', bar; } @keyframes 'foo' {} @keyframes 'bar,, baz' {} @keyframes 'ease, linear , inherit' {} `; const result = shim(css, 'host-a'); expect(result).toContain("@keyframes 'host-a_bar,, baz' {}"); expect(result).toContain("animation: 3s 'host-a_bar,, baz';"); expect(result).toContain( `animation-name: "host-a_bar,, baz", host-a_foo,'host-a_ease, linear , inherit', bar;`, ); }); it('should not ignore animation keywords when they are inside quotes', () => { const css = ` div { animation: 3s 'unset'; } button { animation: 5s "forwards" 1s forwards; } @keyframes unset {} @keyframes forwards {} `; const result = shim(css, 'host-a'); expect(result).toContain('@keyframes host-a_unset {}'); expect(result).toContain('@keyframes host-a_forwards {}'); expect(result).toContain("animation: 3s 'host-a_unset';"); expect(result).toContain('animation: 5s "host-a_forwards" 1s forwards;'); }); it('should handle css functions correctly', () => { const css = ` div { animation: foo 0.5s alternate infinite cubic-bezier(.17, .67, .83, .67); } button { animation: calc(2s / 2) calc; } @keyframes foo {} @keyframes cubic-bezier {} @keyframes calc {} `; const result = shim(css, 'host-a'); expect(result).toContain('@keyframes host-a_cubic-bezier {}'); expect(result).toContain('@keyframes host-a_calc {}'); expect(result).toContain( 'animation: host-a_foo 0.5s alternate infinite cubic-bezier(.17, .67, .83, .67);', ); expect(result).toContain('animation: calc(2s / 2) host-a_calc;'); }); });