2017-02-22 18:14:49 -05:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
2017-04-26 13:44:28 -04:00
|
|
|
import {AnimationEvent, AnimationMetadata, AnimationTriggerMetadata, NoopAnimationPlayer, animate, state, style, transition, trigger} from '@angular/animations';
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
import {TriggerAst} from '../../src/dsl/animation_ast';
|
|
|
|
import {buildAnimationAst} from '../../src/dsl/animation_ast_builder';
|
2017-02-22 18:14:49 -05:00
|
|
|
import {buildTrigger} from '../../src/dsl/animation_trigger';
|
2017-02-24 03:32:19 -05:00
|
|
|
import {AnimationStyleNormalizer, NoopAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
|
2018-04-10 18:10:29 -04:00
|
|
|
import {getBodyNode} from '../../src/render/shared';
|
2019-08-28 19:22:36 -04:00
|
|
|
import {TransitionAnimationEngine, TransitionAnimationPlayer} from '../../src/render/transition_animation_engine';
|
2017-03-13 18:46:44 -04:00
|
|
|
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/src/mock_animation_driver';
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const DEFAULT_NAMESPACE_ID = 'id';
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-12-17 18:10:54 -05:00
|
|
|
(function() {
|
2017-02-22 18:14:49 -05:00
|
|
|
const driver = new MockAnimationDriver();
|
|
|
|
|
|
|
|
// these tests are only mean't to be run within the DOM
|
2018-05-25 16:52:58 -04:00
|
|
|
if (isNode) return;
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
describe('TransitionAnimationEngine', () => {
|
2017-02-22 18:14:49 -05:00
|
|
|
let element: any;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
MockAnimationDriver.log = [];
|
2017-04-26 13:44:28 -04:00
|
|
|
element = document.createElement('div');
|
|
|
|
document.body.appendChild(element);
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
afterEach(() => { document.body.removeChild(element); });
|
|
|
|
|
2017-03-24 12:56:34 -04:00
|
|
|
function makeEngine(normalizer?: AnimationStyleNormalizer) {
|
2018-04-10 18:10:29 -04:00
|
|
|
const engine = new TransitionAnimationEngine(
|
|
|
|
getBodyNode(), driver, normalizer || new NoopAnimationStyleNormalizer());
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.createNamespace(DEFAULT_NAMESPACE_ID, element);
|
|
|
|
return engine;
|
2017-02-22 18:14:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
describe('trigger registration', () => {
|
2017-02-24 19:28:14 -05:00
|
|
|
it('should ignore and not throw an error if the same trigger is registered twice', () => {
|
2017-04-26 13:44:28 -04:00
|
|
|
// TODO (matsko): ask why this is avoided
|
2017-02-22 18:14:49 -05:00
|
|
|
const engine = makeEngine();
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trigger('trig', []));
|
|
|
|
expect(() => { registerTrigger(element, engine, trigger('trig', [])); }).not.toThrow();
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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'}))])
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, 'myTrigger', 'value');
|
|
|
|
engine.flush();
|
|
|
|
expect(engine.players.length).toEqual(1);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
|
|
|
|
expect(player.keyframes).toEqual([
|
|
|
|
{height: '0px', offset: 0}, {height: '100px', offset: 1}
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
it('should not queue an animation if the property value has not changed at all', () => {
|
2017-02-22 18:14:49 -05:00
|
|
|
const engine = makeEngine();
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('myTrigger', [
|
|
|
|
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
|
|
|
]);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(engine.players.length).toEqual(0);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, 'myTrigger', 'abc');
|
|
|
|
engine.flush();
|
|
|
|
expect(engine.players.length).toEqual(1);
|
2017-03-28 19:07:49 -04:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, 'myTrigger', 'abc');
|
2017-03-28 19:07:49 -04:00
|
|
|
engine.flush();
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(engine.players.length).toEqual(1);
|
2017-03-28 19:07:49 -04:00
|
|
|
});
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
it('should throw an error if an animation property without a matching trigger is changed',
|
|
|
|
() => {
|
|
|
|
const engine = makeEngine();
|
|
|
|
expect(() => {
|
|
|
|
setProperty(element, engine, 'myTrigger', 'no');
|
|
|
|
}).toThrowError(/The provided animation trigger "myTrigger" has not been registered!/);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('removal operations', () => {
|
|
|
|
it('should cleanup all inner state that\'s tied to an element once removed', () => {
|
2017-02-22 18:14:49 -05:00
|
|
|
const engine = makeEngine();
|
|
|
|
|
|
|
|
const trig = trigger('myTrigger', [
|
2017-04-26 13:44:28 -04:00
|
|
|
transition(':leave', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
2017-02-22 18:14:49 -05:00
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, 'myTrigger', 'value');
|
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2019-01-14 18:36:08 -05:00
|
|
|
engine.removeNode(DEFAULT_NAMESPACE_ID, element, true, true);
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
it('should create and recreate a namespace for a host element with the same component source',
|
2017-02-22 18:14:49 -05:00
|
|
|
() => {
|
|
|
|
const engine = makeEngine();
|
2017-04-26 13:44:28 -04:00
|
|
|
|
|
|
|
const trig =
|
|
|
|
trigger('myTrigger', [transition('* => *', animate(1234, style({color: 'red'})))]);
|
|
|
|
|
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, 'myTrigger', 'value');
|
|
|
|
engine.flush();
|
2019-08-28 19:22:36 -04:00
|
|
|
expect(((engine.players[0] as TransitionAnimationPlayer)
|
|
|
|
.getRealPlayer() as MockAnimationPlayer)
|
|
|
|
.duration)
|
2017-04-26 13:44:28 -04:00
|
|
|
.toEqual(1234);
|
|
|
|
|
|
|
|
engine.destroy(DEFAULT_NAMESPACE_ID, null);
|
|
|
|
|
|
|
|
registerTrigger(element, engine, trig);
|
2017-07-06 20:33:16 -04:00
|
|
|
setProperty(element, engine, 'myTrigger', 'value2');
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.flush();
|
2019-08-28 19:22:36 -04:00
|
|
|
expect(((engine.players[0] as TransitionAnimationPlayer)
|
|
|
|
.getRealPlayer() as MockAnimationPlayer)
|
|
|
|
.duration)
|
2017-04-26 13:44:28 -04:00
|
|
|
.toEqual(1234);
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
2019-12-14 11:29:35 -05:00
|
|
|
|
|
|
|
it('should clear child node data when a parent node with leave transition is removed', () => {
|
|
|
|
const engine = makeEngine();
|
|
|
|
const child = document.createElement('div');
|
|
|
|
const parentTrigger = trigger('parent', [
|
|
|
|
transition(':leave', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
|
|
|
|
]);
|
|
|
|
const childTrigger = trigger(
|
|
|
|
'child',
|
|
|
|
[transition(':enter', [style({opacity: '0'}), animate(1000, style({opacity: '1'}))])]);
|
|
|
|
|
|
|
|
registerTrigger(element, engine, parentTrigger);
|
|
|
|
registerTrigger(child, engine, childTrigger);
|
|
|
|
|
|
|
|
element.appendChild(child);
|
|
|
|
engine.insertNode(DEFAULT_NAMESPACE_ID, child, element, true);
|
|
|
|
|
|
|
|
setProperty(element, engine, 'parent', 'value');
|
|
|
|
setProperty(child, engine, 'child', 'visible');
|
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
expect(engine.statesByElement.has(element))
|
|
|
|
.toBe(true, 'Expected parent data to be defined.');
|
|
|
|
expect(engine.statesByElement.has(child)).toBe(true, 'Expected child data to be defined.');
|
|
|
|
|
|
|
|
engine.removeNode(DEFAULT_NAMESPACE_ID, element, true, true);
|
|
|
|
engine.flush();
|
|
|
|
engine.players[0].finish();
|
|
|
|
|
|
|
|
expect(engine.statesByElement.has(element))
|
|
|
|
.toBe(false, 'Expected parent data to be cleared.');
|
|
|
|
expect(engine.statesByElement.has(child)).toBe(false, 'Expected child data to be cleared.');
|
|
|
|
});
|
|
|
|
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
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;
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
listen(element, engine, 'myTrigger', 'start', () => count++);
|
|
|
|
setProperty(element, engine, 'myTrigger', 'value');
|
2017-02-22 18:14:49 -05:00
|
|
|
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;
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
listen(element, engine, 'myTrigger', 'done', () => count++);
|
|
|
|
setProperty(element, engine, 'myTrigger', 'value');
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(count).toEqual(0);
|
|
|
|
|
|
|
|
engine.flush();
|
|
|
|
expect(count).toEqual(0);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.players[0].finish();
|
2017-02-22 18:14:49 -05:00
|
|
|
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', []);
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(() => { listen(element, engine, 'myTrigger', 'explode', () => {}); })
|
2017-02-22 18:14:49 -05:00
|
|
|
.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();
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(() => { listen(element, engine, 'myTrigger', 'explode', () => {}); })
|
2017-02-22 18:14:49 -05:00
|
|
|
.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', []);
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
expect(() => { listen(element, engine, 'myTrigger', '', () => {}); })
|
2017-02-22 18:14:49 -05:00
|
|
|
.toThrowError(
|
|
|
|
/Unable to listen on the animation trigger "myTrigger" because the provided event is undefined!/);
|
|
|
|
});
|
|
|
|
|
2017-07-07 19:55:17 -04:00
|
|
|
it('should retain event listeners and call them for successive animation state changes',
|
2017-02-22 18:14:49 -05:00
|
|
|
() => {
|
|
|
|
const engine = makeEngine();
|
|
|
|
const trig = trigger(
|
|
|
|
'myTrigger',
|
|
|
|
[transition(
|
|
|
|
'* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
let count = 0;
|
2017-04-26 13:44:28 -04:00
|
|
|
listen(element, engine, 'myTrigger', 'start', () => count++);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, 'myTrigger', '123');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
expect(count).toEqual(1);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, 'myTrigger', '456');
|
2017-02-22 18:14:49 -05:00
|
|
|
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'}))])]);
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig1);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
const trig2 = trigger(
|
|
|
|
'myTrigger2',
|
|
|
|
[transition(
|
|
|
|
'* => 123', [style({width: '0px'}), animate(1000, style({width: '100px'}))])]);
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig2);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
let count = 0;
|
2017-04-26 13:44:28 -04:00
|
|
|
listen(element, engine, 'myTrigger1', 'start', () => count++);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, 'myTrigger1', '123');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
expect(count).toEqual(1);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, 'myTrigger2', '123');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
expect(count).toEqual(1);
|
|
|
|
});
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
it('should allow a listener to be deregistered, but only after a flush occurs', () => {
|
2017-02-22 18:14:49 -05:00
|
|
|
const engine = makeEngine();
|
|
|
|
const trig = trigger(
|
|
|
|
'myTrigger',
|
|
|
|
[transition(
|
|
|
|
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
let count = 0;
|
2017-04-26 13:44:28 -04:00
|
|
|
const deregisterFn = listen(element, engine, 'myTrigger', 'start', () => count++);
|
|
|
|
setProperty(element, engine, 'myTrigger', '123');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
expect(count).toEqual(1);
|
|
|
|
|
|
|
|
deregisterFn();
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
setProperty(element, engine, 'myTrigger', '456');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
expect(count).toEqual(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should trigger a listener callback with an AnimationEvent argument', () => {
|
|
|
|
const engine = makeEngine();
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(
|
|
|
|
element, engine, trigger('myTrigger', [
|
|
|
|
transition(
|
|
|
|
'* => *', [style({height: '0px'}), animate(1234, style({height: '100px'}))])
|
|
|
|
]));
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-07-07 19:55:17 -04:00
|
|
|
// we do this so that the next transition has a starting value that isn't null
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, 'myTrigger', '123');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
|
2017-03-24 12:56:34 -04:00
|
|
|
let capture: AnimationEvent = null !;
|
2017-04-26 13:44:28 -04:00
|
|
|
listen(element, engine, 'myTrigger', 'start', e => capture = e);
|
|
|
|
listen(element, engine, 'myTrigger', 'done', e => capture = e);
|
|
|
|
setProperty(element, engine, 'myTrigger', '456');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
|
2017-05-18 22:26:20 -04:00
|
|
|
delete (capture as any)['_data'];
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(capture).toEqual({
|
|
|
|
element,
|
|
|
|
triggerName: 'myTrigger',
|
|
|
|
phaseName: 'start',
|
|
|
|
fromState: '123',
|
|
|
|
toState: '456',
|
2018-02-14 13:12:10 -05:00
|
|
|
totalTime: 1234,
|
|
|
|
disabled: false
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
|
2017-03-24 12:56:34 -04:00
|
|
|
capture = null !;
|
2017-04-26 13:44:28 -04:00
|
|
|
const player = engine.players.pop() !;
|
2017-02-22 18:14:49 -05:00
|
|
|
player.finish();
|
|
|
|
|
2017-05-18 22:26:20 -04:00
|
|
|
delete (capture as any)['_data'];
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(capture).toEqual({
|
|
|
|
element,
|
|
|
|
triggerName: 'myTrigger',
|
|
|
|
phaseName: 'done',
|
|
|
|
fromState: '123',
|
|
|
|
toState: '456',
|
2018-02-14 13:12:10 -05:00
|
|
|
totalTime: 1234,
|
|
|
|
disabled: false
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('transition operations', () => {
|
|
|
|
it('should persist the styles on the element as actual styles once the animation is complete',
|
|
|
|
() => {
|
|
|
|
const engine = makeEngine();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('on', style({height: '100px'})), state('off', style({height: '0px'})),
|
|
|
|
transition('on => off', animate(9876))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'on');
|
|
|
|
setProperty(element, engine, trig.name, 'off');
|
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
expect(element.style.height).not.toEqual('0px');
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.players[0].finish();
|
2017-02-22 18:14:49 -05:00
|
|
|
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();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('a', style({height: '100px'})), state('b', style({height: '500px'})),
|
|
|
|
state('c', style({width: '200px'})), transition('* => *', animate(9876))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'a');
|
|
|
|
setProperty(element, engine, trig.name, 'b');
|
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const player1 = engine.players[0];
|
2017-02-22 18:14:49 -05:00
|
|
|
player1.finish();
|
|
|
|
expect(element.style.height).toEqual('500px');
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, trig.name, 'c');
|
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const player2 = engine.players[0];
|
2017-02-22 18:14:49 -05:00
|
|
|
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();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig1 = trigger('something1', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('a', style({width: '100px'})), state('b', style({width: '200px'})),
|
|
|
|
transition('* => *', animate(1000))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig2 = trigger('something2', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('x', style({height: '500px'})), state('y', style({height: '1000px'})),
|
|
|
|
transition('* => *', animate(2000))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig1);
|
|
|
|
registerTrigger(element, engine, trig2);
|
|
|
|
|
2017-02-22 18:14:49 -05:00
|
|
|
let doneCount = 0;
|
|
|
|
function doneCallback() { doneCount++; }
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, trig1.name, 'a');
|
|
|
|
setProperty(element, engine, trig1.name, 'b');
|
|
|
|
setProperty(element, engine, trig2.name, 'x');
|
|
|
|
setProperty(element, engine, trig2.name, 'y');
|
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
const player1 = engine.players[0] !;
|
2017-02-22 18:14:49 -05:00
|
|
|
player1.onDone(doneCallback);
|
|
|
|
expect(doneCount).toEqual(0);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const player2 = engine.players[1] !;
|
2017-02-22 18:14:49 -05:00
|
|
|
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();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-08-17 19:35:55 -04:00
|
|
|
state('x', style({opacity: 0})),
|
|
|
|
state('y', style({opacity: .5})),
|
|
|
|
state('z', style({opacity: 1})),
|
|
|
|
transition('* => *', animate(1000)),
|
2017-02-22 18:14:49 -05:00
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'x');
|
|
|
|
setProperty(element, engine, trig.name, 'y');
|
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const player1 = engine.players[0];
|
|
|
|
setProperty(element, engine, trig.name, 'z');
|
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
const player2 = engine.players[0];
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-08-17 19:35:55 -04:00
|
|
|
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
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();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
|
|
|
|
state('z', style({opacity: 1})), transition('* => *', animate(1000))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'x');
|
|
|
|
setProperty(element, engine, trig.name, 'y');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
2017-04-26 13:44:28 -04:00
|
|
|
|
|
|
|
const player1 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
|
2017-02-22 18:14:49 -05:00
|
|
|
player1.setPosition(0.5);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, trig.name, 'z');
|
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
const player2 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(player2.previousPlayers).toEqual([player1]);
|
|
|
|
player2.finish();
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, trig.name, 'x');
|
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
const player3 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(player3.previousPlayers).toEqual([]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should cancel all existing players if a removal animation is set to occur', () => {
|
|
|
|
const engine = makeEngine();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('m', style({opacity: 0})), state('n', style({opacity: 1})),
|
|
|
|
transition('* => *', animate(1000))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'm');
|
|
|
|
setProperty(element, engine, trig.name, 'n');
|
|
|
|
engine.flush();
|
|
|
|
|
2017-02-22 18:14:49 -05:00
|
|
|
let doneCount = 0;
|
|
|
|
function doneCallback() { doneCount++; }
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const player1 = engine.players[0];
|
2017-02-22 18:14:49 -05:00
|
|
|
player1.onDone(doneCallback);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(doneCount).toEqual(0);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
setProperty(element, engine, trig.name, 'void');
|
2017-02-22 18:14:49 -05:00
|
|
|
engine.flush();
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(doneCount).toEqual(1);
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should only persist styles that exist in the final state styles and not the last keyframe',
|
|
|
|
() => {
|
|
|
|
const engine = makeEngine();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('0', style({width: '0px'})), state('1', style({width: '100px'})),
|
|
|
|
transition('* => *', [animate(1000, style({height: '200px'}))])
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, '0');
|
|
|
|
setProperty(element, engine, trig.name, '1');
|
|
|
|
engine.flush();
|
|
|
|
|
|
|
|
const player = engine.players[0] !;
|
2017-02-22 18:14:49 -05:00
|
|
|
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();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('a', style({opacity: 0})), state('*', style({opacity: .5})),
|
|
|
|
transition('* => *', animate(1000))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'a');
|
|
|
|
setProperty(element, engine, trig.name, 'z');
|
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.players[0].finish();
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(parseFloat(element.style.opacity)).toEqual(.5);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should treat `void` as `void`', () => {
|
|
|
|
const engine = makeEngine();
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('a', style({opacity: 0})), state('void', style({opacity: .8})),
|
|
|
|
transition('* => *', animate(1000))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'a');
|
|
|
|
setProperty(element, engine, trig.name, 'void');
|
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.players[0].finish();
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(parseFloat(element.style.opacity)).toEqual(.8);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('style normalizer', () => {
|
|
|
|
it('should normalize the style values that are animateTransitioned within an a transition animation',
|
|
|
|
() => {
|
|
|
|
const engine = makeEngine(new SuffixNormalizer('-normalized'));
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('on', style({height: 100})), state('off', style({height: 0})),
|
|
|
|
transition('on => off', animate(9876))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'on');
|
|
|
|
setProperty(element, engine, trig.name, 'off');
|
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
|
2017-02-22 18:14:49 -05:00
|
|
|
expect(player.keyframes).toEqual([
|
|
|
|
{'height-normalized': '100-normalized', offset: 0},
|
|
|
|
{'height-normalized': '0-normalized', offset: 1}
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw an error when normalization fails within a transition animation', () => {
|
|
|
|
const engine = makeEngine(new ExactCssValueNormalizer({left: '100px'}));
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const trig = trigger('something', [
|
2017-02-22 18:14:49 -05:00
|
|
|
state('a', style({left: '0px', width: '200px'})),
|
|
|
|
state('b', style({left: '100px', width: '100px'})), transition('a => b', animate(9876))
|
|
|
|
]);
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
registerTrigger(element, engine, trig);
|
|
|
|
setProperty(element, engine, trig.name, 'a');
|
|
|
|
setProperty(element, engine, trig.name, 'b');
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
let errorMessage = '';
|
|
|
|
try {
|
2017-04-26 13:44:28 -04:00
|
|
|
engine.flush();
|
2017-02-22 18:14:49 -05:00
|
|
|
} 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();
|
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
const child1 = document.createElement('div');
|
|
|
|
const child2 = document.createElement('div');
|
|
|
|
element.appendChild(child1);
|
|
|
|
element.appendChild(child2);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
element.appendChild(child1);
|
|
|
|
engine.insertNode(DEFAULT_NAMESPACE_ID, child1, element, true);
|
|
|
|
element.appendChild(child2);
|
|
|
|
engine.insertNode(DEFAULT_NAMESPACE_ID, child2, element, true);
|
2017-02-22 18:14:49 -05:00
|
|
|
|
2017-04-26 13:44:28 -04:00
|
|
|
expect(element.contains(child1)).toBe(true);
|
|
|
|
expect(element.contains(child2)).toBe(true);
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
2018-05-10 18:57:58 -04:00
|
|
|
|
|
|
|
it('should not throw an error if a missing namespace is used', () => {
|
|
|
|
const engine = makeEngine();
|
|
|
|
const ID = 'foo';
|
|
|
|
const TRIGGER = 'fooTrigger';
|
|
|
|
expect(() => { engine.trigger(ID, element, TRIGGER, 'something'); }).not.toThrow();
|
|
|
|
});
|
2018-05-31 17:51:30 -04:00
|
|
|
|
|
|
|
it('should still apply state-styling to an element even if it is not yet inserted into the DOM',
|
|
|
|
() => {
|
|
|
|
const engine = makeEngine();
|
|
|
|
const orphanElement = document.createElement('div');
|
|
|
|
orphanElement.classList.add('orphan');
|
|
|
|
|
|
|
|
registerTrigger(
|
|
|
|
orphanElement, engine, trigger('trig', [
|
|
|
|
state('go', style({opacity: 0.5})), transition('* => go', animate(1000))
|
|
|
|
]));
|
|
|
|
|
|
|
|
setProperty(orphanElement, engine, 'trig', 'go');
|
|
|
|
engine.flush();
|
|
|
|
expect(engine.players.length).toEqual(0);
|
|
|
|
expect(orphanElement.style.opacity).toEqual('0.5');
|
|
|
|
});
|
2017-02-22 18:14:49 -05:00
|
|
|
});
|
|
|
|
});
|
2017-12-16 17:42:55 -05:00
|
|
|
})();
|
2017-02-22 18:14:49 -05:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2017-04-26 13:44:28 -04:00
|
|
|
|
|
|
|
function registerTrigger(
|
|
|
|
element: any, engine: TransitionAnimationEngine, metadata: AnimationTriggerMetadata,
|
|
|
|
id: string = DEFAULT_NAMESPACE_ID) {
|
|
|
|
const errors: any[] = [];
|
2017-08-15 19:11:11 -04:00
|
|
|
const driver = new MockAnimationDriver();
|
2017-04-26 13:44:28 -04:00
|
|
|
const name = metadata.name;
|
2017-08-15 19:11:11 -04:00
|
|
|
const ast = buildAnimationAst(driver, metadata as AnimationMetadata, errors) as TriggerAst;
|
2017-04-26 13:44:28 -04:00
|
|
|
if (errors.length) {
|
|
|
|
}
|
|
|
|
const trigger = buildTrigger(name, ast);
|
2017-05-26 16:39:42 -04:00
|
|
|
engine.register(id, element);
|
|
|
|
engine.registerTrigger(id, name, trigger);
|
2017-04-26 13:44:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function setProperty(
|
|
|
|
element: any, engine: TransitionAnimationEngine, property: string, value: any,
|
|
|
|
id: string = DEFAULT_NAMESPACE_ID) {
|
|
|
|
engine.trigger(id, element, property, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
function listen(
|
|
|
|
element: any, engine: TransitionAnimationEngine, eventName: string, phaseName: string,
|
|
|
|
callback: (event: any) => any, id: string = DEFAULT_NAMESPACE_ID) {
|
|
|
|
return engine.listen(id, element, eventName, phaseName, callback);
|
|
|
|
}
|