angular/packages/compiler/test/shadow_css/keyframes_spec.ts
Matthew Beck 770e505f78 fix(compiler): preserve leading commas in animation definitions
Update the regular expression in `_scopeAnimationRule` to prevent absorbing and deleting leading commas after `animation:`. Also remove the corresponding legacy test case from `keyframes_spec.ts`.
2026-05-12 14:22:56 -07:00

562 lines
20 KiB
TypeScript

/**
* @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) 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;');
});
});