refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
643
packages/platform-browser/animations/test/dsl/animation_spec.ts
Normal file
643
packages/platform-browser/animations/test/dsl/animation_spec.ts
Normal file
@ -0,0 +1,643 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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.io/license
|
||||
*/
|
||||
import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, group, keyframes, sequence, style, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {Animation} from '../../src/dsl/animation';
|
||||
import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction';
|
||||
import {validateAnimationSequence} from '../../src/dsl/animation_validator_visitor';
|
||||
|
||||
export function main() {
|
||||
describe('Animation', () => {
|
||||
describe('validation', () => {
|
||||
it('should throw an error if one or more but not all keyframes() styles contain offsets',
|
||||
() => {
|
||||
const steps = animate(1000, keyframes([
|
||||
style({opacity: 0}),
|
||||
style({opacity: 1, offset: 1}),
|
||||
]));
|
||||
|
||||
expect(() => { validateAndThrowAnimationSequence(steps); })
|
||||
.toThrowError(
|
||||
/Not all style\(\) steps within the declared keyframes\(\) contain offsets/);
|
||||
});
|
||||
|
||||
it('should throw an error if not all offsets are between 0 and 1', () => {
|
||||
let steps = animate(1000, keyframes([
|
||||
style({opacity: 0, offset: -1}),
|
||||
style({opacity: 1, offset: 1}),
|
||||
]));
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps);
|
||||
}).toThrowError(/Please ensure that all keyframe offsets are between 0 and 1/);
|
||||
|
||||
steps = animate(1000, keyframes([
|
||||
style({opacity: 0, offset: 0}),
|
||||
style({opacity: 1, offset: 1.1}),
|
||||
]));
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps);
|
||||
}).toThrowError(/Please ensure that all keyframe offsets are between 0 and 1/);
|
||||
});
|
||||
|
||||
it('should throw an error if a smaller offset shows up after a bigger one', () => {
|
||||
let steps = animate(1000, keyframes([
|
||||
style({opacity: 0, offset: 1}),
|
||||
style({opacity: 1, offset: 0}),
|
||||
]));
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps);
|
||||
}).toThrowError(/Please ensure that all keyframe offsets are in order/);
|
||||
});
|
||||
|
||||
it('should throw an error if any styles overlap during parallel animations', () => {
|
||||
const steps = group([
|
||||
sequence([
|
||||
// 0 -> 2000ms
|
||||
style({opacity: 0}), animate('500ms', style({opacity: .25})),
|
||||
animate('500ms', style({opacity: .5})), animate('500ms', style({opacity: .75})),
|
||||
animate('500ms', style({opacity: 1}))
|
||||
]),
|
||||
animate('1s 500ms', keyframes([
|
||||
// 0 -> 1500ms
|
||||
style({width: 0}),
|
||||
style({opacity: 1, width: 1000}),
|
||||
]))
|
||||
]);
|
||||
|
||||
expect(() => { validateAndThrowAnimationSequence(steps); })
|
||||
.toThrowError(
|
||||
/The CSS property "opacity" that exists between the times of "0ms" and "2000ms" is also being animated in a parallel animation between the times of "0ms" and "1500ms"/);
|
||||
});
|
||||
|
||||
it('should throw an error if an animation time is invalid', () => {
|
||||
const steps = [animate('500xs', style({opacity: 1}))];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps);
|
||||
}).toThrowError(/The provided timing value "500xs" is invalid/);
|
||||
|
||||
const steps2 = [animate('500ms 500ms 500ms ease-out', style({opacity: 1}))];
|
||||
|
||||
expect(() => {
|
||||
validateAndThrowAnimationSequence(steps2);
|
||||
}).toThrowError(/The provided timing value "500ms 500ms 500ms ease-out" is invalid/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyframe building', () => {
|
||||
describe('style() / animate()', () => {
|
||||
it('should produce a balanced series of keyframes given a sequence of animate steps',
|
||||
() => {
|
||||
const steps = [
|
||||
style({width: 0}), animate(1000, style({height: 50})),
|
||||
animate(1000, style({width: 100})), animate(1000, style({height: 150})),
|
||||
animate(1000, style({width: 200}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{height: AUTO_STYLE, width: 0, offset: 0},
|
||||
{height: 50, width: 0, offset: .25},
|
||||
{height: 50, width: 100, offset: .5},
|
||||
{height: 150, width: 100, offset: .75},
|
||||
{height: 150, width: 200, offset: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fill in missing starting steps when a starting `style()` value is not used',
|
||||
() => {
|
||||
const steps = [animate(1000, style({width: 999}))];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{width: AUTO_STYLE, offset: 0}, {width: 999, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should merge successive style() calls together before an animate() call', () => {
|
||||
const steps = [
|
||||
style({width: 0}), style({height: 0}), style({width: 200}), style({opacity: 0}),
|
||||
animate(1000, style({width: 100, height: 400, opacity: 1}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{width: 200, height: 0, opacity: 0, offset: 0},
|
||||
{width: 100, height: 400, opacity: 1, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not merge in successive style() calls to the previous animate() keyframe',
|
||||
() => {
|
||||
const steps = [
|
||||
style({opacity: 0}), animate(1000, style({opacity: .5})), style({opacity: .6}),
|
||||
animate(1000, style({opacity: 1}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const keyframes = humanizeOffsets(players[0].keyframes, 4);
|
||||
|
||||
expect(keyframes).toEqual([
|
||||
{opacity: 0, offset: 0},
|
||||
{opacity: .5, offset: .4998},
|
||||
{opacity: .6, offset: .5002},
|
||||
{opacity: 1, offset: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support an easing value that uses cubic-bezier(...)', () => {
|
||||
const steps = [
|
||||
style({opacity: 0}),
|
||||
animate('1s cubic-bezier(.29, .55 ,.53 ,1.53)', style({opacity: 1}))
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[0];
|
||||
const lastKeyframe = player.keyframes[1];
|
||||
const lastKeyframeEasing = <string>lastKeyframe['easing'];
|
||||
expect(lastKeyframeEasing.replace(/\s+/g, '')).toEqual('cubic-bezier(.29,.55,.53,1.53)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sequence()', () => {
|
||||
it('should not produce extra timelines when multiple sequences are used within each other',
|
||||
() => {
|
||||
const steps = [
|
||||
style({width: 0}), animate(1000, style({width: 100})), sequence([
|
||||
animate(1000, style({width: 200})),
|
||||
sequence([animate(1000, style({width: 300}))])
|
||||
]),
|
||||
animate(1000, style({width: 400})), sequence([animate(1000, style({width: 500}))])
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{width: 0, offset: 0}, {width: 100, offset: .2}, {width: 200, offset: .4},
|
||||
{width: 300, offset: .6}, {width: 400, offset: .8}, {width: 500, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should produce a 1ms animation step if a style call exists before sequence within a call to animate()',
|
||||
() => {
|
||||
const steps = [
|
||||
style({width: 100}), sequence([
|
||||
animate(1000, style({width: 200})),
|
||||
])
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(humanizeOffsets(players[0].keyframes, 4)).toEqual([
|
||||
{width: 100, offset: 0}, {width: 100, offset: .001}, {width: 200, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create a new timeline after a sequence if group() or keyframe() commands are used within',
|
||||
() => {
|
||||
const steps = [
|
||||
style({width: 100, height: 100}), animate(1000, style({width: 150, height: 150})),
|
||||
sequence([
|
||||
group([
|
||||
animate(1000, style({height: 200})),
|
||||
]),
|
||||
animate(1000, keyframes([style({width: 180}), style({width: 200})]))
|
||||
]),
|
||||
animate(1000, style({width: 500, height: 500}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(4);
|
||||
|
||||
const finalPlayer = players[players.length - 1];
|
||||
expect(finalPlayer.keyframes).toEqual([
|
||||
{width: 200, height: 200, offset: 0}, {width: 500, height: 500, offset: 1}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyframes()', () => {
|
||||
it('should produce a sub timeline when `keyframes()` is used within a sequence', () => {
|
||||
const steps = [
|
||||
animate(1000, style({opacity: .5})), animate(1000, style({opacity: 1})),
|
||||
animate(
|
||||
1000, keyframes([style({height: 0}), style({height: 100}), style({height: 50})])),
|
||||
animate(1000, style({height: 0, opacity: 0}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(3);
|
||||
|
||||
const player0 = players[0];
|
||||
expect(player0.delay).toEqual(0);
|
||||
expect(player0.keyframes).toEqual([
|
||||
{opacity: AUTO_STYLE, offset: 0},
|
||||
{opacity: .5, offset: .5},
|
||||
{opacity: 1, offset: 1},
|
||||
]);
|
||||
|
||||
const subPlayer = players[1];
|
||||
expect(subPlayer.delay).toEqual(2000);
|
||||
expect(subPlayer.keyframes).toEqual([
|
||||
{height: 0, offset: 0},
|
||||
{height: 100, offset: .5},
|
||||
{height: 50, offset: 1},
|
||||
]);
|
||||
|
||||
const player1 = players[2];
|
||||
expect(player1.delay).toEqual(3000);
|
||||
expect(player1.keyframes).toEqual([
|
||||
{opacity: 1, height: 50, offset: 0}, {opacity: 0, height: 0, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should propagate inner keyframe style data to the parent timeline if used afterwards',
|
||||
() => {
|
||||
const steps = [
|
||||
style({opacity: 0}), animate(1000, style({opacity: .5})),
|
||||
animate(1000, style({opacity: 1})), animate(1000, keyframes([
|
||||
style({color: 'red'}),
|
||||
style({color: 'blue'}),
|
||||
])),
|
||||
animate(1000, style({color: 'green', opacity: 0}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const finalPlayer = players[players.length - 1];
|
||||
expect(finalPlayer.keyframes).toEqual([
|
||||
{opacity: 1, color: 'blue', offset: 0}, {opacity: 0, color: 'green', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should feed in starting data into inner keyframes if used in an style step beforehand',
|
||||
() => {
|
||||
const steps = [
|
||||
animate(1000, style({opacity: .5})), animate(1000, keyframes([
|
||||
style({opacity: .8, offset: .5}),
|
||||
style({opacity: 1, offset: 1}),
|
||||
]))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(2);
|
||||
|
||||
const topPlayer = players[0];
|
||||
expect(topPlayer.keyframes).toEqual([
|
||||
{opacity: AUTO_STYLE, offset: 0}, {opacity: .5, offset: 1}
|
||||
]);
|
||||
|
||||
const subPlayer = players[1];
|
||||
expect(subPlayer.keyframes).toEqual([
|
||||
{opacity: .5, offset: 0}, {opacity: .8, offset: 0.5}, {opacity: 1, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should set the easing value as an easing value for the entire timeline', () => {
|
||||
const steps = [
|
||||
style({opacity: 0}), animate(1000, style({opacity: .5})),
|
||||
animate(
|
||||
'1s ease-out',
|
||||
keyframes([style({opacity: .8, offset: .5}), style({opacity: 1, offset: 1})]))
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[1];
|
||||
expect(player.easing).toEqual('ease-out');
|
||||
});
|
||||
|
||||
it('should combine the starting time + the given delay as the delay value for the animated keyframes',
|
||||
() => {
|
||||
const steps = [
|
||||
style({opacity: 0}), animate(500, style({opacity: .5})),
|
||||
animate(
|
||||
'1s 2s ease-out',
|
||||
keyframes([style({opacity: .8, offset: .5}), style({opacity: 1, offset: 1})]))
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[1];
|
||||
expect(player.delay).toEqual(2500);
|
||||
});
|
||||
|
||||
it('should not leak in additional styles used later on after keyframe styles have already been declared',
|
||||
() => {
|
||||
const steps = [
|
||||
animate(1000, style({height: '50px'})),
|
||||
animate(
|
||||
2000, keyframes([
|
||||
style({left: '0', transform: 'rotate(0deg)', offset: 0}),
|
||||
style({
|
||||
left: '40%',
|
||||
transform: 'rotate(250deg) translateY(-200px)',
|
||||
offset: .33
|
||||
}),
|
||||
style(
|
||||
{left: '60%', transform: 'rotate(180deg) translateY(200px)', offset: .66}),
|
||||
style({left: 'calc(100% - 100px)', transform: 'rotate(0deg)', offset: 1}),
|
||||
])),
|
||||
group([animate('2s', style({width: '200px'}))]),
|
||||
animate('2s', style({height: '300px'})),
|
||||
group([animate('2s', style({height: '500px', width: '500px'}))])
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(5);
|
||||
|
||||
const firstPlayerKeyframes = players[0].keyframes;
|
||||
expect(firstPlayerKeyframes[0]['width']).toBeFalsy();
|
||||
expect(firstPlayerKeyframes[1]['width']).toBeFalsy();
|
||||
expect(firstPlayerKeyframes[0]['height']).toEqual(AUTO_STYLE);
|
||||
expect(firstPlayerKeyframes[1]['height']).toEqual('50px');
|
||||
|
||||
const keyframePlayerKeyframes = players[1].keyframes;
|
||||
expect(keyframePlayerKeyframes[0]['width']).toBeFalsy();
|
||||
expect(keyframePlayerKeyframes[0]['height']).toBeFalsy();
|
||||
|
||||
const groupPlayerKeyframes = players[2].keyframes;
|
||||
expect(groupPlayerKeyframes[0]['width']).toEqual(AUTO_STYLE);
|
||||
expect(groupPlayerKeyframes[1]['width']).toEqual('200px');
|
||||
expect(groupPlayerKeyframes[0]['height']).toBeFalsy();
|
||||
expect(groupPlayerKeyframes[1]['height']).toBeFalsy();
|
||||
|
||||
const secondToFinalAnimatePlayerKeyframes = players[3].keyframes;
|
||||
expect(secondToFinalAnimatePlayerKeyframes[0]['width']).toBeFalsy();
|
||||
expect(secondToFinalAnimatePlayerKeyframes[1]['width']).toBeFalsy();
|
||||
expect(secondToFinalAnimatePlayerKeyframes[0]['height']).toEqual('50px');
|
||||
expect(secondToFinalAnimatePlayerKeyframes[1]['height']).toEqual('300px');
|
||||
|
||||
const finalAnimatePlayerKeyframes = players[4].keyframes;
|
||||
expect(finalAnimatePlayerKeyframes[0]['width']).toEqual('200px');
|
||||
expect(finalAnimatePlayerKeyframes[1]['width']).toEqual('500px');
|
||||
expect(finalAnimatePlayerKeyframes[0]['height']).toEqual('300px');
|
||||
expect(finalAnimatePlayerKeyframes[1]['height']).toEqual('500px');
|
||||
});
|
||||
|
||||
it('should respect offsets if provided directly within the style data', () => {
|
||||
const steps = animate(1000, keyframes([
|
||||
style({opacity: 0, offset: 0}), style({opacity: .6, offset: .6}),
|
||||
style({opacity: 1, offset: 1})
|
||||
]));
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(1);
|
||||
const player = players[0];
|
||||
|
||||
expect(player.keyframes).toEqual([
|
||||
{opacity: 0, offset: 0}, {opacity: .6, offset: .6}, {opacity: 1, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should respect offsets if provided directly within the style metadata type', () => {
|
||||
const steps =
|
||||
animate(1000, keyframes([
|
||||
{type: AnimationMetadataType.Style, offset: 0, styles: {opacity: 0}},
|
||||
{type: AnimationMetadataType.Style, offset: .4, styles: {opacity: .4}},
|
||||
{type: AnimationMetadataType.Style, offset: 1, styles: {opacity: 1}},
|
||||
]));
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(1);
|
||||
const player = players[0];
|
||||
|
||||
expect(player.keyframes).toEqual([
|
||||
{opacity: 0, offset: 0}, {opacity: .4, offset: .4}, {opacity: 1, offset: 1}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('group()', () => {
|
||||
it('should properly tally style data within a group() for use in a follow-up animate() step',
|
||||
() => {
|
||||
const steps = [
|
||||
style({width: 0, height: 0}), animate(1000, style({width: 20, height: 50})),
|
||||
group([animate('1s 1s', style({width: 200})), animate('1s', style({height: 500}))]),
|
||||
animate(1000, style({width: 1000, height: 1000}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(4);
|
||||
|
||||
const player0 = players[0];
|
||||
expect(player0.duration).toEqual(1000);
|
||||
expect(player0.keyframes).toEqual([
|
||||
{width: 0, height: 0, offset: 0}, {width: 20, height: 50, offset: 1}
|
||||
]);
|
||||
|
||||
const gPlayer1 = players[1];
|
||||
expect(gPlayer1.duration).toEqual(2000);
|
||||
expect(gPlayer1.delay).toEqual(1000);
|
||||
expect(gPlayer1.keyframes).toEqual([
|
||||
{width: 20, offset: 0}, {width: 20, offset: .5}, {width: 200, offset: 1}
|
||||
]);
|
||||
|
||||
const gPlayer2 = players[2];
|
||||
expect(gPlayer2.duration).toEqual(1000);
|
||||
expect(gPlayer2.delay).toEqual(1000);
|
||||
expect(gPlayer2.keyframes).toEqual([
|
||||
{height: 50, offset: 0}, {height: 500, offset: 1}
|
||||
]);
|
||||
|
||||
const player1 = players[3];
|
||||
expect(player1.duration).toEqual(1000);
|
||||
expect(player1.delay).toEqual(3000);
|
||||
expect(player1.keyframes).toEqual([
|
||||
{width: 200, height: 500, offset: 0}, {width: 1000, height: 1000, offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support groups with nested sequences', () => {
|
||||
const steps = [group([
|
||||
sequence([
|
||||
style({opacity: 0}),
|
||||
animate(1000, style({opacity: 1})),
|
||||
]),
|
||||
sequence([
|
||||
style({width: 0}),
|
||||
animate(1000, style({width: 200})),
|
||||
])
|
||||
])];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(2);
|
||||
|
||||
const gPlayer1 = players[0];
|
||||
expect(gPlayer1.delay).toEqual(0);
|
||||
expect(gPlayer1.keyframes).toEqual([
|
||||
{opacity: 0, offset: 0},
|
||||
{opacity: 1, offset: 1},
|
||||
]);
|
||||
|
||||
const gPlayer2 = players[1];
|
||||
expect(gPlayer1.delay).toEqual(0);
|
||||
expect(gPlayer2.keyframes).toEqual([{width: 0, offset: 0}, {width: 200, offset: 1}]);
|
||||
});
|
||||
|
||||
it('should respect delays after group entries', () => {
|
||||
const steps = [
|
||||
style({width: 0, height: 0}), animate(1000, style({width: 50, height: 50})), group([
|
||||
animate(1000, style({width: 100})),
|
||||
animate(1000, style({height: 100})),
|
||||
]),
|
||||
animate('1s 1s', style({height: 200, width: 200}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(4);
|
||||
|
||||
const finalPlayer = players[players.length - 1];
|
||||
expect(finalPlayer.delay).toEqual(2000);
|
||||
expect(finalPlayer.duration).toEqual(2000);
|
||||
expect(finalPlayer.keyframes).toEqual([
|
||||
{width: 100, height: 100, offset: 0},
|
||||
{width: 100, height: 100, offset: .5},
|
||||
{width: 200, height: 200, offset: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should respect delays after multiple calls to group()', () => {
|
||||
const steps = [
|
||||
group([animate('2s', style({opacity: 1})), animate('2s', style({width: '100px'}))]),
|
||||
animate(2000, style({width: 0, opacity: 0})),
|
||||
group([animate('2s', style({opacity: 1})), animate('2s', style({width: '200px'}))]),
|
||||
animate(2000, style({width: 0, opacity: 0}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
const middlePlayer = players[2];
|
||||
expect(middlePlayer.delay).toEqual(2000);
|
||||
expect(middlePlayer.duration).toEqual(2000);
|
||||
|
||||
const finalPlayer = players[players.length - 1];
|
||||
expect(finalPlayer.delay).toEqual(6000);
|
||||
expect(finalPlayer.duration).toEqual(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timing values', () => {
|
||||
it('should properly combine an easing value with a delay into a set of three keyframes',
|
||||
() => {
|
||||
const steps: AnimationMetadata[] =
|
||||
[style({opacity: 0}), animate('3s 1s ease-out', style({opacity: 1}))];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[0];
|
||||
expect(player.keyframes).toEqual([
|
||||
{opacity: 0, offset: 0}, {opacity: 0, offset: .25},
|
||||
{opacity: 1, offset: 1, easing: 'ease-out'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow easing values to exist for each animate() step', () => {
|
||||
const steps: AnimationMetadata[] = [
|
||||
style({width: 0}), animate('1s linear', style({width: 10})),
|
||||
animate('2s ease-out', style({width: 20})), animate('1s ease-in', style({width: 30}))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players.length).toEqual(1);
|
||||
|
||||
const player = players[0];
|
||||
expect(player.keyframes).toEqual([
|
||||
{width: 0, offset: 0}, {width: 10, offset: .25, easing: 'linear'},
|
||||
{width: 20, offset: .75, easing: 'ease-out'},
|
||||
{width: 30, offset: 1, easing: 'ease-in'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should produce a top-level timeline only for the duration that is set as before a group kicks in',
|
||||
() => {
|
||||
const steps: AnimationMetadata[] = [
|
||||
style({width: 0, height: 0, opacity: 0}),
|
||||
animate('1s', style({width: 100, height: 100, opacity: .2})), group([
|
||||
animate('500ms 1s', style({width: 500})), animate('1s', style({height: 500})),
|
||||
sequence([
|
||||
animate(500, style({opacity: .5})),
|
||||
animate(500, style({opacity: .6})),
|
||||
animate(500, style({opacity: .7})),
|
||||
animate(500, style({opacity: 1})),
|
||||
])
|
||||
])
|
||||
];
|
||||
|
||||
const player = invokeAnimationSequence(steps)[0];
|
||||
expect(player.duration).toEqual(1000);
|
||||
expect(player.delay).toEqual(0);
|
||||
});
|
||||
|
||||
it('should offset group() and keyframe() timelines with a delay which is the current time of the previous player when called',
|
||||
() => {
|
||||
const steps: AnimationMetadata[] = [
|
||||
style({width: 0, height: 0}),
|
||||
animate('1500ms linear', style({width: 10, height: 10})), group([
|
||||
animate(1000, style({width: 500, height: 500})),
|
||||
animate(2000, style({width: 500, height: 500}))
|
||||
]),
|
||||
animate(1000, keyframes([
|
||||
style({width: 200}),
|
||||
style({width: 500}),
|
||||
]))
|
||||
];
|
||||
|
||||
const players = invokeAnimationSequence(steps);
|
||||
expect(players[0].delay).toEqual(0); // top-level animation
|
||||
expect(players[1].delay).toEqual(1500); // first entry in group()
|
||||
expect(players[2].delay).toEqual(1500); // second entry in group()
|
||||
expect(players[3].delay).toEqual(3500); // animate(...keyframes())
|
||||
});
|
||||
});
|
||||
|
||||
describe('state based data', () => {
|
||||
it('should create an empty animation if there are zero animation steps', () => {
|
||||
const steps: AnimationMetadata[] = [];
|
||||
|
||||
const fromStyles: ɵStyleData[] = [{background: 'blue', height: 100}];
|
||||
|
||||
const toStyles: ɵStyleData[] = [{background: 'red'}];
|
||||
|
||||
const player = invokeAnimationSequence(steps, fromStyles, toStyles)[0];
|
||||
expect(player.duration).toEqual(0);
|
||||
expect(player.keyframes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should produce an animation from start to end between the to and from styles if there are animate steps in between',
|
||||
() => {
|
||||
const steps: AnimationMetadata[] = [animate(1000)];
|
||||
|
||||
const fromStyles: ɵStyleData[] = [{background: 'blue', height: 100}];
|
||||
|
||||
const toStyles: ɵStyleData[] = [{background: 'red'}];
|
||||
|
||||
const players = invokeAnimationSequence(steps, fromStyles, toStyles);
|
||||
expect(players[0].keyframes).toEqual([
|
||||
{background: 'blue', height: 100, offset: 0},
|
||||
{background: 'red', height: AUTO_STYLE, offset: 1}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function humanizeOffsets(keyframes: ɵStyleData[], digits: number = 3): ɵStyleData[] {
|
||||
return keyframes.map(keyframe => {
|
||||
keyframe['offset'] = Number(parseFloat(<any>keyframe['offset']).toFixed(digits));
|
||||
return keyframe;
|
||||
});
|
||||
}
|
||||
|
||||
function invokeAnimationSequence(
|
||||
steps: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData[] = [],
|
||||
destinationStyles: ɵStyleData[] = []): AnimationTimelineInstruction[] {
|
||||
return new Animation(steps).buildTimelines(startingStyles, destinationStyles);
|
||||
}
|
||||
|
||||
function validateAndThrowAnimationSequence(steps: AnimationMetadata | AnimationMetadata[]) {
|
||||
const ast =
|
||||
Array.isArray(steps) ? sequence(<AnimationMetadata[]>steps) : <AnimationMetadata>steps;
|
||||
const errors = validateAnimationSequence(ast);
|
||||
if (errors.length) {
|
||||
throw new Error(errors.join('\n'));
|
||||
}
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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.io/license
|
||||
*/
|
||||
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {buildTrigger} from '../../src/dsl/animation_trigger';
|
||||
|
||||
function makeTrigger(name: string, steps: any) {
|
||||
const triggerData = trigger(name, steps);
|
||||
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
|
||||
return triggerInstance;
|
||||
}
|
||||
|
||||
export function main() {
|
||||
describe('AnimationTrigger', () => {
|
||||
describe('trigger validation', () => {
|
||||
it('should group errors together for an animation trigger', () => {
|
||||
expect(() => {
|
||||
makeTrigger('myTrigger', [transition('12345', animate(3333))]);
|
||||
}).toThrowError(/Animation parsing for the myTrigger trigger have failed/);
|
||||
});
|
||||
|
||||
it('should throw an error when a transition within a trigger contains an invalid expression',
|
||||
() => {
|
||||
expect(
|
||||
() => { makeTrigger('name', [transition('somethingThatIsWrong', animate(3333))]); })
|
||||
.toThrowError(
|
||||
/- The provided transition expression "somethingThatIsWrong" is not supported/);
|
||||
});
|
||||
|
||||
it('should throw an error if an animation alias is used that is not yet supported', () => {
|
||||
expect(() => {
|
||||
makeTrigger('name', [transition(':angular', animate(3333))]);
|
||||
}).toThrowError(/- The transition alias value ":angular" is not supported/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigger usage', () => {
|
||||
it('should construct a trigger based on the states and transition data', () => {
|
||||
const result = makeTrigger('name', [
|
||||
state('on', style({width: 0})), state('off', style({width: 100})),
|
||||
transition('on => off', animate(1000)), transition('off => on', animate(1000))
|
||||
]);
|
||||
|
||||
expect(result.states).toEqual({'on': {width: 0}, 'off': {width: 100}});
|
||||
|
||||
expect(result.transitionFactories.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should find the first transition that matches', () => {
|
||||
const result = makeTrigger(
|
||||
'name', [transition('a => b', animate(1234)), transition('b => c', animate(5678))]);
|
||||
|
||||
const trans = result.matchTransition('b', 'c');
|
||||
expect(trans.timelines.length).toEqual(1);
|
||||
const timeline = trans.timelines[0];
|
||||
expect(timeline.duration).toEqual(5678);
|
||||
});
|
||||
|
||||
it('should find a transition with a `*` value', () => {
|
||||
const result = makeTrigger('name', [
|
||||
transition('* => b', animate(1234)), transition('b => *', animate(5678)),
|
||||
transition('* => *', animate(9999))
|
||||
]);
|
||||
|
||||
let trans = result.matchTransition('b', 'c');
|
||||
expect(trans.timelines[0].duration).toEqual(5678);
|
||||
|
||||
trans = result.matchTransition('a', 'b');
|
||||
expect(trans.timelines[0].duration).toEqual(1234);
|
||||
|
||||
trans = result.matchTransition('c', 'c');
|
||||
expect(trans.timelines[0].duration).toEqual(9999);
|
||||
});
|
||||
|
||||
it('should null when no results are found', () => {
|
||||
const result = makeTrigger('name', [transition('a => b', animate(1111))]);
|
||||
|
||||
const trans = result.matchTransition('b', 'a');
|
||||
expect(trans).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should allow a function to be used as a predicate for the transition', () => {
|
||||
let returnValue = false;
|
||||
|
||||
const result = makeTrigger('name', [transition((from, to) => returnValue, animate(1111))]);
|
||||
|
||||
expect(result.matchTransition('a', 'b')).toBeFalsy();
|
||||
expect(result.matchTransition('1', 2)).toBeFalsy();
|
||||
expect(result.matchTransition(false, true)).toBeFalsy();
|
||||
|
||||
returnValue = true;
|
||||
|
||||
expect(result.matchTransition('a', 'b')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call each transition predicate function until the first one that returns true',
|
||||
() => {
|
||||
let count = 0;
|
||||
|
||||
function countAndReturn(value: boolean) {
|
||||
return (fromState: any, toState: any) => {
|
||||
count++;
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
const result = makeTrigger('name', [
|
||||
transition(countAndReturn(false), animate(1111)),
|
||||
transition(countAndReturn(false), animate(2222)),
|
||||
transition(countAndReturn(true), animate(3333)),
|
||||
transition(countAndReturn(true), animate(3333))
|
||||
]);
|
||||
|
||||
const trans = result.matchTransition('a', 'b');
|
||||
expect(trans.timelines[0].duration).toEqual(3333);
|
||||
|
||||
expect(count).toEqual(3);
|
||||
});
|
||||
|
||||
it('should support bi-directional transition expressions', () => {
|
||||
const result = makeTrigger('name', [transition('a <=> b', animate(2222))]);
|
||||
|
||||
const t1 = result.matchTransition('a', 'b');
|
||||
expect(t1.timelines[0].duration).toEqual(2222);
|
||||
|
||||
const t2 = result.matchTransition('b', 'a');
|
||||
expect(t2.timelines[0].duration).toEqual(2222);
|
||||
});
|
||||
|
||||
it('should support multiple transition statements in one string', () => {
|
||||
const result = makeTrigger('name', [transition('a => b, b => a, c => *', animate(1234))]);
|
||||
|
||||
const t1 = result.matchTransition('a', 'b');
|
||||
expect(t1.timelines[0].duration).toEqual(1234);
|
||||
|
||||
const t2 = result.matchTransition('b', 'a');
|
||||
expect(t2.timelines[0].duration).toEqual(1234);
|
||||
|
||||
const t3 = result.matchTransition('c', 'a');
|
||||
expect(t3.timelines[0].duration).toEqual(1234);
|
||||
});
|
||||
|
||||
describe('aliases', () => {
|
||||
it('should alias the :enter transition as void => *', () => {
|
||||
const result = makeTrigger('name', [transition(':enter', animate(3333))]);
|
||||
|
||||
const trans = result.matchTransition('void', 'something');
|
||||
expect(trans.timelines[0].duration).toEqual(3333);
|
||||
});
|
||||
|
||||
it('should alias the :leave transition as * => void', () => {
|
||||
const result = makeTrigger('name', [transition(':leave', animate(3333))]);
|
||||
|
||||
const trans = result.matchTransition('something', 'void');
|
||||
expect(trans.timelines[0].duration).toEqual(3333);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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.io/license
|
||||
*/
|
||||
import {WebAnimationsStyleNormalizer} from '../../../src/dsl/style_normalization/web_animations_style_normalizer';
|
||||
|
||||
export function main() {
|
||||
describe('WebAnimationsStyleNormalizer', () => {
|
||||
const normalizer = new WebAnimationsStyleNormalizer();
|
||||
|
||||
describe('normalizePropertyName', () => {
|
||||
it('should normalize CSS property values to camel-case', () => {
|
||||
expect(normalizer.normalizePropertyName('width', [])).toEqual('width');
|
||||
expect(normalizer.normalizePropertyName('border-width', [])).toEqual('borderWidth');
|
||||
expect(normalizer.normalizePropertyName('borderHeight', [])).toEqual('borderHeight');
|
||||
expect(normalizer.normalizePropertyName('-webkit-animation', [
|
||||
])).toEqual('WebkitAnimation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeStyleValue', () => {
|
||||
function normalize(prop: string, val: string | number): string {
|
||||
const errors: string[] = [];
|
||||
const result = normalizer.normalizeStyleValue(prop, prop, val, errors);
|
||||
if (errors.length) {
|
||||
throw new Error(errors.join('\n'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
it('should normalize number-based dimensional properties to use a `px` suffix if missing',
|
||||
() => {
|
||||
expect(normalize('width', 10)).toEqual('10px');
|
||||
expect(normalize('height', 20)).toEqual('20px');
|
||||
});
|
||||
|
||||
it('should report an error when a string-based dimensional value does not contain a suffix at all',
|
||||
() => {
|
||||
expect(() => {
|
||||
normalize('width', '50');
|
||||
}).toThrowError(/Please provide a CSS unit value for width:50/);
|
||||
});
|
||||
|
||||
it('should not normalize non-dimensional properties with `px` values, but only convert them to string',
|
||||
() => {
|
||||
expect(normalize('opacity', 0)).toEqual('0');
|
||||
expect(normalize('opacity', '1')).toEqual('1');
|
||||
expect(normalize('color', 'red')).toEqual('red');
|
||||
expect(normalize('fontWeight', '100')).toEqual('100');
|
||||
});
|
||||
|
||||
it('should not normalize dimensional-based values that already contain a dimensional suffix or a non dimensional value',
|
||||
() => {
|
||||
expect(normalize('width', '50em')).toEqual('50em');
|
||||
expect(normalize('height', '500pt')).toEqual('500pt');
|
||||
expect(normalize('borderWidth', 'inherit')).toEqual('inherit');
|
||||
expect(normalize('paddingTop', 'calc(500px + 200px)')).toEqual('calc(500px + 200px)');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,663 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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.io/license
|
||||
*/
|
||||
import {AnimationEvent, NoopAnimationPlayer, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
|
||||
import {el} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor';
|
||||
import {buildTrigger} from '../../src/dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer, NoopAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
|
||||
import {DomAnimationEngine} from '../../src/render/dom_animation_engine';
|
||||
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/mock_animation_driver';
|
||||
|
||||
function makeTrigger(name: string, steps: any) {
|
||||
const triggerData = trigger(name, steps);
|
||||
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
|
||||
return triggerInstance;
|
||||
}
|
||||
|
||||
export function main() {
|
||||
const driver = new MockAnimationDriver();
|
||||
|
||||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
describe('DomAnimationEngine', () => {
|
||||
let element: any;
|
||||
|
||||
beforeEach(() => {
|
||||
MockAnimationDriver.log = [];
|
||||
element = el('<div></div>');
|
||||
});
|
||||
|
||||
function makeEngine(normalizer: AnimationStyleNormalizer = null) {
|
||||
return new DomAnimationEngine(driver, normalizer || new NoopAnimationStyleNormalizer());
|
||||
}
|
||||
|
||||
describe('trigger registration', () => {
|
||||
it('should ignore and not throw an error if the same trigger is registered twice', () => {
|
||||
const engine = makeEngine();
|
||||
engine.registerTrigger(trigger('trig', []));
|
||||
expect(() => { engine.registerTrigger(trigger('trig', [])); }).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('property setting', () => {
|
||||
it('should invoke a transition based on a property change', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = trigger('myTrigger', [
|
||||
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
||||
]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
|
||||
expect(engine.queuedPlayers.length).toEqual(0);
|
||||
engine.setProperty(element, 'myTrigger', 'value');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
|
||||
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
|
||||
expect(player.keyframes).toEqual([
|
||||
{height: '0px', offset: 0}, {height: '100px', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should always invoke an animation even if the property change is not matched', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = trigger(
|
||||
'myTrigger',
|
||||
[transition(
|
||||
'yes => no', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
expect(engine.queuedPlayers.length).toEqual(0);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'no');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
expect(engine.queuedPlayers.pop() instanceof NoopAnimationPlayer).toBe(true);
|
||||
engine.flush();
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'yes');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
expect(engine.queuedPlayers.pop() instanceof NoopAnimationPlayer).toBe(true);
|
||||
});
|
||||
|
||||
it('should not queue an animation if the property value has not changed at all', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = trigger('myTrigger', [
|
||||
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
||||
]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
expect(engine.queuedPlayers.length).toEqual(0);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'abc');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', 'abc');
|
||||
expect(engine.queuedPlayers.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should throw an error if an animation property without a matching trigger is changed',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
expect(() => {
|
||||
engine.setProperty(element, 'myTrigger', 'no');
|
||||
}).toThrowError(/The provided animation trigger "myTrigger" has not been registered!/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('event listeners', () => {
|
||||
it('should listen to the onStart operation for the animation', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = trigger('myTrigger', [
|
||||
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
||||
]);
|
||||
|
||||
let count = 0;
|
||||
engine.registerTrigger(trig);
|
||||
engine.listen(element, 'myTrigger', 'start', () => count++);
|
||||
engine.setProperty(element, 'myTrigger', 'value');
|
||||
expect(count).toEqual(0);
|
||||
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should listen to the onDone operation for the animation', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = trigger('myTrigger', [
|
||||
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
||||
]);
|
||||
|
||||
let count = 0;
|
||||
engine.registerTrigger(trig);
|
||||
engine.listen(element, 'myTrigger', 'done', () => count++);
|
||||
engine.setProperty(element, 'myTrigger', 'value');
|
||||
expect(count).toEqual(0);
|
||||
|
||||
engine.flush();
|
||||
expect(count).toEqual(0);
|
||||
|
||||
const player = engine.activePlayers.pop();
|
||||
player.finish();
|
||||
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should throw an error when an event is listened to that isn\'t supported', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger('myTrigger', []);
|
||||
engine.registerTrigger(trig);
|
||||
|
||||
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
|
||||
.toThrowError(
|
||||
/The provided animation trigger event "explode" for the animation trigger "myTrigger" is not supported!/);
|
||||
});
|
||||
|
||||
it('should throw an error when an event is listened for a trigger that doesn\'t exist', () => {
|
||||
const engine = makeEngine();
|
||||
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
|
||||
.toThrowError(
|
||||
/Unable to listen on the animation trigger event "explode" because the animation trigger "myTrigger" doesn\'t exist!/);
|
||||
});
|
||||
|
||||
it('should throw an error when an undefined event is listened for', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger('myTrigger', []);
|
||||
engine.registerTrigger(trig);
|
||||
expect(() => { engine.listen(element, 'myTrigger', '', () => {}); })
|
||||
.toThrowError(
|
||||
/Unable to listen on the animation trigger "myTrigger" because the provided event is undefined!/);
|
||||
});
|
||||
|
||||
it('should retain event listeners and call them for sucessive animation state changes',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger(
|
||||
'myTrigger',
|
||||
[transition(
|
||||
'* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
|
||||
engine.registerTrigger(trig);
|
||||
|
||||
let count = 0;
|
||||
engine.listen(element, 'myTrigger', 'start', () => count++);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
|
||||
engine.setProperty(element, 'myTrigger', '456');
|
||||
engine.flush();
|
||||
expect(count).toEqual(2);
|
||||
});
|
||||
|
||||
it('should only fire event listener changes for when the corresponding trigger changes state',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig1 = trigger(
|
||||
'myTrigger1',
|
||||
[transition(
|
||||
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
engine.registerTrigger(trig1);
|
||||
|
||||
const trig2 = trigger(
|
||||
'myTrigger2',
|
||||
[transition(
|
||||
'* => 123', [style({width: '0px'}), animate(1000, style({width: '100px'}))])]);
|
||||
engine.registerTrigger(trig2);
|
||||
|
||||
let count = 0;
|
||||
engine.listen(element, 'myTrigger1', 'start', () => count++);
|
||||
|
||||
engine.setProperty(element, 'myTrigger1', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
|
||||
engine.setProperty(element, 'myTrigger2', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should allow a listener to be deregistered', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = trigger(
|
||||
'myTrigger',
|
||||
[transition(
|
||||
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
||||
engine.registerTrigger(trig);
|
||||
|
||||
let count = 0;
|
||||
const deregisterFn = engine.listen(element, 'myTrigger', 'start', () => count++);
|
||||
engine.setProperty(element, 'myTrigger', '123');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
|
||||
deregisterFn();
|
||||
engine.setProperty(element, 'myTrigger', '456');
|
||||
engine.flush();
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should trigger a listener callback with an AnimationEvent argument', () => {
|
||||
const engine = makeEngine();
|
||||
engine.registerTrigger(trigger(
|
||||
'myTrigger',
|
||||
[transition(
|
||||
'* => *', [style({height: '0px'}), animate(1234, style({height: '100px'}))])]));
|
||||
|
||||
// we do this so that the next transition has a starting value that isnt null
|
||||
engine.setProperty(element, 'myTrigger', '123');
|
||||
engine.flush();
|
||||
|
||||
let capture: AnimationEvent = null;
|
||||
engine.listen(element, 'myTrigger', 'start', (e) => capture = e);
|
||||
engine.listen(element, 'myTrigger', 'done', (e) => capture = e);
|
||||
engine.setProperty(element, 'myTrigger', '456');
|
||||
engine.flush();
|
||||
|
||||
expect(capture).toEqual({
|
||||
element,
|
||||
triggerName: 'myTrigger',
|
||||
phaseName: 'start',
|
||||
fromState: '123',
|
||||
toState: '456',
|
||||
totalTime: 1234
|
||||
});
|
||||
|
||||
capture = null;
|
||||
const player = engine.activePlayers.pop();
|
||||
player.finish();
|
||||
|
||||
expect(capture).toEqual({
|
||||
element,
|
||||
triggerName: 'myTrigger',
|
||||
phaseName: 'done',
|
||||
fromState: '123',
|
||||
toState: '456',
|
||||
totalTime: 1234
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('instructions', () => {
|
||||
it('should animate a transition instruction', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const trig = makeTrigger('something', [
|
||||
state('on', style({height: 100})), state('off', style({height: 0})),
|
||||
transition('on => off', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('on', 'off');
|
||||
|
||||
expect(MockAnimationDriver.log.length).toEqual(0);
|
||||
engine.animateTransition(element, instruction);
|
||||
expect(MockAnimationDriver.log.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should animate a timeline instruction', () => {
|
||||
const engine = makeEngine();
|
||||
const timelines =
|
||||
buildAnimationKeyframes([style({height: 100}), animate(1000, style({height: 0}))]);
|
||||
expect(MockAnimationDriver.log.length).toEqual(0);
|
||||
engine.animateTimeline(element, timelines);
|
||||
expect(MockAnimationDriver.log.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should animate an array of animation instructions', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const instructions = buildAnimationKeyframes([
|
||||
style({height: 100}), animate(1000, style({height: 0})),
|
||||
animate(1000, keyframes([style({width: 0}), style({width: 1000})]))
|
||||
]);
|
||||
|
||||
expect(MockAnimationDriver.log.length).toEqual(0);
|
||||
engine.animateTimeline(element, instructions);
|
||||
expect(MockAnimationDriver.log.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transition operations', () => {
|
||||
it('should persist the styles on the element as actual styles once the animation is complete',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('on', style({height: '100px'})), state('off', style({height: '0px'})),
|
||||
transition('on => off', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('on', 'off');
|
||||
const player = engine.animateTransition(element, instruction);
|
||||
|
||||
expect(element.style.height).not.toEqual('0px');
|
||||
player.finish();
|
||||
expect(element.style.height).toEqual('0px');
|
||||
});
|
||||
|
||||
it('should remove all existing state styling from an element when a follow-up transition occurs on the same trigger',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('a', style({height: '100px'})), state('b', style({height: '500px'})),
|
||||
state('c', style({width: '200px'})), transition('* => *', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction1 = trig.matchTransition('a', 'b');
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
|
||||
player1.finish();
|
||||
expect(element.style.height).toEqual('500px');
|
||||
|
||||
const instruction2 = trig.matchTransition('b', 'c');
|
||||
const player2 = engine.animateTransition(element, instruction2);
|
||||
|
||||
expect(element.style.height).not.toEqual('500px');
|
||||
player2.finish();
|
||||
expect(element.style.width).toEqual('200px');
|
||||
expect(element.style.height).not.toEqual('500px');
|
||||
});
|
||||
|
||||
it('should allow two animation transitions with different triggers to animate in parallel',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig1 = makeTrigger('something1', [
|
||||
state('a', style({width: '100px'})), state('b', style({width: '200px'})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const trig2 = makeTrigger('something2', [
|
||||
state('x', style({height: '500px'})), state('y', style({height: '1000px'})),
|
||||
transition('* => *', animate(2000))
|
||||
]);
|
||||
|
||||
let doneCount = 0;
|
||||
function doneCallback() { doneCount++; }
|
||||
|
||||
const instruction1 = trig1.matchTransition('a', 'b');
|
||||
const instruction2 = trig2.matchTransition('x', 'y');
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
player1.onDone(doneCallback);
|
||||
expect(doneCount).toEqual(0);
|
||||
|
||||
const player2 = engine.animateTransition(element, instruction2);
|
||||
player2.onDone(doneCallback);
|
||||
expect(doneCount).toEqual(0);
|
||||
|
||||
player1.finish();
|
||||
expect(doneCount).toEqual(1);
|
||||
|
||||
player2.finish();
|
||||
expect(doneCount).toEqual(2);
|
||||
|
||||
expect(element.style.width).toEqual('200px');
|
||||
expect(element.style.height).toEqual('1000px');
|
||||
});
|
||||
|
||||
it('should cancel a previously running animation when a follow-up transition kicks off on the same trigger',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
|
||||
state('z', style({opacity: 1})), transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction1 = trig.matchTransition('x', 'y');
|
||||
const instruction2 = trig.matchTransition('y', 'z');
|
||||
|
||||
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
|
||||
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
const player2 = engine.animateTransition(element, instruction2);
|
||||
|
||||
expect(parseFloat(element.style.opacity)).toEqual(.5);
|
||||
|
||||
player2.finish();
|
||||
expect(parseFloat(element.style.opacity)).toEqual(1);
|
||||
|
||||
player1.finish();
|
||||
expect(parseFloat(element.style.opacity)).toEqual(1);
|
||||
});
|
||||
|
||||
it('should pass in the previously running players into the follow-up transition player when cancelled',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
|
||||
state('z', style({opacity: 1})), transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction1 = trig.matchTransition('x', 'y');
|
||||
const instruction2 = trig.matchTransition('y', 'z');
|
||||
const instruction3 = trig.matchTransition('z', 'x');
|
||||
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
engine.flush();
|
||||
player1.setPosition(0.5);
|
||||
|
||||
const player2 = <MockAnimationPlayer>engine.animateTransition(element, instruction2);
|
||||
expect(player2.previousPlayers).toEqual([player1]);
|
||||
player2.finish();
|
||||
|
||||
const player3 = <MockAnimationPlayer>engine.animateTransition(element, instruction3);
|
||||
expect(player3.previousPlayers).toEqual([]);
|
||||
});
|
||||
|
||||
it('should cancel all existing players if a removal animation is set to occur', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('m', style({opacity: 0})), state('n', style({opacity: 1})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
let doneCount = 0;
|
||||
function doneCallback() { doneCount++; }
|
||||
|
||||
const instruction1 = trig.matchTransition('m', 'n');
|
||||
const instructions2 =
|
||||
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 100}))]);
|
||||
const instruction3 = trig.matchTransition('n', 'void');
|
||||
|
||||
const player1 = engine.animateTransition(element, instruction1);
|
||||
player1.onDone(doneCallback);
|
||||
|
||||
const player2 = engine.animateTimeline(element, instructions2);
|
||||
player2.onDone(doneCallback);
|
||||
|
||||
engine.flush();
|
||||
expect(doneCount).toEqual(0);
|
||||
|
||||
const player3 = engine.animateTransition(element, instruction3);
|
||||
expect(doneCount).toEqual(2);
|
||||
});
|
||||
|
||||
it('should only persist styles that exist in the final state styles and not the last keyframe',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('0', style({width: '0px'})), state('1', style({width: '100px'})),
|
||||
transition('* => *', [animate(1000, style({height: '200px'}))])
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('0', '1');
|
||||
const player = engine.animateTransition(element, instruction);
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
player.finish();
|
||||
expect(element.style.height).not.toEqual('200px');
|
||||
expect(element.style.width).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should default to using styling from the `*` state if a matching state is not found',
|
||||
() => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('a', style({opacity: 0})), state('*', style({opacity: .5})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('a', 'z');
|
||||
engine.animateTransition(element, instruction).finish();
|
||||
|
||||
expect(parseFloat(element.style.opacity)).toEqual(.5);
|
||||
});
|
||||
|
||||
it('should treat `void` as `void`', () => {
|
||||
const engine = makeEngine();
|
||||
const trig = makeTrigger('something', [
|
||||
state('a', style({opacity: 0})), state('void', style({opacity: .8})),
|
||||
transition('* => *', animate(1000))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('a', 'void');
|
||||
engine.animateTransition(element, instruction).finish();
|
||||
|
||||
expect(parseFloat(element.style.opacity)).toEqual(.8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline operations', () => {
|
||||
it('should not destroy timeline-based animations after they have finished', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
const log: string[] = [];
|
||||
function capture(value: string) {
|
||||
return () => { log.push(value); };
|
||||
}
|
||||
|
||||
const instructions =
|
||||
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 500}))]);
|
||||
|
||||
const player = engine.animateTimeline(element, instructions);
|
||||
player.onDone(capture('done'));
|
||||
player.onDestroy(capture('destroy'));
|
||||
expect(log).toEqual([]);
|
||||
|
||||
player.finish();
|
||||
expect(log).toEqual(['done']);
|
||||
|
||||
player.destroy();
|
||||
expect(log).toEqual(['done', 'destroy']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('style normalizer', () => {
|
||||
it('should normalize the style values that are animateTransitioned within an a transition animation',
|
||||
() => {
|
||||
const engine = makeEngine(new SuffixNormalizer('-normalized'));
|
||||
|
||||
const trig = makeTrigger('something', [
|
||||
state('on', style({height: 100})), state('off', style({height: 0})),
|
||||
transition('on => off', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('on', 'off');
|
||||
const player = <MockAnimationPlayer>engine.animateTransition(element, instruction);
|
||||
|
||||
expect(player.keyframes).toEqual([
|
||||
{'height-normalized': '100-normalized', offset: 0},
|
||||
{'height-normalized': '0-normalized', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should normalize the style values that are animateTransitioned within an a timeline animation',
|
||||
() => {
|
||||
const engine = makeEngine(new SuffixNormalizer('-normalized'));
|
||||
|
||||
const instructions = buildAnimationKeyframes([
|
||||
style({width: '333px'}),
|
||||
animate(1000, style({width: '999px'})),
|
||||
]);
|
||||
|
||||
const player = <MockAnimationPlayer>engine.animateTimeline(element, instructions);
|
||||
expect(player.keyframes).toEqual([
|
||||
{'width-normalized': '333px-normalized', offset: 0},
|
||||
{'width-normalized': '999px-normalized', offset: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error when normalization fails within a transition animation', () => {
|
||||
const engine = makeEngine(new ExactCssValueNormalizer({left: '100px'}));
|
||||
|
||||
const trig = makeTrigger('something', [
|
||||
state('a', style({left: '0px', width: '200px'})),
|
||||
state('b', style({left: '100px', width: '100px'})), transition('a => b', animate(9876))
|
||||
]);
|
||||
|
||||
const instruction = trig.matchTransition('a', 'b');
|
||||
|
||||
let errorMessage = '';
|
||||
try {
|
||||
engine.animateTransition(element, instruction);
|
||||
} catch (e) {
|
||||
errorMessage = e.toString();
|
||||
}
|
||||
|
||||
expect(errorMessage).toMatch(/Unable to animate due to the following errors:/);
|
||||
expect(errorMessage).toMatch(/- The CSS property `left` is not allowed to be `0px`/);
|
||||
expect(errorMessage).toMatch(/- The CSS property `width` is not allowed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('view operations', () => {
|
||||
it('should perform insert operations immediately ', () => {
|
||||
const engine = makeEngine();
|
||||
|
||||
let container = <any>el('<div></div>');
|
||||
let child1 = <any>el('<div></div>');
|
||||
let child2 = <any>el('<div></div>');
|
||||
|
||||
engine.onInsert(container, () => container.appendChild(child1));
|
||||
engine.onInsert(container, () => container.appendChild(child2));
|
||||
|
||||
expect(container.contains(child1)).toBe(true);
|
||||
expect(container.contains(child2)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class SuffixNormalizer extends AnimationStyleNormalizer {
|
||||
constructor(private _suffix: string) { super(); }
|
||||
|
||||
normalizePropertyName(propertyName: string, errors: string[]): string {
|
||||
return propertyName + this._suffix;
|
||||
}
|
||||
|
||||
normalizeStyleValue(
|
||||
userProvidedProperty: string, normalizedProperty: string, value: string|number,
|
||||
errors: string[]): string {
|
||||
return value + this._suffix;
|
||||
}
|
||||
}
|
||||
|
||||
class ExactCssValueNormalizer extends AnimationStyleNormalizer {
|
||||
constructor(private _allowedValues: {[propName: string]: any}) { super(); }
|
||||
|
||||
normalizePropertyName(propertyName: string, errors: string[]): string {
|
||||
if (!this._allowedValues[propertyName]) {
|
||||
errors.push(`The CSS property \`${propertyName}\` is not allowed`);
|
||||
}
|
||||
return propertyName;
|
||||
}
|
||||
|
||||
normalizeStyleValue(
|
||||
userProvidedProperty: string, normalizedProperty: string, value: string|number,
|
||||
errors: string[]): string {
|
||||
const expectedValue = this._allowedValues[userProvidedProperty];
|
||||
if (expectedValue != value) {
|
||||
errors.push(`The CSS property \`${userProvidedProperty}\` is not allowed to be \`${value}\``);
|
||||
}
|
||||
return expectedValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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.io/license
|
||||
*/
|
||||
import {state, style, trigger} from '@angular/animations';
|
||||
import {el} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {NoopAnimationEngine} from '../src/render/noop_animation_engine';
|
||||
|
||||
export function main() {
|
||||
describe('NoopAnimationEngine', () => {
|
||||
let captures: string[] = [];
|
||||
function capture(value: string = null) { return (v: any = null) => captures.push(value || v); }
|
||||
|
||||
beforeEach(() => { captures = []; });
|
||||
|
||||
it('should immediately issue DOM removals during remove animations and then fire the animation callbacks after flush',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
engine.onRemove(elm1, capture('1'));
|
||||
engine.onRemove(elm2, capture('2'));
|
||||
|
||||
engine.listen(elm1, 'trig', 'start', capture('1-start'));
|
||||
engine.listen(elm2, 'trig', 'start', capture('2-start'));
|
||||
engine.listen(elm1, 'trig', 'done', capture('1-done'));
|
||||
engine.listen(elm2, 'trig', 'done', capture('2-done'));
|
||||
|
||||
expect(captures).toEqual(['1', '2']);
|
||||
engine.flush();
|
||||
|
||||
expect(captures).toEqual(['1', '2', '1-start', '2-start', '1-done', '2-done']);
|
||||
});
|
||||
|
||||
it('should only fire the `start` listener for a trigger that has had a property change', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
const elm3 = {};
|
||||
|
||||
engine.listen(elm1, 'trig1', 'start', capture());
|
||||
engine.setProperty(elm1, 'trig1', 'cool');
|
||||
engine.setProperty(elm2, 'trig2', 'sweet');
|
||||
engine.listen(elm2, 'trig2', 'start', capture());
|
||||
engine.listen(elm3, 'trig3', 'start', capture());
|
||||
|
||||
expect(captures).toEqual([]);
|
||||
engine.flush();
|
||||
|
||||
expect(captures.length).toEqual(2);
|
||||
const trig1Data = captures.shift();
|
||||
const trig2Data = captures.shift();
|
||||
expect(trig1Data).toEqual({
|
||||
element: elm1,
|
||||
triggerName: 'trig1',
|
||||
fromState: 'void',
|
||||
toState: 'cool',
|
||||
phaseName: 'start',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
expect(trig2Data).toEqual({
|
||||
element: elm2,
|
||||
triggerName: 'trig2',
|
||||
fromState: 'void',
|
||||
toState: 'sweet',
|
||||
phaseName: 'start',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
captures = [];
|
||||
engine.flush();
|
||||
expect(captures).toEqual([]);
|
||||
});
|
||||
|
||||
it('should only fire the `done` listener for a trigger that has had a property change', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
|
||||
const elm1 = {};
|
||||
const elm2 = {};
|
||||
const elm3 = {};
|
||||
|
||||
engine.listen(elm1, 'trig1', 'done', capture());
|
||||
engine.setProperty(elm1, 'trig1', 'awesome');
|
||||
engine.setProperty(elm2, 'trig2', 'amazing');
|
||||
engine.listen(elm2, 'trig2', 'done', capture());
|
||||
engine.listen(elm3, 'trig3', 'done', capture());
|
||||
|
||||
expect(captures).toEqual([]);
|
||||
engine.flush();
|
||||
|
||||
expect(captures.length).toEqual(2);
|
||||
const trig1Data = captures.shift();
|
||||
const trig2Data = captures.shift();
|
||||
expect(trig1Data).toEqual({
|
||||
element: elm1,
|
||||
triggerName: 'trig1',
|
||||
fromState: 'void',
|
||||
toState: 'awesome',
|
||||
phaseName: 'done',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
expect(trig2Data).toEqual({
|
||||
element: elm2,
|
||||
triggerName: 'trig2',
|
||||
fromState: 'void',
|
||||
toState: 'amazing',
|
||||
phaseName: 'done',
|
||||
totalTime: 0
|
||||
});
|
||||
|
||||
captures = [];
|
||||
engine.flush();
|
||||
expect(captures).toEqual([]);
|
||||
});
|
||||
|
||||
it('should deregister a listener when the return function is called, but only after flush',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const elm = {};
|
||||
|
||||
const fn1 = engine.listen(elm, 'trig1', 'start', capture('trig1-start'));
|
||||
const fn2 = engine.listen(elm, 'trig2', 'done', capture('trig2-done'));
|
||||
|
||||
engine.setProperty(elm, 'trig1', 'value1');
|
||||
engine.setProperty(elm, 'trig2', 'value2');
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig1-start', 'trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value3');
|
||||
engine.setProperty(elm, 'trig2', 'value4');
|
||||
|
||||
fn1();
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig1-start', 'trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value5');
|
||||
engine.setProperty(elm, 'trig2', 'value6');
|
||||
|
||||
fn2();
|
||||
engine.flush();
|
||||
expect(captures).toEqual(['trig2-done']);
|
||||
|
||||
captures = [];
|
||||
engine.setProperty(elm, 'trig1', 'value7');
|
||||
engine.setProperty(elm, 'trig2', 'value8');
|
||||
engine.flush();
|
||||
expect(captures).toEqual([]);
|
||||
});
|
||||
|
||||
it('should fire a removal listener even if the listener is deregistered prior to flush', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
const elm = {};
|
||||
|
||||
const fn = engine.listen(elm, 'trig', 'start', capture('removal listener'));
|
||||
fn();
|
||||
|
||||
engine.onRemove(elm, capture('dom removal'));
|
||||
engine.flush();
|
||||
|
||||
expect(captures).toEqual(['dom removal', 'removal listener']);
|
||||
});
|
||||
|
||||
describe('styling', () => {
|
||||
// these tests are only mean't to be run within the DOM
|
||||
if (typeof Element == 'undefined') return;
|
||||
|
||||
it('should persist the styles on the element when the animation is complete', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
]));
|
||||
|
||||
const element = el('<div></div>');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
engine.setProperty(element, 'matias', 'a');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
|
||||
engine.flush();
|
||||
expect(element.style.width).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should remove previously persist styles off of the element when a follow-up animation starts',
|
||||
() => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('a', style({width: '100px'})),
|
||||
state('b', style({height: '100px'})),
|
||||
]));
|
||||
|
||||
const element = el('<div></div>');
|
||||
|
||||
engine.setProperty(element, 'matias', 'a');
|
||||
engine.flush();
|
||||
expect(element.style.width).toEqual('100px');
|
||||
|
||||
engine.setProperty(element, 'matias', 'b');
|
||||
expect(element.style.width).not.toEqual('100px');
|
||||
expect(element.style.height).not.toEqual('100px');
|
||||
|
||||
engine.flush();
|
||||
expect(element.style.height).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should fall back to `*` styles incase the target state styles are not found', () => {
|
||||
const engine = new NoopAnimationEngine();
|
||||
engine.registerTrigger(trigger('matias', [
|
||||
state('*', style({opacity: '0.5'})),
|
||||
]));
|
||||
|
||||
const element = el('<div></div>');
|
||||
|
||||
engine.setProperty(element, 'matias', 'xyz');
|
||||
engine.flush();
|
||||
expect(element.style.opacity).toEqual('0.5');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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.io/license
|
||||
*/
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {Component} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {ɵAnimationEngine} from '@angular/platform-browser/animations';
|
||||
import {NoopAnimationsModule} from '../src/module';
|
||||
import {NoopAnimationEngine} from '../src/render/noop_animation_engine';
|
||||
|
||||
export function main() {
|
||||
describe('NoopAnimationsModule', () => {
|
||||
beforeEach(() => { TestBed.configureTestingModule({imports: [NoopAnimationsModule]}); });
|
||||
|
||||
it('the engine should be a Noop engine', () => {
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
expect(engine instanceof NoopAnimationEngine).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should flush and fire callbacks when the zone becomes stable', (async) => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template:
|
||||
'<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'* => state', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
exp: any;
|
||||
startEvent: any;
|
||||
doneEvent: any;
|
||||
onStart(event: any) { this.startEvent = event; }
|
||||
onDone(event: any) { this.doneEvent = event; }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'state';
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.startEvent.phaseName).toEqual('start');
|
||||
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.doneEvent.phaseName).toEqual('done');
|
||||
async();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle leave animation callbacks even if the element is destroyed in the process',
|
||||
(async) => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template:
|
||||
'<div *ngIf="exp" @myAnimation (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
':leave', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
exp: any;
|
||||
startEvent: any;
|
||||
doneEvent: any;
|
||||
onStart(event: any) { this.startEvent = event; }
|
||||
onDone(event: any) { this.doneEvent = event; }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
cmp.startEvent = null;
|
||||
cmp.doneEvent = null;
|
||||
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.startEvent.phaseName).toEqual('start');
|
||||
expect(cmp.startEvent.toState).toEqual('void');
|
||||
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.doneEvent.phaseName).toEqual('done');
|
||||
expect(cmp.doneEvent.toState).toEqual('void');
|
||||
async();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user